//! HTTP handlers. Mirrors `backends/mizan-fastapi/src/mizan_fastapi/router.py`.

use axum::extract::{Path, Query, State};
use axum::http::{header, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use mizan_core::{
    compute_invalidation, compute_merges, lookup_function, lookup_context, FunctionSpec,
    InvalidationTarget, MergeEntry, MizanError, RequestHandle, FUNCTIONS,
};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::any::Any;
use std::collections::BTreeMap;
use std::sync::Arc;

use crate::errors::ApiError;

/// Type-erased application state threaded into every `dispatch()` call via
/// `RequestHandle`. User handlers downcast to their concrete state type.
/// `Arc` keeps the clone cheap across per-request handler invocations.
pub type AppStateAny = Arc<dyn Any + Send + Sync>;

/// Body for POST /call/. Matches the Python `CallBody` shape.
#[derive(Debug, Deserialize)]
pub struct CallBody {
    pub fn_: Option<String>,
    #[serde(rename = "fn")]
    pub function_name: Option<String>,
    #[serde(default)]
    pub args: Map<String, Value>,
}

impl CallBody {
    fn resolved_name(&self) -> Option<&str> {
        self.function_name
            .as_deref()
            .or(self.fn_.as_deref())
    }
}

#[derive(Debug, Serialize)]
pub struct CallResponse {
    pub result: Value,
    pub invalidate: Vec<Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub merge: Option<Vec<Value>>,
}

fn no_store(json: Value) -> Response {
    let mut resp = (StatusCode::OK, Json(json)).into_response();
    resp.headers_mut()
        .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
    resp
}

/// POST /call/  — RPC dispatch.
pub async fn function_call(
    State(app_state): State<AppStateAny>,
    Json(body): Json<CallBody>,
) -> Result<Response, ApiError> {
    let fn_name = body
        .resolved_name()
        .ok_or_else(|| ApiError(MizanError::BadRequest("missing `fn` field".into())))?
        .to_string();

    let fn_spec = lookup_function(&fn_name)
        .ok_or_else(|| ApiError(MizanError::NotFound(format!("function {fn_name:?} not registered"))))?;

    let req = RequestHandle::from_dyn(app_state.as_ref());
    let result = fn_spec.dispatch(req, Value::Object(body.args.clone())).await.map_err(ApiError)?;

    let invalidate: Vec<Value> = compute_invalidation(fn_spec, &body.args)
        .iter()
        .map(InvalidationTarget::to_json)
        .collect();
    let merges = compute_merges(fn_spec, &body.args, &result);
    let merge_payload: Option<Vec<Value>> = if merges.is_empty() {
        None
    } else {
        Some(merges.iter().map(MergeEntry::to_json).collect())
    };

    let payload = CallResponse {
        result,
        invalidate,
        merge: merge_payload,
    };
    Ok(no_store(serde_json::to_value(&payload).unwrap()))
}

/// GET /ctx/:context_name/  — bundled context fetch.
pub async fn context_fetch(
    State(app_state): State<AppStateAny>,
    Path(context_name): Path<String>,
    Query(params): Query<BTreeMap<String, String>>,
) -> Result<Response, ApiError> {
    if lookup_context(&context_name).is_none() {
        return Err(ApiError(MizanError::NotFound(format!(
            "context {context_name:?} not registered"
        ))));
    }

    let members: Vec<&dyn FunctionSpec> = FUNCTIONS
        .iter()
        .copied()
        .filter(|f| f.context() == Some(&context_name))
        .collect();
    if members.is_empty() {
        return Err(ApiError(MizanError::NotFound(format!(
            "context {context_name:?} has no registered members"
        ))));
    }

    // Convert query params (all-string values) to the JSON arg map. Numeric
    // params get parsed via the per-function input_params primitive table.
    let mut bundled = Map::new();
    for fn_spec in &members {
        let args = coerce_query_args(*fn_spec, &params);
        let req = RequestHandle::from_dyn(app_state.as_ref());
        let result = fn_spec.dispatch(req, Value::Object(args)).await.map_err(ApiError)?;
        bundled.insert(fn_spec.name().to_string(), result);
    }

    Ok(no_store(Value::Object(bundled)))
}

/// Coerce string-valued query params into typed JSON values using the
/// function's declared input_params. Strings that don't parse stay as
/// strings — the dispatch wrapper will raise ValidationFailed downstream.
fn coerce_query_args(
    fn_spec: &dyn FunctionSpec,
    params: &BTreeMap<String, String>,
) -> Map<String, Value> {
    let mut out = Map::new();
    for ip in fn_spec.input_params() {
        if let Some(raw) = params.get(ip.name) {
            let parsed = match ip.primitive {
                mizan_core::Primitive::Integer => raw.parse::<i64>().ok().map(Value::from),
                mizan_core::Primitive::Number => raw.parse::<f64>().ok().and_then(|v| {
                    serde_json::Number::from_f64(v).map(Value::Number)
                }),
                mizan_core::Primitive::Boolean => raw.parse::<bool>().ok().map(Value::from),
                mizan_core::Primitive::String => Some(Value::from(raw.clone())),
            };
            if let Some(v) = parsed {
                out.insert(ip.name.into(), v);
            } else {
                out.insert(ip.name.into(), Value::from(raw.clone()));
            }
        }
    }
    out
}

/// GET /session/  — placeholder for the Mizan-protocol session-init endpoint.
/// CSRF is a Django-only concern; the Rust adapter returns a null token so
/// readiness-probe consumers see a well-formed response.
pub async fn session_init() -> Response {
    let body = serde_json::json!({ "csrfToken": null });
    no_store(body)
}
