//! `MizanClient` — the kernel entry point.
//!
//! Mirrors the `configure(opts)` + module-level state in
//! `frontends/mizan-base/src/index.ts`, but as an owned struct because
//! Rust lacks module-level mutable state. Consumers hold an
//! `Arc<MizanClient>` and pass it everywhere the TS code would have
//! used the module-level `config`.
//!
//! Public surface:
//! - `MizanClient::new(config)` — build with reqwest cookie jar.
//! - `client.fetch_context(name, params)` — async, returns parsed JSON bundle.
//! - `client.call(fn_name, args)` — async, applies merge + invalidation
//!   from the response then returns `result`.
//! - `client.register_context(name, params, fetch_fn)` — register an
//!   instance; returns a `ContextHandle`.
//! - `client.invalidate(name)` / `client.invalidate_scoped(name, params)`
//!   — schedule invalidation via the kernel queue.
//! - `client.merge(context, params, slot, value)` — splice a value into
//!   a context bundle slot.

use std::sync::Arc;
use std::time::Duration;

use reqwest::cookie::CookieStore;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, ACCEPT};
use reqwest::Url;
use serde_json::Value;
use tokio::sync::OnceCell;

use crate::context::{ContextHandle, ContextRegistry, FetchFn};
use crate::error::MizanError;
use crate::invalidation::InvalidationQueue;
use crate::transport;


pub struct MizanConfig {
    pub base_url: String,
    pub session: bool,
    pub csrf_cookie_name: String,
    pub csrf_header_name: String,
    pub extra_headers: Vec<(String, String)>,
}


impl Default for MizanConfig {
    fn default() -> Self {
        Self {
            base_url: "/api/mizan".to_string(),
            session: true,
            csrf_cookie_name: "csrftoken".to_string(),
            csrf_header_name: "X-CSRFToken".to_string(),
            extra_headers: Vec::new(),
        }
    }
}


pub struct MizanClient {
    config: Arc<MizanConfig>,
    http: reqwest::Client,
    cookie_jar: Arc<reqwest::cookie::Jar>,
    registry: Arc<ContextRegistry>,
    queue: Arc<InvalidationQueue>,
    session_ready: OnceCell<()>,
}


impl MizanClient {
    pub fn new(config: MizanConfig) -> Arc<Self> {
        let cookie_jar = Arc::new(reqwest::cookie::Jar::default());
        let http = reqwest::Client::builder()
            .cookie_provider(Arc::clone(&cookie_jar))
            .build()
            .expect("reqwest client construction");
        let registry = Arc::new(ContextRegistry::new());
        let queue = InvalidationQueue::new(Arc::clone(&registry));
        Arc::new(Self {
            config: Arc::new(config),
            http,
            cookie_jar,
            registry,
            queue,
            session_ready: OnceCell::new(),
        })
    }

    pub fn config(&self) -> &MizanConfig {
        &self.config
    }

    pub fn http(&self) -> &reqwest::Client {
        &self.http
    }

    pub fn context_registry(&self) -> &Arc<ContextRegistry> {
        &self.registry
    }

    pub fn invalidation_queue(&self) -> &Arc<InvalidationQueue> {
        &self.queue
    }

    /// Hit `/session/` once on first call to bootstrap the CSRF cookie.
    /// No-op when `config.session == false`. Three attempts with 100ms
    /// × attempt backoff.
    pub async fn ensure_session_ready(&self) -> Result<(), MizanError> {
        if !self.config.session {
            return Ok(());
        }
        self.session_ready
            .get_or_try_init(|| async {
                if self.read_csrf_cookie().is_some() {
                    return Ok(());
                }
                let url = Url::parse(&format!("{}/session/", self.config.base_url.trim_end_matches('/')))
                    .map_err(|e| MizanError::transport(format!("invalid base_url: {e}")))?;
                for attempt in 0..3 {
                    let res = self.http.get(url.clone()).send().await;
                    if res.is_ok() && self.read_csrf_cookie().is_some() {
                        return Ok(());
                    }
                    if attempt < 2 {
                        tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await;
                    }
                }
                // Mirror TS: failing to bootstrap is non-fatal — subsequent
                // calls proceed without CSRF and may still succeed (e.g.,
                // FastAPI configs that don't require it).
                Ok(())
            })
            .await
            .copied()
    }

    pub(crate) async fn resolve_headers(&self) -> HeaderMap {
        let mut headers = HeaderMap::new();
        for (name, value) in &self.config.extra_headers {
            if let (Ok(n), Ok(v)) = (HeaderName::try_from(name.as_str()), HeaderValue::try_from(value.as_str())) {
                headers.insert(n, v);
            }
        }
        if let Some(token) = self.read_csrf_cookie() {
            if let (Ok(n), Ok(v)) = (
                HeaderName::try_from(self.config.csrf_header_name.as_str()),
                HeaderValue::try_from(token.as_str()),
            ) {
                headers.insert(n, v);
            }
        }
        headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
        headers
    }

    fn read_csrf_cookie(&self) -> Option<String> {
        let url = Url::parse(&self.config.base_url).ok()?;
        let header = self.cookie_jar.cookies(&url)?;
        let raw = header.to_str().ok()?;
        let needle = format!("{}=", self.config.csrf_cookie_name);
        raw.split(';')
            .map(|p| p.trim())
            .find_map(|p| p.strip_prefix(&needle))
            .map(|v| v.trim_matches('"').to_string())
    }

    // ── High-level API ─────────────────────────────────────────────────

    pub async fn fetch_context(&self, context: &str, params: &Value) -> Result<Value, MizanError> {
        transport::mizan_fetch(self, context, params).await
    }

    pub async fn call(&self, fn_name: &str, args: Value) -> Result<Value, MizanError> {
        transport::mizan_call(self, fn_name, args).await
    }

    pub async fn register_context(
        self: &Arc<Self>,
        name: impl Into<String>,
        params: Value,
        fetch_fn: FetchFn,
    ) -> ContextHandle {
        self.registry.register(name, params, fetch_fn, None).await
    }

    pub async fn invalidate(self: &Arc<Self>, name: impl Into<String>) {
        self.queue.invalidate(name).await;
    }

    pub async fn invalidate_scoped(self: &Arc<Self>, name: impl Into<String>, params: Value) {
        self.queue.invalidate_scoped(name, params).await;
    }

    pub async fn merge(&self, context: &str, params: Option<&Value>, slot: &str, value: &Value) {
        self.registry.merge(context, params, slot, value).await;
    }
}
