//! Rust target — emits a complete Cargo crate consuming the
//! `mizan-rust` kernel. Output shape lives at `templates/rust/*.j2`.

use std::path::PathBuf;

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

use crate::config::{Config, RustKernelSpec};
use crate::emit::CodegenTarget;
use crate::emit::EmittedFile;
use crate::emit::casing::{pascal_case, rust_ident, rust_type_ident, snake_case};
use crate::ir::{
    IsContext, MizanContext, MizanFunction, MizanIR, NamedType, Primitive,
    StructField as IrStructField, TypeShape,
};


pub struct RustCrate;


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

    fn emit(&self, ir: &MizanIR, config: &Config) -> Vec<EmittedFile> {
        let crate_name = config
            .rust_crate_name
            .clone()
            .unwrap_or_else(|| "mizan_client".to_string());

        let kernel_dep = format_kernel_dep(config.rust_kernel.as_ref());

        let mut out: Vec<EmittedFile> = Vec::new();

        out.push(EmittedFile::new(
            "Cargo.toml",
            CargoTemplate { crate_name: &crate_name, kernel_dep: &kernel_dep }
                .render().expect("Cargo.toml renders"),
        ));

        out.push(EmittedFile::new("src/types.rs", emit_types_rs(&ir.types)));

        let mut context_modules: Vec<String> = Vec::new();
        for (ctx_name, ctx_meta) in &ir.contexts {
            let module_name = snake_case(ctx_name);
            out.push(EmittedFile::new(
                PathBuf::from("src/contexts").join(format!("{module_name}.rs")),
                emit_context_file(ctx_name, ctx_meta, &ir.functions),
            ));
            context_modules.push(module_name);
        }
        if !context_modules.is_empty() {
            out.push(EmittedFile::new("src/contexts/mod.rs", emit_mod_file(&context_modules)));
        }

        let mut mutation_modules: Vec<String> = Vec::new();
        let mut function_modules: Vec<String> = Vec::new();
        for fn_meta in &ir.functions {
            if !matches!(fn_meta.is_context, IsContext::No) || fn_meta.is_form { continue; }
            let is_mutation = !fn_meta.affects.is_empty();
            let kind = if is_mutation { "mutations" } else { "functions" };
            let module_name = snake_case(&fn_meta.camel_name);
            out.push(EmittedFile::new(
                PathBuf::from(format!("src/{kind}")).join(format!("{module_name}.rs")),
                emit_call_file(fn_meta),
            ));
            if is_mutation {
                mutation_modules.push(module_name);
            } else {
                function_modules.push(module_name);
            }
        }
        if !mutation_modules.is_empty() {
            out.push(EmittedFile::new("src/mutations/mod.rs", emit_mod_file(&mutation_modules)));
        }
        if !function_modules.is_empty() {
            out.push(EmittedFile::new("src/functions/mod.rs", emit_mod_file(&function_modules)));
        }

        out.push(EmittedFile::new(
            "src/lib.rs",
            LibTemplate {
                has_contexts: !context_modules.is_empty(),
                has_mutations: !mutation_modules.is_empty(),
                has_functions: !function_modules.is_empty(),
            }.render().expect("lib.rs renders"),
        ));

        out
    }
}


#[derive(Template)]
#[template(path = "rust/Cargo.toml.j2", escape = "none")]
struct CargoTemplate<'a> {
    crate_name: &'a str,
    kernel_dep: &'a str,
}


#[derive(Template)]
#[template(path = "rust/lib.rs.j2", escape = "none")]
struct LibTemplate {
    has_contexts: bool,
    has_mutations: bool,
    has_functions: bool,
}


#[derive(Template)]
#[template(path = "rust/mod.rs.j2", escape = "none")]
struct ModTemplate {
    modules: Vec<String>,
}


#[derive(Template)]
#[template(path = "rust/context.rs.j2", escape = "none")]
struct ContextTemplate<'a> {
    pascal: String,
    snake: String,
    ctx_name: &'a str,
    type_imports: Vec<String>,
    data_fields: Vec<RustField>,
    params: Vec<RustField>,
}


#[derive(Template)]
#[template(path = "rust/call.rs.j2", escape = "none")]
struct CallTemplate<'a> {
    snake: String,
    name: &'a str,
    return_type: String,
    type_imports: Vec<String>,
    input_param: String,
    args_value: &'static str,
}


#[derive(Template)]
#[template(path = "rust/types.rs.j2", escape = "none")]
struct TypesTemplate {
    schemas_block: String,
    hoisted_enums_block: String,
}


/// Renderer-side view of a single Rust struct field. Distinct from
/// `ir::StructField` (the IR shape) because the renderer carries
/// already-rendered identifiers and rename flags.
struct RustField {
    raw_name: String,
    ident: String,
    ty: String,
    has_rename: bool,
}


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()
}


// ─── Cargo.toml ────────────────────────────────────────────────────────────


fn format_kernel_dep(spec: Option<&RustKernelSpec>) -> String {
    match spec {
        Some(RustKernelSpec::Path { path }) => format!("{{ path = {} }}", json_str(path)),
        Some(RustKernelSpec::Git { git, tag, rev, branch }) => {
            let mut parts = vec![format!("git = {}", json_str(git))];
            if let Some(t) = tag { parts.push(format!("tag = {}", json_str(t))); }
            if let Some(r) = rev { parts.push(format!("rev = {}", json_str(r))); }
            if let Some(b) = branch { parts.push(format!("branch = {}", json_str(b))); }
            format!("{{ {} }}", parts.join(", "))
        }
        Some(RustKernelSpec::Version { version }) => format!("{{ version = {} }}", json_str(version)),
        None => "{ version = \"0.1\" }".to_string(),
    }
}


fn json_str(s: &str) -> String {
    serde_json::to_string(s).expect("string literal serializes")
}


// ─── mod.rs ────────────────────────────────────────────────────────────────


fn emit_mod_file(module_names: &[String]) -> String {
    let mut sorted = module_names.to_vec();
    sorted.sort();
    ModTemplate { modules: sorted }.render().expect("mod.rs renders")
}


// ─── Context file ──────────────────────────────────────────────────────────


fn emit_context_file(
    ctx_name: &str,
    ctx_meta: &MizanContext,
    all_functions: &[MizanFunction],
) -> String {
    let pascal = pascal_case(ctx_name);
    let snake = snake_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| rust_type_ident(&f.output_type)),
    );

    let data_fields: Vec<RustField> = ctx_fns.iter()
        .map(|f| {
            let ident = rust_ident(&f.name);
            RustField {
                has_rename: ident != f.name,
                raw_name: f.name.clone(),
                ident,
                ty: rust_type_ident(&f.output_type),
            }
        })
        .collect();

    let params: Vec<RustField> = ctx_meta.params.iter()
        .map(|(p_name, p_meta)| {
            let ident = rust_ident(p_name);
            let base = param_rust_type(p_meta.ty);
            let ty = if p_meta.required { base.to_string() } else { format!("Option<{base}>") };
            RustField {
                has_rename: ident != *p_name,
                raw_name: p_name.clone(),
                ident,
                ty,
            }
        })
        .collect();

    ContextTemplate {
        pascal,
        snake,
        ctx_name,
        type_imports,
        data_fields,
        params,
    }.render().expect("context.rs renders")
}


fn param_rust_type(p: Primitive) -> &'static str {
    match p {
        Primitive::Integer => "i64",
        Primitive::Number => "f64",
        Primitive::Boolean => "bool",
        Primitive::String => "String",
    }
}


// ─── Call file ─────────────────────────────────────────────────────────────


fn emit_call_file(fn_meta: &MizanFunction) -> String {
    let output_type = rust_type_ident(&fn_meta.output_type);
    let return_type = if fn_meta.output_nullable {
        format!("Option<{output_type}>")
    } else {
        output_type.clone()
    };

    let input_type = fn_meta.input_type.as_deref().map(rust_type_ident);

    let mut used_seed: Vec<String> = vec![output_type.clone()];
    if let Some(t) = &input_type { used_seed.push(t.clone()); }
    let type_imports = dedupe_preserving_order(used_seed);

    let (input_param, args_value) = if fn_meta.has_input {
        let it = input_type.as_deref().unwrap_or("");
        (
            format!(", args: &{it}"),
            "serde_json::to_value(args).unwrap_or(Value::Object(Default::default()))",
        )
    } else {
        (
            String::new(),
            "Value::Object(Default::default())",
        )
    };

    CallTemplate {
        snake: snake_case(&fn_meta.name),
        name: &fn_meta.name,
        return_type,
        type_imports,
        input_param,
        args_value,
    }.render().expect("call.rs renders")
}


// ─── types.rs ──────────────────────────────────────────────────────────────


/// Per-types-file context tracking enum names hoisted out of inline
/// `field { enum "a" "b" }` declarations into Rust top-level enum types.
struct EnumCtx {
    hoisted: Vec<(String, Vec<String>)>,
    enum_name: Option<String>,
}


fn emit_types_rs(types: &IndexMap<String, NamedType>) -> String {
    let mut ctx = EnumCtx { hoisted: Vec::new(), enum_name: None };

    let schemas_block = types.iter()
        .map(|(raw_name, ty)| {
            let name = rust_type_ident(raw_name);
            match ty {
                NamedType::Struct(fields) => emit_struct_decl(&name, fields, &mut ctx),
                NamedType::List(inner) => emit_transparent_array(&name, inner, &mut ctx),
                NamedType::Enum(variants) => emit_string_enum(&name, variants),
                NamedType::Alias(inner) => emit_type_alias(&name, inner, &mut ctx),
            }
        })
        .collect::<Vec<_>>()
        .join("\n");

    let hoisted_enums_block = ctx.hoisted.iter()
        .map(|(n, v)| emit_string_enum(n, v))
        .collect::<Vec<_>>()
        .join("\n");

    TypesTemplate { schemas_block, hoisted_enums_block }
        .render().expect("types.rs renders")
}


fn emit_string_enum(name: &str, variants: &[String]) -> String {
    let body = variants.iter()
        .map(|v| {
            let ident = pascal_case(v);
            let rename = if ident == *v {
                String::new()
            } else {
                format!("    #[serde(rename = {})]\n", json_str(v))
            };
            format!("{rename}    {ident},")
        })
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        "#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\npub enum {name} {{\n{body}\n}}\n",
        name = rust_type_ident(name),
    )
}


fn emit_transparent_array(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
    ctx.enum_name = None;
    let inner_ty = rust_type_from_shape(inner, ctx);
    format!(
        "#[derive(Debug, Clone, Serialize, Deserialize)]\n#[serde(transparent)]\npub struct {name}(pub Vec<{inner_ty}>);\n",
    )
}


fn emit_struct_decl(
    name: &str,
    fields: &[IrStructField],
    ctx: &mut EnumCtx,
) -> String {
    let fields_body = fields.iter()
        .map(|f| {
            let field_name = rust_ident(&f.name);
            ctx.enum_name = Some(format!("{name}_{}", pascal_case(&f.name)));
            let mut ty = rust_type_from_shape(&f.shape, ctx);
            let is_required = f.required || f.default.is_some();
            if !is_required && !ty.starts_with("Option<") {
                ty = format!("Option<{ty}>");
            }
            let rename = if field_name == f.name {
                String::new()
            } else {
                format!("    #[serde(rename = \"{raw}\")]\n", raw = f.name)
            };
            format!("{rename}    pub {field_name}: {ty},")
        })
        .collect::<Vec<_>>()
        .join("\n");

    format!(
        "#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct {name} {{\n{fields_body}\n}}\n",
    )
}


fn emit_type_alias(name: &str, inner: &TypeShape, ctx: &mut EnumCtx) -> String {
    ctx.enum_name = Some(name.to_string());
    let ty = rust_type_from_shape(inner, ctx);
    format!("pub type {name} = {ty};\n")
}


fn rust_type_from_shape(shape: &TypeShape, ctx: &mut EnumCtx) -> String {
    match shape {
        TypeShape::Ref(name) => rust_type_ident(name),
        TypeShape::Primitive(Primitive::Integer) => "i64".to_string(),
        TypeShape::Primitive(Primitive::Number) => "f64".to_string(),
        TypeShape::Primitive(Primitive::Boolean) => "bool".to_string(),
        TypeShape::Primitive(Primitive::String) => "String".to_string(),
        TypeShape::List(inner) => {
            ctx.enum_name = None;
            format!("Vec<{}>", rust_type_from_shape(inner, ctx))
        }
        TypeShape::Optional(inner) => {
            ctx.enum_name = None;
            format!("Option<{}>", rust_type_from_shape(inner, ctx))
        }
        TypeShape::Enum(variants) => {
            // Inline enums hoist out into top-level Rust enum types so the
            // generated struct field can reference them by name.
            let enum_name = ctx
                .enum_name
                .clone()
                .unwrap_or_else(|| "Enum_inline".to_string());
            ctx.hoisted.push((enum_name.clone(), variants.clone()));
            enum_name
        }
        TypeShape::Union(_branches) => {
            // Rust serde doesn't have a clean way to deserialize an untagged
            // multi-arm union without losing type info; fall back to a JSON
            // Value so the consumer can match on the runtime variant.
            "serde_json::Value".to_string()
        }
    }
}
