//! Service-API codegen for the C# backend.
//!
//! Generates two outputs per [`ServiceDef`]:
//!
//! 1. **P/Invoke declarations** — [`DllImport`] stubs matching the C FFI contract
//!    (handlers, registration, entrypoints).
//! 2. **Service class** — An idiomatic C# wrapper that uses P/Invoke to invoke
//!    the Rust service, with registration methods and run/finalize entrypoints.
//!
//! The C# service class exposes:
//! - A constructor mirroring [`ServiceDef::constructor`].
//! - Configurator methods from [`ServiceDef::configurators`].
//! - Registration methods from [`ServiceDef::registrations`] that accept C# delegates
//!   and marshal them via `[UnmanagedCallersOnly]` trampolines + `GCHandle`.
//! - Entrypoint methods (run/finalize) from [`ServiceDef::entrypoints`].
//!
//! All names and signatures are derived entirely from the [`ApiSurface`] IR — no
//! transport- or domain-specific assumptions are made anywhere in this module.

use crate::codegen::naming::{csharp_type_name, to_csharp_name};
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointDef, ServiceDef, TypeRef};
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;

/// Whether an entrypoint's return type can be represented over the C ABI.
/// Opaque types are representable only when this surface wraps them.
/// Unit/primitive/string/bytes/Named opaques are representable;
/// everything else (foreign framework types) is not representable.
fn entrypoint_return_representable(ep: &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 && t.is_opaque),
        _ => false,
    }
}

/// Map TypeRef to C# type name for metadata parameters and return types.
/// For opaque types in this surface, returns the C# wrapper class name (e.g., "GraphQLRouteConfig").
fn csharp_type_for_metadata(ty: &TypeRef, api: &ApiSurface) -> String {
    match ty {
        TypeRef::String | TypeRef::Char => "string".to_owned(),
        TypeRef::Primitive(p) => {
            use crate::core::ir::PrimitiveType;
            match p {
                PrimitiveType::Bool => "bool".to_owned(),
                PrimitiveType::U8 => "byte".to_owned(),
                PrimitiveType::U16 => "ushort".to_owned(),
                PrimitiveType::U32 => "uint".to_owned(),
                PrimitiveType::U64 => "ulong".to_owned(),
                PrimitiveType::I8 => "sbyte".to_owned(),
                PrimitiveType::I16 => "short".to_owned(),
                PrimitiveType::I32 => "int".to_owned(),
                PrimitiveType::I64 => "long".to_owned(),
                PrimitiveType::F32 => "float".to_owned(),
                PrimitiveType::F64 => "double".to_owned(),
                PrimitiveType::Usize => "nuint".to_owned(),
                PrimitiveType::Isize => "nint".to_owned(),
            }
        }
        TypeRef::Bytes => "byte[]".to_owned(),
        TypeRef::Unit => "void".to_owned(),
        TypeRef::Named(name) => {
            // Check if this is an opaque type in the surface
            if api.types.iter().any(|t| t.name == *name && t.is_opaque) {
                csharp_type_name(name)
            } else {
                "string".to_owned() // Fallback for non-opaque Named or unknown types
            }
        }
        _ => "string".to_owned(), // Fallback for complex types
    }
}

// ──────────────────────────────────────────────────── C# Service Output ──

/// Generate the idiomatic C# service class wrapper.
///
/// The class exposes:
/// - Constructor reflecting the service's Rust constructor
/// - Configurators as fluent builder methods
/// - Registration methods that accept C# delegates
/// - Run/Finalize entrypoint methods
fn gen_service_cs(api: &ApiSurface, service: &ServiceDef, namespace: &str, prefix: &str) -> String {
    let mut out = String::new();

    // File header
    out.push_str("// Auto-generated by alef — DO NOT EDIT\n");
    out.push_str("#nullable enable\n\n");

    out.push_str(&format!("namespace {namespace} {{\n\n"));
    out.push_str("using System;\n");
    out.push_str("using System.Collections.Generic;\n");
    out.push_str("using System.Linq;\n");
    out.push_str("using System.Runtime.InteropServices;\n\n");

    // Service class
    let class_name = to_csharp_name(&service.name);
    out.push_str("/// <summary>\n");
    out.push_str(&format!("/// Service wrapper for {}.\n", service.name));
    out.push_str("/// </summary>\n");
    out.push_str(&format!("public class {class_name} {{\n\n"));

    // Private opaque handle field
    out.push_str("    private IntPtr _handle;\n");
    out.push_str("    private static readonly Dictionary<IntPtr, GCHandle> _registeredCallbacks = new();\n");
    // A single static delegate instance bridges the native callback. Held in a static field so the
    // marshalled function pointer outlives every registration (a transient delegate would be collected).
    out.push_str("    private static readonly NativeMethods.HandlerCallback _handlerCallback = HandlerTrampoline;\n\n");

    // Constructor
    {
        let ctor = &service.constructor;
        out.push_str("    /// <summary>\n");
        out.push_str(&format!("    /// Create a new {}.\n", service.name));
        out.push_str("    /// </summary>\n");

        if ctor.params.is_empty() {
            out.push_str(&format!("    public {class_name}() {{\n"));
            out.push_str(&format!(
                "        _handle = NativeMethods.{}_{}_new();\n",
                prefix.to_lowercase(),
                service.name.to_snake_case()
            ));
            out.push_str("    }\n\n");
        } else {
            out.push_str(&format!("    public {class_name}("));
            for (i, param) in ctor.params.iter().enumerate() {
                if i > 0 {
                    out.push_str(", ");
                }
                let ty = csharp_type_for_metadata(&param.ty, api);
                let name = param.name.to_lower_camel_case();
                out.push_str(&format!("{} {}", ty, name));
            }
            out.push_str(") {\n");
            out.push_str(&format!(
                "        _handle = NativeMethods.{}_{}_new();\n",
                prefix.to_lowercase(),
                service.name.to_snake_case()
            ));
            out.push_str("    }\n\n");
        }
    }

    // Configurator methods
    for method in &service.configurators {
        let method_name = &method.name;
        out.push_str("    /// <summary>\n");
        out.push_str(&format!("    /// Configure {}.\n", method_name));
        out.push_str("    /// </summary>\n");

        out.push_str(&format!("    public {class_name} {method_name}("));
        for (i, param) in method.params.iter().enumerate() {
            if i > 0 {
                out.push_str(", ");
            }
            let ty = csharp_type_for_metadata(&param.ty, api);
            let name = param.name.to_lower_camel_case();
            out.push_str(&format!("{} {}", ty, name));
        }
        out.push_str(") {\n");
        out.push_str("        // Store configuration if needed\n");
        out.push_str("        return this;\n");
        out.push_str("    }\n\n");
    }

    // Registration methods
    for reg in &service.registrations {
        let reg_method = &reg.method;
        let service_snake = service.name.to_snake_case();

        out.push_str("    /// <summary>\n");
        out.push_str(&format!("    /// Register a handler for {}.\n", reg_method));
        out.push_str("    /// </summary>\n");

        out.push_str(&format!("    public int {}(", reg_method));

        // Metadata parameters
        for (i, meta_param) in reg.metadata_params.iter().enumerate() {
            if i > 0 {
                out.push_str(", ");
            }
            let ty = csharp_type_for_metadata(&meta_param.ty, api);
            let name = meta_param.name.to_lower_camel_case();
            out.push_str(&format!("{} {}", ty, name));
        }
        out.push_str(", Delegate handler) {\n");

        // Hold the user handler alive (Normal, not Pinned — delegates are non-blittable and cannot
        // be pinned) and pass its GCHandle as the opaque context the trampoline recovers.
        out.push_str("        var handle = GCHandle.Alloc(handler, GCHandleType.Normal);\n");
        out.push_str("        IntPtr ctx = GCHandle.ToIntPtr(handle);\n\n");

        // Call native registration function
        out.push_str(&format!(
            "        int result = NativeMethods.{}_{}_register_{}(\n",
            prefix.to_lowercase(),
            service_snake,
            reg_method.to_snake_case()
        ));
        out.push_str("            _handle,\n");
        out.push_str("            _handlerCallback,\n");
        out.push_str("            ctx");

        // Add metadata arguments
        for meta_param in &reg.metadata_params {
            let name = meta_param.name.to_lower_camel_case();
            // Check if this is an opaque type — if so, extract .Handle
            let arg = if matches!(&meta_param.ty, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque))
            {
                format!("{}.Handle", name)
            } else {
                name
            };
            out.push_str(&format!(",\n            {}", arg));
        }

        out.push_str("\n        );\n\n");
        out.push_str("        if (result == 0) {\n");
        out.push_str("            // Keep the GCHandle alive for the lifetime of the registration\n");
        out.push_str("            lock (_registeredCallbacks) {\n");
        out.push_str("                _registeredCallbacks[ctx] = handle;\n");
        out.push_str("            }\n");
        out.push_str("        } else {\n");
        out.push_str("            handle.Free();\n");
        out.push_str("        }\n");
        out.push_str("        return result;\n");
        out.push_str("    }\n\n");

        // Registration variants
        for variant in &reg.variants {
            let variant_method_name = variant.name.to_upper_camel_case();
            let variant_fn_name = variant.name.to_snake_case();

            out.push_str("    /// <summary>\n");
            if let Some(doc) = &variant.doc {
                out.push_str(&format!("    /// {}\n", doc));
            } else {
                out.push_str(&format!(
                    "    /// Register a handler via the {} variant.\n",
                    variant.name
                ));
            }
            out.push_str("    /// </summary>\n");

            out.push_str(&format!("    public int {}(", variant_method_name));

            // Signature parameters from the variant
            for (i, sig_param) in variant.signature_params.iter().enumerate() {
                if i > 0 {
                    out.push_str(", ");
                }
                let ty = csharp_type_for_metadata(&sig_param.ty, api);
                let name = sig_param.name.to_lower_camel_case();
                out.push_str(&format!("{} {}", ty, name));
            }
            out.push_str(", Delegate handler) {\n");

            out.push_str("        var handle = GCHandle.Alloc(handler, GCHandleType.Normal);\n");
            out.push_str("        IntPtr ctx = GCHandle.ToIntPtr(handle);\n\n");

            out.push_str(&format!(
                "        int result = NativeMethods.{}_{}_{}(\n",
                prefix.to_lowercase(),
                service_snake,
                variant_fn_name
            ));
            out.push_str("            _handle,\n");
            out.push_str("            _handlerCallback,\n");
            out.push_str("            ctx");

            // Add signature arguments
            for sig_param in &variant.signature_params {
                let name = sig_param.name.to_lower_camel_case();
                // Check if this is an opaque type — if so, extract .Handle
                let arg = if matches!(&sig_param.ty, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque))
                {
                    format!("{}.Handle", name)
                } else {
                    name
                };
                out.push_str(&format!(",\n            {}", arg));
            }

            out.push_str("\n        );\n\n");
            out.push_str("        if (result == 0) {\n");
            out.push_str("            lock (_registeredCallbacks) {\n");
            out.push_str("                _registeredCallbacks[ctx] = handle;\n");
            out.push_str("            }\n");
            out.push_str("        } else {\n");
            out.push_str("            handle.Free();\n");
            out.push_str("        }\n");
            out.push_str("        return result;\n");
            out.push_str("    }\n\n");
        }
    }

    // Entrypoint methods
    for ep in &service.entrypoints {
        let ep_method = &ep.method;
        let service_snake = service.name.to_snake_case();

        out.push_str("    /// <summary>\n");
        out.push_str(&format!("    /// {}.\n", ep_method));
        out.push_str("    /// </summary>\n");

        // Check if return type is representable (skip finalize if not)
        if !entrypoint_return_representable(ep, api) {
            continue;
        }

        // Mirror the C ABI: an entrypoint returns an opaque handle (IntPtr) when its return type is
        // an opaque this surface wraps, otherwise an i32 status code. The async-ness of the Rust
        // method does not change the C return shape.
        let returns_opaque =
            matches!(&ep.return_type, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque));
        let return_type = if returns_opaque { "IntPtr" } else { "int" };
        out.push_str(&format!("    public {} {}(", return_type, ep_method));

        for (i, param) in ep.params.iter().enumerate() {
            if i > 0 {
                out.push_str(", ");
            }
            let ty = csharp_type_for_metadata(&param.ty, api);
            let name = param.name.to_lower_camel_case();
            out.push_str(&format!("{} {}", ty, name));
        }

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

        // Call native entrypoint and return its status / handle.
        out.push_str(&format!(
            "        return NativeMethods.{}_{}_ep_{}(\n",
            prefix.to_lowercase(),
            service_snake,
            ep_method.to_snake_case()
        ));
        out.push_str("            _handle");

        for param in &ep.params {
            let name = param.name.to_lower_camel_case();
            // Check if this is an opaque type — if so, extract .Handle
            let arg = if matches!(&param.ty, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque))
            {
                format!("{}.Handle", name)
            } else {
                name
            };
            out.push_str(&format!(",\n            {}", arg));
        }

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

    // Destructor / Dispose
    let service_snake = service.name.to_snake_case();
    out.push_str("    public void Dispose() {\n");
    out.push_str(&format!(
        "        if (_handle != IntPtr.Zero) {{\n            NativeMethods.{}_{}_free(_handle);\n            _handle = IntPtr.Zero;\n        }}\n",
        prefix.to_lowercase(),
        service_snake
    ));
    out.push_str("        // Clean up all registered callbacks for this instance\n");
    out.push_str("        lock (_registeredCallbacks) {\n");
    out.push_str("            var keys = _registeredCallbacks.Keys.ToList();\n");
    out.push_str("            foreach (var key in keys) {\n");
    out.push_str("                var handle = _registeredCallbacks[key];\n");
    out.push_str("                handle.Free();\n");
    out.push_str("                _registeredCallbacks.Remove(key);\n");
    out.push_str("            }\n");
    out.push_str("        }\n");
    out.push_str("    }\n\n");

    // Static unmanaged callback handler
    out.push_str("    /// <summary>\n");
    out.push_str("    /// Unmanaged callback trampoline that recovers the GC handle and invokes the handler.\n");
    out.push_str("    /// </summary>\n");
    out.push_str("    public static IntPtr HandlerTrampoline(IntPtr ctx, IntPtr requestJson) {\n");
    out.push_str("        try {\n");
    out.push_str("            // Recover the GCHandle and invoke the delegate\n");
    out.push_str("            var handle = GCHandle.FromIntPtr(ctx);\n");
    out.push_str("            if (handle.Target is Func<string, string> handler) {\n");
    out.push_str("                // Unmarshal the request JSON\n");
    out.push_str("                string requestStr = Marshal.PtrToStringUTF8(requestJson) ?? \"{}\";\n");
    out.push_str("                // Invoke the handler and get the response\n");
    out.push_str("                string responseStr = handler(requestStr);\n");
    out.push_str("                // Allocate response string in native memory (malloc-backed on Unix; the\n");
    out.push_str("                // native side takes ownership and frees it).\n");
    out.push_str("                return Marshal.StringToCoTaskMemUTF8(responseStr);\n");
    out.push_str("            }\n");
    out.push_str("        } catch {\n");
    out.push_str("            // Return null on error\n");
    out.push_str("        }\n");
    out.push_str("        return IntPtr.Zero;\n");
    out.push_str("    }\n\n");

    out.push_str("}\n\n"); // Close class
    out.push_str("}\n"); // Close namespace

    out
}

/// Generate P/Invoke declarations for native service functions.
///
/// Mirrors the C FFI contract exactly: constructor/destructor, registration functions,
/// and entrypoint functions with their exact signatures and names.
/// Opaque metadata parameters are marshalled as IntPtr (handle);
/// other Named/complex types are not expected in P/Invoke metadata.
fn gen_native_methods_cs(api: &ApiSurface, namespace: &str, prefix: &str) -> String {
    let mut out = String::new();

    // File header
    out.push_str("// Auto-generated by alef — DO NOT EDIT\n");
    out.push_str("#nullable enable\n\n");

    out.push_str(&format!("namespace {namespace} {{\n\n"));
    out.push_str("using System;\n");
    out.push_str("using System.Runtime.InteropServices;\n\n");

    out.push_str("/// <summary>\n");
    out.push_str("/// Native P/Invoke declarations for service API.\n");
    out.push_str("/// </summary>\n");
    out.push_str("internal static partial class NativeMethods {\n\n");

    // Handler callback delegate typedef
    out.push_str("    /// <summary>\n");
    out.push_str("    /// Native callback signature for service handlers.\n");
    out.push_str("    /// </summary>\n");
    out.push_str("    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]\n");
    out.push_str("    public delegate IntPtr HandlerCallback(IntPtr ctx, IntPtr requestJson);\n\n");

    // Service constructors and destructors
    for service in &api.services {
        let service_snake = service.name.to_snake_case();

        // Constructor
        out.push_str(&format!(
            "    [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
            prefix.to_lowercase()
        ));
        out.push_str(&format!(
            "    public static extern IntPtr {}_{}_new();\n\n",
            prefix.to_lowercase(),
            service_snake
        ));

        // Destructor
        out.push_str(&format!(
            "    [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
            prefix.to_lowercase()
        ));
        out.push_str(&format!(
            "    public static extern void {}_{}_free(IntPtr ptr);\n\n",
            prefix.to_lowercase(),
            service_snake
        ));

        // Registration functions
        for reg in &service.registrations {
            let reg_method_snake = reg.method.to_snake_case();

            out.push_str(&format!(
                "    [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
                prefix.to_lowercase()
            ));
            out.push_str(&format!(
                "    public static extern int {}_{}_register_{}(\n",
                prefix.to_lowercase(),
                service_snake,
                reg_method_snake
            ));
            out.push_str("        IntPtr owner,\n");
            out.push_str("        HandlerCallback callback,\n");
            out.push_str("        IntPtr ctx");

            // Metadata parameters
            for meta_param in &reg.metadata_params {
                let c_type = match &meta_param.ty {
                    TypeRef::String | TypeRef::Char => "string",
                    TypeRef::Primitive(p) => {
                        use crate::core::ir::PrimitiveType;
                        match p {
                            PrimitiveType::Bool => "int",
                            PrimitiveType::U8 => "byte",
                            PrimitiveType::U16 => "ushort",
                            PrimitiveType::U32 => "uint",
                            PrimitiveType::U64 => "ulong",
                            PrimitiveType::I8 => "sbyte",
                            PrimitiveType::I16 => "short",
                            PrimitiveType::I32 => "int",
                            PrimitiveType::I64 => "long",
                            PrimitiveType::F32 => "float",
                            PrimitiveType::F64 => "double",
                            PrimitiveType::Usize => "nuint",
                            PrimitiveType::Isize => "nint",
                        }
                    }
                    _ => "IntPtr",
                };
                out.push_str(&format!(",\n        {} {}", c_type, meta_param.name));
            }

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

            // Variant P/Invoke declarations
            for variant in &reg.variants {
                let variant_fn_name = variant.name.to_snake_case();

                out.push_str(&format!(
                    "    [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
                    prefix.to_lowercase()
                ));
                out.push_str(&format!(
                    "    public static extern int {}_{}_{}(\n",
                    prefix.to_lowercase(),
                    service_snake,
                    variant_fn_name
                ));
                out.push_str("        IntPtr owner,\n");
                out.push_str("        HandlerCallback callback,\n");
                out.push_str("        IntPtr ctx");

                // Variant signature parameters
                for sig_param in &variant.signature_params {
                    let c_type = match &sig_param.ty {
                        TypeRef::String | TypeRef::Char => "string",
                        TypeRef::Primitive(p) => {
                            use crate::core::ir::PrimitiveType;
                            match p {
                                PrimitiveType::Bool => "int",
                                PrimitiveType::U8 => "byte",
                                PrimitiveType::U16 => "ushort",
                                PrimitiveType::U32 => "uint",
                                PrimitiveType::U64 => "ulong",
                                PrimitiveType::I8 => "sbyte",
                                PrimitiveType::I16 => "short",
                                PrimitiveType::I32 => "int",
                                PrimitiveType::I64 => "long",
                                PrimitiveType::F32 => "float",
                                PrimitiveType::F64 => "double",
                                PrimitiveType::Usize => "nuint",
                                PrimitiveType::Isize => "nint",
                            }
                        }
                        _ => "IntPtr",
                    };
                    out.push_str(&format!(",\n        {} {}", c_type, sig_param.name));
                }

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

        // Entrypoint functions
        for ep in &service.entrypoints {
            // Skip non-representable finalize entrypoints (e.g., foreign framework returns)
            if !entrypoint_return_representable(ep, api) {
                continue;
            }

            let ep_method_snake = ep.method.to_snake_case();
            // Mirror the C ABI exactly: the ffi entrypoint glue returns `*mut T` for a finalize
            // whose return this surface wraps (IntPtr), otherwise an `i32` status code — never the
            // Rust method's nominal return type (e.g. Unit/String are still i32 status over C).
            let returns_opaque =
                matches!(&ep.return_type, TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n && t.is_opaque));
            let return_type = if returns_opaque { "IntPtr" } else { "int" };

            out.push_str(&format!(
                "    [DllImport(\"{}_ffi\", CallingConvention = CallingConvention.Cdecl)]\n",
                prefix.to_lowercase()
            ));
            out.push_str(&format!(
                "    public static extern {} {}_{}_ep_{}(\n",
                return_type,
                prefix.to_lowercase(),
                service_snake,
                ep_method_snake
            ));
            out.push_str("        IntPtr owner");

            // Entrypoint parameters
            for param in &ep.params {
                let c_type = match &param.ty {
                    TypeRef::String | TypeRef::Char => "string",
                    TypeRef::Primitive(p) => {
                        use crate::core::ir::PrimitiveType;
                        match p {
                            PrimitiveType::Bool => "int",
                            PrimitiveType::U8 => "byte",
                            PrimitiveType::U16 => "ushort",
                            PrimitiveType::U32 => "uint",
                            PrimitiveType::U64 => "ulong",
                            PrimitiveType::I8 => "sbyte",
                            PrimitiveType::I16 => "short",
                            PrimitiveType::I32 => "int",
                            PrimitiveType::I64 => "long",
                            PrimitiveType::F32 => "float",
                            PrimitiveType::F64 => "double",
                            PrimitiveType::Usize => "nuint",
                            PrimitiveType::Isize => "nint",
                        }
                    }
                    _ => "IntPtr",
                };
                out.push_str(&format!(",\n        {} {}", c_type, param.name));
            }

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

    out.push_str("}\n\n"); // Close class
    out.push_str("}\n"); // Close namespace

    out
}

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

/// Generate all service-API files for the C# backend.
///
/// Returns two `GeneratedFile`s per non-empty service list:
/// - One C# service class file per service
/// - One P/Invoke native methods file (shared across all services)
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
    if api.services.is_empty() {
        return Ok(vec![]);
    }

    let namespace = config.csharp_namespace();
    let prefix = config.ffi_prefix();

    let output_dir = config
        .output_paths
        .get("csharp")
        .map(|p| p.to_string_lossy().into_owned())
        .unwrap_or_else(|| "packages/csharp/".to_owned());

    let base_path = PathBuf::from(&output_dir).join(namespace.replace('.', "/"));

    let mut files = Vec::new();

    // Generate one service class per service
    for service in &api.services {
        let service_cs = gen_service_cs(api, service, &namespace, &prefix);
        let class_name = to_csharp_name(&service.name);
        files.push(GeneratedFile {
            path: base_path.join(format!("{}.cs", class_name)),
            content: service_cs,
            generated_header: false, // Header already included
        });
    }

    // Generate P/Invoke native methods
    let native_methods = gen_native_methods_cs(api, &namespace, &prefix);
    files.push(GeneratedFile {
        path: base_path.join("ServiceNativeMethods.cs"),
        content: native_methods,
        generated_header: false,
    });

    Ok(files)
}

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

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

    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![
                crate::core::ir::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()),
                },
                crate::core::ir::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: None,
                },
            ],
        };

        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_cs_contains_class() {
        let api = make_fixture_surface();
        let service = &api.services[0];
        let cs = gen_service_cs(&api, service, "MyNamespace", "test");

        assert!(cs.contains("public class TestService"));
        assert!(cs.contains("private IntPtr _handle"));
        assert!(cs.contains("public TestService()"));
    }

    #[test]
    fn test_gen_service_cs_contains_registration_method() {
        let api = make_fixture_surface();
        let service = &api.services[0];
        let cs = gen_service_cs(&api, service, "MyNamespace", "test");

        assert!(cs.contains("public int add_handler("));
        assert!(cs.contains("GCHandle.Alloc(handler, GCHandleType.Normal)"));
        assert!(cs.contains("_handlerCallback"));
        assert!(cs.contains("_registeredCallbacks[ctx] = handle"));
    }

    #[test]
    fn test_gen_service_cs_contains_run_method() {
        let api = make_fixture_surface();
        let service = &api.services[0];
        let cs = gen_service_cs(&api, service, "MyNamespace", "test");

        assert!(cs.contains("public int run("));
        assert!(cs.contains("NativeMethods.test_test_service_ep_run"));
    }

    #[test]
    fn test_gen_service_cs_contains_unmanaged_callback() {
        let api = make_fixture_surface();
        let service = &api.services[0];
        let cs = gen_service_cs(&api, service, "MyNamespace", "test");

        assert!(cs.contains("public static IntPtr HandlerTrampoline"));
        assert!(cs.contains("_handlerCallback = HandlerTrampoline"));
        assert!(cs.contains("GCHandle.FromIntPtr(ctx)"));
        assert!(cs.contains("Marshal.PtrToStringUTF8"));
    }

    #[test]
    fn test_gen_service_cs_trampoline_invokes_delegate() {
        let api = make_fixture_surface();
        let service = &api.services[0];
        let cs = gen_service_cs(&api, service, "MyNamespace", "test");

        // Verify the delegate type is Func<string, string>
        assert!(
            cs.contains("if (handle.Target is Func<string, string> handler)"),
            "trampoline must cast to Func<string, string>"
        );

        // Verify the delegate is actually invoked
        assert!(
            cs.contains("handler(requestStr)"),
            "trampoline must invoke the handler with request string"
        );

        // Verify the response from the delegate is marshalled (not a hardcoded response)
        assert!(
            cs.contains("string responseStr = handler(requestStr);"),
            "trampoline must capture delegate result into responseStr"
        );

        // Verify there is NO hardcoded "{}" response or "stub" comment
        assert!(
            !cs.contains("\"stub implementation\""),
            "trampoline must not have stub implementation comment"
        );
        assert!(
            !cs.contains("string responseStr = \"{}\""),
            "trampoline must not return hardcoded {{}} response"
        );

        // Verify the marshalled response is properly allocated in native memory
        assert!(
            cs.contains("Marshal.StringToCoTaskMemUTF8(responseStr)"),
            "trampoline must marshal the response back to native memory"
        );
    }

    #[test]
    fn test_gen_native_methods_cs_contains_callback_typedef() {
        let api = make_fixture_surface();
        let native = gen_native_methods_cs(&api, "MyNamespace", "test");

        assert!(native.contains("delegate IntPtr HandlerCallback"));
        assert!(native.contains("[UnmanagedFunctionPointer(CallingConvention.Cdecl)]"));
    }

    #[test]
    fn test_gen_native_methods_cs_contains_pinvoke_decls() {
        let api = make_fixture_surface();
        let native = gen_native_methods_cs(&api, "MyNamespace", "test");

        assert!(native.contains("[DllImport("));
        assert!(native.contains("test_test_service_new()"));
        assert!(native.contains("test_test_service_free"));
        assert!(native.contains("test_test_service_register_add_handler"));
        assert!(native.contains("test_test_service_ep_run"));
    }

    #[test]
    fn test_generate_returns_files() {
        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!(!files.is_empty(), "expected at least one file");

        let has_service_class = files
            .iter()
            .any(|f| f.path.to_string_lossy().contains("TestService.cs"));
        let has_native_methods = files
            .iter()
            .any(|f| f.path.to_string_lossy().contains("ServiceNativeMethods.cs"));

        assert!(has_service_class, "expected TestService.cs in output");
        assert!(has_native_methods, "expected ServiceNativeMethods.cs in output");
    }

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

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

    #[test]
    fn test_gen_service_cs_contains_variant_methods() {
        let api = make_fixture_surface();
        let service = &api.services[0];
        let cs = gen_service_cs(&api, service, "MyNamespace", "test");

        // Test Get variant method
        assert!(
            cs.contains("public int Get("),
            "expected Get variant method in service class"
        );
        assert!(
            cs.contains("Register a GET handler"),
            "expected Get variant documentation"
        );

        // Test Post variant method
        assert!(
            cs.contains("public int Post("),
            "expected Post variant method in service class"
        );
        assert!(
            cs.contains("Register a handler via the post variant"),
            "expected Post variant auto-generated documentation"
        );

        // Test variant P/Invoke calls
        assert!(
            cs.contains("NativeMethods.test_test_service_get("),
            "expected Get variant P/Invoke call"
        );
        assert!(
            cs.contains("NativeMethods.test_test_service_post("),
            "expected Post variant P/Invoke call"
        );
    }

    #[test]
    fn test_gen_native_methods_cs_contains_variant_pinvoke_decls() {
        let api = make_fixture_surface();
        let native = gen_native_methods_cs(&api, "MyNamespace", "test");

        // Test Get variant P/Invoke declaration
        assert!(
            native.contains("public static extern int test_test_service_get("),
            "expected Get variant P/Invoke declaration"
        );

        // Test Post variant P/Invoke declaration
        assert!(
            native.contains("public static extern int test_test_service_post("),
            "expected Post variant P/Invoke declaration"
        );

        // Both should have the standard parameters
        assert!(
            native.contains("IntPtr owner,") && native.contains("HandlerCallback callback,"),
            "expected variant P/Invoke to have owner and callback parameters"
        );
    }
}
