//! Python target — emits a Pydantic-typed client wrapping the PyO3
//! extension exposed by `mizan-rust`.
//!
//! Output shape lives at `templates/python/*.j2`. Per-method bodies are
//! pre-rendered in Rust before passing into `client.py.j2` so the template
//! only owns top-level section layout, not Python method-signature details.

use std::path::PathBuf;

use askama::Template;

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


pub struct PythonClient;


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

    fn emit(&self, ir: &MizanIR, _config: &Config) -> Vec<EmittedFile> {
        let schemas_block = ir.types.iter()
            .map(|(name, ty)| emit_schema_block(name, ty))
            .collect::<Vec<_>>()
            .join("\n\n");

        let types_py = TypesTemplate { schemas_block }.render().expect("types.py renders");
        let client_py = build_client_template(ir).render().expect("client.py renders");
        let init_py = InitTemplate {}.render().expect("__init__.py renders");

        vec![
            EmittedFile::new(PathBuf::from("types.py"), types_py),
            EmittedFile::new(PathBuf::from("client.py"), client_py),
            EmittedFile::new(PathBuf::from("__init__.py"), init_py),
        ]
    }
}


#[derive(Template)]
#[template(path = "python/__init__.py.j2", escape = "none")]
struct InitTemplate {}


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


#[derive(Template)]
#[template(path = "python/client.py.j2", escape = "none")]
struct ClientTemplate {
    ctx_methods_block: String,
    call_methods_block: String,
    data_classes_block: String,
}


// ─── types.py schema bodies ────────────────────────────────────────────────


fn emit_schema_block(raw_name: &str, ty: &NamedType) -> String {
    let name = pascal_case(raw_name);
    match ty {
        NamedType::Struct(fields) => emit_pydantic_class(&name, fields),
        NamedType::List(inner) => format!("{name} = list[{}]", py_type_expression(inner)),
        NamedType::Enum(variants) => {
            let literal = variants.iter().map(|v| format!("\"{v}\"")).collect::<Vec<_>>().join(", ");
            format!("{name} = Literal[{literal}]")
        }
        NamedType::Alias(inner) => format!("{name} = {}", py_type_expression(inner)),
    }
}


fn emit_pydantic_class(name: &str, fields: &[StructField]) -> String {
    if fields.is_empty() {
        return format!("class {name}(BaseModel):\n    pass");
    }
    let field_lines = fields.iter()
        .map(|f| {
            let mut ty = py_type_expression(&f.shape);
            let is_required = f.required || f.default.is_some();
            if !is_required {
                if !ty.ends_with(" | None") {
                    ty = format!("{ty} | None");
                }
                format!("    {}: {ty} = None", rust_ident(&f.name))
            } else {
                format!("    {}: {ty}", rust_ident(&f.name))
            }
        })
        .collect::<Vec<_>>()
        .join("\n");
    format!("class {name}(BaseModel):\n{field_lines}")
}


fn py_type_expression(shape: &TypeShape) -> String {
    match shape {
        TypeShape::Ref(name) => pascal_case(name),
        TypeShape::Primitive(p) => primitive_to_py(*p).to_string(),
        TypeShape::List(inner) => format!("list[{}]", py_type_expression(inner)),
        TypeShape::Optional(inner) => format!("{} | None", py_type_expression(inner)),
        TypeShape::Enum(variants) => {
            let parts = variants.iter().map(|v| format!("\"{v}\"")).collect::<Vec<_>>().join(", ");
            format!("Literal[{parts}]")
        }
        TypeShape::Union(branches) => branches.iter()
            .map(py_type_expression)
            .collect::<Vec<_>>()
            .join(" | "),
    }
}


fn primitive_to_py(p: Primitive) -> &'static str {
    match p {
        Primitive::Integer => "int",
        Primitive::Number => "float",
        Primitive::Boolean => "bool",
        Primitive::String => "str",
    }
}


// ─── client.py method blocks ───────────────────────────────────────────────


fn build_client_template(ir: &MizanIR) -> ClientTemplate {
    let ctx_methods_block = ir.contexts.iter()
        .map(|(ctx_name, ctx_meta)| {
            let fetch = emit_fetch_method(ctx_name, ctx_meta);
            let subscribe = emit_subscribe_method(ctx_name, ctx_meta);
            format!("{fetch}{subscribe}")
        })
        .collect::<Vec<_>>()
        .join("\n");

    let call_methods_block = ir.functions.iter()
        .filter(|f| matches!(f.is_context, IsContext::No) && !f.is_form)
        .map(emit_call_method)
        .collect::<Vec<_>>()
        .join("\n");

    let data_classes_block = ir.contexts.iter()
        .map(|(ctx_name, _)| {
            let ctx_fns: Vec<&MizanFunction> = ir.functions.iter()
                .filter(|f| f.is_context.as_str() == Some(ctx_name))
                .collect();
            emit_context_data_class(ctx_name, &ctx_fns)
        })
        .collect::<Vec<_>>()
        .join("\n");

    ClientTemplate { ctx_methods_block, call_methods_block, data_classes_block }
}


fn py_arg_type(p: Primitive) -> &'static str {
    match p {
        Primitive::Integer => "int",
        Primitive::Number => "float",
        Primitive::Boolean => "bool",
        Primitive::String => "str",
    }
}


fn emit_fetch_method(ctx_name: &str, ctx_meta: &MizanContext) -> String {
    let method_name = format!("fetch_{}_context", snake_case(ctx_name));
    let param_args = ctx_meta.params.iter()
        .map(|(n, m)| {
            let ident = rust_ident(n);
            let ty = py_arg_type(m.ty);
            if m.required { format!("{ident}: {ty}") }
            else { format!("{ident}: {ty} | None = None") }
        })
        .collect::<Vec<_>>()
        .join(", ");
    let param_dict = if ctx_meta.params.is_empty() {
        "{}".to_string()
    } else {
        let pairs = ctx_meta.params.iter()
            .map(|(n, _)| format!("\"{n}\": {}", rust_ident(n)))
            .collect::<Vec<_>>()
            .join(", ");
        format!("{{{pairs}}}")
    };
    let data_class = format!("{}ContextData", pascal_case(ctx_name));
    let arg_sig = if param_args.is_empty() { String::new() } else { format!(", {param_args}") };

    format!(
        "    def {method_name}(self{arg_sig}) -> \"{data_class}\":\n        raw = self._inner.fetch_context(\"{ctx_name}\", {param_dict})\n        return {data_class}(**raw)\n",
    )
}


fn emit_subscribe_method(ctx_name: &str, ctx_meta: &MizanContext) -> String {
    let param_args = ctx_meta.params.iter()
        .map(|(n, m)| {
            let ident = rust_ident(n);
            let ty = py_arg_type(m.ty);
            if m.required { format!("{ident}: {ty}") }
            else { format!("{ident}: {ty} | None = None") }
        })
        .collect::<Vec<_>>()
        .join(", ");
    let param_dict = if ctx_meta.params.is_empty() {
        "{}".to_string()
    } else {
        let pairs = ctx_meta.params.iter()
            .map(|(n, _)| format!("\"{n}\": {}", rust_ident(n)))
            .collect::<Vec<_>>()
            .join(", ");
        format!("{{{pairs}}}")
    };
    let arg_sig = if param_args.is_empty() { String::new() } else { format!(", {param_args}") };
    let snake = snake_case(ctx_name);
    let indent_39 = " ".repeat(39);

    format!(
        "    def subscribe_{snake}_context(self{arg_sig},\n{indent_39}callback: Callable[[dict[str, Any]], None]) -> PyContextSubscription:\n        return self._inner.subscribe_context(\"{ctx_name}\", {param_dict}, callback)\n",
    )
}


fn emit_call_method(fn_meta: &MizanFunction) -> String {
    let method_name = format!("call_{}", snake_case(&fn_meta.name));
    let pascal_output = pascal_case(&fn_meta.output_type);

    let input_arg = if fn_meta.has_input {
        let it = fn_meta.input_type.as_deref().unwrap_or("");
        format!(", args: {}", pascal_case(it))
    } else {
        String::new()
    };
    let args_expr = if fn_meta.has_input { "args.model_dump()" } else { "{}" };
    let return_type = if fn_meta.output_nullable {
        format!("{pascal_output} | None")
    } else {
        pascal_output.clone()
    };
    let decode_expr = if fn_meta.output_nullable {
        format!("{pascal_output}(**raw) if raw is not None else None")
    } else {
        format!("{pascal_output}(**raw)")
    };

    format!(
        "    def {method_name}(self{input_arg}) -> {return_type}:\n        raw = self._inner.call(\"{wire}\", {args_expr})\n        return {decode_expr}\n",
        wire = fn_meta.name,
    )
}


fn emit_context_data_class(ctx_name: &str, ctx_fns: &[&MizanFunction]) -> String {
    let class_name = format!("{}ContextData", pascal_case(ctx_name));
    let field_lines = ctx_fns.iter()
        .map(|fn_meta| {
            let pascal_out = pascal_case(&fn_meta.output_type);
            let ty = if fn_meta.output_nullable { format!("{pascal_out} | None") } else { pascal_out };
            format!("    {}: {ty}", rust_ident(&fn_meta.name))
        })
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        "class {class_name}(BaseModel):\n    \"\"\"Bundled return of fetch_{snake}_context.\"\"\"\n{field_lines}\n",
        snake = snake_case(ctx_name),
    )
}
