//! ASGI-specific context (caches + scope pool)

use crate::errors::FalcornError;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyMapping, PyString};
use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};

static ASGI_CONTEXT: OnceLock<AsgiContext> = OnceLock::new();

pub struct AsgiContext {
    key_cache: RwLock<HashMap<&'static str, Py<PyAny>>>,
    base_scope: RwLock<Option<Py<PyDict>>>,
    scope_pool: RwLock<Vec<Py<PyDict>>>,
}

impl AsgiContext {
    pub fn global() -> &'static AsgiContext {
        ASGI_CONTEXT.get_or_init(|| AsgiContext {
            key_cache: RwLock::new(HashMap::new()),
            base_scope: RwLock::new(None),
            scope_pool: RwLock::new(Vec::new()),
        })
    }

    pub fn key(&self, key: &'static str, py: Python<'_>) -> Py<PyAny> {
        if let Some(v) = self.key_cache.read().unwrap().get(key).cloned() {
            return v;
        }
        let s = PyString::new(py, key).into_any().unbind();
        self.key_cache.write().unwrap().insert(key, s.clone_ref(py));
        s
    }

    fn get_or_insert_base_scope(&self, py: Python<'_>) -> Result<Py<PyDict>, FalcornError> {
        if let Some(scope) = self.base_scope.read().unwrap().clone() {
            return Ok(scope);
        }

        let dict = PyDict::new(py);
        dict.set_item(self.key("type", py), "http")
            .map_err(|_| FalcornError::runtime("Failed to set scope.type"))?;

        let asgi = PyDict::new(py);
        asgi.set_item("version", "3.0")?;
        asgi.set_item("spec_version", "2.3")?;
        dict.set_item(self.key("asgi", py), asgi)?;

        dict.set_item(self.key("root_path", py), "")?;
        dict.set_item(self.key("scheme", py), "http")?;

        let scope = dict.unbind();
        *self.base_scope.write().unwrap() = Some(scope.clone_ref(py));
        Ok(scope)
    }

    pub fn take_scope(&self, py: Python<'_>) -> Result<Py<PyDict>, FalcornError> {
        if let Some(scope) = self.scope_pool.write().unwrap().pop() {
            return Ok(scope);
        }
        let base = self.get_or_insert_base_scope(py)?;
        let scope_any = base.bind(py).call_method0("copy")?;
        let dict = scope_any
            .cast_into::<PyDict>()
            .map_err(|_| FalcornError::runtime("Failed to clone base scope"))?;
        Ok(dict.unbind())
    }

    pub fn recycle_scope(&self, py: Python<'_>, scope: Py<PyDict>) -> Result<(), FalcornError> {
        let base = self.get_or_insert_base_scope(py)?;
        let scope_b = scope.bind(py);
        scope_b.clear();
        scope_b
            .update(
                base.bind(py)
                    .cast::<PyMapping>()
                    .map_err(|_| FalcornError::runtime("Failed to reset Scope"))?,
            )
            .map_err(|_| FalcornError::runtime("Failed to reset scope"))?;

        const MAX_POOL: usize = 8192;
        let mut pool = self.scope_pool.write().unwrap();
        if pool.len() < MAX_POOL {
            pool.push(scope);
        }
        Ok(())
    }
}
