//
// Syd: rock-solid application kernel
// src/filemap.rs: File descriptor map for path canonicalization
//
// Copyright (c) 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    cell::RefCell,
    ops::{Deref, DerefMut},
    os::fd::{AsRawFd, IntoRawFd},
    sync::Arc,
};

use ahash::HashMapExt;
use libc::c_uint;
use nix::errno::Errno;

use crate::{
    config::{
        NULL_FD, NULL_F_MODE, NULL_MNT_ID, PROC_FD, PROC_F_MODE, PROC_MNT_ID, ROOT_FD, ROOT_F_MODE,
        ROOT_MNT_ID,
    },
    fd::{closeall, AT_BADFD},
    hash::SydHashMap,
    lookup::{FileMapEntry, FileType, MaybeFd},
    XPathBuf,
};

// Type alias for the inner map using SydHashMap.
type InnerMap = SydHashMap<Arc<XPathBuf>, FileMapEntry>;

thread_local! {
    // Thread-local cache for FileMap's inner arena.
    //
    // Static entries (/, /proc, /dev/null) are preserved across uses.
    static FILEMAP_CACHE: RefCell<Option<InnerMap>> = const { RefCell::new(None) };
}

// A map that associates paths with file descriptors and metadata.
//
// Uses `Option<InnerMap>` to allow moving the inner map out for caching.
// Implements `Deref` and `DerefMut` for transparent access to the inner map.
#[derive(Debug)]
pub(crate) struct FileMap(Option<InnerMap>);

impl FileMap {
    // Create a new FileMap, reusing cached arena if available.
    //
    // On first call, allocates a fresh arena and inserts static entries.
    // Subsequent calls reuse the cached arena with static entries already present.
    pub(crate) fn new() -> Result<Self, Errno> {
        // Try cache first to avoid arena allocation.
        if let Some(inner) = FILEMAP_CACHE.with(|c| c.borrow_mut().take()) {
            return Ok(Self(Some(inner)));
        }

        // First call: allocate fresh and insert static entries.
        let mut map = Self(Some(SydHashMap::with_capacity(3)));

        let entry = FileMapEntry::new(
            ROOT_FD().into(),
            Some(FileType::Dir),
            Some(ROOT_F_MODE()),
            Some(ROOT_MNT_ID()),
            None,
        );
        map.try_insert(Arc::new(XPathBuf::from("/")), entry)?;

        let entry = FileMapEntry::new(
            PROC_FD().into(),
            Some(FileType::Dir),
            Some(PROC_F_MODE()),
            Some(PROC_MNT_ID()),
            None,
        );
        map.try_insert(Arc::new(XPathBuf::from("/proc")), entry)?;

        let entry = FileMapEntry::new(
            NULL_FD().into(),
            Some(FileType::Chr),
            Some(NULL_F_MODE()),
            Some(NULL_MNT_ID()),
            None,
        );
        map.try_insert(Arc::new(XPathBuf::from("/dev/null")), entry)?;

        Ok(map)
    }

    // Try to insert an entry, returning ENOMEM on allocation failures.
    pub(crate) fn try_insert(
        &mut self,
        path: Arc<XPathBuf>,
        entry: FileMapEntry,
    ) -> Result<Option<FileMapEntry>, Errno> {
        let inner = self.as_mut();
        inner.try_reserve(1).or(Err(Errno::ENOMEM))?;
        Ok(inner.insert(path, entry))
    }
}

impl Clone for FileMap {
    fn clone(&self) -> Self {
        Self(self.0.clone())
    }
}

impl Deref for FileMap {
    type Target = InnerMap;

    #[expect(clippy::disallowed_methods)]
    fn deref(&self) -> &Self::Target {
        self.0
            .as_ref()
            .expect("BUG: FileMap inner is None (already dropped)")
    }
}

impl DerefMut for FileMap {
    #[expect(clippy::disallowed_methods)]
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.0
            .as_mut()
            .expect("BUG: FileMap inner is None (already dropped)")
    }
}

impl AsRef<InnerMap> for FileMap {
    fn as_ref(&self) -> &InnerMap {
        self.deref()
    }
}

impl AsMut<InnerMap> for FileMap {
    fn as_mut(&mut self) -> &mut InnerMap {
        self.deref_mut()
    }
}

impl Drop for FileMap {
    fn drop(&mut self) {
        let mut inner = if let Some(inner) = self.0.take() {
            inner
        } else {
            return;
        };

        // Collect OwnedFd entries to close.
        let mut closefds = Vec::with_capacity(inner.len());
        for entry in inner.values_mut() {
            // Skip editing static entries.
            let fd = entry.fd.as_raw_fd();
            if fd == ROOT_FD() || fd == PROC_FD() || fd == NULL_FD() {
                continue;
            }

            #[expect(clippy::cast_sign_loss)]
            if let MaybeFd::Owned(fd) = std::mem::take(&mut entry.fd) {
                closefds.push(fd.into_raw_fd() as c_uint);
            }
        }

        // Close all owned file descriptors.
        if !closefds.is_empty() {
            closefds.sort_unstable();
            if let Err(errno) = closeall(&closefds) {
                panic!("BUG! closeall during FileMap drop failed: {errno}!");
            }
        }

        // Retain only RawFd entries (static), discard OwnedFd entries (now empty).
        // Cache the arena for reuse.
        inner.retain(|_k, v| v.fd.as_raw_fd() != AT_BADFD.as_raw_fd());
        FILEMAP_CACHE.with(|c| *c.borrow_mut() = Some(inner));
    }
}
