//! 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, HashSet};
use std::fmt::Write as _;

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

#[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<bynk_check::actors::SumMember>> = table
        .services
        .values()
        .flat_map(|s| s.handlers.iter())
        .filter_map(|h| bynk_check::actors::sum_members_for(h, &table.actors))
        .collect();
    use bynk_check::actors::SumMemberSeam;
    let has_bearer = table.services.values().any(|s| {
        s.handlers
            .iter()
            .any(|h| bynk_check::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| {
                bynk_check::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");
    }
    // v0.104 (real-time track slice 3b): a `from WebSocket` upgrade route resolves
    // the hosting Durable Object by serialising the transfer key, exactly as agent
    // call sites do.
    let has_ws_open = table.services.values().any(|s| {
        s.handlers
            .iter()
            .any(|h| matches!(h.kind, HandlerKind::Open))
    });
    if has_ws_open {
        runtime_imports.push("serialiseAgentKey");
    }
    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;",
            bynk_check::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();
    }

    // v0.79: if any handler in this context uses `~>`, `compose` also takes the
    // request's execution context and threads its `waitUntil` into `deps.__exec`.
    let ctx_uses_send = table.services.values().any(|s| {
        s.handlers
            .iter()
            .any(|h| crate::emitter::block_uses_send(&h.body))
    });
    let compose_sig = if ctx_uses_send {
        "export function compose(env: Env, exec: { waitUntil(promise: Promise<unknown>): void }) {"
    } else {
        "export function compose(env: Env) {"
    };
    let _ = writeln!(out, "{compose_sig}");

    // 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());
    }
    // v0.79: the execution context rides in `deps.__exec` for `~>` sends.
    if ctx_uses_send {
        deps_entries.push("__exec: exec".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) = bynk_check::actors::sum_members_for(h, &table.actors) {
                        emit_http_sum_wrapper(&mut out, sname, h, *method, path, &members);
                    } else {
                        let seam = bynk_check::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 => {
                    // v0.106 (slice 3b-iii): a `from WebSocket` `on message` is an
                    // *inbound* handler that runs in the connection-hosting Durable
                    // Object (`webSocketMessage`), not at the edge — no compose
                    // wrapper. A `from queue` `on message` is the queue consumer.
                    if matches!(service.protocol, ServiceProtocol::WebSocket { .. }) {
                        continue;
                    }
                    emit_queue_wrapper(&mut out, sname, queue_idx, h);
                    queue_idx += 1;
                }
                HandlerKind::Open => {
                    let seam = bynk_check::actors::bearer_seam_for(h, &table.actors);
                    let local_agents: HashSet<String> = table.agents.keys().cloned().collect();
                    emit_websocket_upgrade(&mut out, sname, h, seam.as_ref(), &local_agents);
                }
                // v0.106 (slice 3b-iii): `on close` runs in the DO (`webSocketClose`),
                // not at the edge — no compose wrapper.
                HandlerKind::Close => {}
            }
        }
    }
    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 bynk_check::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, "    }},");
}

/// v0.104 (real-time track slice 3b): emit the WebSocket upgrade route — the
/// edge half of DECISION A. The upgrade authenticates the actor **in the Worker,
/// before any request reaches the Durable Object** (the safety boundary): it
/// reads the Bearer token from the first `Sec-WebSocket-Protocol` element
/// (DECISION C), verifies it fail-closed with the same audited JWT verifier HTTP
/// uses, and only on success forwards the upgrade request to the addressed DO —
/// the agent the `on open` transfers the connection to (DECISION B), keyed by a
/// request parameter. The verified identity rides in a trusted internal header
/// (the DO is only reachable through this Worker, the same Internal-channel trust
/// the cross-context caller seam relies on). A failure returns `401`/`426` and
/// **does not forward** — no socket is accepted unauthenticated.
fn emit_websocket_upgrade(
    out: &mut String,
    sname: &str,
    h: &Handler,
    seam: Option<&bynk_check::actors::BearerSeam>,
    local_agents: &HashSet<String>,
) {
    use crate::emitter::websocket::{WsOpenShape, analyse_open_shape};
    // The route params (e.g. `roomId`) ride as wrapper arguments — the entry
    // extracts them from the upgrade URL's query string and passes them through.
    let mut decls: Vec<String> = vec!["request: Request".to_string()];
    decls.extend(h.params.iter().map(|p| format!("{}: any", p.name.name)));
    let _ = writeln!(out, "    async ws_{sname}_open({}) {{", decls.join(", "));
    // Require an actual WebSocket upgrade before anything else.
    let _ = writeln!(
        out,
        "      if (request.headers.get(\"Upgrade\") !== \"websocket\") return new Response(\"Expected a WebSocket upgrade\", {{ status: 426 }});"
    );

    // DECISION C: a Bearer token arrives as the first `Sec-WebSocket-Protocol`
    // subprotocol element (a browser sets it via `new WebSocket(url, [token])`),
    // verified fail-closed before the request is forwarded. The `on open` requires
    // a `by` actor, but — exactly as an HTTP `by v: Visitor` route — that actor's
    // scheme may be `None` (an intentional anonymous channel): then `seam` is
    // `None` and no token is read. `Signature` is rejected at the WS boundary (a
    // browser cannot sign the handshake), so a present seam is always Bearer.
    if let Some(seam) = seam {
        let secret = seam.secret.replace('\\', "\\\\").replace('"', "\\\"");
        let _ = writeln!(
            out,
            "      const __proto = request.headers.get(\"Sec-WebSocket-Protocol\");"
        );
        let _ = writeln!(
            out,
            "      if (__proto === null) return new Response(\"Unauthorized\", {{ status: 401 }});"
        );
        let _ = writeln!(out, "      const __token = __proto.split(\",\")[0].trim();");
        // The hosting context's `Env` carries the DO binding (a non-empty object
        // type), so the secret probe casts through `unknown` to index it.
        let _ = writeln!(
            out,
            "      const __secret = (env as unknown 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(\"Unauthorized\", {{ status: 401 }});"
        );
        let _ = writeln!(
            out,
            "      const __claims = await verifyBearerJwtHs256(__token, __secret);"
        );
        let _ = writeln!(
            out,
            "      if (__claims.tag === \"Err\") return new Response(\"Unauthorized\", {{ status: 401 }});"
        );
        // A refinement actor's authorisation invariant: scheme verified (401
        // above), a failed claim predicate is 403, checked against verified claims.
        if let Some(pred) = &seam.authorization {
            let js = bynk_check::actors::claim_predicate_to_js(pred, "__claims.value.claims");
            let _ = writeln!(
                out,
                "      if (!({js})) return new Response(\"Forbidden\", {{ status: 403 }});"
            );
        }
    }

    // DECISION B: resolve the hosting Durable Object from the single connection
    // transfer (`Room(roomId)` → the `ROOM` namespace, keyed by `roomId`). The
    // shape constraint guarantees exactly one routable target.
    let target = match analyse_open_shape(&h.body, local_agents) {
        WsOpenShape::One(t) => t,
        // The checker rejects zero / multiple / non-routable shapes
        // (`bynk.ws.open_transfer_shape`); this arm is defensive.
        _ => {
            let _ = writeln!(
                out,
                "      return new Response(\"Internal Server Error\", {{ status: 500 }});"
            );
            let _ = writeln!(out, "    }},");
            return;
        }
    };
    let binding = agent_binding_name(target.agent);
    let key_js = match &target.key.kind {
        ExprKind::Ident(id) => id.name.clone(),
        // v1 keys are request-derivable param idents (DECISION B); a non-ident key
        // falls back to the first route param so the route stays valid TS.
        _ => h
            .params
            .first()
            .map(|p| p.name.name.clone())
            .unwrap_or_else(|| "\"default\"".to_string()),
    };
    // The verified identity (when the actor binds one) is forwarded in the trusted
    // internal header alongside the route arguments. A binder-less `by` verifies
    // but mints no identity.
    let identity_field = if seam.is_some_and(|s| s.binder.is_some()) {
        ", identity: __id.value"
    } else {
        ""
    };
    if seam.is_some_and(|s| s.binder.is_some()) {
        let id_ty = &seam.unwrap().identity_type;
        let _ = writeln!(
            out,
            "      const __id = handlers.{id_ty}.of(__claims.value.sub);"
        );
        let _ = writeln!(
            out,
            "      if (__id.tag === \"Err\") return new Response(\"Unauthorized\", {{ status: 401 }});"
        );
    }
    // The route params arrive attacker-controlled (the upgrade URL's query string).
    // Validate each refined / opaque param through its `.of` constructor fail-closed
    // — a `400` with a `RefinementViolation`, exactly as the HTTP path validates a
    // path param — *before* it addresses a Durable Object or is forwarded to the
    // on-open body. A malformed value must never reach the DO typed as though it had
    // satisfied its refinement. (Validation runs after auth, so an unauthenticated
    // client still sees only `401`.)
    let mut validated: HashMap<String, String> = HashMap::new();
    for p in &h.params {
        let pn = &p.name.name;
        match &p.type_ref {
            TypeRef::Named(id) => {
                let _ = writeln!(out, "      const __r_{pn} = handlers.{}.of({pn});", id.name);
                let _ = writeln!(
                    out,
                    "      if (__r_{pn}.tag === \"Err\") return new Response(JSON.stringify({{ kind: \"RefinementViolation\", path: \"param.{pn}\", violation: __r_{pn}.error }}), {{ status: 400, headers: {{ \"content-type\": \"application/json\" }} }});"
                );
                validated.insert(pn.clone(), format!("__r_{pn}.value"));
            }
            // A plain `String` param (or a shape the static check already rejected)
            // passes through unchanged.
            _ => {
                validated.insert(pn.clone(), pn.clone());
            }
        }
    }
    let key_ref = validated.get(&key_js).cloned().unwrap_or(key_js);
    let args_json = h
        .params
        .iter()
        .map(|p| {
            validated
                .get(&p.name.name)
                .cloned()
                .unwrap_or_else(|| p.name.name.clone())
        })
        .collect::<Vec<_>>()
        .join(", ");
    let _ = writeln!(out, "      const __ns = deps.env.{binding};");
    let _ = writeln!(
        out,
        "      const __stub = __ns.get(__ns.idFromName(serialiseAgentKey({key_ref})));"
    );
    let _ = writeln!(out, "      const __fwd = new Headers(request.headers);");
    let _ = writeln!(
        out,
        "      __fwd.set(\"X-Bynk-Ws-Open\", JSON.stringify({{ args: [{args_json}]{identity_field} }}));"
    );
    let _ = writeln!(
        out,
        "      return __stub.fetch(new Request(\"https://_bynk/_bynk/ws/open/{sname}\", {{ method: request.method, headers: __fwd }}));"
    );
    let _ = writeln!(out, "    }},");
}

fn emit_http_wrapper(
    out: &mut String,
    sname: &str,
    h: &Handler,
    method: HttpMethod,
    path: &str,
    seam: Option<&bynk_check::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 = bynk_check::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: &[bynk_check::actors::SumMember],
) {
    use bynk_check::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, "    }},");
}
