//
// Syd: rock-solid application kernel
// src/hash.rs: Utilities for caching
//
// Copyright (c) 2024, 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    borrow::Borrow,
    collections::HashMap,
    hash::Hash,
    net::IpAddr,
    sync::{Arc, Condvar, Mutex},
};

use ahash::RandomState;
use nix::{errno::Errno, unistd::Pid};
use quick_cache::{sync::Cache, Equivalent};
use serde::{ser::SerializeMap, Serialize, Serializer};

use crate::{
    elf::ExecutableFile,
    fs::CanonicalPath,
    hook::RemoteProcess,
    libseccomp::ScmpSyscall,
    path::{XPath, XPathBuf},
    sandbox::{Action, Capability},
    ScmpNotifReq, SydArch, SydMemoryMap, SydSigSet,
};

/// Value type for action caches.
/// The boolean represents filtering.
pub type CacheVal = (Action, bool);

/// Key type for the Path Cache.
//#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub type PathCacheKey = (Capability, XPathBuf);

/// Key type for the IP Address Cache.
/// u16 represents the port.
pub type AddrCacheKey = (Capability, IpAddr, u16);

/// Path action cache to cache security policies about paths.
/// This is similar to Action Vector Cache of SELinux.
#[derive(Debug)]
pub struct PathCache(pub Cache<PathCacheKey, CacheVal>);

/// IP address action cache to cache security policies about internet addresses.
/// This is similar to Action Vector Cache of SELinux.
#[derive(Debug)]
pub struct AddrCache(pub Cache<AddrCacheKey, CacheVal>);

/// Metadata on a blocking syscall invocation
#[derive(Copy, Clone, Debug)]
pub struct SysInterrupt {
    /// The thread group ID
    pub tgid: Pid,
    /// Syd handler thread ID
    pub handler: Pid,
    /// System call request
    pub request: ScmpNotifReq,
    /// True if `SA_RESTART` is ignored
    /// (e.g. due to a socket timeout).
    pub ignore_restart: bool,
}

/// This is the data type used to handle syscall interrupts.
#[derive(Debug)]
#[allow(clippy::type_complexity)]
pub struct SysInterruptMap {
    /// Map of blocking syscalls by request id.
    pub sys_block: Arc<(Mutex<HashMap<u64, SysInterrupt, RandomState>>, Condvar)>,
    /// Map of restarting signals by TGID.
    /// Used for SA_RESTART tracking.
    pub sig_restart: Arc<Mutex<HashMap<Pid, SydSigSet, RandomState>>>,
}

/// Represents an exec(3) check result
#[derive(Debug)]
pub struct ExecResult {
    pub(crate) file: ExecutableFile,
    pub(crate) arch: u32,
    pub(crate) ip: u64,
    pub(crate) sp: u64,
    pub(crate) args: [u64; 6],
    pub(crate) ip_mem: Option<[u8; 64]>,
    pub(crate) sp_mem: Option<[u8; 64]>,
    pub(crate) memmap: Option<Vec<SydMemoryMap>>,
}

/// Represents a sigreturn(2) check result
#[derive(Debug)]
pub struct SigreturnResult {
    pub(crate) is_realtime: bool,
    pub(crate) ip: u64,
    pub(crate) sp: u64,
    pub(crate) args: [u64; 6],
    pub(crate) ip_mem: Option<[u8; 64]>,
    pub(crate) sp_mem: Option<[u8; 64]>,
}

/// Results map for ptrace(2) hooks chdir, execve, sigaction and sigreturn.
#[derive(Debug)]
#[allow(clippy::type_complexity)]
pub struct SysResultMap<'a> {
    /// syscall-agnostic error map
    pub trace_error: Arc<Mutex<HashMap<RemoteProcess, Errno, RandomState>>>,
    /// chdir(2) result map
    pub trace_chdir: Arc<Mutex<HashMap<RemoteProcess, CanonicalPath<'a>, RandomState>>>,
    /// exec(3) result map
    pub trace_execv: Arc<Mutex<HashMap<RemoteProcess, ExecResult, RandomState>>>,
    /// {rt_,}sigreturn(2) result map
    pub trace_sigret: Arc<Mutex<HashMap<RemoteProcess, SigreturnResult, RandomState>>>,
}

/// Signal map, used by signal counting for SROP mitigation:
/// If a TGID is not in sig_handle_map at the entry of sigreturn(2),
/// we terminate the process because the sigreturn(2) is artificial.
#[derive(Debug)]
#[allow(clippy::type_complexity)]
pub struct SignalMap {
    /// Set of TGIDs that have received count signals for handled signals.
    pub sig_handle: Arc<Mutex<HashMap<Pid, u64, RandomState>>>,
}

impl SysInterrupt {
    pub(crate) fn new(
        request: ScmpNotifReq,
        tgid: Pid,
        handler: Pid,
        ignore_restart: bool,
    ) -> Self {
        Self {
            tgid,
            handler,
            request,
            ignore_restart,
        }
    }
}

impl serde::Serialize for SysInterrupt {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(3))?;

        let data = &self.request.data;
        let syscall = ScmpSyscall::get_name_by_arch(data.syscall, data.arch)
            .unwrap_or_else(|_| format!("{}", i32::from(data.syscall)));
        let _ = map.serialize_entry("pid", &self.request.pid);
        let _ = map.serialize_entry("tgid", &self.tgid.as_raw());
        let _ = map.serialize_entry("sys", &syscall);
        let _ = map.serialize_entry("arch", &SydArch(data.arch));
        let _ = map.serialize_entry("args", &data.args);
        let _ = map.serialize_entry("handler", &self.handler.as_raw());
        let _ = map.serialize_entry("ignore_restart", &self.ignore_restart);

        map.end()
    }
}

/// Convenience type to avoid cloning Paths during lookup.
#[derive(Hash)]
pub struct PathCap<'a>(pub Capability, pub &'a XPath);

impl Equivalent<PathCacheKey> for PathCap<'_> {
    fn equivalent(&self, key: &PathCacheKey) -> bool {
        self.0 == key.0 && *self.1 == *key.1
    }
}

impl ToOwned for PathCap<'_> {
    type Owned = PathCacheKey;

    fn to_owned(&self) -> Self::Owned {
        (self.0, XPathBuf::from(self.1))
    }
}

impl<'a> Borrow<PathCap<'a>> for PathCacheKey {
    fn borrow(&self) -> &PathCap<'a> {
        unreachable!();
    }
}

impl Serialize for PathCache {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(5))?;

        map.serialize_entry("hits", &self.0.hits())?;
        map.serialize_entry("misses", &self.0.misses())?;
        map.serialize_entry("len", &self.0.len())?;
        map.serialize_entry("cap", &self.0.capacity())?;
        //we use UnitWeighter, ie total weight is always equal to the length.
        //map.serialize_entry("weight", &self.0.weight())?;

        map.end()
    }
}

impl Serialize for AddrCache {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(Some(5))?;

        map.serialize_entry("hits", &self.0.hits())?;
        map.serialize_entry("misses", &self.0.misses())?;
        map.serialize_entry("len", &self.0.len())?;
        map.serialize_entry("cap", &self.0.capacity())?;
        //we use UnitWeighter, ie total weight is always equal to the length.
        //map.serialize_entry("weight", &self.0.weight())?;

        map.end()
    }
}

/// Create a new SysInterruptMap.
pub fn sys_interrupt_map_new() -> SysInterruptMap {
    SysInterruptMap {
        sys_block: Arc::new((
            Mutex::new(HashMap::with_hasher(RandomState::new())),
            Condvar::new(),
        )),
        sig_restart: Arc::new(Mutex::new(HashMap::with_hasher(RandomState::new()))),
    }
}

/// Create a new SysResultMap.
pub fn sys_result_map_new<'a>() -> SysResultMap<'a> {
    SysResultMap {
        trace_error: Arc::new(Mutex::new(HashMap::with_hasher(RandomState::new()))),
        trace_chdir: Arc::new(Mutex::new(HashMap::with_hasher(RandomState::new()))),
        trace_execv: Arc::new(Mutex::new(HashMap::with_hasher(RandomState::new()))),
        trace_sigret: Arc::new(Mutex::new(HashMap::with_hasher(RandomState::new()))),
    }
}

/// Create a new SignalMap.
pub fn signal_map_new() -> SignalMap {
    SignalMap {
        sig_handle: Arc::new(Mutex::new(HashMap::with_hasher(RandomState::new()))),
    }
}

/// Create a new Path Action Cache.
pub fn path_cache_new(estimated_items_capacity: usize) -> PathCache {
    let cache = Cache::<PathCacheKey, CacheVal>::new(estimated_items_capacity);
    PathCache(cache)
}

/// Create a new Address Action Cache.
pub fn addr_cache_new(estimated_items_capacity: usize) -> AddrCache {
    let cache = Cache::<AddrCacheKey, CacheVal>::new(estimated_items_capacity);
    AddrCache(cache)
}
