//! Service-API codegen for the Zig backend.
//!
//! Generates Zig glue that wraps the C FFI contract emitted by the FFI backend.
//!
//! **`service.zig`** — Zig service wrapper with:
//! - A service struct with constructor and registration methods.
//! - Extern function declarations matching the C FFI contract.
//! - A host-side handler wrapper marshaling JSON between Zig and C.
//! - Run and finalize methods that call the C entrypoints.
//!
//! Ownership: The Zig service owns a pointer to the C opaque handle.
//! The C side maintains lifetime semantics; Zig calls free() on cleanup.
//! Error handling: JSON serialization/deserialization errors are propagated.

use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{
    ApiSurface, EntrypointKind, HandlerContractDef, RegistrationDef, RegistrationVariant, ServiceDef, TypeRef,
};
use heck::ToSnakeCase;
use std::path::PathBuf;

// ───────────────────────────────────────────────────────────────── helpers ──

/// Find the `HandlerContractDef` by trait name in the surface.
fn find_contract<'a>(api: &'a ApiSurface, trait_name: &str) -> Option<&'a HandlerContractDef> {
    api.handler_contracts.iter().find(|c| c.trait_name == trait_name)
}

/// Whether an entrypoint's return type can be represented over the C ABI as a function return.
/// Unit/primitive/string/bytes map to a status code or scalar; a `Named` type is representable only
/// when this surface wraps it (so it can cross as a `*{TypeName}` opaque). Anything else is not representable.
fn entrypoint_return_representable(ep: &crate::core::ir::EntrypointDef, api: &ApiSurface) -> bool {
    match &ep.return_type {
        TypeRef::Unit | TypeRef::String | TypeRef::Char | TypeRef::Primitive(_) | TypeRef::Bytes => true,
        TypeRef::Named(n) => api.types.iter().any(|t| t.name == *n),
        _ => false,
    }
}

/// Map a `TypeRef` to a Zig type string.
fn typeref_to_zig_type(ty: &TypeRef) -> String {
    match ty {
        TypeRef::String | TypeRef::Char => "[*:0]const u8".to_owned(),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "bool".to_owned(),
                PrimitiveType::U8 => "u8".to_owned(),
                PrimitiveType::U16 => "u16".to_owned(),
                PrimitiveType::U32 => "u32".to_owned(),
                PrimitiveType::U64 => "u64".to_owned(),
                PrimitiveType::I8 => "i8".to_owned(),
                PrimitiveType::I16 => "i16".to_owned(),
                PrimitiveType::I32 => "i32".to_owned(),
                PrimitiveType::I64 => "i64".to_owned(),
                PrimitiveType::F32 => "f32".to_owned(),
                PrimitiveType::F64 => "f64".to_owned(),
                PrimitiveType::Usize => "usize".to_owned(),
                PrimitiveType::Isize => "isize".to_owned(),
            }
        }
        TypeRef::Bytes => "[*:0]const u8".to_owned(),
        TypeRef::Unit => "void".to_owned(),
        TypeRef::Named(n) => n.clone(),
        TypeRef::Json => "[:0]const u8".to_owned(),
        TypeRef::Path => "[:0]const u8".to_owned(),
        TypeRef::Duration => "u64".to_owned(),
        TypeRef::Optional(_) => "?*anyopaque".to_owned(),
        TypeRef::Vec(_) => "[*]anyopaque".to_owned(),
        TypeRef::Map(_, _) => "*anyopaque".to_owned(),
    }
}

// ──────────────────────────────────────────────── Zig service struct ──

/// Generate the Zig service wrapper module (`service.zig`).
///
/// For each service this emits:
/// - Extern declarations for the C FFI symbols (`_new`, `_free`, `_register_*`, `_ep_*`).
/// - A Zig `Service` struct with methods that call into C.
/// - Inline function wrappers for registration and run operations.
fn gen_service_zig(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
    let prefix = config.ffi_prefix();
    let prefix_lower = prefix.to_lowercase();
    let mut out = String::new();

    out.push_str("// Generated by alef. Do not edit by hand.\n\n");
    out.push_str("const std = @import(\"std\");\n");
    out.push_str("const c = @cImport(@cInclude(\"");
    out.push_str(&config.ffi_header_name());
    out.push_str("\"));\n\n");

    // The C FFI symbols are provided by the `@cImport`ed header (as `c.<symbol>`); no manual
    // `extern` redeclarations are needed (and they would duplicate / mistype the header's contract).

    // Emit Zig service structs and methods
    for service in &api.services {
        gen_service_struct(&mut out, service, api, &prefix, &prefix_lower);
    }

    out
}

/// Emit a Zig service struct and methods for one service.
fn gen_service_struct(out: &mut String, service: &ServiceDef, api: &ApiSurface, _prefix: &str, prefix_lower: &str) {
    let service_name = &service.name;
    let service_snake = service_name.to_snake_case();

    // Service struct wrapping the C opaque pointer. The owner is stored as an optional `*anyopaque`
    // (mirroring how this backend wraps every other opaque handle); the concrete C opaque type is
    // reached only through `@ptrCast` at each `c.<symbol>` call, so its cbindgen name is never named.
    out.push_str(&format!(
        "/// Zig wrapper for the {0} service.\n\
         /// Owns an opaque C pointer to the service instance.\n\
         pub const {0} = struct {{\n    \
             owner: ?*anyopaque,\n\n",
        service_name
    ));

    // Constructor
    out.push_str(&format!(
        "    /// Create a new {0} instance.\n    \
         pub fn init() {0} {{\n        \
             return .{{\n            \
                 .owner = @ptrCast(c.{1}_{2}_new()),\n        \
             }};\n    \
         }}\n\n",
        service_name, prefix_lower, service_snake
    ));

    // Destructor
    out.push_str(&format!(
        "    /// Free the {0} instance.\n    \
         pub fn deinit(self: *{0}) void {{\n        \
             if (self.owner) |owner_ptr| {{\n            \
                 c.{1}_{2}_free(@ptrCast(owner_ptr));\n            \
                 self.owner = null;\n        \
             }}\n    \
         }}\n\n",
        service_name, prefix_lower, service_snake
    ));

    // Registration methods
    for reg in &service.registrations {
        gen_registration_method(out, service, reg, api, prefix_lower);
    }

    // Entrypoint methods
    for ep in &service.entrypoints {
        gen_entrypoint_method(out, service, ep, api, prefix_lower);
    }

    out.push_str("};\n\n");
}

/// Emit a Zig registration method wrapper.
fn gen_registration_method(
    out: &mut String,
    service: &ServiceDef,
    reg: &RegistrationDef,
    api: &ApiSurface,
    prefix_lower: &str,
) {
    let service_snake = service.name.to_snake_case();
    let reg_method_snake = reg.method.to_snake_case();
    let service_name = &service.name;

    // Find the contract
    let _contract = find_contract(api, &reg.callback_contract).expect("contract not found");

    out.push_str(&format!(
        "    /// Register a handler for method '{0}'.\n    \
         pub fn {1}(\n        \
             self: *{2},\n        \
             callback: *const fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8,\n        \
             context: *anyopaque",
        reg.method, reg_method_snake, service_name
    ));

    // Metadata parameters (name-first Zig style)
    for meta_param in &reg.metadata_params {
        let zig_type = match &meta_param.ty {
            TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => format!("*{}", n),
            ty => typeref_to_zig_type(ty),
        };
        out.push_str(&format!(",\n        {}: {}", meta_param.name, zig_type));
    }

    out.push_str(") c_int {\n");
    out.push_str("        const owner_ptr = self.owner orelse return 1;\n\n");

    out.push_str(&format!(
        "        return c.{}_{}_register_{}(\n            \
             @ptrCast(owner_ptr),\n            \
             @ptrCast(callback),\n            \
             context",
        prefix_lower, service_snake, reg_method_snake
    ));

    // Metadata arguments: extract _handle from opaque Named params
    for meta_param in &reg.metadata_params {
        match &meta_param.ty {
            TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => {
                out.push_str(&format!(",\n            {0}._handle", meta_param.name));
            }
            _ => {
                out.push_str(&format!(",\n            {}", meta_param.name));
            }
        }
    }

    out.push_str("\n        );\n");
    out.push_str("    }\n\n");

    // Emit registration variants (shortcut methods)
    for variant in &reg.variants {
        gen_registration_variant_method(out, service, variant, reg, api, prefix_lower);
    }
}

/// Emit a Zig registration variant shortcut method.
fn gen_registration_variant_method(
    out: &mut String,
    service: &ServiceDef,
    variant: &RegistrationVariant,
    _reg: &RegistrationDef,
    api: &ApiSurface,
    prefix_lower: &str,
) {
    let service_name = &service.name;
    let service_snake = service.name.to_snake_case();
    let variant_name = &variant.name;

    out.push_str(&format!(
        "    /// {}.\n",
        variant
            .doc
            .as_ref()
            .unwrap_or(&format!("Register a handler for {}", variant_name))
    ));

    out.push_str(&format!(
        "    pub fn {0}(\n        \
             self: *{1},\n        \
             callback: *const fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8,\n        \
             context: *anyopaque",
        variant_name, service_name
    ));

    // Variant signature parameters
    for param in &variant.signature_params {
        let zig_type = match &param.ty {
            TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => format!("*{}", n),
            ty => typeref_to_zig_type(ty),
        };
        out.push_str(&format!(",\n        {}: {}", param.name, zig_type));
    }

    out.push_str(") c_int {\n");
    out.push_str("        const owner_ptr = self.owner orelse return 1;\n\n");

    out.push_str(&format!(
        "        return c.{}_{}_{}(\n            \
             @ptrCast(owner_ptr),\n            \
             @ptrCast(callback),\n            \
             context",
        prefix_lower, service_snake, variant_name
    ));

    // Variant arguments: extract _handle from opaque Named params
    for param in &variant.signature_params {
        match &param.ty {
            TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => {
                out.push_str(&format!(",\n            {0}._handle", param.name));
            }
            _ => {
                out.push_str(&format!(",\n            {}", param.name));
            }
        }
    }

    out.push_str("\n        );\n");
    out.push_str("    }\n\n");
}

/// Emit a Zig entrypoint method wrapper.
fn gen_entrypoint_method(
    out: &mut String,
    service: &ServiceDef,
    ep: &crate::core::ir::EntrypointDef,
    api: &ApiSurface,
    prefix_lower: &str,
) {
    // Skip finalize entrypoints whose return type is not representable over the C ABI
    if matches!(ep.kind, EntrypointKind::Finalize) && !entrypoint_return_representable(ep, api) {
        return;
    }

    let service_snake = service.name.to_snake_case();
    let ep_name_snake = ep.method.to_snake_case();
    let ep_method = &ep.method;
    let service_name = &service.name;

    // Mirror the C ABI: the ffi entrypoint glue returns `*mut T` (an opaque pointer) only when its
    // return type is an opaque this surface wraps, otherwise an `i32` status code. So the Zig method
    // returns `?*anyopaque` for the opaque case and `c_int` for the status case.
    let returns_opaque = matches!(&ep.return_type, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n));
    let return_type = if returns_opaque { "?*anyopaque" } else { "c_int" };

    out.push_str(&format!(
        "    /// Run the service entrypoint '{0}'.\n    \
         pub fn {1}(self: *{2}",
        ep_method, ep_name_snake, service_name
    ));

    // Entrypoint parameters: opaque Named types are accepted as `*anyopaque` handles.
    for ep_param in &ep.params {
        let zig_type = match &ep_param.ty {
            TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => "*anyopaque".to_owned(),
            ty => typeref_to_zig_type(ty),
        };
        out.push_str(&format!(", {}: {}", ep_param.name, zig_type));
    }

    out.push_str(&format!(") {return_type} {{\n"));

    if returns_opaque {
        out.push_str("        const owner_ptr = self.owner orelse return null;\n");
    } else {
        out.push_str("        const owner_ptr = self.owner orelse return 1;\n");
    }

    if returns_opaque {
        out.push_str(&format!(
            "        return @ptrCast(c.{}_{}_ep_{}(\n            \
                 @ptrCast(owner_ptr)",
            prefix_lower, service_snake, ep_name_snake
        ));
    } else {
        out.push_str(&format!(
            "        return c.{}_{}_ep_{}(\n            \
                 @ptrCast(owner_ptr)",
            prefix_lower, service_snake, ep_name_snake
        ));
    }

    // Entrypoint arguments: extract _handle from opaque Named params
    for ep_param in &ep.params {
        match &ep_param.ty {
            TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => {
                out.push_str(&format!(",\n            {0}._handle", ep_param.name));
            }
            _ => {
                out.push_str(&format!(",\n            {}", ep_param.name));
            }
        }
    }

    if returns_opaque {
        out.push_str("\n        ));\n");
    } else {
        out.push_str("\n        );\n");
    }
    out.push_str("    }\n\n");
}

// ──────────────────────────────────────────────────── public entry point ──

/// Generate all service-API files for the Zig backend.
///
/// Returns one `GeneratedFile` when services are present:
/// - `{output_dir}/service.zig`   — Zig service wrapper
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    if api.services.is_empty() {
        return Ok(vec![]);
    }

    let module_name = config.name.replace('-', "_");
    let zig_code = gen_service_zig(api, config);

    let dir = crate::core::config::resolve_output_dir(None, &config.name, "packages/zig/src");
    let path = PathBuf::from(dir).join(format!("{}_service.zig", module_name));

    Ok(vec![GeneratedFile {
        path,
        content: zig_code,
        generated_header: true,
    }])
}

// ───────────────────────────────────────────────────────────────────── tests ──

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::ir::{
        EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, RegistrationDef, ServiceDef, TypeRef,
    };

    /// Construct a minimal but realistic [`ApiSurface`] that exercises:
    /// - A service with a constructor, one registration (bound to a handler contract),
    ///   and Run entrypoint.
    /// - One [`HandlerContractDef`] with wire request/response DTO names.
    fn make_fixture_surface() -> ApiSurface {
        let constructor = MethodDef {
            name: "new".to_owned(),
            params: vec![],
            return_type: TypeRef::Unit,
            is_async: false,
            is_static: true,
            error_type: None,
            doc: "Create a new service owner.".to_owned(),
            receiver: None,
            sanitized: false,
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        };

        let registration = RegistrationDef {
            method: "add_handler".to_owned(),
            callback_param: "handler".to_owned(),
            callback_contract: "RequestHandler".to_owned(),
            metadata_params: vec![ParamDef {
                name: "path".to_owned(),
                ty: TypeRef::String,
                optional: false,
                default: None,
                ..ParamDef::default()
            }],
            receiver: Some(crate::core::ir::ReceiverKind::RefMut),
            return_type: TypeRef::Unit,
            error_type: Some("HandlerError".to_owned()),
            doc: "Register a request handler.".to_owned(),
            variants: vec![],
        };

        let run_entrypoint = EntrypointDef {
            method: "run".to_owned(),
            kind: EntrypointKind::Run,
            is_async: true,
            params: vec![ParamDef {
                name: "addr".to_owned(),
                ty: TypeRef::String,
                optional: false,
                default: None,
                ..ParamDef::default()
            }],
            return_type: TypeRef::Unit,
            error_type: Some("IoError".to_owned()),
            doc: "Start the service.".to_owned(),
        };

        let handler_contract = HandlerContractDef {
            trait_name: "RequestHandler".to_owned(),
            rust_path: "my_crate::RequestHandler".to_owned(),
            dispatch: MethodDef {
                name: "handle".to_owned(),
                params: vec![ParamDef {
                    name: "req".to_owned(),
                    ty: TypeRef::Named("RequestData".to_owned()),
                    optional: false,
                    default: None,
                    ..ParamDef::default()
                }],
                return_type: TypeRef::Named("Response".to_owned()),
                is_async: true,
                is_static: false,
                error_type: None,
                doc: "Handle a request.".to_owned(),
                receiver: Some(crate::core::ir::ReceiverKind::Ref),
                sanitized: false,
                trait_source: None,
                returns_ref: false,
                returns_cow: false,
                return_newtype_wrapper: None,
                has_default_impl: false,
                binding_excluded: false,
                binding_exclusion_reason: None,
            },
            optional_methods: vec![],
            wire_request_type: Some("RequestData".to_owned()),
            wire_response_type: Some("Response".to_owned()),
            dispatch_extra_params: vec![],
            wire_param_name: None,
            dispatch_return_type: None,
            response_adapter: None,
            doc: "Handler contract.".to_owned(),
        };

        ApiSurface {
            crate_name: "test_crate".to_owned(),
            version: "1.0.0".to_owned(),
            services: vec![ServiceDef {
                name: "TestService".to_owned(),
                rust_path: "my_crate::TestService".to_owned(),
                constructor,
                configurators: vec![],
                registrations: vec![registration],
                entrypoints: vec![run_entrypoint],
                doc: "Test service.".to_owned(),
                cfg: None,
            }],
            handler_contracts: vec![handler_contract],
            ..ApiSurface::default()
        }
    }

    #[test]
    fn test_gen_service_zig_produces_valid_zig() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let zig = gen_service_zig(&api, &config);

        // Verify that the generated Zig contains expected markers
        assert!(zig.contains("const std = @import(\"std\")"));
        assert!(zig.contains("const c = @cImport"));
        assert!(zig.contains("TestService"));
        assert!(zig.contains("pub fn init()"));
        assert!(
            zig.contains("pub fn deinit("),
            "Expected 'pub fn deinit(' but got:\n{}",
            zig
        );
    }

    #[test]
    fn test_service_struct_is_generated() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let zig = gen_service_zig(&api, &config);

        // The service struct must be declared with an optional opaque owner
        assert!(zig.contains("pub const TestService = struct"));
        assert!(zig.contains("owner: ?*anyopaque"));
    }

    #[test]
    fn test_c_symbols_are_called_via_cimport() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let zig = gen_service_zig(&api, &config);

        // No manual extern redeclarations — the C symbols come from the @cImport'd header as `c.<sym>`.
        assert!(!zig.contains("extern \"C\" fn"));
        assert!(zig.contains("c.test_crate_test_service_new()"));
        assert!(zig.contains("c.test_crate_test_service_free("));
        assert!(zig.contains("c.test_crate_test_service_register_add_handler("));

        // The callback parameter signature is still the FFI contract shape.
        assert!(zig.contains("fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8"));
    }

    #[test]
    fn test_registration_method_exists() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let zig = gen_service_zig(&api, &config);

        // Registration method should be present
        assert!(zig.contains("pub fn add_handler("));
        assert!(zig.contains("self: *TestService"));
        assert!(zig.contains("callback: *const fn (*anyopaque, [*:0]const u8) callconv(.C) [*:0]u8"));
    }

    #[test]
    fn test_metadata_parameters_included_in_registration() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let zig = gen_service_zig(&api, &config);

        // Metadata param (path: string) should appear in the registration method signature
        assert!(
            zig.contains("path: [*:0]const u8"),
            "Expected 'path: [*:0]const u8' but got:\n{}",
            zig
        );
    }

    #[test]
    fn test_entrypoint_method_exists() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let zig = gen_service_zig(&api, &config);

        // Entrypoint method should be present
        assert!(zig.contains("pub fn run("));
        assert!(zig.contains("addr: [*:0]const u8"));
    }

    #[test]
    fn test_generate_returns_one_file_for_non_empty_services() {
        let api = make_fixture_surface();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let files = generate(&api, &config).expect("generate should not fail");
        assert_eq!(files.len(), 1, "expected 1 generated file, got {}", files.len());

        let file = &files[0];
        assert!(file.path.to_string_lossy().ends_with("_service.zig"));
    }

    #[test]
    fn test_generate_returns_empty_for_no_services() {
        let surface = ApiSurface::default();
        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let files = generate(&surface, &config).expect("generate should not fail");
        assert!(files.is_empty(), "expected no files for surface without services");
    }

    #[test]
    fn test_registration_variants_emit_shortcut_methods() {
        let mut api = make_fixture_surface();

        // Add variants to the registration
        if let Some(reg) = api.services[0].registrations.get_mut(0) {
            reg.variants.push(RegistrationVariant {
                name: "get".to_owned(),
                overrides: vec![],
                wrapper_call: None,
                signature_params: vec![ParamDef {
                    name: "path".to_owned(),
                    ty: TypeRef::String,
                    optional: false,
                    default: None,
                    ..ParamDef::default()
                }],
                doc: Some("Register a GET handler.".to_owned()),
            });
            reg.variants.push(RegistrationVariant {
                name: "post".to_owned(),
                overrides: vec![],
                wrapper_call: None,
                signature_params: vec![ParamDef {
                    name: "path".to_owned(),
                    ty: TypeRef::String,
                    optional: false,
                    default: None,
                    ..ParamDef::default()
                }],
                doc: Some("Register a POST handler.".to_owned()),
            });
        }

        let config = ResolvedCrateConfig {
            name: "test_crate".to_owned(),
            ..ResolvedCrateConfig::default()
        };

        let zig = gen_service_zig(&api, &config);

        // Verify that variant methods are emitted
        assert!(zig.contains("pub fn get("), "Expected 'pub fn get(' in:\n{}", zig);
        assert!(zig.contains("pub fn post("), "Expected 'pub fn post(' in:\n{}", zig);
        assert!(zig.contains("Register a GET handler."));
        assert!(zig.contains("Register a POST handler."));
        // Verify that variant C symbols are called
        assert!(zig.contains("c.test_crate_test_service_get("));
        assert!(zig.contains("c.test_crate_test_service_post("));
    }
}
