//! Mizan IR — strongly-typed deserialization of the backends' schema export.
//!
//! Every Mizan backend (Django, FastAPI, mizan-ts) emits the same OpenAPI
//! document with three load-bearing extension fields:
//!   - `x-mizan-functions`   — array of function entries
//!   - `x-mizan-contexts`    — map of context groups
//!   - `components.schemas`  — OpenAPI Pydantic→JSONSchema per Input/Output
//!
//! The structs here deserialize that JSON envelope into typed Rust values
//! the emit targets walk. The OpenAPI document body (paths, info, etc.) is
//! intentionally not modeled — the codegen consumes only the extensions.

use std::collections::BTreeMap;

use indexmap::IndexMap;
use serde::Deserialize;


#[derive(Debug, Deserialize)]
pub struct MizanIR {
    #[serde(rename = "x-mizan-functions", default)]
    pub functions: Vec<MizanFunction>,

    #[serde(rename = "x-mizan-contexts", default)]
    pub contexts: IndexMap<String, MizanContext>,

    /// Django-only channel registrations. FastAPI backends emit an empty list.
    #[serde(rename = "x-mizan-channels", default)]
    pub channels: Vec<MizanChannel>,

    #[serde(default)]
    pub components: Components,
}


#[derive(Debug, Deserialize, Clone)]
pub struct MizanChannel {
    pub name: String,
    #[serde(rename = "pascalName")]
    pub pascal_name: String,
    #[serde(rename = "hasParams", default)]
    pub has_params: bool,
    #[serde(rename = "hasReactMessage", default)]
    pub has_react_message: bool,
    #[serde(rename = "hasDjangoMessage", default)]
    pub has_django_message: bool,
    #[serde(rename = "paramsType", default)]
    pub params_type: Option<String>,
    #[serde(rename = "reactMessageType", default)]
    pub react_message_type: Option<String>,
    #[serde(rename = "djangoMessageType", default)]
    pub django_message_type: Option<String>,
}


#[derive(Debug, Deserialize, Clone)]
pub struct MizanFunction {
    pub name: String,

    #[serde(rename = "camelName")]
    pub camel_name: String,

    #[serde(rename = "hasInput")]
    pub has_input: bool,

    #[serde(rename = "inputType")]
    pub input_type: Option<String>,

    #[serde(rename = "outputType")]
    pub output_type: String,

    #[serde(rename = "outputNullable", default)]
    pub output_nullable: bool,

    pub transport: Transport,

    #[serde(rename = "isContext", default)]
    pub is_context: IsContext,

    #[serde(rename = "isForm", default)]
    pub is_form: bool,

    #[serde(rename = "formName", default)]
    pub form_name: Option<String>,

    #[serde(rename = "formRole", default)]
    pub form_role: Option<String>,

    #[serde(default)]
    pub affects: Vec<AffectTarget>,

    /// Names of contexts whose state is patched by this function's return
    /// body via the kernel's `splice_slot` merger. Empty when the function
    /// is not a merge target.
    #[serde(default)]
    pub merge: Vec<String>,
}


#[derive(Debug, Deserialize, Default, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Transport {
    #[default]
    Http,
    Websocket,
    Both,
}


/// IR-level `isContext` value. The backends emit `false` for non-context
/// functions and a string (`"global"`, `"user"`, …) for context-grouped
/// functions. Custom Deserialize bridges the boolean/string union into a
/// typed Rust enum.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum IsContext {
    #[default]
    No,
    Yes(String),
}

impl IsContext {
    pub fn as_str(&self) -> Option<&str> {
        match self {
            IsContext::No => None,
            IsContext::Yes(s) => Some(s.as_str()),
        }
    }
}

impl<'de> Deserialize<'de> for IsContext {
    fn deserialize<D>(de: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let v = serde_json::Value::deserialize(de)?;
        match v {
            serde_json::Value::Bool(false) => Ok(IsContext::No),
            serde_json::Value::Bool(true) => Err(serde::de::Error::custom(
                "isContext: bare `true` is not a valid context name",
            )),
            serde_json::Value::String(s) => Ok(IsContext::Yes(s)),
            serde_json::Value::Null => Ok(IsContext::No),
            other => Err(serde::de::Error::custom(format!(
                "isContext: expected `false` or string, got {other:?}"
            ))),
        }
    }
}


#[derive(Debug, Deserialize, Clone)]
pub struct AffectTarget {
    #[serde(rename = "type")]
    pub kind: AffectKind,
    pub name: String,
    #[serde(default)]
    pub context: Option<String>,
}


#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AffectKind {
    Context,
    Function,
}


#[derive(Debug, Deserialize, Default, Clone)]
pub struct MizanContext {
    #[serde(default)]
    pub functions: Vec<String>,
    #[serde(default)]
    pub params: IndexMap<String, ContextParam>,
}


#[derive(Debug, Deserialize, Clone)]
pub struct ContextParam {
    #[serde(rename = "type")]
    pub ty: String,

    pub required: bool,

    #[serde(rename = "sharedBy", default)]
    pub shared_by: Vec<String>,
}


#[derive(Debug, Deserialize, Default)]
pub struct Components {
    #[serde(default)]
    pub schemas: IndexMap<String, JsonSchema>,
}


/// JSON Schema subset used by the emit targets. Mirrors the surface the
/// existing JS adapters traverse (`$ref`, `anyOf`, `enum`, `type`, `items`,
/// `properties`, `required`, `nullable`). Unknown fields are stashed in
/// `extra` so backends can include schema annotations the codegen ignores.
#[derive(Debug, Deserialize, Default, Clone)]
pub struct JsonSchema {
    #[serde(rename = "type", default)]
    pub ty: Option<String>,

    #[serde(rename = "$ref", default)]
    pub r#ref: Option<String>,

    #[serde(rename = "enum", default)]
    pub r#enum: Option<Vec<serde_json::Value>>,

    #[serde(rename = "anyOf", default)]
    pub any_of: Option<Vec<JsonSchema>>,

    #[serde(default)]
    pub nullable: bool,

    #[serde(default)]
    pub items: Option<Box<JsonSchema>>,

    #[serde(default)]
    pub properties: Option<IndexMap<String, JsonSchema>>,

    #[serde(default)]
    pub required: Vec<String>,

    #[serde(rename = "additionalProperties", default)]
    pub additional_properties: Option<serde_json::Value>,

    /// Presence of this field means the schema has a default — the server
    /// always populates it. Consumers can treat the field as non-optional
    /// even if it's absent from `required`.
    #[serde(default)]
    pub default: Option<serde_json::Value>,

    #[serde(flatten)]
    pub extra: BTreeMap<String, serde_json::Value>,
}


impl JsonSchema {
    /// `$ref: "#/components/schemas/Foo"` → `Some("Foo")`.
    pub fn ref_name(&self) -> Option<&str> {
        self.r#ref
            .as_deref()
            .and_then(|s| s.strip_prefix("#/components/schemas/"))
    }
}
