/**
 * Canonical Magogi `LogRecord` — TypeScript reference emitter (Wave 1.5).
 *
 * The TS/SvelteKit half of decision D1 (all runtimes emit the one wire shape).
 * The Python source of truth is `mgf.common.observability.LogRecord`; this is a
 * hand-port pinned to the same golden fixture
 * (`src/mgf/common/observability/schema/log_record_fixture.jsonl`) by
 * `conformance.test.ts`. Lines emitted here are **byte-identical** to Python's
 * `LogRecord.to_jsonl_line()`:
 *
 *   - keys in exactly `EMIT_ORDER`, then any unknown (`extra`) keys appended;
 *   - compact separators (no spaces), raw UTF-8 (no `\uXXXX` escaping —
 *     `JSON.stringify`'s default, matching Python's `ensure_ascii=False`);
 *   - core keys always present; optional keys omitted when empty/null/zero.
 *
 * See `log_record_schema.json` for the language-neutral contract this follows.
 */

export const LOG_SCHEMA_VERSION = "1.0.0";

/** Always-present keys, in emit order. */
export const CORE_KEYS = [
  "ts", "level", "level_no", "logger", "msg", "app", "lang", "schema_version",
] as const;

/** Optional typed keys, in emit order (omitted when empty per OMIT below). */
export const OPTIONAL_KEYS = [
  "module", "func", "line", "thread", "process", "host", "app_version",
  "env", "trace_id", "span_id", "request_id", "exception", "stack",
] as const;

/** Full deterministic key order: core, optional, then the two maps. */
export const EMIT_ORDER = [
  ...CORE_KEYS, ...OPTIONAL_KEYS, "fields", "labels",
] as const;

/** Keys this schema version owns; anything else on the wire goes to `extra`. */
const KNOWN_KEYS = new Set<string>(EMIT_ORDER);

type OmitRule = "always" | "empty" | "zero" | "null" | "object";

/** Per-field omit rule (mirrors the manifest's field type tags). */
const OMIT: Record<string, OmitRule> = {
  ts: "always", level: "always", level_no: "always", logger: "always",
  msg: "always", app: "always", lang: "always", schema_version: "always",
  module: "empty", func: "empty", thread: "empty", host: "empty",
  app_version: "empty",
  line: "zero", process: "zero",
  env: "null", trace_id: "null", span_id: "null", request_id: "null",
  exception: "null", stack: "null",
  fields: "object", labels: "object",
};

export interface LogRecord {
  ts: string;
  level: string;
  level_no: number;
  logger: string;
  msg: string;
  app: string;
  lang: string;
  schema_version: string;
  module?: string;
  func?: string;
  line?: number;
  thread?: string;
  process?: number;
  host?: string;
  app_version?: string;
  env?: string | null;
  trace_id?: string | null;
  span_id?: string | null;
  request_id?: string | null;
  exception?: string | null;
  stack?: string | null;
  fields?: Record<string, unknown>;
  labels?: Record<string, string>;
  /** Forward-compat: unknown keys from a newer schema, preserved verbatim. */
  extra?: Record<string, unknown>;
}

function isEmptyObject(v: unknown): boolean {
  return !v || typeof v !== "object" || Object.keys(v as object).length === 0;
}

/** True when a field's value should be omitted from the wire. */
function omitted(key: string, value: unknown): boolean {
  switch (OMIT[key]) {
    case "always": return false;
    case "empty": return value === undefined || value === "";
    case "zero": return value === undefined || value === 0;
    case "null": return value === undefined || value === null;
    case "object": return isEmptyObject(value);
    default: return value === undefined;
  }
}

/** Serialise to the canonical wire object (ordered; matches Python `to_dict`). */
export function toWire(rec: LogRecord): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const key of EMIT_ORDER) {
    const value = (rec as Record<string, unknown>)[key];
    if (!omitted(key, value)) out[key] = value;
  }
  // Re-emit preserved unknown keys (never clobber a key this version owns).
  for (const [k, v] of Object.entries(rec.extra ?? {})) {
    if (!(k in out)) out[k] = v;
  }
  return out;
}

/** Serialise to one JSONL line (compact + trailing newline) — byte-identical to Python. */
export function toJsonlLine(rec: LogRecord): string {
  return JSON.stringify(toWire(rec)) + "\n";
}

/** Parse one wire object into a LogRecord (unknown keys captured into `extra`). */
export function fromWire(data: Record<string, unknown>): LogRecord {
  const extra: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(data)) {
    if (!KNOWN_KEYS.has(k)) extra[k] = v;
  }
  return {
    ts: String(data.ts),
    level: String(data.level),
    level_no: Number(data.level_no ?? 0),
    logger: String(data.logger),
    msg: String(data.msg),
    app: String(data.app ?? ""),
    lang: String(data.lang ?? "python"),
    schema_version: String(data.schema_version ?? LOG_SCHEMA_VERSION),
    module: data.module as string | undefined,
    func: data.func as string | undefined,
    line: data.line as number | undefined,
    thread: data.thread as string | undefined,
    process: data.process as number | undefined,
    host: data.host as string | undefined,
    app_version: data.app_version as string | undefined,
    env: (data.env ?? undefined) as string | null | undefined,
    trace_id: (data.trace_id ?? undefined) as string | null | undefined,
    span_id: (data.span_id ?? undefined) as string | null | undefined,
    request_id: (data.request_id ?? undefined) as string | null | undefined,
    exception: (data.exception ?? undefined) as string | null | undefined,
    stack: (data.stack ?? undefined) as string | null | undefined,
    fields: data.fields as Record<string, unknown> | undefined,
    labels: data.labels as Record<string, string> | undefined,
    extra: Object.keys(extra).length ? extra : undefined,
  };
}

/** Parse one JSONL line into a LogRecord (trailing newline tolerated). */
export function fromJsonlLine(line: string): LogRecord {
  return fromWire(JSON.parse(line) as Record<string, unknown>);
}
