//! TypeScript emission (spec §7, v0.1 §6, v0.2 §6).
//!
//! Walks the typed AST and writes a single TypeScript module.
//!
//! v0.2 lowering rules:
//! - Refined-base types: branded type alias + constructor object with
//!   `of`/`unsafe` (+ any user-declared methods).
//! - Record types: TypeScript `interface` + namespace object with methods.
//! - Sum types: discriminated-union type alias + namespace object with
//!   variant constructors and methods.
//! - Field access lowers to property access.
//! - Method calls lower to `Type.method(receiver, args)` (UFCS).
//! - `match` lowers to a switch on `.tag`; in tail position it inlines,
//!   otherwise it becomes an IIFE.
//! - `is` lowers to a tag check; bindings become `const` declarations
//!   on the truthy side of `if`/`&&`.

use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use std::path::{Path, PathBuf};

use crate::ast::*;
use crate::builtin_names::methods::{FOLD_EFF, RAW};
use crate::builtin_names::types::*;
use crate::checker::{NamedKind, Ty, TypedCommons};
use crate::project::{BuildTarget, EmitProjectCtx, UnitKind};

pub mod serialisation;
pub mod workers;
pub mod workers_entry;
pub mod wrangler;

pub use workers::emit_worker_compose;
pub use workers_entry::emit_worker_entry;
pub use wrangler::emit_wrangler_toml;

mod lower;
pub(crate) use lower::*;
mod emit;
pub(crate) use emit::*;

const INDENT_STEP: usize = 2;

/// Emit the contents of `out/runtime.ts`. This module ships with every
/// project so the per-context / per-test emissions can `import { Ok, Err,
/// Some, None, ... }` from a single source. It includes:
///
/// - `Result`/`Option` discriminated unions (using `tag` for the
///   discriminant — same shape user sum types lower to).
/// - `ValidationError` (the record shape refined-value constructors return).
/// - The `DurableObjectState`/`DurableObjectStorage` interfaces that agent
///   classes consume, plus an `InMemoryStorage` implementation and a
///   `makeTestState(name)` factory for use in test execution.
///
/// The content is identical across projects — there is no per-project
/// tailoring. Dead code is harmless; tsc handles it.
pub fn emit_runtime_module() -> String {
    RUNTIME_TS.to_string()
}

const RUNTIME_TS: &str = include_str!("firstparty/bindings/runtime.ts");

/// Emit the contents of `out/tsconfig.json`. The CLI uses `tsc -p` against
/// this when running `bynkc test`; users can also drive `tsc` against it
/// directly to produce JS for deployment.
pub fn emit_tsconfig() -> String {
    TSCONFIG_JSON.to_string()
}

const TSCONFIG_JSON: &str = r#"{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noImplicitAny": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "outDir": "../out-js",
    "rootDir": "."
  },
  "include": ["**/*.ts"]
}
"#;

/// Compute the runtime import specifier for a module at `from_source`. For a
/// file at `commerce/payment.ts` the runtime sits two levels up, so this
/// returns `../runtime.js`; for a top-level file it returns `./runtime.js`.
pub fn runtime_import_for(from_source: &Path) -> String {
    let depth = from_source
        .parent()
        .map(|p| {
            p.components()
                .filter(|c| matches!(c, std::path::Component::Normal(_)))
                .count()
        })
        .unwrap_or(0);
    if depth == 0 {
        "./runtime.js".to_string()
    } else {
        let prefix: String = "../".repeat(depth);
        format!("{prefix}runtime.js")
    }
}

/// Emit TypeScript source for the typed commons (single-file mode).
pub fn emit(commons: &TypedCommons) -> String {
    let mut out = String::new();
    write_header_single(&mut out, commons);
    write_commons_doc(&mut out, commons);
    let dummy_ctx = single_file_ctx();
    // Types come first (they define interfaces and namespaces).
    for item in &commons.commons.items {
        if let CommonsItem::Type(t) = item {
            emit_type(&mut out, t, commons, &dummy_ctx);
        }
    }
    // Free functions afterward.
    for item in &commons.commons.items {
        if let CommonsItem::Fn(f) = item
            && let FnName::Free(_) = &f.name
        {
            emit_free_fn(&mut out, f, commons);
        }
    }
    // v0.22b: module-local codec helpers for Json.encode/decode targets.
    emit_json_codec_helpers(
        &mut out,
        commons,
        &dummy_ctx,
        &HashSet::new(),
        &HashSet::new(),
    );
    out
}

/// A no-op project context for single-file emission. Single-file mode never
/// involves contexts or cross-unit imports, so most fields default to empty.
fn single_file_ctx() -> EmitProjectCtx {
    EmitProjectCtx {
        source_path: PathBuf::new(),
        commons_name: String::new(),
        local_files: Vec::new(),
        file_decl_index: crate::project::FileDeclIndex {
            types: HashMap::new(),
            fns: HashMap::new(),
            methods: HashMap::new(),
        },
        imported_from: HashMap::new(),
        imported_from_kind: HashMap::new(),
        imported_decl_paths: HashMap::new(),
        commons_dir: PathBuf::new(),
        unit_kind: UnitKind::Commons,
        owning_context: None,
        exports_local: HashMap::new(),
        exports_for_consumed: HashMap::new(),
        consumed_types: HashMap::new(),
        cross_context: crate::resolver::CrossContextInfo::default(),
        is_consumed_by_others: false,
        target: BuildTarget::Bundle,
        boundary_type_owners: HashMap::new(),
        local_agents: HashSet::new(),
        actors: HashMap::new(),
        consumed_adapters: HashSet::new(),
    }
}

/// Emit TypeScript source for a single file inside a multi-file project,
/// including cross-file and cross-commons imports computed from
/// [`EmitProjectCtx`].
pub fn emit_project(commons: &TypedCommons, ctx: &EmitProjectCtx) -> String {
    let mut out = String::new();
    write_header(&mut out, commons, ctx);
    // Compute which names this file actually references that live elsewhere
    // (sibling file in the same commons/context, or a used commons / consumed
    // context).
    let references = collect_external_references(commons, ctx);
    emit_project_imports(&mut out, commons, ctx, &references);
    if !references.is_empty() {
        writeln!(out).unwrap();
    }
    // v0.6: namespace imports for each consumed context that exposes services.
    // v0.15: also for consumed contexts whose capabilities this context uses.
    emit_cross_context_namespace_imports(&mut out, commons, ctx);
    // For contexts: emit per-context nominal rebrand aliases for each type
    // imported via `uses` that this file references. The structural shape is
    // inherited from the original commons type; the brand makes the
    // rebranded type nominally distinct (v0.4 §6.2).
    if ctx.unit_kind == UnitKind::Context {
        emit_context_rebrands(&mut out, &references, commons, ctx);
    }
    write_commons_doc(&mut out, commons);
    for item in &commons.commons.items {
        if let CommonsItem::Type(t) = item {
            emit_type(&mut out, t, commons, ctx);
        }
    }
    for item in &commons.commons.items {
        if let CommonsItem::Fn(f) = item
            && let FnName::Free(_) = &f.name
        {
            emit_free_fn(&mut out, f, commons);
        }
    }
    // v0.5: behavioural items follow the type/fn declarations.
    for item in &commons.commons.items {
        match item {
            CommonsItem::Capability(c) => emit_capability(&mut out, c),
            CommonsItem::Provider(p) => emit_provider(&mut out, p, commons, ctx),
            CommonsItem::Service(s) => emit_service(&mut out, s, commons, ctx),
            CommonsItem::Agent(a) => emit_agent(&mut out, a, commons, ctx),
            _ => {}
        }
    }
    // v0.9.2: per-test registry reset. The test runner calls this before each
    // test so a fresh test sees clean agent state (finding #10's "fresh per
    // test" half).
    let agent_names: Vec<&str> = commons
        .commons
        .items
        .iter()
        .filter_map(|i| match i {
            CommonsItem::Agent(a) => Some(a.name.name.as_str()),
            _ => None,
        })
        .collect();
    if !agent_names.is_empty() {
        writeln!(out, "export function __resetAgents(): void {{").unwrap();
        for name in &agent_names {
            writeln!(out, "  {}.reset();", agent_registry_name(name)).unwrap();
        }
        writeln!(out, "}}").unwrap();
        writeln!(out).unwrap();
    }
    // v0.6: cross-context surface assembly. Emit `makeSurface` for any
    // context that declares services — the composition root references it
    // for every such context, not just those consumed by others. Skipped
    // in workers mode where each Worker has its own `compose(env)` root.
    if ctx.unit_kind == UnitKind::Context && matches!(ctx.target, BuildTarget::Bundle) {
        let has_services = commons
            .commons
            .items
            .iter()
            .any(|i| matches!(i, CommonsItem::Service(_)));
        if has_services {
            emit_make_surface(&mut out, commons, ctx);
        }
    }
    // v0.8: in workers mode, the context module also exports per-type
    // serialise/deserialise helpers for every type that crosses a
    // boundary. The commons modules likewise carry helpers for their
    // own commons-declared boundary types.
    let (boundary_names, boundary_insts) = if matches!(ctx.target, BuildTarget::Workers) {
        emit_boundary_helpers(&mut out, commons, ctx)
    } else {
        (HashSet::new(), HashSet::new())
    };
    // v0.22b: module-local codec helpers for this file's Json.encode/decode
    // targets, deduped against the workers boundary helpers above.
    emit_json_codec_helpers(&mut out, commons, ctx, &boundary_names, &boundary_insts);
    out
}

/// v0.22b: pre-order expression visitor — visits `e`, then every
/// sub-expression, including statements and tails of nested blocks.
fn walk_exprs(e: &Expr, f: &mut impl FnMut(&Expr)) {
    f(e);
    match &e.kind {
        ExprKind::IntLit(_)
        | ExprKind::FloatLit { .. }
        | ExprKind::StrLit(_)
        | ExprKind::BoolLit(_)
        | ExprKind::Ident(_)
        | ExprKind::None
        | ExprKind::UnitLit => {}
        // v0.43: visit each interpolation hole's expression.
        ExprKind::InterpStr(parts) => {
            for part in parts {
                if let InterpPart::Hole(hole) = part {
                    walk_exprs(hole, f);
                }
            }
        }
        ExprKind::Lambda(l) => walk_exprs(&l.body, f),
        ExprKind::EffectPure(i)
        | ExprKind::Assert(i)
        | ExprKind::UnaryOp(_, i)
        | ExprKind::Paren(i)
        | ExprKind::Ok(i)
        | ExprKind::Err(i)
        | ExprKind::Some(i)
        | ExprKind::Question(i) => walk_exprs(i, f),
        ExprKind::Mock { args, .. }
        | ExprKind::Call { args, .. }
        | ExprKind::ConstructorCall { args, .. } => {
            for a in args {
                walk_exprs(a, f);
            }
        }
        ExprKind::ListLit(elems) => {
            for el in elems {
                walk_exprs(el, f);
            }
        }
        ExprKind::RecordConstruction { fields, .. } => {
            for fld in fields {
                if let Some(v) = &fld.value {
                    walk_exprs(v, f);
                }
            }
        }
        ExprKind::RecordSpread {
            base, overrides, ..
        } => {
            walk_exprs(base, f);
            for fld in overrides {
                if let Some(v) = &fld.value {
                    walk_exprs(v, f);
                }
            }
        }
        ExprKind::BinOp(_, l, r) => {
            walk_exprs(l, f);
            walk_exprs(r, f);
        }
        ExprKind::Block(b) => walk_block_exprs(b, f),
        ExprKind::If {
            cond,
            then_block,
            else_block,
        } => {
            walk_exprs(cond, f);
            walk_block_exprs(then_block, f);
            walk_block_exprs(else_block, f);
        }
        ExprKind::FieldAccess { receiver, .. } => walk_exprs(receiver, f),
        ExprKind::MethodCall { receiver, args, .. } => {
            walk_exprs(receiver, f);
            for a in args {
                walk_exprs(a, f);
            }
        }
        ExprKind::Match { discriminant, arms } => {
            walk_exprs(discriminant, f);
            for arm in arms {
                match &arm.body {
                    MatchBody::Expr(e) => walk_exprs(e, f),
                    MatchBody::Block(b) => walk_block_exprs(b, f),
                }
            }
        }
        ExprKind::Is { value, .. } => walk_exprs(value, f),
    }
}

fn walk_block_exprs(b: &Block, f: &mut impl FnMut(&Expr)) {
    for s in &b.statements {
        match s {
            Statement::Let(l) | Statement::EffectLet(l) => walk_exprs(&l.value, f),
            Statement::Commit(c) => walk_exprs(&c.value, f),
            Statement::Assert(a) => walk_exprs(&a.value, f),
        }
    }
    walk_exprs(&b.tail, f);
}

/// v0.22b: whether any signature or type declaration in this file names
/// `JsonError` — drives the conditional `type JsonError` runtime import.
fn file_mentions_json_error(commons: &TypedCommons) -> bool {
    fn in_type_ref(t: &TypeRef) -> bool {
        match t {
            TypeRef::JsonError(_) => true,
            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
            TypeRef::Option(a, _)
            | TypeRef::Effect(a, _)
            | TypeRef::HttpResult(a, _)
            | TypeRef::List(a, _) => in_type_ref(a),
            TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
            TypeRef::Base(..)
            | TypeRef::Named(_)
            | TypeRef::QueueResult(_)
            | TypeRef::ValidationError(_)
            | TypeRef::Unit(_) => false,
        }
    }
    let sig = |params: &[Param], ret: &TypeRef| {
        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
    };
    commons.commons.items.iter().any(|item| match item {
        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
        CommonsItem::Agent(a) => a.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
        CommonsItem::Type(t) => match &t.body {
            TypeBody::Record(r) => r.fields.iter().any(|f| in_type_ref(&f.type_ref)),
            TypeBody::Sum(s) => s
                .variants
                .iter()
                .any(|v| v.payload.iter().any(|p| in_type_ref(&p.type_ref))),
            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => false,
        },
        _ => false,
    })
}

/// v0.22b: a checker `Ty` rendered back to a `TypeRef` for the codec
/// machinery (which is `TypeRef`-driven). `None` for types the codec
/// rejects anyway (functions, effects, type variables).
fn ty_to_type_ref(t: &Ty) -> Option<TypeRef> {
    let sp = crate::span::Span::new(0, 0);
    Some(match t {
        Ty::Base(b) => TypeRef::Base(*b, sp),
        Ty::Named { name, .. } => TypeRef::Named(Ident {
            name: name.clone(),
            span: sp,
        }),
        Ty::Result(a, b) => TypeRef::Result(
            Box::new(ty_to_type_ref(a)?),
            Box::new(ty_to_type_ref(b)?),
            sp,
        ),
        Ty::Option(a) => TypeRef::Option(Box::new(ty_to_type_ref(a)?), sp),
        Ty::List(a) => TypeRef::List(Box::new(ty_to_type_ref(a)?), sp),
        Ty::Map(k, v) => TypeRef::Map(
            Box::new(ty_to_type_ref(k)?),
            Box::new(ty_to_type_ref(v)?),
            sp,
        ),
        Ty::Unit => TypeRef::Unit(sp),
        Ty::ValidationError => TypeRef::ValidationError(sp),
        Ty::JsonError => TypeRef::JsonError(sp),
        Ty::Effect(_)
        | Ty::HttpResult(_)
        | Ty::QueueResult
        | Ty::Fn { .. }
        | Ty::Var(_)
        | Ty::Actor(_)
        | Ty::ActorSum(_) => {
            return None;
        }
    })
}

/// v0.22b: collect the `Json.encode`/`Json.decode[T]` target type-refs in
/// this file's bodies — the roots of the module-local codec-helper closure.
fn collect_json_codec_roots(commons: &TypedCommons) -> Vec<TypeRef> {
    let mut roots: Vec<TypeRef> = Vec::new();
    {
        let mut visit = |e: &Expr| {
            let ExprKind::MethodCall {
                receiver,
                method,
                args,
                ..
            } = &e.kind
            else {
                return;
            };
            let ExprKind::Ident(id) = &receiver.kind else {
                return;
            };
            if id.name != JSON {
                return;
            }
            match method.name.as_str() {
                "decode" => {
                    if let Some(Ty::Result(t, _)) = commons.expr_types.get(&e.span)
                        && let Some(tr) = ty_to_type_ref(t)
                    {
                        roots.push(tr);
                    }
                }
                "encode" => {
                    if let Some(a) = args.first()
                        && let Some(t) = commons.expr_types.get(&a.span)
                        && let Some(tr) = ty_to_type_ref(t)
                    {
                        roots.push(tr);
                    }
                }
                _ => {}
            }
        };
        for item in &commons.commons.items {
            match item {
                CommonsItem::Fn(f) => walk_block_exprs(&f.body, &mut visit),
                CommonsItem::Service(s) => {
                    for h in &s.handlers {
                        walk_block_exprs(&h.body, &mut visit);
                    }
                }
                CommonsItem::Agent(a) => {
                    for h in &a.handlers {
                        walk_block_exprs(&h.body, &mut visit);
                    }
                }
                CommonsItem::Provider(p) => {
                    for op in &p.ops {
                        walk_block_exprs(&op.body, &mut visit);
                    }
                }
                _ => {}
            }
        }
    }
    roots
}

/// v0.22b: module-local serialise/deserialise helpers for the types this
/// file's `Json.encode`/`Json.decode[T]` calls reference (ADR 0045). The
/// closure machinery is shared with the workers boundary path; `skip_names`
/// / `skip_insts` dedupe against helpers that path already emitted into
/// this module.
fn emit_json_codec_helpers(
    out: &mut String,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
    skip_names: &HashSet<String>,
    skip_insts: &HashSet<String>,
) {
    use serialisation::{collect_codec_closure, emit_generic_helpers, emit_helpers_for_owner};
    let roots = collect_json_codec_roots(commons);
    if roots.is_empty() {
        return;
    }
    let (names, insts) = collect_codec_closure(&roots, &commons.types);
    let names: Vec<String> = names
        .into_iter()
        .filter(|n| !skip_names.contains(n))
        .collect();
    emit_helpers_for_owner(out, &names, &commons.types, &ctx.commons_name);
    let insts: Vec<serialisation::GenericInst> = insts
        .into_iter()
        .filter(|i| !skip_insts.contains(&i.ts_name()))
        .collect();
    if !insts.is_empty() {
        emit_generic_helpers(out, &insts);
    }
}

/// Emit boundary serialise/deserialise helpers (v0.8 §3.4 / §5.2) for
/// every named type declared in this file that flows through a
/// cross-context call, plus the specialised generic helpers for any
/// Result/Option instantiation used at the boundary. Returns the emitted
/// (or locally-bound) helper type names and generic-instantiation names so
/// the v0.22b codec emission can dedupe against them.
fn emit_boundary_helpers(
    out: &mut String,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
) -> (HashSet<String>, HashSet<String>) {
    use serialisation::{
        collect_boundary_types, collect_generic_instantiations, emit_generic_helpers,
        emit_helpers_for_owner,
    };

    // For contexts: walk the local services to discover boundary types.
    // For commons: walk every consumer's services that reference us
    // (approximated as: emit for every type declared in this file).
    let services: HashMap<String, ServiceDecl> = commons
        .commons
        .items
        .iter()
        .filter_map(|i| match i {
            CommonsItem::Service(s) => Some((s.name.name.clone(), s.clone())),
            _ => None,
        })
        .collect();

    let locally_declared: HashSet<String> = ctx.file_decl_index.types.keys().cloned().collect();
    if ctx.unit_kind == UnitKind::Context {
        let boundary_types_all = collect_boundary_types(&commons.types, &services);
        // Locally-declared boundary types get full helpers in this module.
        let local_boundary: Vec<String> = boundary_types_all
            .iter()
            .filter(|n| locally_declared.contains(*n))
            .cloned()
            .collect();
        emit_helpers_for_owner(
            out,
            &local_boundary,
            &commons.types,
            ctx.commons_name.as_str(),
        );

        // Re-export helpers for commons-owned boundary types so consumers
        // can address them through this context's handlers.ts namespace
        // (matching the namespace import they already use for cross-
        // context types). Grouped by source commons.
        let mut by_commons: HashMap<String, Vec<String>> = HashMap::new();
        for n in &boundary_types_all {
            if locally_declared.contains(n) {
                continue;
            }
            if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
                && let Some(commons_name) = ctx.imported_from.get(n)
            {
                by_commons
                    .entry(commons_name.clone())
                    .or_default()
                    .push(n.clone());
            }
        }
        let mut commons_keys: Vec<&String> = by_commons.keys().collect();
        commons_keys.sort();
        for commons_name in commons_keys {
            let names = by_commons.get(commons_name).unwrap();
            let mut sorted_names: Vec<String> = names.clone();
            sorted_names.sort();
            sorted_names.dedup();
            let target_path = ctx
                .imported_decl_paths
                .get(commons_name)
                .and_then(|m| sorted_names.iter().find_map(|n| m.get(n).cloned()))
                .unwrap_or_else(|| EmitProjectCtx::commons_path(commons_name));
            let import_spec =
                cross_commons_import_specifier_for_path(&ctx.source_path, &target_path);
            let mut parts: Vec<String> = Vec::new();
            for n in &sorted_names {
                parts.push(format!("serialise_{n}"));
                parts.push(format!("deserialise_{n}"));
            }
            // v0.9.1: emit both a regular import (so the names are bound
            // locally for use inside this file's serialisation helpers) and a
            // re-export (so downstream consumers can still reach them
            // through this module). A bare `export { ... } from "..."`
            // re-export does not create a local binding, which `tsc --strict`
            // catches when the body calls one of the helpers directly.
            writeln!(
                out,
                "import {{ {} }} from \"{import_spec}\";",
                parts.join(", ")
            )
            .unwrap();
            writeln!(out, "export {{ {} }};", parts.join(", ")).unwrap();
        }
        if !by_commons.is_empty() {
            writeln!(out).unwrap();
        }

        // Specialised Result_/Option_ helpers for the instantiations used —
        // in handler signatures or in boundary-type fields (v0.18).
        let insts = collect_generic_instantiations(&services, &boundary_types_all, &commons.types);
        emit_generic_helpers(out, &insts);
        (
            boundary_types_all.into_iter().collect(),
            insts.iter().map(|i| i.ts_name()).collect(),
        )
    } else {
        // Commons/adapters: emit helpers for every type declared in this
        // file, plus (v0.18) the generic instantiations their fields use —
        // a record like the bynk surface's `Request` carries
        // `Option[String]` fields whose serialisers delegate to the
        // specialised helpers.
        let mut locally: Vec<String> = locally_declared.into_iter().collect();
        locally.sort();
        emit_helpers_for_owner(out, &locally, &commons.types, ctx.commons_name.as_str());
        let insts = collect_generic_instantiations(&HashMap::new(), &locally, &commons.types);
        emit_generic_helpers(out, &insts);
        (
            locally.into_iter().collect(),
            insts.iter().map(|i| i.ts_name()).collect(),
        )
    }
}

/// For each type imported via `uses` that's referenced in this file, emit:
/// 1. (Done in imports) an aliased import: `import { Money as __CommonsMoney } from ...`
/// 2. A rebranded type alias: `export type Money = __CommonsMoney & { readonly __ctxBrand: "..." }`
///
/// The brand makes two contexts that both `uses` the same commons see distinct
/// nominal `Money` types in their TypeScript output (v0.4 §3.4 / §6.2).
fn emit_context_rebrands(
    out: &mut String,
    refs: &ExternalReferences,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
) {
    let Some(owning) = &ctx.owning_context else {
        return;
    };
    // Collect names imported via `uses` (kind == Commons in imported_from_kind).
    let mut names: Vec<String> = Vec::new();
    for set in refs.by_commons.values() {
        for n in set {
            // v0.20b: only *types* get the context rebrand — a
            // `uses`-imported function is a value and imports plainly.
            if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
                && commons.types.contains_key(n)
            {
                names.push(n.clone());
            }
        }
    }
    names.sort();
    names.dedup();
    if names.is_empty() {
        return;
    }
    for name in &names {
        writeln!(
            out,
            "export type {name} = __Commons{name} & {{ readonly __ctxBrand: \"{owning}\" }};",
        )
        .unwrap();
        // v0.9.2: a commons refined/opaque type carries a value-side
        // constructor (`.of` / `.unsafe`). Re-export it under the rebranded
        // name so a context calling `ShortCode.of(...)` resolves to a value —
        // delegating to the imported commons constructor but reporting the
        // context-branded type. (Without this, `ShortCode` is type-only in the
        // context and `.of` fails to resolve.)
        if let Some(base) = commons.types.get(name).and_then(refined_or_opaque_base) {
            let ts_base = ts_base(base);
            writeln!(out, "export const {name} = {{").unwrap();
            writeln!(
                out,
                "  of(value: {ts_base}): Result<{name}, ValidationError> {{ return __Commons{name}.of(value) as unknown as Result<{name}, ValidationError>; }},",
            )
            .unwrap();
            writeln!(
                out,
                "  unsafe(value: {ts_base}): {name} {{ return __Commons{name}.unsafe(value) as unknown as {name}; }},",
            )
            .unwrap();
            writeln!(out, "}};").unwrap();
        }
    }
    writeln!(out).unwrap();
}

/// If a type declaration is a refined or opaque base type, return its base
/// (both lower to a branded base with a `.of` / `.unsafe` constructor object).
fn refined_or_opaque_base(decl: &TypeDecl) -> Option<BaseType> {
    match &decl.body {
        TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => Some(*base),
        _ => None,
    }
}

/// Names that this file needs to import from elsewhere (sibling files of
/// the same commons, or other commons via `uses`).
#[derive(Default)]
struct ExternalReferences {
    /// `commons name` → set of names to import.
    by_commons: HashMap<String, HashSet<String>>,
    /// `sibling source path` → set of names to import (same-commons).
    by_sibling: HashMap<PathBuf, HashSet<String>>,
}

impl ExternalReferences {
    fn is_empty(&self) -> bool {
        self.by_commons.is_empty() && self.by_sibling.is_empty()
    }
}

fn collect_external_references(commons: &TypedCommons, ctx: &EmitProjectCtx) -> ExternalReferences {
    // Names declared in this file (so we know what's local-to-file).
    let local_to_file: HashSet<String> = commons
        .commons
        .items
        .iter()
        .map(|i| i.name().name.clone())
        .collect();

    let mut refs = ExternalReferences::default();

    // Walk every expression and TypeRef in this file's items, recording
    // any reference that resolves to a name declared in a sibling file or
    // an imported commons.
    for item in &commons.commons.items {
        match item {
            CommonsItem::Type(t) => {
                collect_refs_in_type_decl(t, &local_to_file, ctx, &mut refs);
            }
            CommonsItem::Fn(f) => {
                collect_refs_in_fn(f, &local_to_file, commons, ctx, &mut refs);
            }
            CommonsItem::Capability(c) => {
                for op in &c.ops {
                    for p in &op.params {
                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
                    }
                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
                }
            }
            CommonsItem::Provider(p) => {
                // Reference to the capability so we can import it (locally
                // declared, so usually no extra work).
                let _ = &p.capability;
                for op in &p.ops {
                    for param in &op.params {
                        collect_refs_in_typeref(&param.type_ref, &local_to_file, ctx, &mut refs);
                    }
                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
                    collect_refs_in_block(&op.body, &local_to_file, commons, ctx, &mut refs);
                }
            }
            CommonsItem::Service(s) => {
                for h in &s.handlers {
                    for p in &h.params {
                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
                    }
                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
                }
            }
            CommonsItem::Agent(a) => {
                collect_refs_in_typeref(&a.key_type, &local_to_file, ctx, &mut refs);
                for f in &a.state_fields {
                    collect_refs_in_typeref(&f.type_ref, &local_to_file, ctx, &mut refs);
                }
                for h in &a.handlers {
                    for p in &h.params {
                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
                    }
                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
                }
            }
            CommonsItem::Actor(a) => {
                if let Some(id) = &a.identity {
                    collect_refs_in_typeref(id, &local_to_file, ctx, &mut refs);
                }
            }
        }
    }
    refs
}

fn collect_refs_in_type_decl(
    t: &TypeDecl,
    local_to_file: &HashSet<String>,
    ctx: &EmitProjectCtx,
    out: &mut ExternalReferences,
) {
    match &t.body {
        TypeBody::Record(r) => {
            for f in &r.fields {
                collect_refs_in_typeref(&f.type_ref, local_to_file, ctx, out);
            }
        }
        TypeBody::Sum(s) => {
            for v in &s.variants {
                for p in &v.payload {
                    collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
                }
            }
        }
        _ => {}
    }
}

fn collect_refs_in_fn(
    f: &FnDecl,
    local_to_file: &HashSet<String>,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
    out: &mut ExternalReferences,
) {
    for p in &f.params {
        collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
    }
    collect_refs_in_typeref(&f.return_type, local_to_file, ctx, out);
    // For methods: the attached type may also be elsewhere.
    if let FnName::Method { type_name, .. } = &f.name {
        record_name_ref(&type_name.name, local_to_file, ctx, out);
    }
    collect_refs_in_block(&f.body, local_to_file, commons, ctx, out);
}

fn collect_refs_in_typeref(
    r: &TypeRef,
    local_to_file: &HashSet<String>,
    ctx: &EmitProjectCtx,
    out: &mut ExternalReferences,
) {
    match r {
        TypeRef::Named(id) => record_name_ref(&id.name, local_to_file, ctx, out),
        TypeRef::Result(t, e, _) => {
            collect_refs_in_typeref(t, local_to_file, ctx, out);
            collect_refs_in_typeref(e, local_to_file, ctx, out);
        }
        TypeRef::Option(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
        TypeRef::Effect(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
        TypeRef::HttpResult(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
        _ => {}
    }
}

fn collect_refs_in_block(
    b: &Block,
    local_to_file: &HashSet<String>,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
    out: &mut ExternalReferences,
) {
    for stmt in &b.statements {
        match stmt {
            Statement::Let(l) | Statement::EffectLet(l) => {
                if let Some(t) = &l.type_annot {
                    collect_refs_in_typeref(t, local_to_file, ctx, out);
                }
                collect_refs_in_expr(&l.value, local_to_file, commons, ctx, out);
            }
            Statement::Commit(c) => {
                collect_refs_in_expr(&c.value, local_to_file, commons, ctx, out);
            }
            Statement::Assert(a) => {
                collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
            }
        }
    }
    collect_refs_in_expr(&b.tail, local_to_file, commons, ctx, out);
}

fn collect_refs_in_expr(
    e: &Expr,
    local_to_file: &HashSet<String>,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
    out: &mut ExternalReferences,
) {
    match &e.kind {
        // A bare ident the checker typed as a sum is a nullary variant
        // constructor — the lowering qualifies it to `Type.Variant`, so the
        // owning type must be imported (v0.18: first hit by `Get` from the
        // consumed bynk surface's `Method`).
        ExprKind::Ident(id) => {
            if let Some(type_name) = sum_owner_of_variant(&id.name, e.span, commons) {
                record_name_ref(&type_name, local_to_file, ctx, out);
            }
        }
        ExprKind::IntLit(_)
        | ExprKind::FloatLit { .. }
        | ExprKind::StrLit(_)
        | ExprKind::BoolLit(_)
        | ExprKind::None
        | ExprKind::UnitLit => {}
        // v0.43: a hole's expression may reference imported names.
        ExprKind::InterpStr(parts) => {
            for part in parts {
                if let InterpPart::Hole(hole) = part {
                    collect_refs_in_expr(hole, local_to_file, commons, ctx, out);
                }
            }
        }
        // v0.20a: a lambda — its annotated param types may reference
        // imported types; the body walks like any expression.
        ExprKind::Lambda(lambda) => {
            for p in &lambda.params {
                if let Some(tr) = &p.type_ref {
                    collect_refs_in_typeref(tr, local_to_file, ctx, out);
                }
            }
            collect_refs_in_expr(&lambda.body, local_to_file, commons, ctx, out);
        }
        ExprKind::EffectPure(inner) => {
            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
        }
        ExprKind::Assert(inner) => {
            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
        }
        ExprKind::Mock { args, .. } => {
            for a in args {
                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
            }
        }
        ExprKind::ListLit(elems) => {
            for el in elems {
                collect_refs_in_expr(el, local_to_file, commons, ctx, out);
            }
        }
        ExprKind::RecordSpread {
            type_name,
            base,
            overrides,
        } => {
            if let Some(tn) = type_name {
                record_name_ref(&tn.name, local_to_file, ctx, out);
            }
            collect_refs_in_expr(base, local_to_file, commons, ctx, out);
            for f in overrides {
                if let Some(v) = &f.value {
                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
                }
            }
        }
        ExprKind::Call { name, args, .. } => {
            record_name_ref(&name.name, local_to_file, ctx, out);
            // A payload-carrying bare variant call (`Won(prize)`) lowers to
            // `Type.Variant(…)` — import the owning sum type too.
            if let Some(type_name) = sum_owner_of_variant(&name.name, e.span, commons) {
                record_name_ref(&type_name, local_to_file, ctx, out);
            }
            for a in args {
                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
            }
        }
        ExprKind::BinOp(_, l, r) => {
            collect_refs_in_expr(l, local_to_file, commons, ctx, out);
            collect_refs_in_expr(r, local_to_file, commons, ctx, out);
        }
        ExprKind::UnaryOp(_, i)
        | ExprKind::Paren(i)
        | ExprKind::Ok(i)
        | ExprKind::Err(i)
        | ExprKind::Some(i)
        | ExprKind::Question(i) => collect_refs_in_expr(i, local_to_file, commons, ctx, out),
        ExprKind::Block(b) => collect_refs_in_block(b, local_to_file, commons, ctx, out),
        ExprKind::If {
            cond,
            then_block,
            else_block,
        } => {
            collect_refs_in_expr(cond, local_to_file, commons, ctx, out);
            collect_refs_in_block(then_block, local_to_file, commons, ctx, out);
            collect_refs_in_block(else_block, local_to_file, commons, ctx, out);
        }
        ExprKind::ConstructorCall {
            type_name,
            method: _,
            args,
        } => {
            record_name_ref(&type_name.name, local_to_file, ctx, out);
            for a in args {
                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
            }
        }
        ExprKind::RecordConstruction { type_name, fields } => {
            record_name_ref(&type_name.name, local_to_file, ctx, out);
            for f in fields {
                if let Some(v) = &f.value {
                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
                }
            }
        }
        ExprKind::FieldAccess { receiver, field: _ } => {
            // The bare-ident-as-type case (`TypeName.Variant`) — record the
            // name so we import the type.
            if let ExprKind::Ident(id) = &receiver.kind {
                record_name_ref(&id.name, local_to_file, ctx, out);
            } else {
                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
            }
        }
        ExprKind::MethodCall {
            receiver,
            method: _,
            args,
            ..
        } => {
            if let ExprKind::Ident(id) = &receiver.kind {
                record_name_ref(&id.name, local_to_file, ctx, out);
            } else {
                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
            }
            for a in args {
                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
            }
        }
        ExprKind::Match { discriminant, arms } => {
            collect_refs_in_expr(discriminant, local_to_file, commons, ctx, out);
            for arm in arms {
                if let Pattern::Variant {
                    type_name: Some(tn),
                    ..
                } = &arm.pattern
                {
                    record_name_ref(&tn.name, local_to_file, ctx, out);
                }
                match &arm.body {
                    MatchBody::Expr(e) => collect_refs_in_expr(e, local_to_file, commons, ctx, out),
                    MatchBody::Block(b) => {
                        collect_refs_in_block(b, local_to_file, commons, ctx, out)
                    }
                }
            }
        }
        ExprKind::Is { value, pattern } => {
            collect_refs_in_expr(value, local_to_file, commons, ctx, out);
            if let Pattern::Variant {
                type_name: Some(tn),
                ..
            } = pattern
            {
                record_name_ref(&tn.name, local_to_file, ctx, out);
            }
        }
    }
}

/// If `name` at `span` is a bare reference to a variant of a sum type (per
/// the checker's expression type), return the owning sum's name — the same
/// test the lowering uses to qualify it as `Type.Variant` (see the
/// `ExprKind::Ident` arm of `lower_expr`).
fn sum_owner_of_variant(
    name: &str,
    span: crate::span::Span,
    commons: &TypedCommons,
) -> Option<String> {
    if let Some(Ty::Named {
        kind: NamedKind::Sum,
        name: type_name,
    }) = commons.expr_types.get(&span)
        && let Some(decl) = commons.types.get(type_name)
        && let TypeBody::Sum(s) = &decl.body
        && s.variants.iter().any(|v| v.name.name == name)
    {
        return Some(type_name.clone());
    }
    None
}

fn record_name_ref(
    name: &str,
    local_to_file: &HashSet<String>,
    ctx: &EmitProjectCtx,
    out: &mut ExternalReferences,
) {
    if local_to_file.contains(name) {
        return;
    }
    // Imported from another commons?
    if let Some(commons_name) = ctx.imported_from.get(name) {
        out.by_commons
            .entry(commons_name.clone())
            .or_default()
            .insert(name.to_string());
        return;
    }
    // Sibling file in the same commons?
    if let Some(path) = ctx.file_decl_index.types.get(name)
        && path != &ctx.source_path
    {
        out.by_sibling
            .entry(path.clone())
            .or_default()
            .insert(name.to_string());
        return;
    }
    if let Some(path) = ctx.file_decl_index.fns.get(name)
        && path != &ctx.source_path
    {
        out.by_sibling
            .entry(path.clone())
            .or_default()
            .insert(name.to_string());
    }
}

/// Emit `import * as <ns> from "..."` for each consumed context that
/// exposes services (so the consuming file can reference its `makeSurface`
/// return type and brand the cross-context call arguments).
fn emit_cross_context_namespace_imports(
    out: &mut String,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
) {
    let info = &ctx.cross_context;
    // Consumed contexts that expose services (v0.6) plus, v0.15, those whose
    // capabilities this context references via `given B.Cap`.
    let mut needed: std::collections::BTreeSet<String> = info
        .consumed_services
        .iter()
        .filter(|(_, svcs)| !svcs.is_empty())
        .map(|(q, _)| q.clone())
        .collect();
    needed.extend(cross_context_cap_namespaces(commons, info));
    if needed.is_empty() {
        return;
    }
    let consumed_with_services: Vec<&String> = needed.iter().collect();
    for q in &consumed_with_services {
        // Pick the first known file path for the consumed context as the
        // import target. (The composition root lives in the consumed
        // context's directory; any of its files would work as an import
        // target since they're all in the same module namespace, but we
        // currently emit one file per .bynk source so a single import per
        // consumed name suffices for the surface contract.)
        let target_paths = ctx.imported_decl_paths.get(q.as_str());
        let target = target_paths
            .and_then(|m| m.values().next().cloned())
            .unwrap_or_else(|| {
                // No imported declaration pins the path (e.g. a capability-only
                // consumed context, v0.15). Fall back to the unit's own module:
                // its per-Worker handlers in workers mode, or its <segment>.bynk
                // source in bundle mode. v0.17: a consumed *adapter* is not a
                // Worker — its capability types live in its root module
                // (`<adapter>.ts`) in both targets.
                if ctx.consumed_adapters.contains(q.as_str()) {
                    let mut p = EmitProjectCtx::commons_path(q);
                    p.set_extension("bynk");
                    p
                } else {
                    match ctx.target {
                        BuildTarget::Workers => crate::project::worker_handlers_source_path(q),
                        BuildTarget::Bundle => {
                            let mut p = EmitProjectCtx::commons_path(q);
                            p.set_extension("bynk");
                            p
                        }
                    }
                }
            });
        let import = cross_commons_import_specifier_for_path(&ctx.source_path, &target);
        let ns = qualified_to_ns(q);
        writeln!(out, "import * as {ns} from \"{import}\";").unwrap();
    }
    writeln!(out).unwrap();
}

fn emit_project_imports(
    out: &mut String,
    commons: &TypedCommons,
    ctx: &EmitProjectCtx,
    refs: &ExternalReferences,
) {
    // Sibling imports: relative path within the same commons/context directory.
    let mut sibling_paths: Vec<(&PathBuf, &HashSet<String>)> = refs.by_sibling.iter().collect();
    sibling_paths.sort_by(|a, b| a.0.cmp(b.0));
    for (path, names) in sibling_paths {
        let import = sibling_import_specifier(&ctx.source_path, path);
        let mut sorted: Vec<&String> = names.iter().collect();
        sorted.sort();
        let joined = sorted
            .iter()
            .map(|s| s.as_str())
            .collect::<Vec<_>>()
            .join(", ");
        writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
    }
    // Cross-unit imports: group by *target file path*.
    let mut unit_names: Vec<(&String, &HashSet<String>)> = refs.by_commons.iter().collect();
    unit_names.sort_by(|a, b| a.0.cmp(b.0));
    for (unit_name, names) in unit_names {
        let target_paths = ctx.imported_decl_paths.get(unit_name.as_str());
        let mut by_target: std::collections::BTreeMap<PathBuf, Vec<&String>> =
            std::collections::BTreeMap::new();
        for n in names {
            let path = target_paths
                .and_then(|p| p.get(n))
                .cloned()
                .unwrap_or_else(|| EmitProjectCtx::commons_path(unit_name));
            by_target.entry(path).or_default().push(n);
        }
        for (target, mut name_list) in by_target {
            name_list.sort();
            let import = cross_commons_import_specifier_for_path(&ctx.source_path, &target);
            // For context units, aliase commons-source imports so we can emit
            // rebrand aliases of the same short name. Imports from consumed
            // contexts keep their original name. v0.20b: the rebrand applies
            // to *types* only — a `uses`-imported function (bynk.list's
            // `traverse`) is a value, imports plainly, and is never branded.
            let mut parts: Vec<String> = Vec::new();
            for n in &name_list {
                let from_kind = ctx.imported_from_kind.get(n.as_str()).copied();
                if ctx.unit_kind == UnitKind::Context
                    && from_kind == Some(UnitKind::Commons)
                    && commons.types.contains_key(n.as_str())
                {
                    parts.push(format!("{n} as __Commons{n}"));
                } else {
                    parts.push((*n).clone());
                }
            }
            let joined = parts.join(", ");
            writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
        }
    }
}

/// Compute a relative import specifier from `from_source` (a `.bynk` path)
/// to `to_source` (another `.bynk` path), with `.bynk` rewritten to `.js`
/// for compatibility with NodeNext/strict TS resolution.
fn sibling_import_specifier(from_source: &Path, to_source: &Path) -> String {
    let from_dir = from_source.parent().unwrap_or(Path::new(""));
    let target = to_source.with_extension("js");
    let rel = relative_to(from_dir, &target);
    format!("./{}", ts_specifier(&rel))
}

/// Render a path as a TypeScript module specifier: **always forward
/// slashes**. `Path::display()` uses the platform separator, and on Windows
/// that emitted `import ... from "./commerce\orders.js"` — broken ESM
/// output, caught by the first CI matrix run on windows-latest.
pub(crate) fn ts_specifier(p: &Path) -> String {
    p.to_string_lossy().replace('\\', "/")
}

/// Compute a relative import specifier from this file's location to a
/// specific source file in another commons. `target_source` is the project-
/// relative path of the target `.bynk` file. The result is suitable for
/// `import { ... } from "..."` in NodeNext/strict TypeScript.
fn cross_commons_import_specifier_for_path(from_source: &Path, target_source: &Path) -> String {
    let from_dir = from_source.parent().unwrap_or(Path::new(""));
    let target = target_source.with_extension("js");
    let rel = relative_to(from_dir, &target);
    let display = ts_specifier(&rel);
    if display.starts_with("../") || display.starts_with("./") {
        display
    } else {
        format!("./{display}")
    }
}

/// Compute `target` as a path relative to `from`. Handles parent traversal
/// (`..`) for cases where `target` lives in a sibling directory.
fn relative_to(from: &Path, target: &Path) -> PathBuf {
    use std::path::Component as C;
    let f_comps: Vec<C> = from.components().collect();
    let t_comps: Vec<C> = target.components().collect();
    let mut shared = 0;
    while shared < f_comps.len() && shared < t_comps.len() && f_comps[shared] == t_comps[shared] {
        shared += 1;
    }
    let mut out = PathBuf::new();
    for _ in shared..f_comps.len() {
        out.push("..");
    }
    for c in &t_comps[shared..] {
        out.push(c.as_os_str());
    }
    if out.as_os_str().is_empty() {
        out.push(".");
    }
    out
}

fn write_header(out: &mut String, commons: &TypedCommons, ctx: &EmitProjectCtx) {
    writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
    let kind = match ctx.unit_kind {
        UnitKind::Commons => "commons",
        UnitKind::Context => "context",
        UnitKind::Test => "test",
        UnitKind::Integration => "integration test",
        UnitKind::Adapter => "adapter",
    };
    writeln!(out, "// {kind} {}", commons.commons.name.joined()).unwrap();
    writeln!(out).unwrap();
    if !commons.commons.items.is_empty() {
        let runtime_import = runtime_import_for(&ctx.source_path);
        let has_agent = commons
            .commons
            .items
            .iter()
            .any(|i| matches!(i, CommonsItem::Agent(_)));
        let has_http = commons.commons.items.iter().any(|i| match i {
            CommonsItem::Service(s) => s
                .handlers
                .iter()
                .any(|h| matches!(h.kind, HandlerKind::Http { .. })),
            _ => false,
        });
        let has_queue = commons.commons.items.iter().any(|i| match i {
            CommonsItem::Service(s) => s
                .handlers
                .iter()
                .any(|h| matches!(h.kind, HandlerKind::Message)),
            _ => false,
        });
        let workers = matches!(ctx.target, BuildTarget::Workers);
        let mut parts: Vec<&str> = vec![
            "Ok",
            "Err",
            "Some",
            "None",
            "type Result",
            "type Option",
            "type ValidationError",
        ];
        // v0.22b: the codec types are imported only when the file uses the
        // `Json` codec (or names `JsonError` in a signature) — keeping every
        // non-codec module's header byte-identical to v0.22a.
        let uses_codec = !collect_json_codec_roots(commons).is_empty();
        let mentions_json_error = file_mentions_json_error(commons);
        if uses_codec || mentions_json_error {
            parts.push("type JsonError");
        }
        if has_agent {
            // v0.9.2: agent-declaring files lower instantiation through the
            // `makeAgent` helper and a per-agent `StateRegistry`, and the
            // generated factory's signature names `DurableObjectNamespace`.
            parts.push("type DurableObjectState");
            parts.push("type DurableObjectNamespace");
            parts.push("StateRegistry");
            parts.push("makeAgent");
        }
        if has_http {
            // `HttpResult` is both a value (the constructor namespace) and a
            // type (the discriminated union). A bare named import brings both
            // in — `type HttpResult` would duplicate the identifier.
            parts.push(HTTP_RESULT);
        }
        if has_queue {
            // v0.44: `QueueResult` is both a value (the verdict namespace) and a
            // type; a bare named import brings both in.
            parts.push(QUEUE_RESULT);
        }
        if workers {
            parts.push("type JsonValue");
            parts.push("type BoundaryError");
            parts.push("type ServiceBinding");
            parts.push("callService");
            parts.push("boundaryError");
        } else if uses_codec {
            // v0.22b: the bundle-mode codec helpers reference JsonValue and
            // BoundaryError.
            parts.push("type JsonValue");
            parts.push("type BoundaryError");
        }
        writeln!(
            out,
            "import {{ {} }} from \"{runtime_import}\";",
            parts.join(", ")
        )
        .unwrap();
        writeln!(out).unwrap();
    }
}

/// Variant of write_header for single-file (no project context) emission.
fn write_header_single(out: &mut String, commons: &TypedCommons) {
    writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
    writeln!(out, "// commons {}", commons.commons.name.joined()).unwrap();
    writeln!(out).unwrap();
    if !commons.commons.items.is_empty() {
        // v0.22b: codec imports only when the file uses the `Json` codec.
        let uses_codec = !collect_json_codec_roots(commons).is_empty();
        let codec_imports = if uses_codec {
            ", type JsonError, type JsonValue, type BoundaryError"
        } else if file_mentions_json_error(commons) {
            ", type JsonError"
        } else {
            ""
        };
        writeln!(
            out,
            "import {{ Ok, Err, Some, None, type Result, type Option, type ValidationError{codec_imports} }} from \"./runtime.js\";",
        )
        .unwrap();
        writeln!(out).unwrap();
    }
}

/// Emit the commons-level doc block (if any) at the current position.
fn write_commons_doc(out: &mut String, commons: &TypedCommons) {
    if let Some(doc) = &commons.commons.documentation {
        emit_doc_block(out, Some(doc), 0);
        writeln!(out).unwrap();
    }
}

/// The module-level state-registry constant name for an agent class.
fn agent_registry_name(agent: &str) -> String {
    format!("__{agent}Registry")
}

/// The exported agent-construction factory name for an agent class.
pub fn agent_factory_name(agent: &str) -> String {
    format!("__make{agent}")
}

/// Per-function lowering context: fresh-temp counter + typed-commons handle
/// (used to look up receiver types for method-call UFCS lowering).
pub(crate) struct LowerCtx<'a> {
    next_tmp: u32,
    commons: &'a TypedCommons,
    /// Names of capabilities in scope as `given C1, C2, ...`. Used to lower
    /// `Capability.op(args)` calls to `deps.Capability.op(args)`.
    capabilities: HashSet<String>,
    /// True when lowering an agent handler body. Used to rewrite `self.state`
    /// and `self.<keyField>` access into the appropriate locals.
    in_agent_handler: bool,
    /// The local variable holding the loaded state inside an agent handler.
    agent_state_var: Option<String>,
    /// The name of the agent's `key id` field (so `self.<id>` resolves).
    agent_key_field: Option<String>,
    /// Cross-context info for v0.6 cross-context call lowering.
    cross_context: &'a crate::resolver::CrossContextInfo,
    /// True if the current handler made at least one cross-context call
    /// (drives whether `deps` gets a `surface` field type).
    cross_context_used: bool,
    /// v0.7: when lowering a test case body, the target context's local
    /// service names. A `service.call(args)` or `service(args)` invocation
    /// where `service` is in this set lowers to `<service>.call(args, deps)`
    /// so the test wires its `deps` through.
    pub test_services: HashSet<String>,
    /// v0.7: when lowering a test case body, the target context's local
    /// agent names. `<Agent>(<key>).method(args)` lowers to
    /// `new Agent(makeTestState(...)).method(args, {})`.
    pub test_agents: HashSet<String>,
    /// Agent names declared in the surrounding context. Drives lowering of
    /// `Agent(key)` (to `new Agent(makeTestState(String(key)))`) and of
    /// `agent_instance.method(args)` (to `instance.method(args, deps)`) in
    /// service and agent-handler bodies. Populated by the caller for non-test
    /// emission and from `test_agents` in test emission.
    pub local_agents: HashSet<String>,
    /// Variable bindings that point at agent instances. Updated by the
    /// statement emitter when it sees `let x = AgentName(key)`. Used by
    /// the method-call lowering so `x.method(args)` resolves through
    /// the agent's class rather than via the receiver-namespace lookup.
    pub local_agent_vars: HashMap<String, String>,
    /// v0.8 build target. In workers mode cross-context calls lower to
    /// `callService(...)` instead of `deps.surface.<key>.<method>(...)`.
    pub target: BuildTarget,
    /// v0.9.2: set when the body instantiates a local agent. In workers mode
    /// this drives `env` (carrying the DO namespaces) into the handler's deps
    /// type so the agent factory can reach its Durable Object binding.
    pub agents_instantiated: bool,
    /// When an `is` receiver is not a simple, repeatable lvalue (e.g. a call
    /// like `parse(x) is Ok(n)`), it is evaluated once into a temp; the temp
    /// name is cached here keyed by the receiver expression's span so the
    /// `.tag` check and every pattern binding reference the *same* single
    /// evaluation. Simple receivers (idents / field chains) are never cached
    /// and continue to be rendered inline as before.
    is_receiver_temps: HashMap<crate::span::Span, String>,
    /// v0.12: the receiver expression a capability call resolves against —
    /// `deps` in a handler body, `this.deps` in a composed provider body.
    cap_deps_expr: String,
    /// v0.47: when lowering a Bearer handler body, the `by` binder whose
    /// `.identity` is threaded through `deps` (so `<binder>.identity` lowers to
    /// `deps.identity` rather than the unit-value `undefined`).
    pub deps_identity_binder: Option<String>,
    /// v0.52: when lowering a multi-actor sum handler body, the `by` binder that
    /// names the resolved-actor value (threaded through `deps`, so the binder
    /// ident lowers to `deps.who` — the tagged union the body `match`es).
    pub actor_sum_binder: Option<String>,
}

impl<'a> LowerCtx<'a> {
    fn new(
        commons: &'a TypedCommons,
        cross_context: &'a crate::resolver::CrossContextInfo,
    ) -> Self {
        Self {
            next_tmp: 0,
            commons,
            capabilities: HashSet::new(),
            in_agent_handler: false,
            agent_state_var: None,
            agent_key_field: None,
            cross_context,
            cross_context_used: false,
            test_services: HashSet::new(),
            test_agents: HashSet::new(),
            local_agents: HashSet::new(),
            local_agent_vars: HashMap::new(),
            target: BuildTarget::Bundle,
            agents_instantiated: false,
            is_receiver_temps: HashMap::new(),
            cap_deps_expr: "deps".to_string(),
            deps_identity_binder: None,
            actor_sum_binder: None,
        }
    }
    /// v0.9.2: lower an agent instantiation `AgentName(key)` to its factory
    /// call. Bundle/test mode passes only the key; workers mode also threads
    /// `deps.env` so the factory can reach the agent's DO namespace.
    fn agent_construct(&mut self, agent: &str, key_expr: &str) -> String {
        self.agents_instantiated = true;
        let factory = agent_factory_name(agent);
        if matches!(self.target, BuildTarget::Workers) {
            format!("{factory}({key_expr}, deps.env)")
        } else {
            format!("{factory}({key_expr})")
        }
    }
    fn fresh(&mut self) -> String {
        let n = self.next_tmp;
        self.next_tmp += 1;
        format!("__r{n}")
    }
    /// Return a stable textual reference to an `is` receiver, used by the
    /// `.tag` check in `lower_is`. A simple, repeatable lvalue is lowered
    /// inline exactly as before (preserving rewrites such as `self.state` or
    /// capability access). A complex receiver (anything `value_text_for_is`
    /// could not render — e.g. a call) is evaluated once into a fresh temp
    /// emitted into `stmts` and cached by span, so the bindings gathered later
    /// reference the same evaluation rather than re-running the expression.
    fn is_receiver_ref(&mut self, value: &Expr, stmts: &mut Vec<String>) -> String {
        if let Some(t) = self.is_receiver_temps.get(&value.span) {
            return t.clone();
        }
        let lowered = lower_expr(value, stmts, self);
        if is_simple_is_receiver(value) {
            return lowered;
        }
        let tmp = self.fresh();
        stmts.push(format!("const {tmp} = {lowered};"));
        self.is_receiver_temps.insert(value.span, tmp.clone());
        tmp
    }

    /// v0.13: like `is_receiver_ref` but always lifts to a temp, even for a
    /// simple ident. A refined `is`-narrowing re-binds the value's name to the
    /// branded refined type (`const n = <temp> as Quantity`); that shadowing
    /// const cannot reference the same name (TDZ), so the value is captured in a
    /// temp first and both the check and the binding read the temp.
    fn is_receiver_ref_forced(&mut self, value: &Expr, stmts: &mut Vec<String>) -> String {
        if let Some(t) = self.is_receiver_temps.get(&value.span) {
            return t.clone();
        }
        let lowered = lower_expr(value, stmts, self);
        let tmp = self.fresh();
        stmts.push(format!("const {tmp} = {lowered};"));
        self.is_receiver_temps.insert(value.span, tmp.clone());
        tmp
    }

    /// v0.13: true when `value is Name` is a *refinement* check — the value is a
    /// base/refined value and `Name` is a refined type — rather than a sum
    /// variant test. Mirrors the checker's disambiguation.
    fn is_refined_is_check(&self, value: &Expr, name: &str) -> bool {
        let value_baseish = matches!(
            self.commons.expr_types.get(&value.span),
            Some(Ty::Base(_))
                | Some(Ty::Named {
                    kind: NamedKind::Refined(_),
                    ..
                })
        );
        let name_refined = matches!(
            self.commons.types.get(name).map(|d| &d.body),
            Some(TypeBody::Refined { .. })
        );
        value_baseish && name_refined
    }
    /// Read-only counterpart for the binding gatherer (which has no `stmts`
    /// and cannot lift). If the receiver was already lifted to a temp during
    /// condition lowering, reuse that temp; otherwise it must be a simple
    /// repeatable lvalue, rendered inline. The "lower the condition before
    /// gathering its bindings" ordering in `emit_if_tail` / `lower_and_with_is`
    /// guarantees the temp exists before this is called for complex receivers.
    fn is_receiver_text(&self, value: &Expr) -> String {
        if let Some(t) = self.is_receiver_temps.get(&value.span) {
            return t.clone();
        }
        value_text_for_is(value)
    }
    fn receiver_namespace(&self, e: &Expr) -> Option<String> {
        let ty = self.commons.expr_types.get(&e.span)?;
        if let Ty::Named { name, .. } = ty {
            Some(name.clone())
        } else {
            None
        }
    }
    /// Resolve the payload field name for the i-th positional binding of
    /// a variant. Built-ins are recognised by name; user variants are
    /// looked up via the type tables.
    fn positional_field_name(
        &self,
        discriminant_ty: Option<&Ty>,
        variant: &str,
        idx: usize,
    ) -> String {
        match (variant, idx) {
            ("Ok", 0) | ("Some", 0) => return "value".to_string(),
            ("Err", 0) => return "error".to_string(),
            _ => {}
        }
        // v0.52: a multi-actor sum arm binds the resolved actor's identity,
        // carried in the `identity` field of the tagged object.
        if let Some(Ty::ActorSum(_)) = discriminant_ty {
            return "identity".to_string();
        }
        if let Some(Ty::Named {
            kind: NamedKind::Sum,
            name,
        }) = discriminant_ty
            && let Some(decl) = self.commons.types.get(name)
            && let TypeBody::Sum(s) = &decl.body
            && let Some(v) = s.variants.iter().find(|v| v.name.name == variant)
            && let Some(f) = v.payload.get(idx)
        {
            return f.name.name.clone();
        }
        // Single-field fallback. The checker rejects mixed bindings already.
        "value".to_string()
    }
}

fn ts_base(b: BaseType) -> &'static str {
    match b {
        BaseType::Int => "number",
        BaseType::String => "string",
        BaseType::Bool => "boolean",
        BaseType::Float => "number",
    }
}

pub(crate) fn ts_type_ref(r: &TypeRef) -> String {
    ts_type_ref_with(r, None)
}

/// Like `ts_type_ref`, but qualifies named types that live in `scope` with the
/// namespace `ns` (`Order` → `Ns.Order`). Used by the test-emission harness for
/// mock method signatures that sit outside the destructuring that brings a
/// namespace's value-side names into local scope, so the types must be
/// referenced fully qualified. Qualification recurses through generic
/// arguments; base/unit types are unaffected.
pub(crate) fn ts_type_ref_qualified(r: &TypeRef, scope: &HashSet<String>, ns: &str) -> String {
    ts_type_ref_with(r, Some((scope, ns)))
}

/// Shared renderer behind `ts_type_ref` (`qualify = None`) and
/// `ts_type_ref_qualified` (`qualify = Some((scope, ns))`). With `None` it is
/// output-identical to the historic `ts_type_ref`; the only divergence is the
/// `Named` arm, which qualifies in-scope names when `qualify` is set.
fn ts_type_ref_with(r: &TypeRef, qualify: Option<(&HashSet<String>, &str)>) -> String {
    match r {
        TypeRef::Base(b, _) => ts_base(*b).to_string(),
        TypeRef::Named(id) => {
            if let Some((scope, ns)) = qualify
                && scope.contains(&id.name)
            {
                format!("{ns}.{}", id.name)
            } else {
                id.name.clone()
            }
        }
        TypeRef::Result(t, e, _) => format!(
            "Result<{}, {}>",
            ts_type_ref_with(t, qualify),
            ts_type_ref_with(e, qualify)
        ),
        TypeRef::Option(t, _) => format!("Option<{}>", ts_type_ref_with(t, qualify)),
        TypeRef::Effect(t, _) => {
            let inner = ts_type_ref_with(t, qualify);
            if inner == "()" || inner == "void" {
                "Promise<void>".to_string()
            } else {
                format!("Promise<{inner}>")
            }
        }
        TypeRef::HttpResult(t, _) => format!("HttpResult<{}>", ts_type_ref_with(t, qualify)),
        // v0.20b: collections lower to immutable TS shapes.
        TypeRef::List(t, _) => format!("readonly {}[]", ts_type_ref_with(t, qualify)),
        TypeRef::Map(k, v, _) => {
            format!(
                "ReadonlyMap<{}, {}>",
                ts_type_ref_with(k, qualify),
                ts_type_ref_with(v, qualify)
            )
        }
        TypeRef::QueueResult(_) => "QueueResult".to_string(),
        TypeRef::ValidationError(_) => "ValidationError".to_string(),
        TypeRef::JsonError(_) => "JsonError".to_string(),
        TypeRef::Unit(_) => "void".to_string(),
        // v0.20a: a function type lowers to a TS function type. Positional
        // parameter names (`a0`, `a1`, …) — TS requires names in function
        // type syntax; an Effect return is already Promise via recursion.
        TypeRef::Fn(params, ret, _) => {
            let params: Vec<String> = params
                .iter()
                .enumerate()
                .map(|(i, p)| format!("a{i}: {}", ts_type_ref_with(p, qualify)))
                .collect();
            let ret = match ts_type_ref_with(ret, qualify).as_str() {
                "()" => "void".to_string(),
                other => other.to_string(),
            };
            format!("({}) => {ret}", params.join(", "))
        }
    }
}

/// v0.20b: render a checker `Ty` as a TypeScript type. Used by the inline
/// kernel-method lowerings, whose IIFE parameters must be annotated
/// (`noImplicitAny`). Rigid type variables render as themselves — inside an
/// emitted generic function they are in scope as TS type parameters.
fn ts_ty(t: &Ty) -> String {
    match t {
        Ty::Base(BaseType::Int) => "number".to_string(),
        Ty::Base(BaseType::String) => "string".to_string(),
        Ty::Base(BaseType::Bool) => "boolean".to_string(),
        Ty::Base(BaseType::Float) => "number".to_string(),
        Ty::Named { name, .. } => name.clone(),
        Ty::Result(t, e) => format!("Result<{}, {}>", ts_ty(t), ts_ty(e)),
        Ty::Option(t) => format!("Option<{}>", ts_ty(t)),
        Ty::Effect(t) => match &**t {
            Ty::Unit => "Promise<void>".to_string(),
            other => format!("Promise<{}>", ts_ty(other)),
        },
        Ty::HttpResult(t) => format!("HttpResult<{}>", ts_ty(t)),
        Ty::List(t) => format!("readonly {}[]", ts_ty(t)),
        Ty::Map(k, v) => format!("ReadonlyMap<{}, {}>", ts_ty(k), ts_ty(v)),
        Ty::QueueResult => "QueueResult".to_string(),
        Ty::ValidationError => "ValidationError".to_string(),
        Ty::JsonError => "JsonError".to_string(),
        Ty::Unit => "void".to_string(),
        Ty::Fn { params, ret } => {
            let params: Vec<String> = params
                .iter()
                .enumerate()
                .map(|(i, p)| format!("a{i}: {}", ts_ty(p)))
                .collect();
            format!("({}) => {}", params.join(", "), ts_ty(ret))
        }
        Ty::Var(n) => n.clone(),
        // The identity type the actor binding yields (`name.identity`).
        Ty::Actor(id) => ts_ty(id),
        // v0.52: a resolved multi-actor sum lowers to a discriminated union
        // tagged by actor name; non-unit members carry their identity.
        Ty::ActorSum(members) => members
            .iter()
            .map(|(name, id)| match id {
                Ty::Unit => format!("{{ tag: \"{name}\" }}"),
                _ => format!("{{ tag: \"{name}\", identity: {} }}", ts_ty(id)),
            })
            .collect::<Vec<_>>()
            .join(" | "),
    }
}

fn ts_binop(op: BinOp) -> &'static str {
    match op {
        BinOp::Or => "||",
        BinOp::And => "&&",
        BinOp::Eq => "===",
        BinOp::NotEq => "!==",
        BinOp::Lt => "<",
        BinOp::LtEq => "<=",
        BinOp::Gt => ">",
        BinOp::GtEq => ">=",
        BinOp::Add => "+",
        BinOp::Sub => "-",
        BinOp::Mul => "*",
        BinOp::Div => "/",
    }
}

pub(crate) fn escape_ts_string(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\t' => out.push_str("\\t"),
            '\r' => out.push_str("\\r"),
            c => out.push(c),
        }
    }
    out
}

#[allow(dead_code)]
fn _unused_hashmap(_h: HashMap<String, ()>) {}

#[cfg(test)]
mod runtime_tests {
    use super::*;

    #[test]
    fn runtime_emits_all_required_exports() {
        let s = emit_runtime_module();
        // Core types and constructors used by every emitted module.
        assert!(s.contains("export type Result<T, E>"));
        assert!(s.contains("export const Ok"));
        assert!(s.contains("export const Err"));
        assert!(s.contains("export type Option<T>"));
        assert!(s.contains("export const Some"));
        assert!(s.contains("export const None"));
        assert!(s.contains("export interface ValidationError"));
        // Durable Object surface used by agent classes.
        assert!(s.contains("export interface DurableObjectStorage"));
        assert!(s.contains("export interface DurableObjectState"));
        assert!(s.contains("export class InMemoryStorage"));
        assert!(s.contains("export function makeTestState"));
        // Discriminator must be `tag` to match emitted code.
        assert!(s.contains("tag: \"Ok\""));
        assert!(s.contains("tag: \"Err\""));
        assert!(s.contains("tag: \"Some\""));
        assert!(s.contains("tag: \"None\""));
    }

    #[test]
    fn tsconfig_is_well_formed_json() {
        let s = emit_tsconfig();
        // Spot-check the key fields; we don't reach for a JSON parser.
        assert!(s.contains("\"target\": \"ES2022\""));
        assert!(s.contains("\"strict\": true"));
        assert!(s.contains("\"include\""));
    }

    #[test]
    fn workers_dir_name_replaces_dots_with_dashes() {
        assert_eq!(
            crate::project::worker_dir_name("commerce.payment"),
            "commerce-payment"
        );
        assert_eq!(crate::project::worker_dir_name("a.b.c"), "a-b-c");
    }

    // Refactor track: characterisation pin for the canonical `escape_ts_string`.
    // It escapes backslash/quote/newline/tab and carriage return (`\r` → `\r`).
    #[test]
    fn escape_ts_string_escapes_cr() {
        assert_eq!(escape_ts_string("a\\b"), "a\\\\b");
        assert_eq!(escape_ts_string("a\"b"), "a\\\"b");
        assert_eq!(escape_ts_string("a\nb"), "a\\nb");
        assert_eq!(escape_ts_string("a\tb"), "a\\tb");
        assert_eq!(escape_ts_string("a\rb"), "a\\rb"); // CR escaped here; raw in project copy
    }

    #[test]
    fn runtime_import_depth_resolves_correctly() {
        assert_eq!(runtime_import_for(Path::new("compose.ts")), "./runtime.js");
        assert_eq!(
            runtime_import_for(Path::new("commerce/payment.ts")),
            "../runtime.js"
        );
        assert_eq!(
            runtime_import_for(Path::new("commerce/orders/types.ts")),
            "../../runtime.js"
        );
        assert_eq!(
            runtime_import_for(Path::new("tests/commerce_payment.test.ts")),
            "../runtime.js"
        );
    }
}
