//! Per-Worker composition root generation (v0.8 §4.5, v0.9 §5.1).
//!
//! Each Worker's `compose.ts` exports a `compose(env)` function that
//! assembles the context's deps and returns the surface the entry point
//! invokes — `on call` services for the internal Service Binding protocol
//! plus `on http` route wrappers for the external HTTP router.

use std::collections::{BTreeSet, HashMap};
use std::fmt::Write as _;

use crate::ast::*;
use crate::emitter::http_handler_method_name;
use crate::emitter::wrangler::{agent_binding_name, consumed_binding_name};
use crate::project::UnitTable;

#[allow(clippy::too_many_arguments)]
pub fn emit_worker_compose(
    context: &str,
    table: &UnitTable,
    consumes: &[String],
    aliases: &HashMap<String, String>,
    unit_tables: &HashMap<String, UnitTable>,
    // v0.17: adapter unit name → its binding module path (relative to the out
    // root, `.js`). An adapter's external provider class lives in this module,
    // not in a `handlers.ts`.
    binding_modules: &HashMap<String, String>,
    // v0.17: bare flattened capability → the unit it was flattened from.
    flattened: &HashMap<String, String>,
    // v0.18: the whole-project consume/alias/flattening maps, so external
    // provider deps recurse across adapters (spec §4.5).
    unit_consumes: &HashMap<String, Vec<String>>,
    unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
    unit_flattened: &HashMap<String, HashMap<String, String>>,
    // v0.19 (C1): this Worker's closure reaches bynk.cloudflare — type the
    // `KV` namespace into Env (the matching `[[kv_namespaces]]` stanza is
    // emitted into wrangler.toml).
    needs_kv: bool,
) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "// Generated by bynkc — do not edit by hand.");
    let _ = writeln!(out, "// composition root for `{context}` Worker.");
    writeln!(out).unwrap();
    // v0.15: cross-context capabilities this Worker uses (in handlers or in a
    // local provider's `given`) → `deps_key → consumed_context`. Their
    // providers are instantiated locally (model A1).
    let cross_caps = worker_cross_caps(table, consumes, aliases, flattened);

    // v0.18: build each cross-cap provider expression up front, recording the
    // units it references — an external provider's `given` may pull in another
    // adapter's binding (the transitive given-closure), which must be imported.
    let mut referenced_units: BTreeSet<String> = BTreeSet::new();
    let mut cross_cap_exprs: Vec<(String, String)> = Vec::new();
    for (key, cctx) in &cross_caps {
        let expr = crate::project::instantiate_provider_expr(
            cctx,
            key,
            unit_tables,
            unit_consumes,
            unit_consumes_aliases,
            unit_flattened,
            true,
            Some("env"),
            &mut referenced_units,
        );
        cross_cap_exprs.push((key.clone(), expr));
    }

    // v0.47/v0.52: a context with a Bearer handler — or a sum with a Bearer
    // member — imports the JWT verifier; a sum with a Signature member imports
    // the HMAC verifier. Any verifying wrapper returns `HttpResult` (the 401/400
    // shaping the entry maps).
    let sum_handlers: Vec<Vec<crate::actors::SumMember>> = table
        .services
        .values()
        .flat_map(|s| s.handlers.iter())
        .filter_map(|h| crate::actors::sum_members_for(h, &table.actors))
        .collect();
    use crate::actors::SumMemberSeam;
    let has_bearer = table.services.values().any(|s| {
        s.handlers
            .iter()
            .any(|h| crate::actors::bearer_seam_for(h, &table.actors).is_some())
    }) || sum_handlers
        .iter()
        .flatten()
        .any(|m| matches!(m.seam, SumMemberSeam::Bearer { .. }));
    let has_sum_signature = sum_handlers
        .iter()
        .flatten()
        .any(|m| matches!(m.seam, SumMemberSeam::Signature(_)));
    let has_sum = !sum_handlers.is_empty();
    // A sum wrapper references `JsonValue` only when it parses a `body`.
    let sum_parses_body = sum_handlers.iter().flatten().any(|m| m.needs_body())
        || table
            .services
            .values()
            .flat_map(|s| s.handlers.iter())
            .any(|h| {
                crate::actors::sum_members_for(h, &table.actors).is_some()
                    && h.params.iter().any(|p| p.name.name == "body")
            });
    let mut runtime_imports: Vec<&str> = Vec::new();
    if needs_kv {
        runtime_imports.push("type KVNamespace");
    }
    runtime_imports.push("type ServiceBinding");
    if has_bearer || has_sum {
        runtime_imports.push("HttpResult");
    }
    if has_bearer {
        runtime_imports.push("verifyBearerJwtHs256");
    }
    if has_sum_signature {
        runtime_imports.push("verifySignatureHmacSha256");
    }
    if sum_parses_body {
        runtime_imports.push("type JsonValue");
    }
    let _ = writeln!(
        out,
        "import {{ {} }} from \"../../runtime.js\";",
        runtime_imports.join(", ")
    );
    let _ = writeln!(out, "import * as handlers from \"./handlers.js\";");
    // Import each referenced unit's provider classes. A *context*'s providers
    // live in its Worker's `handlers.js`; an *adapter*'s external provider
    // classes live in its binding module at the out root.
    for cctx in &referenced_units {
        let ns = cctx.replace('.', "_");
        if let Some(module) = binding_modules.get(cctx) {
            let _ = writeln!(out, "import * as {ns}__binding from \"../../{module}\";");
        } else {
            let dir = crate::project::worker_dir_name(cctx);
            let _ = writeln!(
                out,
                "import * as handlers_{ns} from \"../{dir}/handlers.js\";"
            );
        }
    }
    writeln!(out).unwrap();

    // Only consumed *contexts* become Service Bindings — a consumed adapter is
    // not a Worker (its capability is provided in-process via the binding).
    let mut sorted_consumes: Vec<&String> = consumes
        .iter()
        .filter(|t| !binding_modules.contains_key(*t))
        .collect();
    sorted_consumes.sort();

    // Env shape: one Service Binding per consumed context + DO bindings.
    // v0.19: plus the typed KV namespace when the closure reaches the
    // cloudflare platform adapter (decision C1 — one fixed `KV` binding).
    let _ = writeln!(out, "export interface Env {{");
    for t in &sorted_consumes {
        let bind = consumed_binding_name(t);
        let _ = writeln!(out, "  {bind}: ServiceBinding;");
    }
    if needs_kv {
        let _ = writeln!(
            out,
            "  {}: KVNamespace;",
            crate::firstparty::KV_BINDING_NAME
        );
    }
    let mut agent_names: Vec<&String> = table.agents.keys().collect();
    agent_names.sort();
    for a in &agent_names {
        let bind = agent_binding_name(a);
        let _ = writeln!(out, "  {bind}: DurableObjectNamespace;");
    }
    let _ = writeln!(out, "}}");
    writeln!(out).unwrap();

    if !agent_names.is_empty() {
        let _ = writeln!(
            out,
            "type DurableObjectNamespace = {{ idFromName(name: string): {{ toString(): string }}; get(id: any): any }};"
        );
        writeln!(out).unwrap();
    }

    let _ = writeln!(out, "export function compose(env: Env) {{");

    // v0.15: instantiate consumed-unit capability providers locally first, so
    // local providers (and handlers) can depend on them by their deps key.
    // v0.18: the expression recursively wires an external provider's `given`
    // deps (built in the pre-pass above; cross_caps is a BTreeMap, so the
    // pairs are already key-sorted).
    let cross_keys: Vec<&String> = cross_caps.keys().collect();
    for (key, expr) in &cross_cap_exprs {
        let _ = writeln!(out, "  const {key} = {expr};");
    }

    // Capabilities: instantiate each capability's provider. v0.12: providers
    // are emitted in dependency order (a composed provider's `given` deps must
    // exist first) as local `const` bindings, injecting each provider's deps;
    // then assembled into the `deps` object. v0.15: a provider's `given` may
    // include cross-context capability keys (instantiated above).
    let order = crate::emitter::topo_order_providers(&table.providers);
    for cap in &order {
        let provider = table.providers.get(cap).unwrap();
        let provider_ts = &provider.provider_name.name;
        if provider.given.is_empty() {
            let _ = writeln!(out, "  const {cap} = new handlers.{provider_ts}();");
        } else {
            let dep_obj = provider
                .given
                .iter()
                .map(|c| c.key().to_string())
                .collect::<Vec<_>>()
                .join(", ");
            let _ = writeln!(
                out,
                "  const {cap} = new handlers.{provider_ts}({{ {dep_obj} }});"
            );
        }
    }
    let mut deps_entries: Vec<String> = {
        let mut caps: Vec<String> = order.clone();
        caps.extend(cross_keys.iter().map(|k| (*k).clone()));
        caps.sort();
        caps
    };
    // env passes through so handlers' cross-context calls (Service Bindings)
    // and agent instantiations (Durable Object namespaces) can reach it.
    if !sorted_consumes.is_empty() || !table.agents.is_empty() {
        deps_entries.push("env".to_string());
    }
    let _ = writeln!(out, "  const deps = {{ {} }};", deps_entries.join(", "));

    // Local-surface object: one async wrapper per service operation plus
    // one wrapper per `on http` handler.
    let mut service_names: Vec<&String> = table.services.keys().collect();
    service_names.sort();
    let _ = writeln!(out, "  return {{");
    for sname in &service_names {
        let service = table.services.get(*sname).unwrap();
        let mut cron_idx = 0usize;
        let mut queue_idx = 0usize;
        for h in &service.handlers {
            match &h.kind {
                HandlerKind::Call => {
                    emit_call_wrapper(&mut out, sname, h, &table.actors);
                }
                HandlerKind::Http { method, path } => {
                    // v0.52: a multi-actor sum handler gets the first-wins
                    // resolution wrapper; otherwise the single-actor path.
                    if let Some(members) = crate::actors::sum_members_for(h, &table.actors) {
                        emit_http_sum_wrapper(&mut out, sname, h, *method, path, &members);
                    } else {
                        let seam = crate::actors::bearer_seam_for(h, &table.actors);
                        emit_http_wrapper(&mut out, sname, h, *method, path, seam.as_ref());
                    }
                }
                HandlerKind::Cron { .. } => {
                    emit_cron_wrapper(&mut out, sname, cron_idx, h);
                    cron_idx += 1;
                }
                HandlerKind::Message => {
                    emit_queue_wrapper(&mut out, sname, queue_idx, h);
                    queue_idx += 1;
                }
            }
        }
    }
    let _ = writeln!(out, "  }};");

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

/// v0.15: cross-context capabilities this Worker references in handlers or in
/// a local provider's `given`, as `deps_key → consumed_context`.
fn worker_cross_caps(
    table: &UnitTable,
    consumes: &[String],
    aliases: &HashMap<String, String>,
    flattened: &HashMap<String, String>,
) -> std::collections::BTreeMap<String, String> {
    fn resolve(
        prefix: &str,
        consumes: &[String],
        aliases: &HashMap<String, String>,
    ) -> Option<String> {
        if let Some(q) = aliases.get(prefix) {
            return Some(q.clone());
        }
        if consumes.iter().any(|c| c == prefix) {
            return Some(prefix.to_string());
        }
        None
    }
    let mut out = std::collections::BTreeMap::new();
    let mut givens: Vec<&[CapRef]> = Vec::new();
    for s in table.services.values() {
        for h in &s.handlers {
            givens.push(&h.given);
        }
    }
    for a in table.agents.values() {
        for h in &a.handlers {
            givens.push(&h.given);
        }
    }
    for p in table.providers.values() {
        givens.push(&p.given);
    }
    for given in givens {
        for c in given {
            if let Some(p) = c.prefix() {
                if let Some(ctx) = resolve(&p, consumes, aliases) {
                    out.entry(c.key().to_string()).or_insert(ctx);
                }
            } else if let Some(unit) = flattened.get(c.key()) {
                // v0.17: a bare flattened capability is provided by its source unit.
                out.entry(c.key().to_string())
                    .or_insert_with(|| unit.clone());
            }
        }
    }
    out
}

fn emit_call_wrapper(
    out: &mut String,
    sname: &str,
    h: &Handler,
    actors: &HashMap<String, ActorDecl>,
) {
    let mut param_decls: Vec<String> = h
        .params
        .iter()
        .map(|p| format!("{}: any", p.name.name))
        .collect();
    let param_args: Vec<String> = h.params.iter().map(|p| p.name.name.clone()).collect();
    // v0.54: a `by c: Caller` handler's wrapper takes the caller's context name
    // (read from the header in the entry dispatch) and threads it into `deps`
    // as the `CallerId` identity — mirroring the Bearer identity threading.
    let deps_expr = if crate::actors::caller_binder_for(h, actors).is_some() {
        param_decls.insert(0, "__caller: string".to_string());
        "{ ...deps, identity: __caller }"
    } else {
        "deps"
    };
    let _ = writeln!(out, "    async {sname}({}) {{", param_decls.join(", "));
    let _ = writeln!(
        out,
        "      return handlers.{sname}.call({}{}{deps_expr});",
        param_args.join(", "),
        if param_args.is_empty() { "" } else { ", " },
    );
    let _ = writeln!(out, "    }},");
}

fn emit_cron_wrapper(out: &mut String, sname: &str, cron_idx: usize, h: &Handler) {
    let method_key = crate::emitter::cron_handler_method_name(sname, cron_idx);
    // A cron handler takes an optional scheduled-time parameter; forward it (if
    // any) to the bound handler, with deps trailing.
    let param_decls: Vec<String> = h
        .params
        .iter()
        .map(|p| format!("{}: any", p.name.name))
        .collect();
    let param_args: Vec<String> = h.params.iter().map(|p| p.name.name.clone()).collect();
    let _ = writeln!(out, "    async {method_key}({}) {{", param_decls.join(", "));
    let _ = writeln!(
        out,
        "      return handlers.{sname}.{method_key}({}{}deps);",
        param_args.join(", "),
        if param_args.is_empty() { "" } else { ", " },
    );
    let _ = writeln!(out, "    }},");
}

fn emit_queue_wrapper(out: &mut String, sname: &str, queue_idx: usize, h: &Handler) {
    let method_key = crate::emitter::queue_handler_method_name(sname, queue_idx);
    // The queue handler takes its message parameter; forward it with deps.
    let param_decls: Vec<String> = h
        .params
        .iter()
        .map(|p| format!("{}: any", p.name.name))
        .collect();
    let param_args: Vec<String> = h.params.iter().map(|p| p.name.name.clone()).collect();
    let _ = writeln!(out, "    async {method_key}({}) {{", param_decls.join(", "));
    let _ = writeln!(
        out,
        "      return handlers.{sname}.{method_key}({}{}deps);",
        param_args.join(", "),
        if param_args.is_empty() { "" } else { ", " },
    );
    let _ = writeln!(out, "    }},");
}

fn emit_http_wrapper(
    out: &mut String,
    sname: &str,
    h: &Handler,
    method: HttpMethod,
    path: &str,
    seam: Option<&crate::actors::BearerSeam>,
) {
    let method_key = http_handler_method_name(method, path);
    let param_args: Vec<String> = h.params.iter().map(|p| p.name.name.clone()).collect();

    // v0.47: a Bearer handler's wrapper takes the request, runs the fail-closed
    // verification seam, mints the identity, and threads it into `deps`. The
    // boundary owns `env` (the secret source) and `deps`, so the whole seam is
    // one cohesive block here; any failure returns `Unauthorized` (401), which
    // the entry's `httpResultToResponse` maps. The body never runs unverified.
    if let Some(seam) = seam {
        let mut decls: Vec<String> = vec!["request: Request".to_string()];
        decls.extend(h.params.iter().map(|p| format!("{}: any", p.name.name)));
        let secret = seam.secret.replace('\\', "\\\\").replace('"', "\\\"");
        let _ = writeln!(out, "    async {method_key}({}) {{", decls.join(", "));
        let _ = writeln!(
            out,
            "      const __authz = request.headers.get(\"Authorization\");"
        );
        let _ = writeln!(
            out,
            "      if (__authz === null || !__authz.startsWith(\"Bearer \")) return HttpResult.Unauthorized;"
        );
        // Source the signing secret from the same env the `Secrets` capability
        // reads (explicit env first, then a `process.env` probe).
        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 HttpResult.Unauthorized;"
        );
        let _ = writeln!(
            out,
            "      const __claims = await verifyBearerJwtHs256(__authz.slice(7), __secret);"
        );
        let _ = writeln!(
            out,
            "      if (__claims.tag === \"Err\") return HttpResult.Unauthorized;"
        );
        // v0.53: a refinement actor's authorisation invariant — the scheme
        // verified (else 401 above), so a failed claim predicate is 403, not
        // 401. Checked against the *verified* claims, before the identity mints
        // or the body runs.
        if let Some(pred) = &seam.authorization {
            let js = crate::actors::claim_predicate_to_js(pred, "__claims.value.claims");
            let _ = writeln!(out, "      if (!({js})) return HttpResult.Forbidden;");
        }
        if seam.binder.is_some() {
            // Capture the identity: construct the declared type from `sub`
            // (fail-closed on a refinement violation) and thread it into deps.
            let _ = writeln!(
                out,
                "      const __id = handlers.{}.of(__claims.value.sub);",
                seam.identity_type
            );
            let _ = writeln!(
                out,
                "      if (__id.tag === \"Err\") return HttpResult.Unauthorized;"
            );
            let _ = writeln!(
                out,
                "      return handlers.{sname}.{method_key}({}{}{{ ...deps, identity: __id.value }});",
                param_args.join(", "),
                if param_args.is_empty() { "" } else { ", " },
            );
        } else {
            // Binder-less: the token is verified (fail-closed above); the
            // identity is not captured, so call the handler with plain deps.
            let _ = writeln!(
                out,
                "      return handlers.{sname}.{method_key}({}{}deps);",
                param_args.join(", "),
                if param_args.is_empty() { "" } else { ", " },
            );
        }
        let _ = writeln!(out, "    }},");
        return;
    }

    let param_decls: Vec<String> = h
        .params
        .iter()
        .map(|p| format!("{}: any", p.name.name))
        .collect();
    let _ = writeln!(out, "    async {method_key}({}) {{", param_decls.join(", "));
    let _ = writeln!(
        out,
        "      return handlers.{sname}.{method_key}({}{}deps);",
        param_args.join(", "),
        if param_args.is_empty() { "" } else { ", " },
    );
    let _ = writeln!(out, "    }},");
}

/// Source a string secret from the same env the `Secrets` capability reads
/// (explicit `env` first, then a `process.env` probe), binding it to `var`.
fn emit_secret_lookup(out: &mut String, var: &str, secret: &str, indent: &str) {
    let secret = secret.replace('\\', "\\\\").replace('"', "\\\"");
    let _ = writeln!(
        out,
        "{indent}const {var} = (env as Record<string, unknown>)[\"{secret}\"] ?? (globalThis as {{ process?: {{ env?: Record<string, unknown> }} }}).process?.env?.[\"{secret}\"];"
    );
}

/// v0.52: the compose wrapper for a **multi-actor sum** handler (`by who: A |
/// B`). Unlike the single-actor wrappers, this one owns the *whole* boundary:
/// it reads the raw body once (when any member needs it or the handler takes a
/// `body`), tries each member's scheme in declared order, binds the first that
/// verifies into a tagged `__who`, and — fail-closed → 401 if none verifies —
/// parses the body and dispatches with `who` threaded through `deps`. The body
/// `match`es on `who`. The entry passes `request` (+ any path params); no body
/// read happens in the entry for a sum route.
fn emit_http_sum_wrapper(
    out: &mut String,
    sname: &str,
    h: &Handler,
    method: HttpMethod,
    path: &str,
    members: &[crate::actors::SumMember],
) {
    use crate::actors::SumMemberSeam;
    let method_key = http_handler_method_name(method, path);
    // The wrapper takes the request first (it reads the body / headers), then
    // the path params (parsed in the entry and passed through); the `body`
    // param is parsed here, not passed in.
    let path_params: Vec<&String> = h
        .params
        .iter()
        .map(|p| &p.name.name)
        .filter(|n| *n != "body")
        .collect();
    let has_body = h.params.iter().any(|p| p.name.name == "body");
    let mut decls = vec!["request: Request".to_string()];
    decls.extend(path_params.iter().map(|n| format!("{n}: any")));
    let _ = writeln!(out, "    async {method_key}({}) {{", decls.join(", "));

    // Read the raw body once if a member verifies over it (Signature) or the
    // handler takes a `body` param (parsed from the same bytes).
    let needs_raw = has_body || members.iter().any(|m| m.needs_body());
    if needs_raw {
        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 HttpResult.BadRequest(\"Invalid request body\");"
        );
        let _ = writeln!(out, "      }}");
    }

    // First-wins resolution: try each member in order; the first to verify sets
    // `__who`. A `None` (catch-all) member always succeeds.
    let _ = writeln!(out, "      let __who: any = undefined;");
    for member in members {
        let tag = member.actor_name.replace('\\', "\\\\").replace('"', "\\\"");
        let _ = writeln!(out, "      if (__who === undefined) {{");
        match &member.seam {
            SumMemberSeam::None => {
                let _ = writeln!(out, "        __who = {{ tag: \"{tag}\" }};");
            }
            SumMemberSeam::Bearer {
                secret,
                identity_type,
            } => {
                let _ = writeln!(
                    out,
                    "        const __authz = request.headers.get(\"Authorization\");"
                );
                let _ = writeln!(
                    out,
                    "        if (__authz !== null && __authz.startsWith(\"Bearer \")) {{"
                );
                emit_secret_lookup(out, "__secret", secret, "          ");
                let _ = writeln!(out, "          if (typeof __secret === \"string\") {{");
                let _ = writeln!(
                    out,
                    "            const __claims = await verifyBearerJwtHs256(__authz.slice(7), __secret);"
                );
                let _ = writeln!(out, "            if (__claims.tag === \"Ok\") {{");
                let _ = writeln!(
                    out,
                    "              const __id = handlers.{identity_type}.of(__claims.value.sub);"
                );
                let _ = writeln!(
                    out,
                    "              if (__id.tag === \"Ok\") __who = {{ tag: \"{tag}\", identity: __id.value }};"
                );
                let _ = writeln!(out, "            }}");
                let _ = writeln!(out, "          }}");
                let _ = writeln!(out, "        }}");
            }
            SumMemberSeam::Signature(seam) => {
                let header = seam.header.replace('\\', "\\\\").replace('"', "\\\"");
                emit_secret_lookup(out, "__secret", &seam.secret, "        ");
                let _ = writeln!(out, "        if (typeof __secret === \"string\") {{");
                let ts_expr = match &seam.timestamp_header {
                    Some(th) => {
                        let th = th.replace('\\', "\\\\").replace('"', "\\\"");
                        let _ =
                            writeln!(out, "          const __ts = request.headers.get(\"{th}\");");
                        "__ts"
                    }
                    None => "null",
                };
                let tol = match seam.tolerance_secs {
                    Some(n) => n.to_string(),
                    None => "null".to_string(),
                };
                let _ = writeln!(
                    out,
                    "          const __sig_ok = await verifySignatureHmacSha256(__raw, __secret, request.headers.get(\"{header}\"), {ts_expr}, {tol});"
                );
                let _ = writeln!(out, "          if (__sig_ok) __who = {{ tag: \"{tag}\" }};");
                let _ = writeln!(out, "        }}");
            }
        }
        let _ = writeln!(out, "      }}");
    }
    let _ = writeln!(
        out,
        "      if (__who === undefined) return HttpResult.Unauthorized;"
    );

    // Parse the body param from the raw bytes already read (fail-closed → 400).
    let mut call_args: Vec<String> = path_params.iter().map(|n| n.to_string()).collect();
    if let Some(body_param) = h.params.iter().find(|p| p.name.name == "body") {
        let _ = writeln!(out, "      let __body_json: JsonValue;");
        let _ = writeln!(out, "      try {{");
        let _ = writeln!(out, "        __body_json = JSON.parse(__raw) as JsonValue;");
        let _ = writeln!(out, "      }} catch {{");
        let _ = writeln!(
            out,
            "        return HttpResult.BadRequest(\"Invalid request body\");"
        );
        let _ = writeln!(out, "      }}");
        let dser = super::workers_entry::deserialise_call(&body_param.type_ref, "__body_json", "$");
        let _ = writeln!(out, "      const __r_body = {dser};");
        let _ = writeln!(
            out,
            "      if (__r_body.tag === \"Err\") return HttpResult.BadRequest(\"Invalid request body\");"
        );
        let _ = writeln!(out, "      const body = __r_body.value;");
        call_args.push("body".to_string());
    }
    let _ = writeln!(
        out,
        "      return handlers.{sname}.{method_key}({}{}{{ ...deps, who: __who }});",
        call_args.join(", "),
        if call_args.is_empty() { "" } else { ", " },
    );
    let _ = writeln!(out, "    }},");
}
