//! Stage 1 — framework-agnostic TypeScript emission.
//!
//! Output mirrors `protocol/mizan-generate/generator/lib/stage1.mjs`:
//!
//!   types.ts              — typed declarations for every Pydantic model
//!   contexts/<name>.ts    — `fetch<Name>Context(params)` per context group
//!   mutations/<name>.ts   — `call<Name>(args)` per mutation
//!   functions/<name>.ts   — `call<Name>(args)` per plain function
//!   index.ts              — re-exports
//!
//! The deterministic per-function/per-context files match the JS codegen
//! byte-for-byte against an identical IR; types.ts emits Pydantic schemas
//! directly as TS interfaces instead of routing through openapi-typescript.
//! Consumers import by name from index.ts so the structural shape of
//! types.ts is not load-bearing — only the named exports are.

use std::path::PathBuf;

use askama::Template;
use indexmap::IndexMap;

use crate::config::Config;
use crate::ir::{IsContext, JsonSchema, MizanContext, MizanFunction, MizanIR};
use crate::emit::CodegenTarget;
use crate::emit::EmittedFile;
use crate::emit::casing::pascal_case;


#[derive(Template)]
#[template(path = "stage1/call.ts.j2", escape = "none")]
struct CallTemplate<'a> {
    pascal: &'a str,
    name: &'a str,
    has_input: bool,
    input_type: &'a str,
    output_type: &'a str,
    type_imports: Vec<String>,
}


#[derive(Template)]
#[template(path = "stage1/context.ts.j2", escape = "none")]
struct ContextTemplate<'a> {
    pascal: &'a str,
    ctx_name: &'a str,
    type_imports: Vec<String>,
    data_fields: Vec<ContextDataField<'a>>,
    has_params: bool,
    params: Vec<ContextParamField<'a>>,
}


struct ContextDataField<'a> {
    name: &'a str,
    output_type: &'a str,
}


struct ContextParamField<'a> {
    name: &'a str,
    ts_type: &'static str,
    required: bool,
}


#[derive(Template)]
#[template(path = "stage1/index.ts.j2", escape = "none")]
struct IndexTemplate<'a> {
    contexts: Vec<IndexContext<'a>>,
    calls: Vec<IndexCall<'a>>,
    framework_adapters: Vec<&'static str>,
}


struct IndexContext<'a> {
    pascal: String,
    name: &'a str,
}


struct IndexCall<'a> {
    pascal: String,
    camel_name: &'a str,
    dir: &'static str,
}


pub struct Stage1;


impl CodegenTarget for Stage1 {
    fn name(&self) -> &'static str { "stage1" }

    fn emit(&self, ir: &MizanIR, config: &Config) -> Vec<EmittedFile> {
        let mut out: Vec<EmittedFile> = Vec::new();

        out.push(EmittedFile::new("types.ts", emit_types(&ir.components.schemas)));

        for (ctx_name, ctx_meta) in &ir.contexts {
            let content = emit_context_file(ctx_name, ctx_meta, &ir.functions);
            out.push(EmittedFile::new(
                PathBuf::from("contexts").join(format!("{ctx_name}.ts")),
                content,
            ));
        }

        for fn_meta in regular_functions(&ir.functions) {
            let dir = if fn_meta.affects.is_empty() { "functions" } else { "mutations" };
            let content = emit_call_file(fn_meta);
            out.push(EmittedFile::new(
                PathBuf::from(dir).join(format!("{}.ts", fn_meta.camel_name)),
                content,
            ));
        }

        out.push(EmittedFile::new("index.ts", emit_stage1_index(ir, config)));

        out
    }
}


fn regular_functions(functions: &[MizanFunction]) -> impl Iterator<Item = &MizanFunction> {
    functions.iter().filter(|f| matches!(f.is_context, IsContext::No) && !f.is_form)
}


fn dedupe_preserving_order(items: impl IntoIterator<Item = String>) -> Vec<String> {
    let mut seen = std::collections::HashSet::new();
    items.into_iter().filter(|s| seen.insert(s.clone())).collect()
}


// ─── Per-context file ──────────────────────────────────────────────────────


fn emit_context_file(
    ctx_name: &str,
    ctx_meta: &MizanContext,
    all_functions: &[MizanFunction],
) -> String {
    let pascal = pascal_case(ctx_name);
    let ctx_fns: Vec<&MizanFunction> = all_functions
        .iter()
        .filter(|f| f.is_context.as_str() == Some(ctx_name))
        .collect();

    let type_imports = dedupe_preserving_order(
        ctx_fns.iter().map(|f| f.output_type.clone()),
    );

    let data_fields: Vec<ContextDataField> = ctx_fns
        .iter()
        .map(|f| ContextDataField { name: &f.name, output_type: &f.output_type })
        .collect();

    let params: Vec<ContextParamField> = ctx_meta.params.iter()
        .map(|(name, meta)| ContextParamField {
            name,
            ts_type: json_ty_to_ts(&meta.ty),
            required: meta.required,
        })
        .collect();

    let template = ContextTemplate {
        pascal: &pascal,
        ctx_name,
        type_imports,
        data_fields,
        has_params: !ctx_meta.params.is_empty(),
        params,
    };
    template.render().expect("context template renders")
}


fn json_ty_to_ts(json_ty: &str) -> &'static str {
    match json_ty {
        "integer" | "number" => "number",
        "boolean" => "boolean",
        _ => "string",
    }
}


// ─── Per-function (call) file — same shape for mutations + plain ──────────


fn emit_call_file(fn_meta: &MizanFunction) -> String {
    let pascal = pascal_case(&fn_meta.camel_name);

    let mut imports: Vec<String> = Vec::new();
    if fn_meta.has_input {
        if let Some(t) = &fn_meta.input_type { imports.push(t.clone()); }
    }
    imports.push(fn_meta.output_type.clone());
    let type_imports = dedupe_preserving_order(imports);

    let template = CallTemplate {
        pascal: &pascal,
        name: &fn_meta.name,
        has_input: fn_meta.has_input,
        input_type: fn_meta.input_type.as_deref().unwrap_or(""),
        output_type: &fn_meta.output_type,
        type_imports,
    };
    template.render().expect("call template renders")
}


// ─── Stage 1 index ─────────────────────────────────────────────────────────


fn emit_stage1_index(ir: &MizanIR, config: &Config) -> String {
    let contexts: Vec<IndexContext> = ir.contexts.keys()
        .map(|ctx_name| IndexContext { pascal: pascal_case(ctx_name), name: ctx_name })
        .collect();

    let calls: Vec<IndexCall> = regular_functions(&ir.functions)
        .map(|fn_meta| IndexCall {
            pascal: pascal_case(&fn_meta.camel_name),
            camel_name: &fn_meta.camel_name,
            dir: if fn_meta.affects.is_empty() { "functions" } else { "mutations" },
        })
        .collect();

    // Stage 2 single-file frontend adapters get re-exported from index.ts so
    // consumers can `import { MizanContext, useEcho } from './api'`.
    let framework_adapters: Vec<&'static str> = ["react", "vue", "svelte"].iter()
        .copied()
        .filter(|t| config.targets.iter().any(|cfg_t| cfg_t == t))
        .collect();

    IndexTemplate { contexts, calls, framework_adapters }
        .render().expect("index template renders")
}


// ─── types.ts ──────────────────────────────────────────────────────────────


fn emit_types(schemas: &IndexMap<String, JsonSchema>) -> String {
    let mut out = String::new();
    out.push_str("// AUTO-GENERATED by mizan — do not edit\n\n");
    for (raw_name, schema) in schemas {
        out.push_str(&emit_schema_decl(raw_name, schema));
        out.push('\n');
    }
    out
}


fn emit_schema_decl(name: &str, schema: &JsonSchema) -> String {
    // String enum → union of string literals.
    if let Some(values) = &schema.r#enum {
        if schema.ty.as_deref() == Some("string") {
            let union = values
                .iter()
                .filter_map(|v| v.as_str())
                .map(|s| format!("\"{s}\""))
                .collect::<Vec<_>>()
                .join(" | ");
            return format!("export type {name} = {union}\n");
        }
    }

    // Top-level array → array alias.
    if schema.ty.as_deref() == Some("array") {
        let elem = ts_type_expression(schema.items.as_deref().unwrap_or(&JsonSchema::default()));
        return format!("export type {name} = {elem}[]\n");
    }

    // Object with properties → interface declaration.
    if schema.ty.as_deref() == Some("object") {
        if let Some(props) = &schema.properties {
            return emit_interface(name, schema, props);
        }
    }

    // Fallback — alias to a structural expression.
    let expr = ts_type_expression(schema);
    format!("export type {name} = {expr}\n")
}


fn emit_interface(
    name: &str,
    schema: &JsonSchema,
    properties: &IndexMap<String, JsonSchema>,
) -> String {
    let required: std::collections::HashSet<&str> =
        schema.required.iter().map(String::as_str).collect();

    let fields = properties
        .iter()
        .map(|(field_name, field_schema)| {
            // Fields are non-optional if they're explicitly required OR
            // if they carry a default value (server always populates).
            let is_required = required.contains(field_name.as_str())
                || field_schema.default.is_some();
            let opt = if is_required { "" } else { "?" };
            let ty = ts_type_expression(field_schema);
            format!("    {field_name}{opt}: {ty}")
        })
        .collect::<Vec<_>>()
        .join("\n");

    if fields.is_empty() {
        format!("export interface {name} {{}}\n")
    } else {
        format!("export interface {name} {{\n{fields}\n}}\n")
    }
}


fn ts_type_expression(schema: &JsonSchema) -> String {
    // `$ref` → bare type name reference into components.schemas.
    if let Some(ref_name) = schema.ref_name() {
        return ref_name.to_string();
    }

    // `anyOf` with a null variant → `T | null`.
    if let Some(any_of) = &schema.any_of {
        let has_null = any_of.iter().any(|s| s.ty.as_deref() == Some("null"));
        let non_null: Vec<&JsonSchema> = any_of
            .iter()
            .filter(|s| s.ty.as_deref() != Some("null"))
            .collect();
        if has_null && non_null.len() == 1 {
            return format!("{} | null", ts_type_expression(non_null[0]));
        }
        let union = any_of
            .iter()
            .map(ts_type_expression)
            .collect::<Vec<_>>()
            .join(" | ");
        return union;
    }

    if let Some(values) = &schema.r#enum {
        if schema.ty.as_deref() == Some("string") {
            return values
                .iter()
                .filter_map(|v| v.as_str())
                .map(|s| format!("\"{s}\""))
                .collect::<Vec<_>>()
                .join(" | ");
        }
    }

    let base = match schema.ty.as_deref() {
        Some("integer") | Some("number") => "number".to_string(),
        Some("boolean") => "boolean".to_string(),
        Some("string") => "string".to_string(),
        Some("array") => {
            let elem = ts_type_expression(schema.items.as_deref().unwrap_or(&JsonSchema::default()));
            format!("{elem}[]")
        }
        Some("object") => "Record<string, unknown>".to_string(),
        Some("null") => "null".to_string(),
        _ => "unknown".to_string(),
    };

    if schema.nullable {
        format!("{base} | null")
    } else {
        base
    }
}
