// Thin mizchi/mars middleware adapter (T-05) for the
// vicoproplus/moon_ua_parser library.
//
// Responsibility boundary (plan R2 — four duties only, zero inlined
// business semantics):
//
// 1. Registration adaptation — `middleware()` builds a mars `Handler`
//    (`async (Context) -> Unit`, Hono-style sequential chain) for
//    `Server::middleware`.
// 2. Request-entry helper call — the middleware reads the `User-Agent`
//    request header and calls
//    `@middleware_core.assemble(ua, @ua_parser.parse(ua))`. Assembly,
//    degradation judgment and forensics-record construction all live in
//    `vicoproplus/moon_ua_parser_middleware_core` (T-03); nothing is
//    re-implemented in this file.
// 3. Context mounting — the three mounted groups (browser / os / device
//    families of the assembled `UaInfo`) are written to mars's typed
//    `Variables` request store (`ctx.vars`, the framework's own custom
//    key-value context) BEFORE the route handler runs — see "Mounting
//    mechanism" in README.mbt.md for why families beat a JSON round-trip
//    here: mars `Variables::set` serializes every value through the
//    `Var` trait to `Json` and `Variables::get` returns `Json?`
//    (env.mbt:82-100/140-166, verified against upstream commit ff4485e0),
//    so a full `UaInfo` mount would need a ~165-line JSON transport codec,
//    while the three families are natively storable `Var::String` values
//    with zero serialization. The mount therefore carries exactly the
//    browser/os/device three-group contract of plan R3.
// 4. Log-channel adaptation — a degraded request's `ForensicsRecord` is
//    forwarded to an injectable sink (default `console_forensics_sink`,
//    one console line via `println`). mars DOES ship a logger middleware
//    (`@mars/middleware.logger()` / `logger_simple()`, verified against
//    upstream commit ff4485e0: logger.mbt:114/162) whose output sink is
//    also `println` (logger.mbt:145/166) — the default sink is the same
//    channel, so forensics lines and the framework's own request logs
//    share one console stream; applications can inject any other backend.
//
// Degradation behavior (spec §3, implemented entirely by the helper): a
// parse `Err` or an all-three-families-at-fallback result mounts the EMPTY
// `UaInfo` (empty-string families), never raises into the request chain,
// and emits `{ua_summary, reason}` forensics to the sink. The response
// therefore keeps its normal (non-5xx) status with empty families on the
// mount.

///|
/// Typed-`Variables` key carrying the mounted browser family.
pub const KEY_UA_BROWSER : String = "ua_parser.browser"

///|
/// Typed-`Variables` key carrying the mounted OS family.
pub const KEY_UA_OS : String = "ua_parser.os"

///|
/// Typed-`Variables` key carrying the mounted device family.
pub const KEY_UA_DEVICE : String = "ua_parser.device"

///|
/// Prefix of the default console forensics line (FO channel).
pub const FORENSICS_LOG_PREFIX : String = "[moon_ua_parser]"

///|
/// Default FO channel: one console line per degraded request.
///
/// Format: ` degraded user-agent parse: reason= ua_summary=""`
/// where `ua_summary` is already truncated to
/// `UA_SUMMARY_MAX_LENGTH` characters by the helper. `println` is the same
/// output sink mars's own logger middleware uses (upstream logger.mbt:145),
/// so this line lands on the framework's log channel.
pub fn console_forensics_sink(rec : @middleware_core.ForensicsRecord) -> Unit {
  println(
    "\{FORENSICS_LOG_PREFIX} degraded user-agent parse: reason=\{rec.reason} ua_summary=\"\{rec.ua_summary}\"",
  )
}

///|
/// Registration adapter: builds the UA-parser middleware for
/// `Server::middleware`.
///
/// The middleware, per request:
/// 1. reads the `User-Agent` request header (missing header = empty
///    string; the helper's semantics decide what that mounts);
/// 2. calls `@middleware_core.assemble(ua, @ua_parser.parse(ua))`;
/// 3. mounts `outcome.ua_info`'s three families on `ctx.vars` before the
///    route handler runs;
/// 4. forwards `outcome.forensics` (iff degraded) to the sink.
///
/// `forensics_sink` defaults to `console_forensics_sink` when omitted.
pub fn middleware(
  forensics_sink? : (@middleware_core.ForensicsRecord) -> Unit,
) -> @mars.Handler {
  let sink = forensics_sink.unwrap_or(console_forensics_sink)
  ctx => {
    let ua = request_user_agent(ctx)
    let outcome = @middleware_core.assemble(ua, @ua_parser.parse(ua))
    mount(ctx, outcome.ua_info)
    match outcome.forensics {
      Some(rec) => sink(rec)
      None => ()
    }
  }
}

///|
/// Handler-side read-back: the `UaInfo` mounted for this request,
/// reconstructed from the typed `Variables` mount. `None` when the
/// middleware is not registered upstream (no mount keys present).
///
/// Mount contract: the mars mount carries the three group families
/// (browser / os / device). The version, brand, model and `rule_index`
/// fields of the returned `UaInfo` are `None` by construction — they are
/// not part of the mount (see the mounting-mechanism note in the file
/// header). On degradation the reconstruction is structurally equal to
/// `@middleware_core.empty_ua_info()` (empty families, `None` elsewhere),
/// so callers can distinguish "middleware had nothing" (`Some(empty)`)
/// from "middleware not registered" (`None`).
pub fn mounted_ua_info(ctx : @mars.Context) -> @ua_parser.UaInfo? {
  let browser = mounted_family(ctx, KEY_UA_BROWSER)
  let os = mounted_family(ctx, KEY_UA_OS)
  let device = mounted_family(ctx, KEY_UA_DEVICE)
  match (browser, os, device) {
    (Some(browser_family), Some(os_family), Some(device_family)) =>
      Some({
        browser: {
          family: browser_family,
          major: None,
          minor: None,
          patch: None,
          patch_minor: None,
          rule_index: None,
        },
        os: {
          family: os_family,
          major: None,
          minor: None,
          patch: None,
          patch_minor: None,
          rule_index: None,
        },
        device: {
          family: device_family,
          brand: None,
          model: None,
          rule_index: None,
        },
      })
    _ => None
  }
}

///|
/// Read the `User-Agent` request header with a case-pair lookup.
///
/// Why two lookups: mars `Context::header` is an exact-case map lookup
/// (context.mbt:58-60, upstream ff4485e0) while the mizchi/x native
/// transport lowercases request-header keys when building the framework
/// request (`http_native.mbt to_request`). Trying the canonical
/// `User-Agent` spelling first keeps the adapter correct on transports
/// that preserve case; the lowercase fallback covers the native stack.
/// Missing header = empty string (the helper's degradation semantics
/// decide what an empty UA mounts).
fn request_user_agent(ctx : @mars.Context) -> String {
  match ctx.header("User-Agent") {
    Some(ua) => ua
    None =>
      match ctx.header("user-agent") {
        Some(ua) => ua
        None => ""
      }
  }
}

///|
/// Mount `info`'s three families on the request context (duty 3). Pure
/// `Variables::set` writes with `Var::String` values — no serialization —
/// before the route handler runs, so every downstream handler sees the
/// mount via `mounted_ua_info` / the raw `ctx.vars` store.
fn mount(ctx : @mars.Context, info : @ua_parser.UaInfo) -> Unit {
  ctx.vars.set(KEY_UA_BROWSER, info.browser.family)
  ctx.vars.set(KEY_UA_OS, info.os.family)
  ctx.vars.set(KEY_UA_DEVICE, info.device.family)
}

///|
/// Read one mounted family back from the typed `Variables` store.
/// `Variables::get` returns `Json?` (`Var::String` values are stored as
/// `Json::String`); `None` when the middleware did not run for this
/// request (key absent) or the stored value is not a JSON string.
fn mounted_family(ctx : @mars.Context, key : String) -> String? {
  match ctx.vars.get(key) {
    Some(Json::String(family)) => Some(family)
    _ => None
  }
}