//! 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, MizanContext, MizanFunction, MizanIR, NamedType, Primitive, StructField, TypeShape,
};
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.types)));

        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: primitive_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 primitive_to_ts(p: Primitive) -> &'static str {
    match p {
        Primitive::Integer | Primitive::Number => "number",
        Primitive::Boolean => "boolean",
        Primitive::String => "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(types: &IndexMap<String, NamedType>) -> String {
    let mut out = String::new();
    out.push_str("// AUTO-GENERATED by mizan — do not edit\n\n");
    for (name, ty) in types {
        out.push_str(&emit_named_type(name, ty));
        out.push('\n');
    }
    out
}


fn emit_named_type(name: &str, ty: &NamedType) -> String {
    match ty {
        NamedType::Struct(fields) => emit_interface(name, fields),
        NamedType::List(inner) => format!("export type {name} = {}[]\n", ts_type_expression(inner)),
        NamedType::Enum(variants) => {
            let union = variants.iter().map(|v| format!("\"{v}\"")).collect::<Vec<_>>().join(" | ");
            format!("export type {name} = {union}\n")
        }
        NamedType::Alias(inner) => format!("export type {name} = {}\n", ts_type_expression(inner)),
    }
}


fn emit_interface(name: &str, fields: &[StructField]) -> String {
    if fields.is_empty() {
        return format!("export interface {name} {{}}\n");
    }
    let body = fields.iter()
        .map(|f| {
            // Field is non-optional if required OR has a default (server always populates).
            let is_required = f.required || f.default.is_some();
            let opt = if is_required { "" } else { "?" };
            format!("    {}{opt}: {}", f.name, ts_type_expression(&f.shape))
        })
        .collect::<Vec<_>>()
        .join("\n");
    format!("export interface {name} {{\n{body}\n}}\n")
}


fn ts_type_expression(shape: &TypeShape) -> String {
    match shape {
        TypeShape::Ref(name) => name.clone(),
        TypeShape::Primitive(p) => primitive_to_ts(*p).to_string(),
        TypeShape::List(inner) => format!("{}[]", ts_type_expression(inner)),
        TypeShape::Optional(inner) => format!("{} | null", ts_type_expression(inner)),
        TypeShape::Enum(variants) => variants.iter()
            .map(|v| format!("\"{v}\""))
            .collect::<Vec<_>>()
            .join(" | "),
        TypeShape::Union(branches) => branches.iter()
            .map(ts_type_expression)
            .collect::<Vec<_>>()
            .join(" | "),
    }
}
