#![allow(clippy::too_many_arguments, clippy::type_complexity)]

use super::buf::CodeBuf;
use crate::{AFast, Error, Handler, HandlerMeta, ParamMeta, Service, TagKind, TagMeta};
use std::path::Path;

/// Returns an iterator over fields that should be included in generated code,
/// filtering out `skip` fields and `skip_with` fields whose marker matches.
fn included(
    fields: &[crate::handler::FieldMeta],
) -> impl Iterator<Item = &crate::handler::FieldMeta> {
    fields
        .iter()
        .filter(|f| crate::marker::should_include_field(f))
}

// ─── Enum tag size ─────────────────────────────────────────────────
// The tag width must be consistent with the afastdata wire format used
// by the server.  The feature flag selects the correct reader/writer
// method name for the BinaryReader/BinaryWriter generated below.

fn tag_write() -> &'static str {
    if cfg!(feature = "tag-u32") {
        "wU32"
    } else if cfg!(feature = "tag-u16") {
        "wU16"
    } else {
        "wU8"
    }
}

fn tag_read() -> &'static str {
    if cfg!(feature = "tag-u32") {
        "rU32"
    } else if cfg!(feature = "tag-u16") {
        "rU16"
    } else {
        "rU8"
    }
}

// ─── Type mapping ──────────────────────────────────────────────────
// Maps Rust types to their Kotlin equivalents.  Primitive integers
// are widened to the smallest Kotlin type that can hold the full range.
// Complex types (Option, Vec) are mapped to nullable types and List
// respectively.

fn normalize_rust_type(ty: &str) -> String {
    let ty = ty.trim();
    let ty = ty.strip_prefix("afast :: ").unwrap_or(ty);
    let ty = ty.strip_prefix("afast::").unwrap_or(ty);
    ty.replace(" <", "<")
        .replace("< ", "<")
        .replace(" >", ">")
        .replace("> ", ">")
        .replace(" ,", ",")
        .replace(", ", ",")
}

#[cfg(feature = "ordinary-http")]
fn unwrap_json_type(ty: &str) -> Option<String> {
    let ty = normalize_rust_type(ty);
    if ty.starts_with("Json<") && ty.ends_with('>') {
        let inner = &ty[5..ty.len() - 1].trim();
        Some(normalize_rust_type(inner))
    } else {
        None
    }
}

fn rust_type_to_kt(ty: &str) -> String {
    let ty = normalize_rust_type(ty);
    match ty.as_str() {
        "i8" => "Byte".to_string(),
        "u8" => "Byte".to_string(),
        "i16" => "Short".to_string(),
        "u16" => "Short".to_string(),
        "i32" => "Int".to_string(),
        "u32" => "Int".to_string(),
        "i64" => "Long".to_string(),
        "u64" => "Long".to_string(),
        "usize" => "Long".to_string(),
        "f32" => "Float".to_string(),
        "f64" => "Double".to_string(),
        "bool" => "Boolean".to_string(),
        "String" => "String".to_string(),
        "&str" => "String".to_string(),
        "Vec<u8>" => "ByteArray".to_string(),
        s if s.starts_with("Option<") => {
            let inner = &s[7..s.len() - 1];
            format!("{}?", rust_type_to_kt(inner))
        }
        s if s.starts_with("Vec<") => {
            let inner = &s[4..s.len() - 1];
            format!("List<{}>", rust_type_to_kt(inner))
        }
        other => other.to_string(),
    }
}

// ─── Common file generation ────────────────────────────────────────
// Produces common.kt which contains BinaryWriter, BinaryReader,
// AFastValidationError, AfastSocket, and all shared type definitions
// (Custom and Header types) used across service-specific files.

#[allow(clippy::vec_init_then_push)]
fn generate_common_kt(services: &[Service], _seq64: bool, _has_ws: bool, _has_tcp: bool) -> String {
    let mut lines = CodeBuf::new();
    lines.push("// Auto-generated by afast. DO NOT EDIT.".to_string());
    lines.b();
    lines.push("package afast.generated".to_string());
    lines.b();
    lines.push("import java.net.Socket".to_string());
    lines.push("import java.io.*".to_string());
    lines.push("import java.nio.ByteBuffer".to_string());
    lines.push("import java.nio.ByteOrder".to_string());
    lines.push("import java.util.concurrent.ConcurrentHashMap".to_string());
    lines.push("import kotlinx.coroutines.*".to_string());
    lines.b();

    // BinaryWriter: little-endian binary serializer used to construct
    // request payloads before sending them over the wire.
    lines.push("class BinaryWriter {".to_string());
    lines.push("    private val _b = mutableListOf<Byte>()".to_string());
    lines.push("    private val _v = ByteArray(8)".to_string());
    lines.b();
    lines.push("    fun wU8(n: Int) { _b.add((n and 0xFF).toByte()) }".to_string());
    lines.push("    fun wI8(n: Int) { _b.add((n and 0xFF).toByte()) }".to_string());
    lines.push("    fun wU16(n: Int) { _v[0] = (n and 0xFF).toByte(); _v[1] = ((n shr 8) and 0xFF).toByte(); _b.addAll(_v.take(2).toList()) }".to_string());
    lines.push("    fun wI16(n: Int) { wU16(n) }".to_string());
    lines.push("    fun wU32(n: Int) { _v[0] = (n and 0xFF).toByte(); _v[1] = ((n shr 8) and 0xFF).toByte(); _v[2] = ((n shr 16) and 0xFF).toByte(); _v[3] = ((n shr 24) and 0xFF).toByte(); _b.addAll(_v.take(4).toList()) }".to_string());
    lines.push("    fun wI32(n: Int) { wU32(n) }".to_string());
    lines.push("    fun wU64(n: Long) { wU32((n and 0xFFFFFFFFL).toInt()); wU32(((n ushr 32) and 0xFFFFFFFFL).toInt()) }".to_string());
    lines.push("    fun wI64(n: Long) { wU64(n) }".to_string());
    lines.push("    fun wF32(n: Float) { java.nio.ByteBuffer.allocate(4).order(java.nio.ByteOrder.LITTLE_ENDIAN).putFloat(n).array().also { _b.addAll(it.asList()) } }".to_string());
    lines.push("    fun wF64(n: Double) { java.nio.ByteBuffer.allocate(8).order(java.nio.ByteOrder.LITTLE_ENDIAN).putDouble(n).array().also { _b.addAll(it.asList()) } }".to_string());
    lines.push("    fun wB(b: Boolean) { _b.add(if (b) 1.toByte() else 0.toByte()) }".to_string());
    lines.push("    fun wS(s: String) { val u = s.toByteArray(Charsets.UTF_8); wU32(u.size); _b.addAll(u.asList()) }".to_string());
    lines.push("    fun wBytes(a: ByteArray) { wU32(a.size); _b.addAll(a.asList()) }".to_string());
    lines.push("    fun wRaw(a: ByteArray) { _b.addAll(a.asList()) }".to_string());
    lines.push("    fun toBytes(): ByteArray = _b.toByteArray()".to_string());
    lines.push("}".to_string());
    lines.b();

    // BinaryReader: little-endian binary deserializer used to decode
    // server responses back into Kotlin values.
    lines.push("class BinaryReader(private val _d: ByteArray) {".to_string());
    lines.push("    private var _o = 0".to_string());
    lines.b();
    lines.push("    fun rU8(): Int = _d[_o++].toInt() and 0xFF".to_string());
    lines.push("    fun rI8(): Int = _d[_o++].toInt()".to_string());
    lines.push("    fun rU16(): Int { val v = _d[_o].toInt() and 0xFF or ((_d[_o+1].toInt() and 0xFF) shl 8); _o += 2; return v }".to_string());
    lines.push("    fun rI16(): Int { val v = rU16().toShort().toInt(); return v }".to_string());
    lines.push("    fun rU32(): Int { val v = _d[_o].toInt() and 0xFF or ((_d[_o+1].toInt() and 0xFF) shl 8) or ((_d[_o+2].toInt() and 0xFF) shl 16) or ((_d[_o+3].toInt() and 0xFF) shl 24); _o += 4; return v }".to_string());
    lines.push("    fun rI32(): Int = rU32()".to_string());
    lines.push("    fun rU64(): Long { val lo = rU32().toLong() and 0xFFFFFFFFL; val hi = rU32().toLong() and 0xFFFFFFFFL; return (hi shl 32) or lo }".to_string());
    lines.push("    fun rI64(): Long = rU64()".to_string());
    lines.push("    fun rF32(): Float { val buf = ByteArray(4); System.arraycopy(_d, _o, buf, 0, 4); _o += 4; return java.nio.ByteBuffer.wrap(buf).order(java.nio.ByteOrder.LITTLE_ENDIAN).float }".to_string());
    lines.push("    fun rF64(): Double { val buf = ByteArray(8); System.arraycopy(_d, _o, buf, 0, 8); _o += 8; return java.nio.ByteBuffer.wrap(buf).order(java.nio.ByteOrder.LITTLE_ENDIAN).double }".to_string());
    lines.push("    fun rB(): Boolean = _d[_o++].toInt() == 1".to_string());
    lines.push("    fun rS(): String { val len = rU32(); val s = String(_d, _o, len, Charsets.UTF_8); _o += len; return s }".to_string());
    lines.push("    fun rBytes(len: Int): ByteArray { val a = ByteArray(len); System.arraycopy(_d, _o, a, 0, len); _o += len; return a }".to_string());
    lines.push("    fun eof(): Boolean = _o >= _d.size".to_string());
    lines.push("}".to_string());
    lines.b();

    // AFastValidationError: thrown when a request field fails server-side
    // validation.  Carries the error code, field name, and message.
    lines.push("class AFastValidationError(val code: Long, val field: String, message: String) : Exception(message)".to_string());
    lines.b();

    // AfastErrorResponse: server error response for HTTP endpoints.
    #[cfg(feature = "ordinary-http")]
    {
        lines.push("data class AfastErrorResponse(val code: Int, val message: String)".to_string());
        lines.b();
    }

    // AfastSocket: long-connection handle.  Uses a sendRaw lambda so that
    // the socket class is independent of any specific transport client type.
    lines.push("class AfastSocket(".to_string());
    lines.push("    private val connId: Int,".to_string());
    lines.push("    private val sendRaw: (ByteArray) -> Unit,".to_string());
    lines.push("    private val callback: (ByteArray, (Any) -> Unit) -> Unit".to_string());
    lines.push(") {".to_string());
    lines.push("    var isClosed = false".to_string());
    lines.push("    private var _closing = false".to_string());
    lines.b();
    lines.push("    fun send(data: Any) {".to_string());
    lines.push(
        "        if (_closing || isClosed) throw RuntimeException(\"socket closed\")".to_string(),
    );
    lines.push("        val payload = when (data) {".to_string());
    lines.push("            is String -> data.toByteArray(Charsets.UTF_8)".to_string());
    lines.push("            is ByteArray -> data".to_string());
    lines.push("            else -> data.toString().toByteArray(Charsets.UTF_8)".to_string());
    lines.push("        }".to_string());
    lines.push("        val buf = BinaryWriter()".to_string());
    lines.push("        buf.wU32(0)".to_string());
    lines.push("        buf.wU32(connId)".to_string());
    lines.push("        buf.wU32(payload.size)".to_string());
    lines.push("        buf.wBytes(payload)".to_string());
    lines.push("        sendRaw(buf.toBytes())".to_string());
    lines.push("    }".to_string());
    lines.b();
    lines.push("    suspend fun close() {".to_string());
    lines.push("        if (isClosed) return".to_string());
    lines.push("        _closing = true".to_string());
    lines.push("        val buf = BinaryWriter()".to_string());
    lines.push("        buf.wU32(0)".to_string());
    lines.push("        buf.wU32(connId)".to_string());
    lines.push("        buf.wU32(0)".to_string());
    lines.push("        sendRaw(buf.toBytes())".to_string());
    lines.push("        delay(500)".to_string());
    lines.push("        isClosed = true".to_string());
    lines.push("    }".to_string());
    lines.b();
    lines.push("    fun _onMessage(data: ByteArray) {".to_string());
    lines.push("        if (_closing && data.isEmpty()) {".to_string());
    lines.push("            isClosed = true".to_string());
    lines.push("            _closing = false".to_string());
    lines.push("            return".to_string());
    lines.push("        }".to_string());
    lines.push("        if (!isClosed) {".to_string());
    lines.push("            callback(data) { d -> send(d) }".to_string());
    lines.push("        }".to_string());
    lines.push("    }".to_string());
    lines.push("}".to_string());
    lines.b();

    // Gather every Custom extractor type from every service so that
    // the common file can emit all shared type definitions once.
    let mut all_customs = Vec::new();
    for svc in services {
        if svc.name.is_empty() {
            continue;
        }
        let customs = collect_customs(&svc.handlers);
        for ci in customs {
            if !all_customs.iter().any(|c: &CustomInfo| c.ty == ci.ty) {
                all_customs.push(ci);
            }
        }
    }

    if !all_customs.is_empty() {
        // Emit Kotlin data class / sealed class definitions for every
        // Custom type, recursively discovering nested types.
        let mut emitted = Vec::new();
        for svc in services {
            if svc.name.is_empty() {
                continue;
            }
            collect_custom_type_exports(&svc.handlers, &[], &mut emitted, &mut lines);
        }
        lines.b();
        for ci in &all_customs {
            lines.push(format!(
                "typealias CustomFn_{} = suspend () -> {}",
                ci.ty, ci.ty
            ));
        }
    }

    #[cfg(feature = "ordinary-http")]
    {
        // Collect all headers across all services (ordinary HTTP only)
        let mut all_headers = Vec::new();
        for svc in services {
            if svc.name.is_empty() {
                continue;
            }
            let headers = collect_headers(&svc.handlers);
            for ci in headers {
                if !all_headers.iter().any(|c: &CustomInfo| c.ty == ci.ty) {
                    all_headers.push(ci);
                }
            }
        }

        if !all_headers.is_empty() {
            // Emit Kotlin data class / sealed class definitions for every
            // Header type, recursively discovering nested types.
            let mut emitted = Vec::new();
            for svc in services {
                if svc.name.is_empty() {
                    continue;
                }
                collect_header_type_exports(&svc.handlers, &[], &mut emitted, &mut lines);
            }
            lines.b();
            for ci in &all_headers {
                lines.push(format!(
                    "typealias HeaderFn_{} = suspend () -> {}",
                    ci.ty, ci.ty
                ));
            }
        }
    }

    lines.build()
}

fn collect_custom_type_exports(
    handlers: &[Handler],
    path: &[&str],
    emitted: &mut Vec<String>,
    lines: &mut CodeBuf,
) {
    for h in handlers {
        let child_path = {
            let mut p = path.to_vec();
            p.push(h.name);
            p
        };
        if !h.meta.name.is_empty() {
            for param in h.meta.params {
                if param.extractor == "Custom"
                    && let Some(structure_fn) = param.structure
                {
                    let structure = structure_fn();
                    if !emitted.contains(&structure.name.to_string()) {
                        lines.push(kt_type_def(structure));
                        emitted.push(structure.name.to_string());
                        extract_nested_types_kt(structure, lines, emitted);
                    }
                }
            }
        }
        if !h.children.is_empty() {
            collect_custom_type_exports(&h.children, &child_path, emitted, lines);
        }
    }
}

#[cfg(feature = "ordinary-http")]
fn collect_header_type_exports(
    handlers: &[Handler],
    path: &[&str],
    emitted: &mut Vec<String>,
    lines: &mut CodeBuf,
) {
    for h in handlers {
        let child_path = {
            let mut p = path.to_vec();
            p.push(h.name);
            p
        };
        if !h.meta.name.is_empty() {
            for param in h.meta.params {
                if param.extractor == "Header"
                    && let Some(structure_fn) = param.structure
                {
                    let structure = structure_fn();
                    if !emitted.contains(&structure.name.to_string()) {
                        lines.push(kt_type_def(structure));
                        emitted.push(structure.name.to_string());
                        extract_nested_types_kt(structure, lines, emitted);
                    }
                }
            }
        }
        if !h.children.is_empty() {
            collect_header_type_exports(&h.children, &child_path, emitted, lines);
        }
    }
}

// ─── Helpers ────────────────────────────────────────────────────────
// String conversion and naming utilities shared across the generator.

fn to_pascal_case(s: &str) -> String {
    s.split('_')
        .map(|word| {
            let mut c = word.chars();
            match c.next() {
                None => String::new(),
                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
            }
        })
        .collect()
}

fn to_camel_case(s: &str) -> String {
    if s.starts_with("__") {
        return s.to_string();
    }
    let pascal = to_pascal_case(s);
    let mut c = pascal.chars();
    match c.next() {
        None => String::new(),
        Some(f) => f.to_lowercase().collect::<String>() + c.as_str(),
    }
}

fn prefixed_type(prefix: &str, type_name: &str) -> String {
    if type_name.starts_with(prefix) {
        return type_name.to_string();
    }
    // Detect overlapping suffix/prefix to avoid stuttering in generated
    // type names (e.g. "TestNewFeaturesTestNewFeaturesResponse").
    for i in 1..prefix.len() {
        let suffix = &prefix[i..];
        if type_name.starts_with(suffix) {
            return format!("{}{}", &prefix[..i], type_name);
        }
    }
    format!("{}{}", prefix, type_name)
}

fn ordinary_suffix(extractor: &str, data_idx: usize) -> String {
    match extractor {
        "Data" if data_idx == 0 => "Request".to_string(),
        "Data" => format!("Request{}", data_idx + 1),
        "Query" => "Query".to_string(),
        "Param" => "PathParams".to_string(),
        "Body" => "Body".to_string(),
        _ => "Response".to_string(),
    }
}

fn handler_prefix(path: &[&str]) -> String {
    path.iter()
        .map(|s| to_pascal_case(s.trim_start_matches(':')))
        .collect::<String>()
}

struct CustomInfo {
    ty: String,
}

/// Recursively checks whether any handler in the tree has `cache_seconds > 0`.
fn has_cache_handlers_kt(handlers: &[Handler]) -> bool {
    for h in handlers {
        if h.meta.cache_seconds > 0 {
            return true;
        }
        if has_cache_handlers_kt(&h.children) {
            return true;
        }
    }
    false
}

fn collect_customs(handlers: &[Handler]) -> Vec<CustomInfo> {
    let mut customs: Vec<CustomInfo> = Vec::new();
    collect_customs_recursive(handlers, &mut customs);
    customs
}

fn collect_customs_recursive(handlers: &[Handler], customs: &mut Vec<CustomInfo>) {
    for h in handlers {
        for param in h.meta.params {
            if param.extractor == "Custom" {
                let ty = param.ty.to_string();
                if !customs.iter().any(|c| c.ty == ty) {
                    customs.push(CustomInfo { ty });
                }
            }
        }
        collect_customs_recursive(&h.children, customs);
    }
}

#[cfg(feature = "ordinary-http")]
fn collect_headers(handlers: &[Handler]) -> Vec<CustomInfo> {
    let mut headers: Vec<CustomInfo> = Vec::new();
    collect_headers_recursive(handlers, &mut headers);
    headers
}

#[cfg(feature = "ordinary-http")]
fn collect_headers_recursive(handlers: &[Handler], headers: &mut Vec<CustomInfo>) {
    for h in handlers {
        for param in h.meta.params {
            if param.extractor == "Header" {
                let ty = param.ty.to_string();
                if !headers.iter().any(|c| c.ty == ty) {
                    headers.push(CustomInfo { ty });
                }
            }
        }
        collect_headers_recursive(&h.children, headers);
    }
}

fn custom_index(handlers: &[Handler], ty: &str) -> usize {
    let customs = collect_customs(handlers);
    customs.iter().position(|c| c.ty == ty).unwrap_or(0)
}

// ─── Type definition generation ─────────────────────────────────────
// Emits Kotlin data class (struct) or sealed class (enum) definitions
// from the TagMeta metadata produced by #[derive(Tag)].

fn kt_type_def(meta: &TagMeta) -> String {
    match meta.kind {
        TagKind::Struct(fields) => {
            let mut field_lines = Vec::new();
            for field in included(fields) {
                let kt_ty = rust_type_to_kt(field.ty);
                field_lines.push(format!("    val {}: {}", to_camel_case(field.name), kt_ty));
            }
            format!("data class {}(\n{}\n)", meta.name, field_lines.join(",\n"))
        }
        TagKind::Enum(variants) => {
            let mut variant_lines = Vec::new();
            for variant in variants {
                if variant.fields.is_empty() {
                    variant_lines.push(format!(
                        "    data object {} : {}()",
                        variant.name, meta.name
                    ));
                } else if variant.fields.len() == 1 && variant.fields[0].name.starts_with("__") {
                    let kt_ty = rust_type_to_kt(variant.fields[0].ty);
                    variant_lines.push(format!(
                        "    data class {}(val __0: {}) : {}()",
                        variant.name, kt_ty, meta.name
                    ));
                } else {
                    let mut field_entries = Vec::new();
                    for field in included(variant.fields) {
                        let kt_ty = rust_type_to_kt(field.ty);
                        field_entries.push(format!(
                            "        val {}: {}",
                            to_camel_case(field.name),
                            kt_ty
                        ));
                    }
                    variant_lines.push(format!(
                        "    data class {}(\n{}\n    ) : {}()",
                        variant.name,
                        field_entries.join(",\n"),
                        meta.name
                    ));
                }
            }
            format!(
                "sealed class {} {{\n{}\n}}",
                meta.name,
                variant_lines.join("\n")
            )
        }
    }
}

fn kt_type_def_named(type_name: &str, meta: &TagMeta) -> String {
    match meta.kind {
        TagKind::Struct(fields) => {
            let mut field_lines = Vec::new();
            for field in included(fields) {
                let kt_ty = rust_type_to_kt(field.ty);
                field_lines.push(format!("    val {}: {}", to_camel_case(field.name), kt_ty));
            }
            format!("data class {}(\n{}\n)", type_name, field_lines.join(",\n"))
        }
        TagKind::Enum(variants) => {
            let mut variant_lines = Vec::new();
            for variant in variants {
                if variant.fields.is_empty() {
                    variant_lines.push(format!(
                        "    data object {} : {}()",
                        variant.name, type_name
                    ));
                } else if variant.fields.len() == 1 && variant.fields[0].name.starts_with("__") {
                    let kt_ty = rust_type_to_kt(variant.fields[0].ty);
                    variant_lines.push(format!(
                        "    data class {}(val __0: {}) : {}()",
                        variant.name, kt_ty, type_name
                    ));
                } else {
                    let mut field_entries = Vec::new();
                    for field in included(variant.fields) {
                        let kt_ty = rust_type_to_kt(field.ty);
                        field_entries.push(format!(
                            "        val {}: {}",
                            to_camel_case(field.name),
                            kt_ty
                        ));
                    }
                    variant_lines.push(format!(
                        "    data class {}(\n{}\n    ) : {}()",
                        variant.name,
                        field_entries.join(",\n"),
                        type_name
                    ));
                }
            }
            format!(
                "sealed class {} {{\n{}\n}}",
                type_name,
                variant_lines.join("\n")
            )
        }
    }
}

// ─── Type exports (recursive) ───────────────────────────────────────
// Walks a handler's parameter and return types, emitting Kotlin data
// class / sealed class definitions for every structured type referenced.
// Nested types discovered via FieldMeta.structure function pointers are
// recursively emitted so the generated file is self-contained.

fn handler_type_exports_kt(prefix: &str, meta: &HandlerMeta, emitted: &mut Vec<String>) -> String {
    let mut lines = CodeBuf::new();

    let mut data_idx = 0;
    for param in meta.params {
        match param.extractor {
            "Custom" => {
                // Custom types are emitted in common.kt, not repeated
                // in per-service files.
            }
            "Data" | "Query" | "Param" | "Body" => {
                if let Some(structure_fn) = param.structure {
                    let structure = structure_fn();
                    let type_name = if meta.is_ordinary {
                        let suffix = ordinary_suffix(param.extractor, data_idx);
                        if param.extractor == "Data" {
                            data_idx += 1;
                        }
                        format!("{}{}", prefix, suffix)
                    } else {
                        prefixed_type(prefix, structure.name)
                    };
                    if !emitted.contains(&type_name) {
                        lines.push(kt_type_def_named(&type_name, structure));
                        emitted.push(type_name.clone());
                        emitted.push(structure.name.to_string());
                        extract_nested_types_kt(structure, &mut lines, emitted);
                    }
                }
            }
            _ => {}
        }
    }

    if meta.return_type != "()"
        && let Some(structure_fn) = meta.return_structure
    {
        let structure = structure_fn();
        let type_name = if meta.is_ordinary {
            format!("{}Response", prefix)
        } else {
            prefixed_type(prefix, structure.name)
        };
        if !emitted.contains(&type_name) {
            lines.push(kt_type_def_named(&type_name, structure));
            emitted.push(type_name.clone());
            emitted.push(structure.name.to_string());
            extract_nested_types_kt(structure, &mut lines, emitted);
        }
    }

    lines.build()
}

fn extract_nested_types_kt(meta: &'static TagMeta, lines: &mut CodeBuf, emitted: &mut Vec<String>) {
    if !emitted.contains(&meta.name.to_string()) {
        emitted.push(meta.name.to_string());
        lines.push(kt_type_def(meta));
    }
    match meta.kind {
        TagKind::Struct(fields) => {
            for field in included(fields) {
                if let Some(structure_fn) = field.structure {
                    extract_nested_types_kt(structure_fn(), lines, emitted);
                }
            }
        }
        TagKind::Enum(variants) => {
            for variant in variants {
                for field in included(variant.fields) {
                    if let Some(structure_fn) = field.structure {
                        extract_nested_types_kt(structure_fn(), lines, emitted);
                    }
                }
            }
        }
    }
}

// ─── Response deserialization ───────────────────────────────────────
// Generates Kotlin expressions that read a binary response from a
// BinaryReader and reconstruct the corresponding Kotlin value.
// The generated code mirrors the server-side AFastSerialize wire format.

fn response_expr_kt(
    reader: &str,
    ty: &str,
    indent: &str,
    structure: Option<fn() -> &'static TagMeta>,
    override_name: Option<&str>,
) -> String {
    let ty = normalize_rust_type(ty);
    match ty.as_str() {
        "i8" => format!("{}.rI8().toByte()", reader),
        "i16" => format!("{}.rI16().toShort()", reader),
        "i32" => format!("{}.rI32()", reader),
        "i64" => format!("{}.rI64()", reader),
        "u8" => format!("{}.rU8().toByte()", reader),
        "u16" => format!("{}.rU16().toShort()", reader),
        "u32" => format!("{}.rU32()", reader),
        "u64" => format!("{}.rU64()", reader),
        "usize" => format!("{}.rU64()", reader),
        "f32" => format!("{}.rF32()", reader),
        "f64" => format!("{}.rF64()", reader),
        "bool" => format!("{}.rB()", reader),
        "String" => format!("{}.rS()", reader),
        "&str" => format!("{}.rS()", reader),
        "Vec<u8>" => format!("{}.rBytes({}.rU32().toInt())", reader, reader),
        s if s.starts_with("Vec<") => {
            let inner = &s[4..s.len() - 1];
            let inner_override = override_name.map(|n| {
                n.trim_end_matches('?')
                    .trim_start_matches("List<")
                    .trim_end_matches('>')
            });
            let elem = response_expr_kt(reader, inner, indent, structure, inner_override);
            format!("(0 until {}.rU32()).map {{ {} }}", reader, elem)
        }
        s if s.starts_with("Option<") => {
            let inner = &s[7..s.len() - 1];
            // Strip trailing '?' from override_name for the inner type
            let inner_override = override_name.map(|n| n.trim_end_matches('?'));
            let body = response_expr_kt(reader, inner, indent, structure, inner_override);
            format!("if ({}.rU8() == 1) {} else null", reader, body)
        }
        _ => {
            if let Some(s) = structure {
                let meta = s();
                let type_name = override_name.unwrap_or(meta.name);
                match meta.kind {
                    TagKind::Struct(fields) => {
                        let inner_indent = format!("{}    ", indent);
                        let field_lines: Vec<String> = included(fields)
                            .map(|f| {
                                let expr = response_expr_kt(
                                    reader,
                                    f.ty,
                                    &inner_indent,
                                    f.structure,
                                    None,
                                );
                                format!("{}    {} = {}", inner_indent, to_camel_case(f.name), expr)
                            })
                            .collect();
                        format!("{}({}\n{})", type_name, field_lines.join(",\n"), indent)
                    }
                    TagKind::Enum(variants) => {
                        let tr = tag_read();
                        let inner_indent = format!("{}    ", indent);
                        let deep_indent = format!("{}        ", indent);
                        let mut arms = Vec::new();
                        for (i, variant) in variants.iter().enumerate() {
                            if variant.fields.is_empty() {
                                arms.push(format!(
                                    "{}    {} -> {}.{}",
                                    deep_indent, i, type_name, variant.name
                                ));
                            } else if variant.fields.len() == 1
                                && variant.fields[0].name.starts_with("__")
                            {
                                let expr = response_expr_kt(
                                    reader,
                                    variant.fields[0].ty,
                                    &deep_indent,
                                    variant.fields[0].structure,
                                    None,
                                );
                                arms.push(format!(
                                    "{}    {} -> {}.{}({})",
                                    deep_indent, i, type_name, variant.name, expr
                                ));
                            } else {
                                let mut field_entries = Vec::new();
                                for field in included(variant.fields) {
                                    let expr = response_expr_kt(
                                        reader,
                                        field.ty,
                                        &format!("{}        ", inner_indent),
                                        field.structure,
                                        None,
                                    );
                                    field_entries.push(format!(
                                        "{}        {} = {}",
                                        deep_indent,
                                        to_camel_case(field.name),
                                        expr
                                    ));
                                }
                                arms.push(format!(
                                    "{}    {} -> {}.{}(\n{}\n{}    )",
                                    deep_indent,
                                    i,
                                    type_name,
                                    variant.name,
                                    field_entries.join(",\n"),
                                    deep_indent
                                ));
                            }
                        }
                        arms.push(format!(
                            "{}    else -> throw RuntimeException(\"unknown enum tag\")",
                            deep_indent
                        ));
                        format!(
                            "when ({}.{}()) {{\n{}\n{}}}",
                            reader,
                            tr,
                            arms.join("\n"),
                            inner_indent
                        )
                    }
                }
            } else {
                format!("{}.rBytes({}.rU32().toInt())", reader, reader)
            }
        }
    }
}

fn generate_return_kt(
    lines: &mut CodeBuf,
    reader: &str,
    ty: &str,
    resp_type: &str,
    indent: &str,
    structure: Option<fn() -> &'static TagMeta>,
) {
    if ty == "()" {
        lines.push(format!("{}return", indent));
        return;
    }
    let expr = response_expr_kt(reader, ty, indent, structure, Some(resp_type));
    lines.push(format!("{}return {}", indent, expr));
}

/// Like `generate_return_kt` but assigns to `_result` instead of returning.
/// Used by cached handlers that need to store the result before returning.
fn generate_return_kt_assign(
    lines: &mut CodeBuf,
    reader: &str,
    ty: &str,
    resp_type: &str,
    indent: &str,
    structure: Option<fn() -> &'static TagMeta>,
) {
    if ty == "()" {
        lines.push(format!("{}val _result = Unit", indent));
        return;
    }
    let expr = response_expr_kt(reader, ty, indent, structure, Some(resp_type));
    lines.push(format!("{}val _result = {}", indent, expr));
}

// ─── Request serialization ──────────────────────────────────────────
// Generates Kotlin statements that write a request value into a
// BinaryWriter.  The serialization order matches the server-side
// AFastDeserialize wire format so the server can decode the payload.

fn generate_request_serialize_kt(
    lines: &mut CodeBuf,
    var: &str,
    ty: &str,
    indent: &str,
    structure: Option<fn() -> &'static TagMeta>,
) {
    let ty = normalize_rust_type(ty);
    match ty.as_str() {
        "i8" => lines.push(format!("{}w.wI8({});", indent, var)),
        "i16" => lines.push(format!("{}w.wI16({});", indent, var)),
        "i32" => lines.push(format!("{}w.wI32({});", indent, var)),
        "i64" => lines.push(format!("{}w.wI64({});", indent, var)),
        "u8" => lines.push(format!("{}w.wU8({});", indent, var)),
        "u16" => lines.push(format!("{}w.wU16({});", indent, var)),
        "u32" => lines.push(format!("{}w.wU32({});", indent, var)),
        "u64" => lines.push(format!("{}w.wU64({});", indent, var)),
        "usize" => lines.push(format!("{}w.wU64({}.toLong());", indent, var)),
        "f32" => lines.push(format!("{}w.wF32({});", indent, var)),
        "f64" => lines.push(format!("{}w.wF64({});", indent, var)),
        "bool" => lines.push(format!("{}w.wB({});", indent, var)),
        "String" => lines.push(format!("{}w.wS({});", indent, var)),
        "&str" => lines.push(format!("{}w.wS({});", indent, var)),
        s if s.starts_with("Vec<") => {
            let inner = &s[4..s.len() - 1];
            lines.push(format!("{}w.wU32({}.size);", indent, var));
            lines.push(format!("{}for (_e in {}) {{", indent, var));
            generate_request_serialize_kt(
                lines,
                "_e",
                inner,
                &format!("{}    ", indent),
                structure,
            );
            lines.push(format!("{}}}", indent));
        }
        s if s.starts_with("Option<") => {
            let inner = &s[7..s.len() - 1];
            lines.push(format!(
                "{}if ({} == null) {{ w.wU8(0) }} else {{ w.wU8(1)",
                indent, var
            ));
            generate_request_serialize_kt(lines, var, inner, &format!("{}    ", indent), structure);
            lines.push(format!("{}}}", indent));
        }
        _ => {
            if let Some(s) = structure {
                let meta = s();
                match meta.kind {
                    TagKind::Struct(fields) => {
                        for field in included(fields) {
                            let field_var = format!("{}.{}", var, to_camel_case(field.name));
                            generate_request_serialize_kt(
                                lines,
                                &field_var,
                                field.ty,
                                indent,
                                field.structure,
                            );
                        }
                    }
                    TagKind::Enum(variants) => {
                        let tw = tag_write();
                        let deep_indent = format!("{}        ", indent);
                        lines.push(format!("{}when ({}) {{", indent, var));
                        for (i, variant) in variants.iter().enumerate() {
                            if variant.fields.is_empty() {
                                lines.push(format!(
                                    "{}    is {}.{Variant} -> w.{tw}({i})",
                                    deep_indent,
                                    meta.name,
                                    Variant = variant.name,
                                    tw = tw,
                                    i = i
                                ));
                            } else if variant.fields.len() == 1
                                && variant.fields[0].name.starts_with("__")
                            {
                                lines.push(format!(
                                    "{}    is {}.{Variant} -> {{",
                                    deep_indent,
                                    meta.name,
                                    Variant = variant.name
                                ));
                                lines.push(format!(
                                    "{}        w.{tw}({i});",
                                    deep_indent,
                                    tw = tw,
                                    i = i
                                ));
                                let field_var =
                                    format!("{}.{}", var, to_camel_case(variant.fields[0].name));
                                generate_request_serialize_kt(
                                    lines,
                                    &field_var,
                                    variant.fields[0].ty,
                                    &format!("{}        ", indent),
                                    variant.fields[0].structure,
                                );
                                lines.push(format!("{}    }}", deep_indent));
                            } else {
                                lines.push(format!(
                                    "{}    is {}.{Variant} -> {{",
                                    deep_indent,
                                    meta.name,
                                    Variant = variant.name
                                ));
                                lines.push(format!(
                                    "{}        w.{tw}({i});",
                                    deep_indent,
                                    tw = tw,
                                    i = i
                                ));
                                for field in included(variant.fields) {
                                    let field_var =
                                        format!("{}.{}", var, to_camel_case(field.name));
                                    generate_request_serialize_kt(
                                        lines,
                                        &field_var,
                                        field.ty,
                                        &format!("{}        ", indent),
                                        field.structure,
                                    );
                                }
                                lines.push(format!("{}    }}", deep_indent));
                            }
                        }
                        lines.push(format!("{}}}", indent));
                    }
                }
            } else {
                lines.push(format!("{}w.wBytes({}._raw);", indent, var));
            }
        }
    }
}

// ─── Validation code generation ────────────────────────────────────
// Emits Kotlin validation checks that run client-side before sending
// a request.  Each ValidateRule (Gt, Gte, Lt, Lte, Len, Of) produces
// a conditional that throws AFastValidationError on failure.
// Struct and enum nesting is handled recursively.

fn generate_validation_kt(
    lines: &mut CodeBuf,
    var_prefix: &str,
    fields: &[crate::handler::FieldMeta],
    indent: &str,
) {
    for field in included(fields) {
        let field_ty = normalize_rust_type(field.ty);
        let is_option = field_ty.starts_with("Option<");
        let field_path = format!("{}.{}", var_prefix, to_camel_case(field.name));

        if is_option && !field.validations.is_empty() {
            let tmp = format!("_tmp{}", to_pascal_case(field.name));
            lines.push(format!(
                "{indent}if ({field_path} != null) {{ val {tmp} = {field_path}!!",
                indent = indent,
                field_path = field_path,
                tmp = tmp,
            ));
            let inner_indent = format!("{}    ", indent);
            emit_validation_checks_kt(lines, &tmp, field.name, field.validations, &inner_indent);
            if let Some(structure_fn) = field.structure {
                let structure = structure_fn();
                match structure.kind {
                    crate::handler::TagKind::Struct(nested_fields) => {
                        generate_validation_kt(lines, &tmp, nested_fields, &inner_indent);
                    }
                    crate::handler::TagKind::Enum(variants) => {
                        generate_enum_validation_kt(
                            lines,
                            &tmp,
                            structure.name,
                            variants,
                            &inner_indent,
                        );
                    }
                }
            }
            lines.push(format!("{indent}}}", indent = indent));
        } else {
            if !field.validations.is_empty() {
                emit_validation_checks_kt(
                    lines,
                    &field_path,
                    field.name,
                    field.validations,
                    indent,
                );
            }
            if let Some(structure_fn) = field.structure {
                let structure = structure_fn();
                let prefix = if is_option {
                    let tmp = format!("_tmp{}", to_pascal_case(field.name));
                    lines.push(format!(
                        "{indent}if ({field_path} != null) {{ val {tmp} = {field_path}!!",
                        indent = indent,
                        field_path = field_path,
                        tmp = tmp,
                    ));
                    tmp
                } else {
                    field_path.clone()
                };
                let inner_indent = if is_option {
                    format!("{}    ", indent)
                } else {
                    indent.to_string()
                };
                match structure.kind {
                    crate::handler::TagKind::Struct(nested_fields) => {
                        generate_validation_kt(lines, &prefix, nested_fields, &inner_indent);
                    }
                    crate::handler::TagKind::Enum(variants) => {
                        generate_enum_validation_kt(
                            lines,
                            &prefix,
                            structure.name,
                            variants,
                            &inner_indent,
                        );
                    }
                }
                if is_option {
                    lines.push(format!("{indent}}}", indent = indent));
                }
            }
        }
    }
}

fn emit_validation_checks_kt(
    lines: &mut CodeBuf,
    field_path: &str,
    field_name: &str,
    validations: &[crate::handler::ValidateRule],
    indent: &str,
) {
    for rule in validations {
        match rule {
            crate::handler::ValidateRule::Gt {
                value,
                code,
                message,
            } => {
                lines.push(format!(
                    "{indent}if (!({field_path} > {value}L)) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                    indent = indent, field_path = field_path, value = value,
                    code = code, field_name = field_name, message = message,
                ));
            }
            crate::handler::ValidateRule::Gte {
                value,
                code,
                message,
            } => {
                lines.push(format!(
                    "{indent}if (!({field_path} >= {value}L)) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                    indent = indent, field_path = field_path, value = value,
                    code = code, field_name = field_name, message = message,
                ));
            }
            crate::handler::ValidateRule::Lt {
                value,
                code,
                message,
            } => {
                lines.push(format!(
                    "{indent}if (!({field_path} < {value}L)) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                    indent = indent, field_path = field_path, value = value,
                    code = code, field_name = field_name, message = message,
                ));
            }
            crate::handler::ValidateRule::Lte {
                value,
                code,
                message,
            } => {
                lines.push(format!(
                    "{indent}if (!({field_path} <= {value}L)) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                    indent = indent, field_path = field_path, value = value,
                    code = code, field_name = field_name, message = message,
                ));
            }
            crate::handler::ValidateRule::Len {
                min,
                max,
                code,
                message,
            } => {
                if *min >= 0 && *max >= 0 {
                    lines.push(format!(
                        "{indent}if ({field_path}.length < {min} || {field_path}.length > {max}) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                        indent = indent, field_path = field_path, min = min, max = max,
                        code = code, field_name = field_name, message = message,
                    ));
                } else if *min >= 0 {
                    lines.push(format!(
                        "{indent}if ({field_path}.length < {min}) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                        indent = indent, field_path = field_path, min = min,
                        code = code, field_name = field_name, message = message,
                    ));
                } else if *max >= 0 {
                    lines.push(format!(
                        "{indent}if ({field_path}.length > {max}) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                        indent = indent, field_path = field_path, max = max,
                        code = code, field_name = field_name, message = message,
                    ));
                }
            }
            crate::handler::ValidateRule::Of {
                values,
                code,
                message,
            } => {
                let list = values.join(", ");
                lines.push(format!(
                    "{indent}if ({field_path} !in listOf({list})) throw AFastValidationError({code}L, \"{field_name}\", \"{message}\")",
                    indent = indent, list = list, field_path = field_path,
                    code = code, field_name = field_name, message = message,
                ));
            }
        }
    }
}

fn generate_enum_validation_kt(
    lines: &mut CodeBuf,
    var_prefix: &str,
    enum_name: &str,
    variants: &[crate::handler::EnumVariantMeta],
    indent: &str,
) {
    for variant in variants {
        if variant.fields.is_empty() {
            continue;
        }
        let variant_tag = format!("{}.{}", enum_name, variant.name);
        if variant.fields.len() == 1 && variant.fields[0].name.starts_with("__") {
            let inner = &variant.fields[0];
            let inner_path = format!("{}.{}", var_prefix, to_camel_case(inner.name));
            if !inner.validations.is_empty() {
                lines.push(format!(
                    "{indent}if ({var_prefix} is {variant_tag}) {{",
                    indent = indent,
                    var_prefix = var_prefix,
                    variant_tag = variant_tag,
                ));
                let inner_indent = format!("{}    ", indent);
                emit_validation_checks_kt(
                    lines,
                    &inner_path,
                    inner.name,
                    inner.validations,
                    &inner_indent,
                );
                lines.push(format!("{indent}}}"));
            }
            if let Some(structure_fn) = inner.structure {
                let structure = structure_fn();
                match structure.kind {
                    crate::handler::TagKind::Struct(nested_fields) => {
                        lines.push(format!(
                            "{indent}if ({var_prefix} is {variant_tag}) {{",
                            indent = indent,
                            var_prefix = var_prefix,
                            variant_tag = variant_tag,
                        ));
                        let inner_indent = format!("{}    ", indent);
                        generate_validation_kt(lines, var_prefix, nested_fields, &inner_indent);
                        lines.push(format!("{indent}}}"));
                    }
                    crate::handler::TagKind::Enum(nested_variants) => {
                        lines.push(format!(
                            "{indent}if ({var_prefix} is {variant_tag}) {{",
                            indent = indent,
                            var_prefix = var_prefix,
                            variant_tag = variant_tag,
                        ));
                        let inner_indent = format!("{}    ", indent);
                        generate_enum_validation_kt(
                            lines,
                            var_prefix,
                            structure.name,
                            nested_variants,
                            &inner_indent,
                        );
                        lines.push(format!("{indent}}}"));
                    }
                }
            }
        } else {
            let has_work = variant
                .fields
                .iter()
                .any(|f| !f.validations.is_empty() || f.structure.is_some());
            if !has_work {
                continue;
            }
            lines.push(format!(
                "{indent}if ({var_prefix} is {variant_tag}) {{",
                indent = indent,
                var_prefix = var_prefix,
                variant_tag = variant_tag,
            ));
            let inner_indent = format!("{}    ", indent);
            generate_validation_kt(lines, var_prefix, variant.fields, &inner_indent);
            lines.push(format!("{indent}}}"));
        }
    }
}

// ─── Handler method generation ──────────────────────────────────────
// Produces a single suspend fun for a binary-protocol handler.
// The generated method serialises Data parameters and Custom values
// into a binary payload, calls _call(handlerId, payload), then
// deserialises the response.  Long-connection handlers return an
// AfastSocket instead of a deserialised value.

fn handler_method_kt(
    handler: &Handler,
    all_handlers: &[Handler],
    prefix: &str,
    base_indent: &str,
    svc_name: &str,
    class_name: &str,
    cache_key: &str,
) -> String {
    let meta = handler.meta;
    let func_name = if !meta.api_name.is_empty() {
        meta.api_name
    } else {
        meta.name
    };
    let cache_seconds = meta.cache_seconds;
    let id = handler.stable_id;
    let indent = format!("{}    ", base_indent);

    let mut custom_indices: Vec<(usize, &str, Option<fn() -> &'static TagMeta>)> = Vec::new();
    let mut data_params: Vec<(String, &ParamMeta)> = Vec::new();

    for param in meta.params {
        match param.extractor {
            "State" | "Receiver" | "Sender" => continue,
            "Custom" => {
                let idx = custom_index(all_handlers, param.ty);
                custom_indices.push((idx, param.ty, param.structure));
            }
            "Data" => {
                let idx = data_params.len();
                let var = if idx == 0 {
                    "request".to_string()
                } else {
                    format!("request{}", idx + 1)
                };
                data_params.push((var, param));
            }
            _ => {}
        }
    }

    // Build the function parameter list.  Only Data extractors become
    // explicit parameters; State, Receiver, Sender, and Custom are
    // injected by the framework or the caller.
    let mut fn_params = Vec::new();
    for &(ref var, param) in &data_params {
        if let Some(structure_fn) = param.structure {
            let structure = structure_fn();
            let kt_ty = type_name_for(prefix, structure);
            fn_params.push(format!("{}: {}", var, kt_ty));
        }
    }

    // Long-connection handlers require a callback that receives raw
    // push messages and a send function.
    if meta.long_connection {
        fn_params.push("callback: (ByteArray, (Any) -> Unit) -> Unit".to_string());
    }

    // Cache: add force param
    if cache_seconds > 0 {
        fn_params.push("force: Boolean = false".to_string());
    }

    let params_str = fn_params.join(", ");

    // Determine the Kotlin return type.  Long-connection handlers
    // return AfastSocket; ordinary handlers return the deserialised
    // response type (or Unit for void).  Option<T> maps to nullable T?.
    let is_option_return =
        meta.return_type.starts_with("Option<") || meta.return_type.starts_with("Option <");
    let return_type = if meta.long_connection {
        "AfastSocket".to_string()
    } else {
        if let Some(structure_fn) = meta.return_structure {
            let structure = structure_fn();
            let base = type_name_for(prefix, structure);
            if is_option_return {
                format!("{}?", base)
            } else {
                base
            }
        } else {
            "Unit".to_string()
        }
    };

    // Build the method body.  The generated code:
    // 1. Invokes Custom provider functions.
    // 2. Validates Custom and Data parameters.
    // 3. Serialises everything into a BinaryWriter.
    // 4. Calls _call(handlerId, payload).
    // 5. Deserialises the response (or creates an AfastSocket).
    let ind = format!("{}    ", indent);
    let mut body_lines = CodeBuf::new();

    // Cache check (before serialization)
    if cache_seconds > 0 {
        let cache_key_parts: Vec<String> = data_params.iter().map(|(v, _)| v.clone()).collect();
        let req_expr = if cache_key_parts.is_empty() {
            "\"[]\"".to_string()
        } else if cache_key_parts.len() == 1 {
            format!("{}.toString()", cache_key_parts[0])
        } else {
            format!("listOf({}).toString()", cache_key_parts.join(", "))
        };
        body_lines.push(format!("{}val _cacheKey = \"{}\"", ind, cache_key));
        body_lines.push(format!("{}val _currentReq = {}", ind, req_expr));
        body_lines.push(format!("{}if (!force) {{", ind));
        body_lines.push(format!(
            "{}    val _cached = {client}Client._cache[_cacheKey]",
            ind,
            client = class_name
        ));
        body_lines.push(format!(
            "{}    if (_cached != null && _currentReq == _cached.first && System.currentTimeMillis() < _cached.second.first) {{",
            ind
        ));
        let cast_return = if meta.long_connection || meta.return_type == "()" {
            "".to_string()
        } else {
            format!(" as {}", return_type)
        };
        body_lines.push(format!(
            "{}        return _cached.second.second{}",
            ind, cast_return
        ));
        body_lines.push(format!("{}    }}", ind));
        body_lines.push(format!("{}}}", ind));
    }

    body_lines.push(format!("{}val w = BinaryWriter()", ind));

    // Execute Custom provider functions to obtain authentication or
    // context values before serialisation begins.
    for &(_ci, ty, _structure) in &custom_indices {
        let var = format!("_c{}", ty);
        body_lines.push(format!(
            "{}val {} = customs[\"{}\"]!!() as {}",
            ind, var, ty, ty
        ));
    }

    // Run client-side validation on Custom parameters.
    for &(_ci, ty, structure) in &custom_indices {
        let var = format!("_c{}", ty);
        if let Some(structure_fn) = structure {
            let meta = structure_fn();
            if let TagKind::Struct(fields) = meta.kind {
                generate_validation_kt(&mut body_lines, &var, fields, &ind);
            }
        }
    }

    // Run client-side validation on Data parameters.
    for &(ref var, param) in &data_params {
        if let Some(structure_fn) = param.structure {
            let structure = structure_fn();
            if let TagKind::Struct(fields) = structure.kind {
                generate_validation_kt(&mut body_lines, var, fields, &ind);
            }
        }
    }

    // Serialise Custom struct fields into the binary payload.
    for &(_ci, ty, structure) in &custom_indices {
        let var = format!("_c{}", ty);
        if let Some(structure_fn) = structure {
            let meta = structure_fn();
            if let TagKind::Struct(fields) = meta.kind {
                for field in included(fields) {
                    let field_var = format!("{}.{}", var, to_camel_case(field.name));
                    generate_request_serialize_kt(
                        &mut body_lines,
                        &field_var,
                        field.ty,
                        &ind,
                        field.structure,
                    );
                }
            }
        }
    }

    // Serialise Data struct fields into the binary payload.
    for &(ref var, param) in &data_params {
        generate_request_serialize_kt(&mut body_lines, var, param.ty, &ind, param.structure);
    }

    // Finalise the binary payload from the writer.
    body_lines.push(format!("{}val data = w.toBytes()", ind));

    // Dispatch through the transport layer.  handler_id (stable_id) is a
    // u32 that identifies the target handler on the server.
    // Append 'L' to the literal because u32 values can exceed Int.MAX_VALUE.
    body_lines.push(format!("{}val resp = _call({}L, data)", ind, id));

    if meta.long_connection {
        body_lines.push(format!("{}val r = BinaryReader(resp)", ind));
        body_lines.push(format!("{}val connId = r.rU32()", ind));
        body_lines.push(format!(
            "{}val socket = AfastSocket(connId, {{ d -> this@{}Client._sendRaw(d) }}, callback)",
            ind,
            to_pascal_case(svc_name)
        ));
        body_lines.push(format!(
            "{}pushHandlers[connId] = {{ raw -> socket._onMessage(raw) }}",
            ind
        ));
        body_lines.push(format!("{}return socket", ind));
    } else {
        body_lines.push(format!("{}val r = BinaryReader(resp)", ind));
        if cache_seconds > 0 {
            generate_return_kt_assign(
                &mut body_lines,
                "r",
                meta.return_type,
                &return_type,
                &ind,
                meta.return_structure,
            );
            body_lines.push(format!(
                "{ind}{client}Client._cache[_cacheKey] = _currentReq to (System.currentTimeMillis() + {cache_ms}L to _result)",
                ind = ind,
                client = class_name,
                cache_ms = cache_seconds * 1000,
            ));
            body_lines.push(format!("{}return _result", ind));
        } else {
            generate_return_kt(
                &mut body_lines,
                "r",
                meta.return_type,
                &return_type,
                &ind,
                meta.return_structure,
            );
        }
    }

    let body = body_lines.build();

    // Every generated method is suspend because the underlying _call
    // function (which performs network I/O) is a suspend function.
    let suspend_kw = "suspend ";

    format!(
        "{indent}{suspend_kw}fun {func_name}({params_str}): {return_type} {{\n{body}\n{indent}}}",
        indent = indent,
        suspend_kw = suspend_kw,
        func_name = func_name,
        params_str = params_str,
        return_type = return_type,
        body = body,
    )
}

// ─── Ordinary HTTP handler method (KT) ──────────────────────────────
// Produces a suspend fun for an ordinary (REST-style) HTTP handler.
// Unlike binary-protocol handlers, these use standard HTTP methods
// (GET/POST/PUT/DELETE/PATCH), path parameters, query strings, JSON
// request bodies, and JSON responses via Gson.

#[cfg(feature = "ordinary-http")]
fn ordinary_handler_method_kt(
    handler: &Handler,
    prefix: &str,
    group_path: &[&str],
    base_indent: &str,
    _svc_name: &str,
    debug: bool,
    class_name: &str,
    cache_key: &str,
) -> String {
    let meta = handler.meta;
    let func_name = if !meta.api_name.is_empty() {
        meta.api_name
    } else {
        meta.name
    };
    let cache_seconds = meta.cache_seconds;
    let method = if meta.method.is_empty() {
        "GET"
    } else {
        meta.method
    };
    let indent = format!("{}    ", base_indent);
    let ind = format!("{}    ", indent);

    let mut param_param: Option<&ParamMeta> = None;
    let mut query_param: Option<&ParamMeta> = None;
    let mut body_param: Option<&ParamMeta> = None;

    for param in meta.params {
        match param.extractor {
            "Param" => param_param = Some(param),
            "Query" => query_param = Some(param),
            "Body" => body_param = Some(param),
            _ => {}
        }
    }

    // Build the function parameter list from Param, Query, and Body
    // extractors.  Each becomes a named parameter with a type derived
    // from the handler prefix and the extractor kind.
    let kt_type_of = |p: &ParamMeta, suffix: &str| -> String {
        if p.structure.is_some() {
            format!("{}{}", prefix, suffix)
        } else {
            rust_type_to_kt(p.ty)
        }
    };
    let mut fn_params: Vec<String> = Vec::new();
    if let Some(p) = param_param {
        fn_params.push(format!("params: {}", kt_type_of(p, "PathParams")));
    }
    if let Some(p) = query_param {
        fn_params.push(format!("queries: {}", kt_type_of(p, "Query")));
    }
    if let Some(p) = body_param {
        fn_params.push(format!("body: {}", kt_type_of(p, "Body")));
    }
    if cache_seconds > 0 {
        fn_params.push("force: Boolean = false".to_string());
    }
    let params_str = fn_params.join(", ");

    // Determine the Kotlin return type.  Structured returns use the
    // "{Prefix}Response" naming convention.  Special types (Text, Html,
    // File, Status, Redirect) map to their Kotlin equivalents.
    let kt_return = if meta.return_structure.is_some() {
        format!("{}Response", prefix)
    } else if let Some(inner) = unwrap_json_type(meta.return_type) {
        rust_type_to_kt(&inner)
    } else {
        let normalized = normalize_rust_type(meta.return_type);
        match normalized.as_str() {
            "()" => "Unit".to_string(),
            "Text" | "Html" => "String".to_string(),
            "File" => "ByteArray".to_string(),
            "Status" | "Redirect" => "Unit".to_string(),
            _ if !normalized.is_empty() => rust_type_to_kt(&normalized),
            _ => "Unit".to_string(),
        }
    };

    // Build the method body.  The generated code:
    // 1. Builds the URL with substituted path parameters.
    // 2. Appends query string values.
    // 3. Opens an HttpURLConnection with the declared HTTP method.
    // 4. Sends a JSON body (for POST/PUT/PATCH).
    // 5. Reads and deserialises the JSON response.
    let mut body_lines = CodeBuf::new();

    // Cache check (before I/O dispatch)
    if cache_seconds > 0 {
        let mut req_var_names: Vec<String> = Vec::new();
        if param_param.is_some() {
            req_var_names.push("params".to_string());
        }
        if query_param.is_some() {
            req_var_names.push("queries".to_string());
        }
        if body_param.is_some() {
            req_var_names.push("body".to_string());
        }
        let req_expr = if req_var_names.is_empty() {
            "\"[]\"".to_string()
        } else if req_var_names.len() == 1 {
            format!("{}.toString()", req_var_names[0])
        } else {
            format!("listOf({}).toString()", req_var_names.join(", "))
        };
        body_lines.push(format!("{}val _cacheKey = \"{}\"", ind, cache_key));
        body_lines.push(format!("{}val _currentReq = {}", ind, req_expr));
        body_lines.push(format!("{}if (!force) {{", ind));
        body_lines.push(format!(
            "{}    val _cached = {client}Client._cache[_cacheKey]",
            ind,
            client = class_name
        ));
        body_lines.push(format!(
            "{}    if (_cached != null && _currentReq == _cached.first && System.currentTimeMillis() < _cached.second.first) {{",
            ind
        ));
        if meta.return_type == "()" {
            body_lines.push(format!("{}        return", ind));
        } else {
            body_lines.push(format!(
                "{}        return _cached.second.second as {}",
                ind, kt_return
            ));
        }
        body_lines.push(format!("{}    }}", ind));
        body_lines.push(format!("{}}}", ind));
    }

    if cache_seconds > 0 {
        body_lines.push(format!(
            "{}val _result = withContext(Dispatchers.IO) {{",
            ind
        ));
    } else {
        body_lines.push(format!("{}return withContext(Dispatchers.IO) {{", ind));
    }
    let in_ctx = format!("{}    ", ind);

    let group_prefix = group_path.join("/");
    let normalized_path = if !handler.path.is_empty() && !handler.path.starts_with('/') {
        format!("/{}", handler.path)
    } else {
        handler.path.to_string()
    };
    let full_path = if group_prefix.is_empty() {
        normalized_path
    } else {
        format!("/{}{}", group_prefix, normalized_path)
    };

    // Build the URL by substituting path parameters into the route
    // pattern.  Placeholders like :id are replaced with field access
    // expressions on the params object.
    let mut url_parts: Vec<String> = Vec::new();
    url_parts.push("url".to_string());
    // Walk the path string, replacing each :param placeholder with
    // the corresponding field from the params struct.
    let mut remaining = full_path.clone();
    if let Some(p) = param_param {
        if let Some(s) = p.structure {
            let structure = s();
            match structure.kind {
                TagKind::Struct(fields) => {
                    for field in included(fields) {
                        let placeholder = format!(":{}", field.name);
                        if let Some(pos) = remaining.find(&placeholder) {
                            if pos > 0 {
                                url_parts.push(format!("\"{}\"", &remaining[..pos]));
                            }
                            url_parts.push(format!("params.{}", to_camel_case(field.name)));
                            remaining = remaining[pos + placeholder.len()..].to_string();
                        }
                    }
                    if !remaining.is_empty() {
                        url_parts.push(format!("\"{}\"", remaining));
                    }
                }
                _ => {
                    // For a primitive path parameter (not a struct), replace
                    // the first :param placeholder with the param value directly.
                    if let Some(pos) = remaining.find(':') {
                        if pos > 0 {
                            url_parts.push(format!("\"{}\"", &remaining[..pos]));
                        }
                        url_parts.push("params".to_string());
                        // Advance past the :param token.
                        let after = &remaining[pos..];
                        if let Some(slash) = after.find('/') {
                            remaining = after[slash..].to_string();
                        } else {
                            remaining = String::new();
                        }
                        if !remaining.is_empty() {
                            url_parts.push(format!("\"{}\"", remaining));
                        }
                    }
                }
            }
        } else {
            url_parts.push(format!("\"{}\"", remaining));
        }
    } else {
        url_parts.push(format!("\"{}\"", remaining));
    }
    body_lines.push(format!("{}var url = {}", in_ctx, url_parts.join(" + ")));

    // Build query string from the Query extractor's struct fields.
    // Each non-null field is appended as a key=value pair.
    if query_param.is_some() {
        body_lines.push(format!("{}val qsParts = mutableListOf<String>()", in_ctx));
        if let Some(p) = query_param
            && let Some(s) = p.structure
        {
            let structure = s();
            match structure.kind {
                TagKind::Struct(fields) => {
                    for field in included(fields) {
                        body_lines.push(format!(
                            "{}if (queries.{} != null) qsParts.add(\"{}=\" + queries.{})",
                            in_ctx, field.name, field.name, field.name
                        ));
                    }
                }
                _ => {
                    body_lines.push(format!(
                        "{}if (queries != null) qsParts.add(\"{}=\" + queries.toString())",
                        in_ctx, p.name
                    ));
                }
            }
        }
        body_lines.push(format!(
            "{}if (qsParts.isNotEmpty()) url += \"?\" + qsParts.joinToString(\"&\")",
            in_ctx
        ));
    }

    // Open an HTTP connection and set the request method and headers.
    body_lines.push(format!(
        "{}val conn = URI(url).toURL().openConnection() as HttpURLConnection",
        in_ctx
    ));
    body_lines.push(format!("{}conn.requestMethod = \"{}\"", in_ctx, method));
    body_lines.push(format!(
        "{}conn.setRequestProperty(\"Accept\", \"application/json\")",
        in_ctx
    ));

    // Merge global header provider values into the request headers.
    // Each header provider returns an object whose non-null fields
    // are added as HTTP headers.
    body_lines.push(format!(
        "{}val hdrs = mutableMapOf<String, String>()",
        in_ctx
    ));
    body_lines.push(format!("{}for ((name, fn) in headers) {{", in_ctx));
    body_lines.push(format!("{}    val obj = fn()", in_ctx));
    body_lines.push(format!(
        "{}    val jsonObj = gson.toJsonTree(obj).asJsonObject",
        in_ctx
    ));
    body_lines.push(format!(
        "{}    for ((k, v) in jsonObj.entrySet()) {{",
        in_ctx
    ));
    body_lines.push(format!(
        "{}        if (!v.isJsonNull) hdrs[k] = v.asString",
        in_ctx
    ));
    body_lines.push(format!("{}    }}", in_ctx));
    body_lines.push(format!("{}}}", in_ctx));

    if body_param.is_some() {
        body_lines.push(format!(
            "{}hdrs[\"Content-Type\"] = \"application/json\"",
            in_ctx
        ));
    }
    body_lines.push(format!("{}for ((k, v) in hdrs) {{", in_ctx));
    body_lines.push(format!("{}    conn.setRequestProperty(k, v)", in_ctx));
    body_lines.push(format!("{}}}", in_ctx));

    if body_param.is_some() {
        body_lines.push(format!("{}conn.doOutput = true", in_ctx));
        body_lines.push(format!("{}val bodyJson = gson.toJson(body)", in_ctx));
        body_lines.push(format!(
            "{}conn.outputStream.write(bodyJson.toByteArray(Charsets.UTF_8))",
            in_ctx
        ));
        body_lines.push(format!("{}conn.outputStream.flush()", in_ctx));
    }

    if debug {
        if body_param.is_some() {
            body_lines.push(format!(
                "{}if (debug) println(\"[afast:debug] → request {} $url\" + \" body=\" + (bodyJson ?: \"\"))",
                in_ctx,
                method,
            ));
        } else {
            body_lines.push(format!(
                "{}if (debug) println(\"[afast:debug] → request {} $url\")",
                in_ctx, method,
            ));
        }
    }
    body_lines.push(format!("{}conn.connect()", in_ctx));
    body_lines.push(format!("{}val status = conn.responseCode", in_ctx));
    body_lines.push(format!(
        "{}val respBytes = if (status in 200..299) {{",
        in_ctx
    ));
    body_lines.push(format!("{}    conn.inputStream.readBytes()", in_ctx));
    body_lines.push(format!("{}}} else {{", in_ctx));
    body_lines.push(format!(
        "{}    conn.errorStream?.readBytes() ?: ByteArray(0)",
        in_ctx
    ));
    body_lines.push(format!("{}}}", in_ctx));
    body_lines.push(format!("{}conn.disconnect()", in_ctx));
    if debug {
        body_lines.push(format!(
            "{}if (debug) println(\"[afast:debug] ← response {} $url status=$status\")",
            in_ctx, method
        ));
    }

    // On non-2xx responses, attempt to parse the error body as JSON.
    // Falls back to a plain HTTP status message if parsing fails.
    body_lines.push(format!("{}if (status !in 200..299) {{", in_ctx));
    body_lines.push(format!(
        "{}    val errBody = String(respBytes, Charsets.UTF_8)",
        in_ctx
    ));
    body_lines.push(format!("{}    try {{", in_ctx));
    body_lines.push(format!(
        "{}        val err = gson.fromJson(errBody, AfastErrorResponse::class.java)",
        in_ctx
    ));
    body_lines.push(format!(
        "{}        throw RuntimeException(\"AfastError(${{err.code}}): ${{err.message}}\")",
        in_ctx
    ));
    body_lines.push(format!(
        "{}    }} catch (e: RuntimeException) {{ throw e }}",
        in_ctx
    ));
    body_lines.push(format!("{}    catch (e: Exception) {{", in_ctx));
    body_lines.push(format!(
        "{}        throw RuntimeException(\"HTTP ${{status}}: ${{errBody}}\")",
        in_ctx
    ));
    body_lines.push(format!("{}    }}", in_ctx));
    body_lines.push(format!("{}}}", in_ctx));

    // Deserialise the response body from JSON into the declared return type.
    if meta.return_type != "()" {
        body_lines.push(format!(
            "{}val respStr = String(respBytes, Charsets.UTF_8)",
            in_ctx
        ));
        body_lines.push(format!(
            "{}gson.fromJson(respStr, {}::class.java)",
            in_ctx, kt_return
        ));
    } else {
        body_lines.push(format!("{}Unit", in_ctx));
    }

    body_lines.push(format!("{}}}", ind));

    // Cache store (after withContext, before return)
    if cache_seconds > 0 {
        body_lines.push(format!(
            "{ind}{client}Client._cache[_cacheKey] = _currentReq to (System.currentTimeMillis() + {cache_ms}L to _result)",
            ind = ind,
            client = class_name,
            cache_ms = cache_seconds * 1000,
        ));
        body_lines.push(format!("{}return _result", ind));
    }

    let body = body_lines.build();

    format!(
        "{indent}suspend fun {func_name}({params_str}): {kt_return} {{\n{body}\n{indent}}}",
        indent = indent,
        func_name = func_name,
        params_str = params_str,
        kt_return = kt_return,
        body = body,
    )
}

fn type_name_for(prefix: &str, meta: &TagMeta) -> String {
    prefixed_type(prefix, meta.name)
}

// ─── Service-level code generation ──────────────────────────────────
// Produces a complete {Name}Client Kotlin class for a single service.
// The class contains an inner {Name}Apis class with nested inner
// classes for handler groups and suspend methods for leaf handlers.
// Transport is selected at construction time (Http, Ws, or Tcp).

/// Generates a complete Kotlin source file for a single service,
/// returning the source as a String.  The file contains a {Name}Client
/// class with transport-aware _call dispatch and nested inner classes
/// representing handler groups.
pub(crate) fn generate_service_kt(
    svc: &Service,
    calls: &[crate::KtCallType],
    debug: bool,
) -> String {
    let mut lines = CodeBuf::new();

    lines.push("// Auto-generated by afast. DO NOT EDIT.".to_string());
    lines.b();

    let seq64 = cfg!(feature = "seq64");
    let has_http = calls.contains(&crate::KtCallType::Http);
    let has_ws = calls.contains(&crate::KtCallType::Ws);
    let has_tcp = calls.contains(&crate::KtCallType::Tcp);
    let has_okhttp = calls.contains(&crate::KtCallType::OkHttp);

    lines.push(format!("package afast.generated.{}", svc.name));
    lines.b();
    lines.push("import afast.generated.*".to_string());
    lines.push("import java.io.*".to_string());
    lines.push("import java.nio.ByteBuffer".to_string());
    lines.push("import java.nio.ByteOrder".to_string());
    if has_okhttp {
        lines.push("import okhttp3.*".to_string());
        lines.push("import okhttp3.MediaType.Companion.toMediaType".to_string());
        lines.push("import okhttp3.RequestBody.Companion.toRequestBody".to_string());
        lines.push("import okio.ByteString.Companion.toByteString".to_string());
    }
    // HttpURLConnection is always needed for ordinary HTTP route handlers
    lines.push("import java.net.HttpURLConnection".to_string());
    lines.push("import java.net.URI".to_string());
    if has_ws && !has_okhttp {
        lines.push("import java.net.http.*".to_string());
    }
    if has_tcp || has_ws {
        lines.push("import java.net.Socket".to_string());
    }
    lines.push("import java.util.concurrent.ConcurrentHashMap".to_string());
    lines.push("import java.util.concurrent.atomic.AtomicInteger".to_string());
    lines.push("import kotlinx.coroutines.*".to_string());
    #[cfg(feature = "ordinary-http")]
    {
        lines.push("import com.google.gson.Gson".to_string());
    }
    lines.b();
    let class_name = to_pascal_case(&svc.name);
    #[cfg(feature = "ordinary-http")]
    let has_cache = has_cache_handlers_kt(&svc.handlers)
        || svc
            .ordinary_routes
            .iter()
            .any(|r| r.handler_entry.meta.cache_seconds > 0);
    #[cfg(not(feature = "ordinary-http"))]
    let has_cache = has_cache_handlers_kt(&svc.handlers);
    let mut emitted = Vec::new();
    collect_type_exports_kt(&svc.handlers, &[], &mut emitted, &mut lines);
    let customs = collect_customs(&svc.handlers);
    #[cfg(feature = "ordinary-http")]
    let headers = collect_headers(&svc.handlers);
    lines.b();
    lines.push(format!("class {class_name}Client("));
    lines.push("    private val host: String,".to_string());
    lines.push("    private val port: Int,".to_string());
    lines.push("    private val tls: Boolean = false,".to_string());
    lines.push("    private val transport: Transport = Transport.Http,".to_string());
    if debug {
        lines.push("    private val debug: Boolean = true,".to_string());
    }
    if customs.is_empty() {
        lines.push(
            "    private val customs: Map<String, suspend () -> Any> = emptyMap(),".to_string(),
        );
    } else {
        lines.push("    private val customs: Map<String, suspend () -> Any>,".to_string());
    }
    #[cfg(feature = "ordinary-http")]
    {
        if headers.is_empty() {
            lines.push(
                "    private val headers: Map<String, suspend () -> Any> = emptyMap(),".to_string(),
            );
        } else {
            lines.push("    private val headers: Map<String, suspend () -> Any>,".to_string());
        }
    }
    // Strip the trailing comma from the last constructor parameter
    // so the generated Kotlin syntax is valid.
    let last = lines.last_mut().unwrap();
    if last.ends_with(',') {
        *last = last.trim_end_matches(',').to_string();
    }
    lines.push(") {".to_string());

    // Static cache (shared across all instances)
    if has_cache {
        lines.push("    companion object {".to_string());
        lines.push(
            "        private val _cache = mutableMapOf<String, Pair<String, Pair<Long, Any?>>>()"
                .to_string(),
        );
        lines.push("    }".to_string());
        lines.b();
    }

    lines.push("    enum class Transport { Http, Ws, Tcp, OkHttp }".to_string());
    lines.b();
    lines.push("    private val url: String get() = (if (tls) \"https\" else \"http\") + \"://$host:$port\"".to_string());
    lines.b();
    #[cfg(feature = "ordinary-http")]
    {
        lines.push("    private val gson = Gson()".to_string());
        lines.b();
    }
    if has_tcp {
        lines.push("    internal var tcpSocket: Socket? = null".to_string());
        lines.push("    internal var tcpIn: DataInputStream? = null".to_string());
        lines.push("    internal var tcpOut: DataOutputStream? = null".to_string());
    }
    if has_ws && !has_okhttp {
        lines.push("    internal var wsClient: java.net.http.WebSocket? = null".to_string());
    }
    if has_okhttp {
        lines.push("    private val okHttpClient = OkHttpClient()".to_string());
        lines.push("    internal var okHttpWs: okhttp3.WebSocket? = null".to_string());
    }
    if has_ws || has_tcp {
        lines.push(
            "    private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())"
                .to_string(),
        );
        if seq64 {
            lines.push(
                "    private val nextId = java.util.concurrent.atomic.AtomicLong(1L)".to_string(),
            );
            lines.push("    private val pending = ConcurrentHashMap<Long, CompletableDeferred<ByteArray>>()".to_string());
        } else {
            lines.push("    private val nextId = AtomicInteger(1)".to_string());
            lines.push("    private val pending = ConcurrentHashMap<Int, CompletableDeferred<ByteArray>>()".to_string());
        }
        lines.push(
            "    private val pushHandlers = ConcurrentHashMap<Int, (ByteArray) -> Unit>()"
                .to_string(),
        );
    }
    lines.b();
    // TCP and WS connection init — must come after scope is initialized
    if has_tcp || has_ws {
        lines.push("    init {".to_string());
        if has_tcp {
            lines.push("        if (transport == Transport.Tcp) {".to_string());
            lines.push("            val s = Socket(host, port)".to_string());
            lines.push("            tcpSocket = s".to_string());
            lines.push(
                "            tcpIn = DataInputStream(java.io.BufferedInputStream(s.getInputStream()))"
                    .to_string(),
            );
            lines.push("            tcpOut = DataOutputStream(java.io.BufferedOutputStream(s.getOutputStream()))".to_string());
            lines.push("            _startTcpReader()".to_string());
            lines.push("        }".to_string());
        }
        if has_ws {
            lines.push("        if (transport == Transport.Ws) {".to_string());
            lines.push(
                "            val wsUrl = (if (tls) \"wss\" else \"ws\") + \"://$host:$port/_ws\""
                    .to_string(),
            );
            lines
                .push("            val latch = java.util.concurrent.CountDownLatch(1)".to_string());
            lines.push(
                "            wsClient = java.net.http.HttpClient.newHttpClient()".to_string(),
            );
            lines.push("                .newWebSocketBuilder()".to_string());
            lines.push("                .buildAsync(java.net.URI(wsUrl), object : java.net.http.WebSocket.Listener {".to_string());
            lines.push("                    private val buf = mutableListOf<Byte>()".to_string());
            lines.push("                    override fun onBinary(webSocket: java.net.http.WebSocket, data: java.nio.ByteBuffer, last: Boolean): java.util.concurrent.CompletionStage<*>? {".to_string());
            lines.push(
                "                        val bytes = ByteArray(data.remaining())".to_string(),
            );
            lines.push("                        data.get(bytes)".to_string());
            lines.push("                        buf.addAll(bytes.toList())".to_string());
            lines.push("                        webSocket.request(1)".to_string());
            lines.push("                        if (last) {".to_string());
            lines.push("                            val raw = buf.toByteArray()".to_string());
            lines.push("                            buf.clear()".to_string());
            lines.push("                            _handleMessage(raw)".to_string());
            lines.push("                        }".to_string());
            lines.push("                        return null".to_string());
            lines.push("                    }".to_string());
            lines.push(
                "                    override fun onOpen(webSocket: java.net.http.WebSocket) {"
                    .to_string(),
            );
            lines.push("                        webSocket.request(Long.MAX_VALUE)".to_string());
            lines.push("                        latch.countDown()".to_string());
            lines.push("                    }".to_string());
            lines.push("                }).join()".to_string());
            lines.push("            latch.await()".to_string());
            lines.push("        }".to_string());
        }
        lines.push("    }".to_string());
        lines.b();
    }
    lines.push(
        "    private suspend fun _call(handlerId: Long, payload: ByteArray): ByteArray {"
            .to_string(),
    );
    if debug {
        lines.push("        if (debug) println(\"[afast:debug] → call handlerId=$handlerId payloadLen=${payload.size}\")".to_string());
    }
    lines.push("        val resp = when (transport) {".to_string());
    if has_http {
        lines.push("            Transport.Http -> callFetch(handlerId, payload)".to_string());
    }
    if has_ws {
        lines.push("            Transport.Ws -> callWs(handlerId, payload)".to_string());
    }
    if has_tcp {
        lines.push("            Transport.Tcp -> callTcp(handlerId, payload)".to_string());
    }
    if has_okhttp {
        lines.push("            Transport.OkHttp -> callOkHttp(handlerId, payload)".to_string());
    }
    lines.push(
        "            else -> throw RuntimeException(\"transport not enabled: $transport\")"
            .to_string(),
    );
    lines.push("        }".to_string());
    if debug {
        lines.push("        if (debug) println(\"[afast:debug] ← call handlerId=$handlerId respLen=${resp.size}\")".to_string());
    }
    lines.push("        return resp".to_string());
    lines.push("    }".to_string());
    lines.b();
    if has_http {
        lines.push(
            "    private suspend fun callFetch(handlerId: Long, payload: ByteArray): ByteArray {"
                .to_string(),
        );
        lines.push("        return withContext(Dispatchers.IO) {".to_string());
        lines.push("            val w = BinaryWriter()".to_string());
        lines.push("            w.wU32(handlerId.toInt())".to_string());
        lines.push("            w.wRaw(payload)".to_string());
        lines.push("            val body = w.toBytes()".to_string());
        lines.push("            val conn = java.net.URI(\"$url/_api\").toURL().openConnection() as java.net.HttpURLConnection".to_string());
        lines.push("            conn.requestMethod = \"POST\"".to_string());
        lines.push(
            "            conn.setRequestProperty(\"Content-Type\", \"application/octet-stream\")"
                .to_string(),
        );
        lines.push("            conn.doOutput = true".to_string());
        lines.push("            conn.outputStream.write(body)".to_string());
        lines.push("            conn.outputStream.flush()".to_string());
        lines.push("            val respBytes = conn.inputStream.readBytes()".to_string());
        lines.push("            conn.disconnect()".to_string());
        lines.push(
            "            if (respBytes.size < 9) throw RuntimeException(\"response too short\")"
                .to_string(),
        );
        lines.push("            val status = respBytes[0].toInt() and 0xFF".to_string());
        lines.push("            if (status == 1) {".to_string());
        lines.push("                val code = ByteBuffer.wrap(respBytes, 1, 8).order(ByteOrder.LITTLE_ENDIAN).long".to_string());
        lines.push(
            "                val msg = String(respBytes, 9, respBytes.size - 9, Charsets.UTF_8)"
                .to_string(),
        );
        lines.push("                throw RuntimeException(\"AfError($code): $msg\")".to_string());
        lines.push("            }".to_string());
        lines.push("            respBytes.copyOfRange(9, respBytes.size)".to_string());
        lines.push("        }".to_string());
        lines.push("    }".to_string());
        lines.b();
    }
    if has_okhttp {
        lines.push(
            "    private suspend fun callOkHttp(handlerId: Int, payload: ByteArray): ByteArray {"
                .to_string(),
        );
        lines.push("        return withContext(Dispatchers.IO) {".to_string());
        lines.push("            val w = BinaryWriter()".to_string());
        lines.push("            w.wU32(handlerId.toInt())".to_string());
        lines.push("            w.wRaw(payload)".to_string());
        lines.push("            val body = w.toBytes()".to_string());
        lines.push(
            "            val mediaType = \"application/octet-stream\".toMediaType()".to_string(),
        );
        lines.push("            val request = Request.Builder()".to_string());
        lines.push("                .url(\"$url/_api\")".to_string());
        lines.push("                .post(body.toRequestBody(mediaType))".to_string());
        lines.push("                .build()".to_string());
        lines.push(
            "            val respBytes = okHttpClient.newCall(request).execute().body!!.bytes()"
                .to_string(),
        );
        lines.push(
            "            if (respBytes.size < 9) throw RuntimeException(\"response too short\")"
                .to_string(),
        );
        lines.push("            val status = respBytes[0].toInt() and 0xFF".to_string());
        lines.push("            if (status == 1) {".to_string());
        lines.push("                val code = ByteBuffer.wrap(respBytes, 1, 8).order(ByteOrder.LITTLE_ENDIAN).long".to_string());
        lines.push(
            "                val msg = String(respBytes, 9, respBytes.size - 9, Charsets.UTF_8)"
                .to_string(),
        );
        lines.push("                throw RuntimeException(\"AfError($code): $msg\")".to_string());
        lines.push("            }".to_string());
        lines.push("            respBytes.copyOfRange(9, respBytes.size)".to_string());
        lines.push("        }".to_string());
        lines.push("    }".to_string());
        lines.b();
    }
    if has_ws {
        lines.push(
            "    private suspend fun callWs(handlerId: Long, payload: ByteArray): ByteArray {"
                .to_string(),
        );
        lines.push("        val id = nextId.getAndIncrement()".to_string());
        lines.push("        val deferred = CompletableDeferred<ByteArray>()".to_string());
        lines.push("        pending[id] = deferred".to_string());
        lines.push("        scope.launch {".to_string());
        lines.push("            delay(5000)".to_string());
        lines.push("            if (pending.remove(id) != null) {".to_string());
        lines.push(
            "                deferred.completeExceptionally(RuntimeException(\"timeout id=$id\"))"
                .to_string(),
        );
        lines.push("            }".to_string());
        lines.push("        }".to_string());
        lines.b();
        // Build the WS frame: [req_id: SeqId][handler_id: u32][len: Len][payload].
        // The seq_id and len widths are controlled by seq64/len64 feature flags.
        lines.push("        val w = BinaryWriter()".to_string());
        if seq64 {
            lines.push("        w.wU32((id and 0xFFFFFFFFL).toInt())".to_string());
            lines.push("        w.wU32(((id ushr 32) and 0xFFFFFFFFL).toInt())".to_string());
        } else {
            lines.push("        w.wU32(id)".to_string());
        }
        lines.push("        w.wU32(handlerId.toInt())".to_string());
        if cfg!(feature = "len64") {
            lines.push("        w.wU64(payload.size.toLong())".to_string());
        } else {
            lines.push("        w.wU32(payload.size)".to_string());
        }
        lines.push("        w.wRaw(payload)".to_string());
        lines.push(
            "        wsClient!!.sendBinary(java.nio.ByteBuffer.wrap(w.toBytes()), true)"
                .to_string(),
        );
        lines.push("        return deferred.await()".to_string());
        lines.push("    }".to_string());
        lines.b();
    }
    if has_tcp {
        lines.push(
            "    private suspend fun callTcp(handlerId: Long, payload: ByteArray): ByteArray {"
                .to_string(),
        );
        lines.push("        val id = nextId.getAndIncrement()".to_string());
        lines.push("        val deferred = CompletableDeferred<ByteArray>()".to_string());
        lines.push("        pending[id] = deferred".to_string());
        lines.push("        scope.launch {".to_string());
        lines.push("            delay(5000)".to_string());
        lines.push("            if (pending.remove(id) != null) {".to_string());
        lines.push(
            "                deferred.completeExceptionally(RuntimeException(\"timeout id=$id\"))"
                .to_string(),
        );
        lines.push("            }".to_string());
        lines.push("        }".to_string());
        lines.b();
        // Build the inner frame carried inside a TCP length-prefixed envelope:
        // [req_id: SeqId][handler_id: u32][len: Len][payload].
        lines.push("        val inner = BinaryWriter()".to_string());
        if seq64 {
            lines.push("        inner.wU32((id and 0xFFFFFFFFL).toInt())".to_string());
            lines.push("        inner.wU32(((id ushr 32) and 0xFFFFFFFFL).toInt())".to_string());
        } else {
            lines.push("        inner.wU32(id)".to_string());
        }
        lines.push("        inner.wU32(handlerId.toInt())".to_string());
        if cfg!(feature = "len64") {
            lines.push("        inner.wU64(payload.size.toLong())".to_string());
        } else {
            lines.push("        inner.wU32(payload.size)".to_string());
        }
        lines.push("        inner.wRaw(payload)".to_string());
        lines.push("        val innerBytes = inner.toBytes()".to_string());
        // Wrap the inner frame in a TCP length-prefixed envelope: [len: Len][inner].
        lines.push("        val w = BinaryWriter()".to_string());
        if cfg!(feature = "len64") {
            lines.push("        w.wU64(innerBytes.size.toLong())".to_string());
        } else {
            lines.push("        w.wU32(innerBytes.size)".to_string());
        }
        lines.push("        w.wRaw(innerBytes)".to_string());
        lines.push("        tcpOut!!.write(w.toBytes())".to_string());
        lines.push("        tcpOut!!.flush()".to_string());
        lines.push("        return deferred.await()".to_string());
        lines.push("    }".to_string());
        lines.b();
    }
    if has_ws || has_tcp {
        lines.push("    fun _handleMessage(raw: ByteArray) {".to_string());
        if seq64 {
            lines.push("        if (raw.size < 8) return".to_string());
            lines.push("        val lo = raw[0].toInt() and 0xFF or ((raw[1].toInt() and 0xFF) shl 8) or ((raw[2].toInt() and 0xFF) shl 16) or ((raw[3].toInt() and 0xFF) shl 24)".to_string());
            lines.push("        val hi = raw[4].toInt() and 0xFF or ((raw[5].toInt() and 0xFF) shl 8) or ((raw[6].toInt() and 0xFF) shl 16) or ((raw[7].toInt() and 0xFF) shl 24)".to_string());
            lines.push(
                "        val reqId = (hi.toLong() shl 32) or (lo.toLong() and 0xFFFFFFFFL)"
                    .to_string(),
            );
        } else {
            lines.push("        if (raw.size < 4) return".to_string());
            lines.push("        val reqId = raw[0].toInt() and 0xFF or ((raw[1].toInt() and 0xFF) shl 8) or ((raw[2].toInt() and 0xFF) shl 16) or ((raw[3].toInt() and 0xFF) shl 24)".to_string());
        }
        if seq64 {
            lines.push("        if (reqId == 0L) {".to_string());
        } else {
            lines.push("        if (reqId == 0) {".to_string());
        }
        let sid: usize = if seq64 { 8 } else { 4 };
        let len_bytes: usize = if cfg!(feature = "len64") { 8 } else { 4 };
        let cid = sid;
        let lid = sid + 4;
        let push_hdr = sid + 4 + len_bytes;
        lines.push(format!("            val connId = raw[{cid}].toInt() and 0xFF or ((raw[{cid}+1].toInt() and 0xFF) shl 8) or ((raw[{cid}+2].toInt() and 0xFF) shl 16) or ((raw[{cid}+3].toInt() and 0xFF) shl 24)"));
        lines.push(format!("            val len = raw[{lid}].toInt() and 0xFF or ((raw[{lid}+1].toInt() and 0xFF) shl 8) or ((raw[{lid}+2].toInt() and 0xFF) shl 16) or ((raw[{lid}+3].toInt() and 0xFF) shl 24)"));
        lines.push(format!(
            "            val payload = raw.copyOfRange({push_hdr}, {push_hdr} + len)"
        ));
        lines.push("            pushHandlers[connId]?.invoke(payload)".to_string());
        lines.push("            return".to_string());
        lines.push("        }".to_string());
        lines.push("        val p = pending.remove(reqId) ?: return".to_string());
        let status_off: usize =
            (if seq64 { 8 } else { 4 }) + (if cfg!(feature = "len64") { 8 } else { 4 });
        let code_off = status_off + 1;
        let payload_off = code_off + 8;
        lines.push(format!("        if (raw.size < {payload_off}) {{ p.completeExceptionally(RuntimeException(\"response too short\")); return }}"));
        lines.push(format!(
            "        val status = raw[{status_off}].toInt() and 0xFF"
        ));
        lines.push("        if (status == 1) {".to_string());
        lines.push(format!("            val code = ByteBuffer.wrap(raw, {code_off}, 8).order(ByteOrder.LITTLE_ENDIAN).long"));
        lines.push(format!("            val msg = String(raw, {payload_off}, raw.size - {payload_off}, Charsets.UTF_8)"));
        lines.push(
            "            p.completeExceptionally(RuntimeException(\"AfError($code): $msg\"))"
                .to_string(),
        );
        lines.push("        } else {".to_string());
        lines.push(format!(
            "            p.complete(raw.copyOfRange({payload_off}, raw.size))"
        ));
        lines.push("        }".to_string());
        lines.push("    }".to_string());
    } // end if has_ws || has_tcp
    if has_ws || has_tcp {
        lines.push("    fun _sendRaw(data: ByteArray) {".to_string());
        if has_ws && has_tcp {
            lines.push("        if (transport == Transport.Ws) {".to_string());
            lines.push(
                "            wsClient!!.sendBinary(java.nio.ByteBuffer.wrap(data), true)"
                    .to_string(),
            );
            lines.push("        } else {".to_string());
            lines.push("            val w = BinaryWriter()".to_string());
            if cfg!(feature = "len64") {
                lines.push("            w.wU64(data.size.toLong())".to_string());
            } else {
                lines.push("            w.wU32(data.size)".to_string());
            }
            lines.push("            w.wRaw(data)".to_string());
            lines.push("            tcpOut!!.write(w.toBytes())".to_string());
            lines.push("            tcpOut!!.flush()".to_string());
            lines.push("        }".to_string());
        } else if has_ws {
            lines.push(
                "        wsClient!!.sendBinary(java.nio.ByteBuffer.wrap(data), true)".to_string(),
            );
        } else {
            lines.push("        val w = BinaryWriter()".to_string());
            if cfg!(feature = "len64") {
                lines.push("        w.wU64(data.size.toLong())".to_string());
            } else {
                lines.push("        w.wU32(data.size)".to_string());
            }
            lines.push("        w.wRaw(data)".to_string());
            lines.push("        tcpOut!!.write(w.toBytes())".to_string());
            lines.push("        tcpOut!!.flush()".to_string());
        }
        lines.push("    }".to_string());
    }
    if has_tcp {
        lines.b();
        lines.push("    fun _startTcpReader() {".to_string());
        lines.push("        scope.launch(Dispatchers.IO) {".to_string());
        lines.push("            try {".to_string());
        lines.push("                while (true) {".to_string());
        if cfg!(feature = "len64") {
            lines.push("                    val lenBuf = ByteArray(8)".to_string());
        } else {
            lines.push("                    val lenBuf = ByteArray(4)".to_string());
        }
        lines.push("                    tcpIn!!.readFully(lenBuf)".to_string());
        if cfg!(feature = "len64") {
            lines.push("                    val len64 = ByteBuffer.wrap(lenBuf).order(ByteOrder.LITTLE_ENDIAN).long".to_string());
            lines.push("                    val frameLen = len64.toInt()".to_string());
        } else {
            lines.push("                    val frameLen = ByteBuffer.wrap(lenBuf).order(ByteOrder.LITTLE_ENDIAN).int".to_string());
        }
        lines.push("                    if (frameLen == 0) continue".to_string());
        lines.push("                    val frame = ByteArray(frameLen)".to_string());
        lines.push("                    tcpIn!!.readFully(frame)".to_string());
        lines.push("                    _handleMessage(frame)".to_string());
        lines.push("                }".to_string());
        lines.push("            } catch (e: Exception) {".to_string());
        lines.push("                // connection closed".to_string());
        lines.push("            }".to_string());
        lines.push("        }".to_string());
        lines.push("    }".to_string());
    }
    lines.b();
    // Recursively generate handler methods.  Handler groups become
    // named inner classes; leaf handlers become suspend methods.
    #[allow(clippy::only_used_in_recursion)]
    fn gen_handler_object(
        handlers: &[Handler],
        all: &[Handler],
        path: &[&str],
        indent: &str,
        svc_name: &str,
        _debug: bool,
        lines: &mut CodeBuf,
    ) {
        let inner_indent = format!("{}    ", indent);
        for h in handlers {
            let child_path = {
                let mut p = path.to_vec();
                p.push(h.name);
                p
            };
            if h.meta.name.is_empty() {
                // Group node: emit a named inner class that contains child
                // handlers, with a val property for instantiation.
                let clean_name = h.name.trim_start_matches(':');
                let class_name = format!("{}Api", to_pascal_case(clean_name));
                lines.push(format!("{}inner class {} {{", inner_indent, class_name));
                gen_handler_object(
                    &h.children,
                    all,
                    &child_path,
                    &inner_indent,
                    svc_name,
                    _debug,
                    lines,
                );
                lines.push(format!("{}}}", inner_indent));
                lines.push(format!(
                    "{}val {} = {}()",
                    inner_indent, clean_name, class_name
                ));
            } else {
                // Leaf handler: emit a suspend method that serialises
                // parameters, calls the transport, and deserialises the
                // response.
                let prefix_str = handler_prefix(&child_path);
                let cache_key_parts: Vec<&str> = path
                    .iter()
                    .chain(std::iter::once(&h.name))
                    .copied()
                    .collect();
                let cache_key = cache_key_parts.join(".");
                if h.meta.is_ordinary {
                    #[cfg(feature = "ordinary-http")]
                    {
                        lines.push(ordinary_handler_method_kt(
                            h,
                            &prefix_str,
                            path,
                            &inner_indent,
                            svc_name,
                            _debug,
                            svc_name,
                            &cache_key,
                        ));
                    }
                    #[cfg(not(feature = "ordinary-http"))]
                    {
                        let fn_name = if !h.meta.api_name.is_empty() {
                            h.meta.api_name
                        } else {
                            h.meta.name
                        };
                        lines.push(format!("{}suspend fun {}() = throw RuntimeException(\"ordinary-http not enabled\")", inner_indent, fn_name));
                    }
                } else {
                    lines.push(handler_method_kt(
                        h,
                        all,
                        &prefix_str,
                        &inner_indent,
                        svc_name,
                        svc_name,
                        &cache_key,
                    ));
                }
            }
        }
    }

    let apis_class_name = format!("{}Apis", class_name);
    lines.push(format!("    inner class {} {{", apis_class_name));
    gen_handler_object(
        &svc.handlers,
        &svc.handlers,
        &[],
        "    ",
        &class_name,
        debug,
        &mut lines,
    );

    // ── Ordinary-ws routes ────────────────────────────────────
    #[cfg(feature = "ordinary-ws")]
    {
        for ws_route in &svc.ws_routes {
            let path = ws_route.path;
            let handler_name = ws_route.handler_name;

            let trimmed = path.trim_start_matches('/').trim_end_matches('/');
            let segments: Vec<&str> = trimmed.split('/').collect();

            let param_names: Vec<&str> = segments
                .iter()
                .filter_map(|s| s.strip_prefix(':'))
                .collect();

            // Build Kotlin function signature
            let sig_params = if param_names.is_empty() {
                "query: Map<String, String>? = null".to_string()
            } else {
                let params_str = param_names
                    .iter()
                    .map(|p| format!("{}: String", p))
                    .collect::<Vec<_>>()
                    .join(", ");
                format!("{}, query: Map<String, String>? = null", params_str)
            };

            // Build path substitution
            let mut path_expr = String::new();
            for (i, seg) in segments.iter().enumerate() {
                if i > 0 {
                    path_expr.push('/');
                }
                if let Some(param) = seg.strip_prefix(':') {
                    path_expr.push_str(&format!("${{{}}}", param));
                } else {
                    path_expr.push_str(seg);
                }
            }

            let ind = "        ";
            let mut body = String::new();
            body.push_str(&format!(
                "{}val scheme = if (this@{class_name}Client.tls) \"wss\" else \"ws\"\n",
                ind
            ));
            body.push_str(&format!(
                "{}var url = \"$scheme://${{this@{class_name}Client.host}}:${{this@{class_name}Client.port}}/{}\"\n",
                ind, path_expr
            ));
            body.push_str(&format!("{}if (query != null) {{\n", ind));
            body.push_str(&format!(
                "    {}val qs = query.entries.joinToString(\"&\") {{ \"${{it.key}}=${{it.value}}\" }}\n",
                ind
            ));
            body.push_str(&format!(
                "    {}if (qs.isNotEmpty()) url += \"?\" + qs\n",
                ind
            ));
            body.push_str(&format!("{}}}\n", ind));
            if has_okhttp {
                body.push_str(&format!(
                    "{}return this@{class_name}Client.okHttpClient.newWebSocket(\n",
                    ind
                ));
                body.push_str(&format!("{}    Request.Builder().url(url).build(),\n", ind));
                body.push_str(&format!(
                    "{}    object : okhttp3.WebSocketListener() {{}}\n",
                    ind
                ));
                body.push_str(&format!("{})\n", ind));
            } else {
                body.push_str(&format!(
                    "{}val wsBuilder = java.net.http.HttpClient.newHttpClient().newWebSocketBuilder()\n",
                    ind
                ));
                body.push_str(&format!(
                    "{}return wsBuilder.buildAsync(java.net.URI.create(url), object : java.net.http.WebSocket.Listener {{}}).get()\n",
                    ind
                ));
            }

            let camel_name = to_camel_case(handler_name);
            lines.push(format!("        /** WebSocket: {} */", path));
            if has_okhttp {
                lines.push(format!(
                    "        fun {}({}): okhttp3.WebSocket {{\n{}        }}",
                    camel_name, sig_params, body
                ));
            } else {
                lines.push(format!(
                    "        fun {}({}): java.net.http.WebSocket {{\n{}        }}",
                    camel_name, sig_params, body
                ));
            }
        }
    }

    // ── Ordinary-sse routes ────────────────────────────────────
    #[cfg(feature = "ordinary-sse")]
    {
        for sse_route in &svc.sse_routes {
            let path = sse_route.path;
            let handler_name = sse_route.handler_name;

            let trimmed = path.trim_start_matches('/').trim_end_matches('/');
            let segments: Vec<&str> = trimmed.split('/').collect();

            let param_names: Vec<&str> = segments
                .iter()
                .filter_map(|s| s.strip_prefix(':'))
                .collect();

            let sig_params = if param_names.is_empty() {
                "query: Map<String, String>? = null".to_string()
            } else {
                let params_str = param_names
                    .iter()
                    .map(|p| format!("{}: String", p))
                    .collect::<Vec<_>>()
                    .join(", ");
                format!("{}, query: Map<String, String>? = null", params_str)
            };

            let mut path_expr = String::new();
            for (i, seg) in segments.iter().enumerate() {
                if i > 0 {
                    path_expr.push('/');
                }
                if let Some(param) = seg.strip_prefix(':') {
                    path_expr.push_str(&format!("${{{}}}", param));
                } else {
                    path_expr.push_str(seg);
                }
            }

            let camel_name = handler_name;

            let ind = "        ";
            let mut body = String::new();
            body.push_str(&format!(
                "{}val scheme = if (this@{class_name}Client.tls) \"https\" else \"http\"\n",
                ind
            ));
            body.push_str(&format!(
                "{}var url = \"$scheme://${{this@{class_name}Client.host}}:${{this@{class_name}Client.port}}/{}\"\n",
                ind, path_expr
            ));
            body.push_str(&format!("{}if (query != null) {{\n", ind));
            body.push_str(&format!(
                "    {}val qs = query.entries.joinToString(\"&\") {{ \"${{it.key}}=${{it.value}}\" }}\n",
                ind
            ));
            body.push_str(&format!(
                "    {}if (qs.isNotEmpty()) url += \"?\" + qs\n",
                ind
            ));
            body.push_str(&format!("{}}}\n", ind));

            if has_okhttp {
                body.push_str(&format!(
                    "{}val request = okhttp3.Request.Builder().url(url).build()\n",
                    ind
                ));
                body.push_str(&format!(
                    "{}val listener = okhttp3.sse.EventSourceListener()\n",
                    ind
                ));
                body.push_str(&format!(
                    "{}return okhttp3.sse.EventSources.createFactory(this@{class_name}Client.okHttpClient).newEventSource(request, listener)\n",
                    ind
                ));
                lines.push(format!("        /** SSE: {} */", path));
                lines.push(format!(
                    "        fun {}({}): okhttp3.sse.EventSource {{\n{}        }}",
                    camel_name, sig_params, body
                ));
            } else {
                body.push_str(&format!(
                    "{}val client = java.net.http.HttpClient.newHttpClient()\n",
                    ind
                ));
                body.push_str(&format!(
                    "{}val request = java.net.http.HttpRequest.newBuilder(java.net.URI.create(url)).header(\"Accept\", \"text/event-stream\").build()\n",
                    ind
                ));
                body.push_str(&format!(
                    "{}val response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofLines())\n",
                    ind
                ));
                body.push_str(&format!("{}return response\n", ind));
                lines.push(format!("        /** SSE: {} */", path));
                lines.push(format!(
                    "        fun {}({}): java.net.http.HttpResponse<java.util.stream.Stream<String>> {{\n{}        }}",
                    camel_name, sig_params, body
                ));
            }
        }
    }

    lines.push("    }".to_string());
    lines.push(format!("    val apis = {}()", apis_class_name));

    lines.push("}".to_string());
    lines.build()
}

fn collect_type_exports_kt(
    handlers: &[Handler],
    path: &[&str],
    emitted: &mut Vec<String>,
    lines: &mut CodeBuf,
) {
    for h in handlers {
        let child_path = {
            let mut p = path.to_vec();
            p.push(h.name);
            p
        };
        if !h.meta.name.is_empty() {
            let prefix_str = handler_prefix(&child_path);
            lines.push(handler_type_exports_kt(&prefix_str, h.meta, emitted));
        }
        if !h.children.is_empty() {
            collect_type_exports_kt(&h.children, &child_path, emitted, lines);
        }
    }
}

// ─── File output ────────────────────────────────────────────────────

/// Writes a single service's generated Kotlin code to a `.kt` file.
#[cfg(feature = "kt")]
fn write_service_kt(
    svc: &Service,
    dir: &Path,
    calls: &[crate::KtCallType],
    debug: bool,
) -> Result<(), Error> {
    use std::fs;

    let code = generate_service_kt(svc, calls, debug);
    // Each service gets its own subdirectory and package
    let svc_dir = dir.join(&svc.name);
    fs::create_dir_all(&svc_dir).map_err(|e| Error::Io {
        message: e.to_string(),
    })?;
    let file_name = format!("{}.kt", svc.name);
    let file_path = svc_dir.join(&file_name);
    fs::write(&file_path, &code).map_err(|e| Error::Io {
        message: e.to_string(),
    })?;

    Ok(())
}

impl AFast {
    /// Returns the complete generated Kotlin code as a single string,
    /// including common.kt content concatenated with every service file.
    /// Primarily used for testing and inspection.
    #[cfg(feature = "kt")]
    pub fn get_kt_code(&self) -> String {
        let seq64 = cfg!(feature = "seq64");
        let has_ws = true;
        let has_tcp = true;
        let common = generate_common_kt(&self.services, seq64, has_ws, has_tcp);
        let mut parts = vec![common];
        for svc in &self.services {
            if svc.name.is_empty() {
                continue;
            }
            parts.push(generate_service_kt(
                svc,
                &[
                    crate::KtCallType::Http,
                    crate::KtCallType::Ws,
                    crate::KtCallType::Tcp,
                ],
                false,
            ));
        }
        parts.join("\n\n")
    }

    /// Generates Kotlin client code for all registered services and writes
    /// the files to `dir`.  Produces one `common.kt` with shared types and
    /// one `{service}.kt` per service.  The `calls` slice controls which
    /// transport methods (Http, Ws, Tcp) are included.
    #[cfg(feature = "kt")]
    pub fn generate_kt(
        &self,
        dir: &Path,
        calls: &[crate::KtCallType],
        debug: bool,
    ) -> Result<(), Error> {
        use std::fs;

        fs::create_dir_all(dir).map_err(|e| Error::Io {
            message: e.to_string(),
        })?;

        let seq64 = cfg!(feature = "seq64");
        let has_ws = calls.iter().any(|c| matches!(c, crate::KtCallType::Ws));
        let has_tcp = calls.iter().any(|c| matches!(c, crate::KtCallType::Tcp));
        let common = generate_common_kt(&self.services, seq64, has_ws, has_tcp);
        fs::write(dir.join("common.kt"), &common).map_err(|e| Error::Io {
            message: e.to_string(),
        })?;

        for svc in &self.services {
            if svc.name.is_empty() {
                continue;
            }
            write_service_kt(svc, dir, calls, debug)?;
        }

        Ok(())
    }
}
