//! Per-Worker `index.ts` generation (v0.8 §4.3, v0.9 §5.1).
//!
//! Each Worker exports a `default { fetch }` handler that first checks
//! for the internal Service Binding prefix (`/_bynk/call/`) and dispatches
//! to the matching `on call` service operation, then matches the request
//! method + path against any `on http` routes the context declares.
//! Validation and uncaught errors map to 400 / 500 respectively.

use std::fmt::Write as _;

use crate::ast::*;
use crate::emitter::http_handler_method_name;
use crate::project::UnitTable;

pub fn emit_worker_entry(context: &str, table: &UnitTable) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "// Generated by bynkc — do not edit by hand.");
    let _ = writeln!(out, "// Worker entry point for context `{context}`.");
    writeln!(out).unwrap();

    // Collect HTTP routes across all services, sorted so literal-segment
    // routes precede parameter-segment routes that share the same prefix.
    let mut http_routes: Vec<HttpRoute> = Vec::new();
    let mut service_names: Vec<&String> = table.services.keys().collect();
    service_names.sort();
    for sname in &service_names {
        let service = table.services.get(*sname).unwrap();
        for h in &service.handlers {
            if let HandlerKind::Http { method, path } = &h.kind {
                http_routes.push(HttpRoute {
                    service: (*sname).clone(),
                    method: *method,
                    path: path.clone(),
                    handler: h.clone(),
                    // v0.47: a Bearer handler's surface wrapper runs the
                    // verification seam and needs the request passed in.
                    bearer: crate::actors::bearer_seam_for(h, &table.actors).is_some(),
                    signature: crate::actors::signature_seam_for(h, &table.actors),
                    // v0.52: a multi-actor sum handler's wrapper owns the whole
                    // boundary (raw read, first-wins resolution, body parse), so
                    // the entry just passes `request` and skips the body parse.
                    sum: crate::actors::sum_members_for(h, &table.actors).is_some(),
                });
            }
        }
    }
    http_routes.sort_by(|a, b| {
        // Sort by literal-specificity: a path with fewer parameter segments
        // (more literals) wins. Stable secondary sort by method + path keeps
        // the diff deterministic.
        param_count(&a.path)
            .cmp(&param_count(&b.path))
            .then_with(|| a.method.as_str().cmp(b.method.as_str()))
            .then_with(|| a.path.cmp(&b.path))
    });

    // v0.10a: collect cron handlers across all services, carrying the per-service
    // declaration index (the method-name key) and sorting by schedule expression
    // for deterministic switch output.
    let mut cron_routes: Vec<CronRoute> = Vec::new();
    for sname in &service_names {
        let service = table.services.get(*sname).unwrap();
        let mut cron_idx = 0usize;
        for h in &service.handlers {
            if let HandlerKind::Cron { expr } = &h.kind {
                cron_routes.push(CronRoute {
                    service: (*sname).clone(),
                    index: cron_idx,
                    expr: expr.clone(),
                    has_param: !h.params.is_empty(),
                });
                cron_idx += 1;
            }
        }
    }
    cron_routes.sort_by(|a, b| a.expr.cmp(&b.expr));

    // v0.10b: collect queue consumers across all services, carrying the
    // per-service declaration index and sorting by queue name.
    let mut queue_routes: Vec<QueueRoute> = Vec::new();
    for sname in &service_names {
        let service = table.services.get(*sname).unwrap();
        let mut queue_idx = 0usize;
        for h in &service.handlers {
            if let HandlerKind::Message = &h.kind {
                // v0.44: the bound queue name lives on the service header
                // (`from queue("name")`), not on the handler.
                let ServiceProtocol::Queue { name } = &service.protocol else {
                    continue;
                };
                let msg_type = h.params.first().map(|p| p.type_ref.clone());
                queue_routes.push(QueueRoute {
                    service: (*sname).clone(),
                    index: queue_idx,
                    name: name.clone(),
                    msg_type,
                });
                queue_idx += 1;
            }
        }
    }
    queue_routes.sort_by(|a, b| a.name.cmp(&b.name));

    let has_http = !http_routes.is_empty();

    let mut imports: Vec<&str> = vec![
        "Ok",
        "Err",
        "type Result",
        "type JsonValue",
        "type BoundaryError",
        "boundaryError",
    ];
    if has_http {
        imports.push("matchPath");
        imports.push("httpResultToResponse");
    }
    // v0.51: a context with a Signature handler imports the HMAC verifier.
    if http_routes.iter().any(|r| r.signature.is_some()) {
        imports.push("verifySignatureHmacSha256");
    }
    let _ = writeln!(
        out,
        "import {{ {} }} from \"../../runtime.js\";",
        imports.join(", ")
    );
    let _ = writeln!(out, "import {{ compose, type Env }} from \"./compose.js\";");
    let _ = writeln!(out, "import * as handlers from \"./handlers.js\";");
    writeln!(out).unwrap();

    // v0.9.2: re-export each agent's Durable Object class from the Worker
    // entry. Cloudflare resolves a `class_name` binding against the named
    // exports of the Worker's `main`, so the DO classes declared in
    // `handlers.ts` must be visible here.
    let mut agent_names: Vec<&String> = table.agents.keys().collect();
    agent_names.sort();
    if !agent_names.is_empty() {
        let joined = agent_names
            .iter()
            .map(|n| n.as_str())
            .collect::<Vec<_>>()
            .join(", ");
        let _ = writeln!(out, "export {{ {joined} }} from \"./handlers.js\";");
        writeln!(out).unwrap();
    }

    let _ = writeln!(out, "export default {{");
    let _ = writeln!(
        out,
        "  async fetch(request: Request, env: Env): Promise<Response> {{"
    );
    let _ = writeln!(out, "    const url = new URL(request.url);");
    let _ = writeln!(out, "    const path = url.pathname;");
    let _ = writeln!(out, "    const method = request.method;");
    let _ = writeln!(out, "    const surface = compose(env);");
    let _ = writeln!(out, "    try {{");

    // 1. Internal Service Binding dispatch.
    let _ = writeln!(out, "      if (path.startsWith(\"/_bynk/call/\")) {{");
    let _ = writeln!(
        out,
        "        const servicePath = path.slice(\"/_bynk/call/\".length);"
    );
    let _ = writeln!(out, "        switch (servicePath) {{");
    for sname in &service_names {
        let service = table.services.get(*sname).unwrap();
        let Some(h) = service
            .handlers
            .iter()
            .find(|h| matches!(h.kind, HandlerKind::Call))
        else {
            continue;
        };

        let _ = writeln!(out, "          case \"{sname}\": {{");
        let _ = writeln!(
            out,
            "            const args = await request.json() as JsonValue;"
        );
        emit_call_handler_dispatch(&mut out, sname, h, &table.actors);
        let _ = writeln!(out, "          }}");
    }
    let _ = writeln!(out, "          default:");
    let _ = writeln!(
        out,
        "            return new Response(\"Not found\", {{ status: 404 }});"
    );
    let _ = writeln!(out, "        }}");
    let _ = writeln!(out, "      }}");
    writeln!(out).unwrap();

    // 2. External HTTP routes.
    for route in &http_routes {
        emit_http_route_dispatch(&mut out, route);
    }

    let _ = writeln!(
        out,
        "      return new Response(\"Not Found\", {{ status: 404 }});"
    );
    let _ = writeln!(out, "    }} catch {{");
    let _ = writeln!(
        out,
        "      return new Response(\"Internal Server Error\", {{ status: 500 }});"
    );
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out, "  }},");

    // v0.10a: scheduled (cron) entry point. Dispatches on `event.cron`. A
    // failing run has no retry channel, so an `Err` is logged and the run
    // completes (v0.10 §5.1, [DECISION 3]).
    if !cron_routes.is_empty() {
        emit_scheduled_handler(&mut out, &cron_routes);
    }

    // v0.10b: queue (consumer) entry point. Dispatches on `batch.queue`,
    // deserialises each message, invokes the handler, and acks on `Ok` /
    // retries on `Err` (a deserialisation failure also retries).
    if !queue_routes.is_empty() {
        emit_queue_handler(&mut out, &queue_routes);
    }

    let _ = writeln!(out, "}};");

    out
}

/// Emit the Worker `scheduled` handler aggregating every `on cron` handler in
/// the context. `event` is typed structurally (`{ cron: string }`) to avoid a
/// dependency on `@cloudflare/workers-types`, matching how the rest of the
/// emitter hand-declares the minimal ambient shapes it needs.
fn emit_scheduled_handler(out: &mut String, cron_routes: &[CronRoute]) {
    let _ = writeln!(
        out,
        "  async scheduled(event: {{ readonly cron: string; readonly scheduledTime: number }}, env: Env): Promise<void> {{"
    );
    let _ = writeln!(out, "    const surface = compose(env);");
    let _ = writeln!(out, "    switch (event.cron) {{");
    for route in cron_routes {
        let method_key = crate::emitter::cron_handler_method_name(&route.service, route.index);
        let expr_lit = route.expr.replace('\\', "\\\\").replace('"', "\\\"");
        // Pass the scheduled fire time (epoch ms) when the handler asked for it.
        let arg = if route.has_param {
            "event.scheduledTime"
        } else {
            ""
        };
        let _ = writeln!(out, "      case \"{expr_lit}\": {{");
        let _ = writeln!(
            out,
            "        const result = await surface.{method_key}({arg});"
        );
        let _ = writeln!(
            out,
            "        if (result.tag === \"Err\") console.error(\"cron {expr_lit} failed\", result.error);"
        );
        let _ = writeln!(out, "        return;");
        let _ = writeln!(out, "      }}");
    }
    let _ = writeln!(out, "      default:");
    let _ = writeln!(out, "        return;");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out, "  }},");
}

/// Emit the Worker `queue` handler aggregating every `on queue` consumer in the
/// context. Dispatches on `batch.queue`; for each message it deserialises the
/// body (v0.8 wire-format), invokes the handler, and acks on `Ok` / retries on
/// `Err`. A deserialisation failure or a thrown error also retries. `batch` is
/// typed structurally to avoid a `@cloudflare/workers-types` dependency.
fn emit_queue_handler(out: &mut String, queue_routes: &[QueueRoute]) {
    let _ = writeln!(
        out,
        "  async queue(batch: {{ readonly queue: string; readonly messages: ReadonlyArray<{{ readonly body: unknown; ack(): void; retry(): void }}> }}, env: Env): Promise<void> {{"
    );
    let _ = writeln!(out, "    const surface = compose(env);");
    let _ = writeln!(out, "    switch (batch.queue) {{");
    for route in queue_routes {
        let method_key = crate::emitter::queue_handler_method_name(&route.service, route.index);
        let name_lit = route.name.replace('\\', "\\\\").replace('"', "\\\"");
        let dser = match &route.msg_type {
            Some(t) => deserialise_call(t, "(msg.body as JsonValue)", "$"),
            None => "Ok(msg.body as any) as Result<any, BoundaryError>".to_string(),
        };
        let _ = writeln!(out, "      case \"{name_lit}\": {{");
        let _ = writeln!(out, "        for (const msg of batch.messages) {{");
        let _ = writeln!(out, "          try {{");
        let _ = writeln!(out, "            const __r = {dser};");
        let _ = writeln!(
            out,
            "            if (__r.tag === \"Err\") {{ console.error(\"queue {name_lit} deserialise failed\", __r.error); msg.retry(); continue; }}"
        );
        let _ = writeln!(
            out,
            "            const result = await surface.{method_key}(__r.value);"
        );
        let _ = writeln!(out, "            if (result.tag === \"Ack\") msg.ack();");
        let _ = writeln!(
            out,
            "            else {{ console.error(\"queue {name_lit} retry\", result.reason); msg.retry(); }}"
        );
        let _ = writeln!(out, "          }} catch (e) {{");
        let _ = writeln!(
            out,
            "            console.error(\"queue {name_lit} threw\", e); msg.retry();"
        );
        let _ = writeln!(out, "          }}");
        let _ = writeln!(out, "        }}");
        let _ = writeln!(out, "        return;");
        let _ = writeln!(out, "      }}");
    }
    let _ = writeln!(out, "      default:");
    let _ = writeln!(out, "        return;");
    let _ = writeln!(out, "    }}");
    let _ = writeln!(out, "  }},");
}

/// Count the number of `:param` segments in a path pattern.
fn param_count(path: &str) -> usize {
    path.split('/')
        .filter(|s| s.starts_with(':') && s.len() > 1)
        .count()
}

#[derive(Debug, Clone)]
struct HttpRoute {
    service: String,
    method: HttpMethod,
    path: String,
    handler: Handler,
    /// v0.47: the handler's `by` clause names a Bearer actor, so its surface
    /// wrapper runs the verification seam and takes the request as its first
    /// argument.
    bearer: bool,
    /// v0.51: the handler's `by` clause names a Signature actor — the entry
    /// dispatch reads the raw body, verifies the HMAC, and parses the body from
    /// those same bytes.
    signature: Option<crate::actors::SignatureSeam>,
    /// v0.52: the handler's `by` clause names a multi-actor sum — its wrapper
    /// owns the boundary, so the entry passes `request` (+ path params) and does
    /// not read or parse the body itself.
    sum: bool,
}

/// One `on cron` handler, identified by its service and per-service declaration
/// index (which together form its `cron_<service>_<index>` method key).
#[derive(Debug, Clone)]
struct CronRoute {
    service: String,
    index: usize,
    expr: String,
    /// Whether the handler declares the optional scheduled-time parameter.
    has_param: bool,
}

/// One `on queue` consumer, identified by its service and per-service
/// declaration index (its `queue_<service>_<index>` method key), plus the
/// message parameter's type for wire-format deserialisation.
#[derive(Debug, Clone)]
struct QueueRoute {
    service: String,
    index: usize,
    name: String,
    msg_type: Option<TypeRef>,
}

/// Generate the per-route dispatch block for one `on http` handler. The
/// router has already been entered via `try`; this block extracts path
/// parameters, deserialises the body (when present), invokes the handler,
/// and serialises the HttpResult through `httpResultToResponse`.
fn emit_http_route_dispatch(out: &mut String, route: &HttpRoute) {
    let h = &route.handler;
    let method_key = http_handler_method_name(route.method, &route.path);
    let has_path_params = param_count(&route.path) > 0;
    let path_lit = route.path.replace('"', "\\\"");
    let _ = writeln!(out, "      {{");
    if has_path_params {
        let _ = writeln!(out, "        const __m = matchPath(\"{path_lit}\", path);");
        let _ = writeln!(
            out,
            "        if (method === \"{}\" && __m) {{",
            route.method.as_str()
        );
    } else {
        let _ = writeln!(
            out,
            "        if (method === \"{}\" && path === \"{path_lit}\") {{",
            route.method.as_str()
        );
    }
    // Extract path parameters from the matched params map.
    let mut call_args: Vec<String> = Vec::new();
    for p in &h.params {
        let pname = &p.name.name;
        if pname == "body" {
            // Body deserialisation happens below.
            continue;
        }
        // It's a path parameter — extract from __m.params and construct.
        let _ = writeln!(
            out,
            "          const __raw_{pname} = __m.params[\"{pname}\"];"
        );
        emit_path_param_construction(out, pname, &p.type_ref);
        call_args.push(pname.clone());
    }
    // Body parameter (POST/PUT/PATCH). A multi-actor sum route's wrapper reads
    // and parses the body itself (it must verify over the raw bytes first), so
    // the entry skips the body here and passes only `request`.
    if !route.sum
        && let Some(body_param) = h.params.iter().find(|p| p.name.name == "body")
    {
        let _ = writeln!(out, "          let __body_json: JsonValue;");
        if let Some(seam) = &route.signature {
            // v0.51: read the raw body once, verify the HMAC fail-closed (401),
            // then parse the body param from the *same* bytes (not a re-read /
            // re-serialisation — the signature is over these exact bytes).
            let secret = seam.secret.replace('\\', "\\\\").replace('"', "\\\"");
            let header = seam.header.replace('\\', "\\\\").replace('"', "\\\"");
            let _ = writeln!(out, "          let __raw: string;");
            let _ = writeln!(out, "          try {{");
            let _ = writeln!(out, "            __raw = await request.text();");
            let _ = writeln!(out, "          }} catch {{");
            let _ = writeln!(
                out,
                "            return new Response(JSON.stringify({{ kind: \"MalformedJson\", details: \"Invalid request body\" }}), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
            );
            let _ = writeln!(out, "          }}");
            let _ = writeln!(
                out,
                "          const __secret = (env as Record<string, unknown>)[\"{secret}\"] ?? (globalThis as {{ process?: {{ env?: Record<string, unknown> }} }}).process?.env?.[\"{secret}\"];"
            );
            let _ = writeln!(
                out,
                "          if (typeof __secret !== \"string\") return new Response(null, {{ status: 401 }});"
            );
            // Timestamp (when bound): must be present; passed to the verifier.
            let ts_expr = match &seam.timestamp_header {
                Some(th) => {
                    let th = th.replace('\\', "\\\\").replace('"', "\\\"");
                    let _ = writeln!(out, "          const __ts = request.headers.get(\"{th}\");");
                    let _ = writeln!(
                        out,
                        "          if (__ts === null) return new Response(null, {{ status: 401 }});"
                    );
                    "__ts"
                }
                None => "null",
            };
            let tol = match seam.tolerance_secs {
                Some(n) => n.to_string(),
                None => "null".to_string(),
            };
            let _ = writeln!(
                out,
                "          const __ok = await verifySignatureHmacSha256(__raw, __secret, request.headers.get(\"{header}\"), {ts_expr}, {tol});"
            );
            let _ = writeln!(
                out,
                "          if (!__ok) return new Response(null, {{ status: 401 }});"
            );
            let _ = writeln!(out, "          try {{");
            let _ = writeln!(
                out,
                "            __body_json = JSON.parse(__raw) as JsonValue;"
            );
            let _ = writeln!(out, "          }} catch {{");
            let _ = writeln!(
                out,
                "            return new Response(JSON.stringify({{ kind: \"MalformedJson\", details: \"Invalid request body\" }}), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
            );
            let _ = writeln!(out, "          }}");
        } else {
            let _ = writeln!(out, "          try {{");
            let _ = writeln!(
                out,
                "            __body_json = (await request.json()) as JsonValue;"
            );
            let _ = writeln!(out, "          }} catch {{");
            let _ = writeln!(
                out,
                "            return new Response(JSON.stringify({{ kind: \"MalformedJson\", details: \"Invalid request body\" }}), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
            );
            let _ = writeln!(out, "          }}");
        }
        let dser = deserialise_call(&body_param.type_ref, "__body_json", "$");
        let _ = writeln!(out, "          const __r_body = {dser};");
        let _ = writeln!(
            out,
            "          if (__r_body.tag === \"Err\") return new Response(JSON.stringify(__r_body.error), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
        );
        let _ = writeln!(out, "          const body = __r_body.value;");
        call_args.push("body".to_string());
    }
    // Invoke the handler and serialise the HttpResult. The handler is
    // wrapped on the surface so its deps are wired by `compose`. v0.47: a
    // Bearer wrapper takes the request first (it runs the verification seam).
    let surface_args = if route.bearer || route.sum {
        // The Bearer and sum wrappers take the request first (they run the
        // verification seam); a sum wrapper also reads/parses the body itself,
        // so `call_args` here carries only the path params.
        let mut a = vec!["request".to_string()];
        a.extend(call_args.iter().cloned());
        a.join(", ")
    } else {
        call_args.join(", ")
    };
    let _ = writeln!(
        out,
        "          const result = await surface.{method_key}({surface_args});",
    );
    let _ = route.service.as_str();
    let inner = http_result_inner(&h.return_type);
    let ser_fn = http_value_serialiser(&inner);
    let _ = writeln!(
        out,
        "          return httpResultToResponse(result, {ser_fn});"
    );
    let _ = writeln!(out, "        }}");
    let _ = writeln!(out, "      }}");
}

/// Synthesise a deps object literal for invoking a handler from the
/// fetch entry point. Mirrors `compose`'s deps construction so handlers
/// see the same shape whether invoked through the surface or directly.
fn emit_call_handler_dispatch(
    out: &mut String,
    sname: &str,
    h: &Handler,
    actors: &std::collections::HashMap<String, crate::ast::ActorDecl>,
) {
    // v0.54: a `by c: Caller` handler reads the caller's context name from the
    // `X-Bynk-Caller` header (stamped by `callService`) and threads it into the
    // surface call. The internal channel is trusted, but a missing caller means
    // a malformed / non-Bynk call — fail-closed (the `Internal` 401-analogue).
    let caller_args = if crate::actors::caller_binder_for(h, actors).is_some() {
        let _ = writeln!(
            out,
            "            const __caller = request.headers.get(\"X-Bynk-Caller\");"
        );
        let _ = writeln!(
            out,
            "            if (__caller === null || __caller === \"\") return new Response(JSON.stringify({{ kind: \"Unauthorized\", details: \"missing caller identity\" }}), {{ status: 401, headers: {{ \"content-type\": \"application/json\" }} }});"
        );
        "__caller, "
    } else {
        ""
    };
    if h.params.len() == 1 {
        let p = &h.params[0];
        let pname = &p.name.name;
        let dser_call = deserialise_call(&p.type_ref, "args", "$");
        let _ = writeln!(out, "            const __r_{pname} = {dser_call};");
        let _ = writeln!(
            out,
            "            if (__r_{pname}.tag === \"Err\") return new Response(JSON.stringify(__r_{pname}.error), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
        );
        let _ = writeln!(out, "            const {pname} = __r_{pname}.value;");
        let _ = writeln!(
            out,
            "            const result = await surface.{sname}({caller_args}{pname});"
        );
    } else {
        let _ = writeln!(
            out,
            "            if (typeof args !== \"object\" || args === null || Array.isArray(args)) return new Response(JSON.stringify({{ kind: \"StructuralMismatch\", path: \"$\", expected: \"object\", actual: typeof args }}), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
        );
        let _ = writeln!(
            out,
            "            const argsObj = args as {{ [k: string]: JsonValue }};"
        );
        let mut names = Vec::new();
        for p in &h.params {
            let pname = &p.name.name;
            let dser = deserialise_call(
                &p.type_ref,
                &format!("argsObj[\"{pname}\"]"),
                &format!("$.{pname}"),
            );
            let _ = writeln!(out, "            const __r_{pname} = {dser};");
            let _ = writeln!(
                out,
                "            if (__r_{pname}.tag === \"Err\") return new Response(JSON.stringify(__r_{pname}.error), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
            );
            let _ = writeln!(out, "            const {pname} = __r_{pname}.value;");
            names.push(pname.clone());
        }
        // Prepend the caller (when present) without a dangling comma for the
        // zero-param case.
        let mut call_args: Vec<&str> = Vec::new();
        if !caller_args.is_empty() {
            call_args.push("__caller");
        }
        call_args.extend(names.iter().map(String::as_str));
        let _ = writeln!(
            out,
            "            const result = await surface.{sname}({});",
            call_args.join(", ")
        );
    }
    let ser_expr = serialise_call(&h.return_type, "result");
    let _ = writeln!(out, "            const body = {ser_expr};");
    let _ = writeln!(
        out,
        "            return new Response(JSON.stringify(body), {{ status: 200, headers: {{ \"content-type\": \"application/json\" }} }});"
    );
}

/// Emit code that converts the raw path-parameter string to the parameter's
/// declared type. For plain `String`, this is a direct assignment; for
/// refined or opaque `String`-based types we lower through `T.of(...)`
/// which performs refinement validation and returns 400 on failure.
fn emit_path_param_construction(out: &mut String, pname: &str, t: &TypeRef) {
    match t {
        TypeRef::Base(BaseType::String, _) => {
            let _ = writeln!(out, "          const {pname} = __raw_{pname};");
        }
        TypeRef::Named(id) => {
            let _ = writeln!(
                out,
                "          const __r_{pname} = handlers.{}.of(__raw_{pname});",
                id.name
            );
            let _ = writeln!(
                out,
                "          if (__r_{pname}.tag === \"Err\") return new Response(JSON.stringify({{ kind: \"RefinementViolation\", path: \"path.{pname}\", violation: __r_{pname}.error }}), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
            );
            let _ = writeln!(out, "          const {pname} = __r_{pname}.value;");
        }
        _ => {
            // Other shapes are rejected by the static check; emit a fallback.
            let _ = writeln!(out, "          const {pname} = __raw_{pname} as any;");
        }
    }
}

/// Strip the `Effect[HttpResult[_]]` wrapper to expose the inner payload type
/// `T`. Used to choose the right serialiser when emitting the HttpResult
/// body.
fn http_result_inner(t: &TypeRef) -> TypeRef {
    let inner = match t {
        TypeRef::Effect(t, _) => t.as_ref(),
        other => other,
    };
    match inner {
        TypeRef::HttpResult(payload, _) => (**payload).clone(),
        other => other.clone(),
    }
}

/// TypeScript expression that serialises a value of the `HttpResult[T]`
/// payload type to a `JsonValue`. Wraps the per-type serialise helper for
/// named types; uses pass-through for base types.
fn http_value_serialiser(t: &TypeRef) -> String {
    match t {
        TypeRef::Base(_, _) => "(v: any) => v as JsonValue".to_string(),
        // v0.20a: function types are confined to non-boundary positions
        // (`bynk.types.function_at_boundary`), so the serialisation machinery
        // can never legally see one.
        TypeRef::Fn(..) => unreachable!("function types are rejected at boundaries"),
        TypeRef::Unit(_) => "(_v: any) => null".to_string(),
        TypeRef::Named(id) => format!("handlers.serialise_{}", id.name),
        TypeRef::Result(_, _, _)
        | TypeRef::Option(_, _)
        | TypeRef::List(_, _)
        | TypeRef::Map(_, _, _) => {
            let inst_name = inner_ts_name(t);
            format!("handlers.serialise_{inst_name}")
        }
        TypeRef::Effect(inner, _) => http_value_serialiser(inner),
        TypeRef::HttpResult(_, _)
        | TypeRef::QueueResult(_)
        | TypeRef::ValidationError(_)
        | TypeRef::JsonError(_) => "(v: any) => v as JsonValue".to_string(),
    }
}

pub(crate) fn deserialise_call(t: &TypeRef, json_expr: &str, path: &str) -> String {
    match t {
        // v0.20a: function types are confined to non-boundary positions
        // (`bynk.types.function_at_boundary`), so the serialisation machinery
        // can never legally see one.
        TypeRef::Fn(..) => unreachable!("function types are rejected at boundaries"),
        TypeRef::Base(b, _) => {
            let typeof_str = match b {
                BaseType::Int => "number",
                BaseType::String => "string",
                BaseType::Bool => "boolean",
                BaseType::Float => "number",
            };
            // v0.22b: bare `Int` params validate integrality (ADR 0049).
            if *b == BaseType::Int {
                return format!(
                    "(typeof {json_expr} === \"number\" && Number.isInteger({json_expr}) ? Ok({json_expr}) : Err({{ kind: \"StructuralMismatch\", path: \"{path}\", expected: \"integer\", actual: String({json_expr}) }}) as Result<any, BoundaryError>)"
                );
            }
            // v0.21: boundary `Float` values are finite (ADR 0040).
            if *b == BaseType::Float {
                return format!(
                    "(typeof {json_expr} === \"number\" && Number.isFinite({json_expr}) ? Ok({json_expr}) : Err({{ kind: \"StructuralMismatch\", path: \"{path}\", expected: \"finite number\", actual: String({json_expr}) }}) as Result<any, BoundaryError>)"
                );
            }
            format!(
                "(typeof {json_expr} === \"{typeof_str}\" ? Ok({json_expr}) : Err({{ kind: \"StructuralMismatch\", path: \"{path}\", expected: \"{typeof_str}\", actual: typeof {json_expr} }}) as Result<any, BoundaryError>)"
            )
        }
        TypeRef::Named(id) => {
            format!("handlers.deserialise_{}({json_expr}, \"{path}\")", id.name)
        }
        TypeRef::Result(_, _, _)
        | TypeRef::Option(_, _)
        | TypeRef::List(_, _)
        | TypeRef::Map(_, _, _) => {
            let inst_name = inner_ts_name(t);
            format!("handlers.deserialise_{inst_name}({json_expr}, \"{path}\")")
        }
        TypeRef::Effect(inner, _) => deserialise_call(inner, json_expr, path),
        TypeRef::HttpResult(_, _)
        | TypeRef::QueueResult(_)
        | TypeRef::ValidationError(_)
        | TypeRef::JsonError(_)
        | TypeRef::Unit(_) => {
            format!("Ok({json_expr} as any) as Result<any, BoundaryError>")
        }
    }
}

fn serialise_call(t: &TypeRef, value: &str) -> String {
    match t {
        // v0.21: serialising a non-finite `Float` is a contract violation
        // (ADR 0040) — `JSON.stringify(NaN)` would silently produce `null`.
        TypeRef::Base(BaseType::Float, _) => format!(
            "((v: number) => {{ if (!Number.isFinite(v)) throw new Error(\"non-finite Float at boundary\"); return v as JsonValue; }})({value})"
        ),
        TypeRef::Base(_, _) => format!("{value} as JsonValue"),
        // v0.20a: function types are confined to non-boundary positions
        // (`bynk.types.function_at_boundary`), so the serialisation machinery
        // can never legally see one.
        TypeRef::Fn(..) => unreachable!("function types are rejected at boundaries"),
        TypeRef::Named(id) => format!("handlers.serialise_{}({value})", id.name),
        TypeRef::Result(_, _, _)
        | TypeRef::Option(_, _)
        | TypeRef::List(_, _)
        | TypeRef::Map(_, _, _) => {
            let inst_name = inner_ts_name(t);
            format!("handlers.serialise_{inst_name}({value})")
        }
        TypeRef::Effect(inner, _) => serialise_call(inner, value),
        // Unit serialises to JSON `null` (the value is `void`, which cannot be
        // cast to `JsonValue` under `tsc --strict`).
        TypeRef::Unit(_) => "null".to_string(),
        TypeRef::HttpResult(_, _)
        | TypeRef::QueueResult(_)
        | TypeRef::ValidationError(_)
        | TypeRef::JsonError(_) => {
            format!("{value} as JsonValue")
        }
    }
}

fn inner_ts_name(t: &TypeRef) -> String {
    match t {
        TypeRef::Base(b, _) => b.name().to_string(),
        // v0.20a: function types are confined to non-boundary positions
        // (`bynk.types.function_at_boundary`), so the serialisation machinery
        // can never legally see one.
        TypeRef::Fn(..) => unreachable!("function types are rejected at boundaries"),
        TypeRef::Named(id) => id.name.clone(),
        TypeRef::Result(a, b, _) => format!("Result_{}_{}", inner_ts_name(a), inner_ts_name(b)),
        TypeRef::Option(a, _) => format!("Option_{}", inner_ts_name(a)),
        TypeRef::Effect(a, _) => format!("Effect_{}", inner_ts_name(a)),
        TypeRef::HttpResult(a, _) => format!("HttpResult_{}", inner_ts_name(a)),
        TypeRef::List(a, _) => format!("List_{}", inner_ts_name(a)),
        TypeRef::Map(k, v, _) => format!("Map_{}_{}", inner_ts_name(k), inner_ts_name(v)),
        TypeRef::QueueResult(_) => "QueueResult".to_string(),
        TypeRef::ValidationError(_) => "ValidationError".to_string(),
        TypeRef::JsonError(_) => "JsonError".to_string(),
        TypeRef::Unit(_) => "Unit".to_string(),
    }
}
