/**
 * fingerprint.ts — Phase 2: deterministic base fingerprint.
 *
 * Identity inputs (ported from pi's fingerprint.ts):
 *   - model id as sent by opencode (e.g. "qwen")
 *   - physical model identity (GGUF path + size + mtime)
 *   - llama-server build id (env-provided)
 *   - opencode version + plugin version (this code)
 *   - tools[] exactly as serialized on the wire (array order preserved,
 *     object keys sorted — order matters for template rendering, key
 *     order does not)
 *   - chat_template_kwargs (opencode never sends any; kept for parity
 *     and for providers that do)
 *   - stable prefix text (whitespace-stripped; see prefix.ts)
 *
 * Excluded by construction: the volatile tail of the system prompt
 * (<env>, AGENTS.md instructions beyond the cut, MCP instructions,
 * skills list), session history, user messages.
 *
 * NOTE: the *whole* system string must never be hashed — opencode's
 * MCP instruction block mutates every turn ("0 calls | 0 tok saved"),
 * so a whole-system fingerprint would miss on every single request.
 */
import { createHash } from "node:crypto";

const FINGERPRINT_FORMAT = "oc-prefix-v1";

/** Canonical JSON: object keys sorted recursively, array order preserved. */
export function canonicalJson(value: unknown): string {
  if (value === null) return "null";
  if (Array.isArray(value)) {
    return "[" + value.map(canonicalJson).join(",") + "]";
  }
  switch (typeof value) {
    case "string":
    case "number":
    case "boolean":
      return JSON.stringify(value);
    case "object": {
      const obj = value as Record<string, unknown>;
      const keys = Object.keys(obj).sort();
      return (
        "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(obj[k])).join(",") + "}"
      );
    }
    default:
      return "null"; // undefined, function, etc. → normalize to null
  }
}

export interface FingerprintInputs {
  model: string;
  /** Physical model identity: path + file size + mtime ("path" if the
   *  file cannot be stat'ed). Catches a GGUF replaced at the same path. */
  modelIdentity: string;
  /** Optional llama-server build/runtime identity (env-provided; the
   *  server exposes no JSON build endpoint). Checkpoint state is
   *  coupled to the server implementation, so a server upgrade with
   *  a new id forces a clean rebuild. */
  serverId: string;
  opencodeVersion: string;
  extensionVersion: string;
  stablePrefix: string;
  tools: unknown;
  chatTemplateKwargs: unknown;
}

export function computeFingerprint(inputs: FingerprintInputs): string {
  const h = createHash("sha256");
  h.update(FINGERPRINT_FORMAT + "\n");
  h.update(inputs.model + "\n");
  h.update("model_identity\n");
  h.update(inputs.modelIdentity + "\n");
  h.update("server_id\n");
  h.update(inputs.serverId + "\n");
  h.update(inputs.opencodeVersion + "\n");
  h.update(inputs.extensionVersion + "\n");
  h.update("tools\n");
  h.update(canonicalJson(inputs.tools) + "\n");
  h.update("chat_template_kwargs\n");
  h.update(canonicalJson(inputs.chatTemplateKwargs) + "\n");
  h.update("stable_prefix\n");
  h.update(inputs.stablePrefix);
  return h.digest("hex");
}
