/**
 * CC Bridge Transforms — config-driven request body normalization for the
 * Claude Code Compatible (`anthropic-compatible-cc-*`) bridge.
 *
 * Goal: ensure the final request body OmniRoute sends to Anthropic's
 * `/v1/messages?beta=true` endpoint has classifier-correct structure
 * regardless of which client (OpenCode, Cline, Cursor, Continue, raw API
 * consumer) supplied the prompt.
 *
 * Approach: an ordered pipeline of declarative `TransformOp` entries that
 * mutate the request body in place. Each op is idempotent; the executor is
 * pure (no I/O); new defenses can be added through Settings UI by appending
 * a new op — no new TypeScript needed.
 *
 * Reference implementation: ex-machina/opencode-anthropic-auth `transform.ts`
 * and `cch.ts`. Ported with the same defaults (paragraph anchors, identity
 * prefixes, text replacements, billing header algorithm) but generalised
 * behind a discriminated-union DSL so future fingerprints are configurable.
 *
 * Related: OmniRoute issue #2260.
 */
import { createHash } from "node:crypto";

// ────────────────────────────────────────────────────────────────────────────
// DSL types
// ────────────────────────────────────────────────────────────────────────────

export type TransformOp =
  | DropParagraphIfContainsOp
  | DropParagraphIfStartsWithOp
  | ReplaceTextOp
  | ReplaceRegexOp
  | DropBlockIfContainsOp
  | PrependSystemBlockOp
  | AppendSystemBlockOp
  | InjectBillingHeaderOp;

export interface DropParagraphIfContainsOp {
  kind: "drop_paragraph_if_contains";
  needles: string[];
  caseSensitive?: boolean;
}

export interface DropParagraphIfStartsWithOp {
  kind: "drop_paragraph_if_starts_with";
  prefixes: string[];
  caseSensitive?: boolean;
}

export interface ReplaceTextOp {
  kind: "replace_text";
  match: string;
  replacement: string;
  allOccurrences?: boolean;
}

export interface ReplaceRegexOp {
  kind: "replace_regex";
  pattern: string;
  flags?: string;
  replacement: string;
}

export interface DropBlockIfContainsOp {
  kind: "drop_block_if_contains";
  needles: string[];
}

export interface PrependSystemBlockOp {
  kind: "prepend_system_block";
  text: string;
  /** Skip if any earlier block already starts with this prefix. */
  idempotencyKey?: string;
}

export interface AppendSystemBlockOp {
  kind: "append_system_block";
  text: string;
  idempotencyKey?: string;
}

export interface InjectBillingHeaderOp {
  kind: "inject_billing_header";
  /** Anthropic billing entrypoint label (e.g. `sdk-cli`, `cli`). */
  entrypoint: string;
  /**
   * Version suffix algorithm:
   *   - ex-machina: sha256(SALT + chars-at-positions + version).slice(0,3)
   *   - omniroute-daystamp: sha256(YYYY-MM-DD + version).slice(0,3)
   */
  versionFormat: "ex-machina" | "omniroute-daystamp";
  /**
   * CCH attestation algorithm:
   *   - sha256-first-user: sha256(firstUserMessageText).slice(0,5)  (ex-machina)
   *   - xxhash64-body: defer to body-level signRequestBody; emit "00000" here
   *   - static-zero: emit "00000" (relay endpoints don't validate)
   */
  cchAlgo: "sha256-first-user" | "xxhash64-body" | "static-zero";
  /** Override the embedded `cc_version=` value. Defaults to `2.1.137`. */
  version?: string;
}

export interface CcBridgeTransformsConfig {
  enabled: boolean;
  pipeline: TransformOp[];
}

// ────────────────────────────────────────────────────────────────────────────
// Ported constants (ex-machina/constants.ts)
// ────────────────────────────────────────────────────────────────────────────

/** Stable salt used by ex-machina/cch.ts for the version-suffix hash. */
export const CCH_SALT = "59cf53e54c78";
/** Character positions sampled from the first user message text. */
export const CCH_POSITIONS = [4, 7, 20] as const;
/** Default `cc_version=` value embedded in the billing header. */
export const DEFAULT_CLAUDE_CODE_VERSION = "2.1.158";
/** Identity sentinel prepended for Claude Agent SDK callers. */
export const CLAUDE_AGENT_SDK_IDENTITY =
  "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
/** Paragraph anchors from ex-machina (URLs identifying third-party agents). */
export const DEFAULT_PARAGRAPH_REMOVAL_ANCHORS = [
  "github.com/anomalyco/opencode",
  "opencode.ai/docs",
  "github.com/cline/cline",
  "github.com/getcursor/cursor",
  "continue.dev",
];
/** Identity paragraph prefixes that signal a third-party agent. */
export const DEFAULT_IDENTITY_PREFIXES = ["You are OpenCode"];
/** Text replacements (last entry is the v1.7.5 phrase-shape filter fix). */
export const DEFAULT_TEXT_REPLACEMENTS: Array<{ match: string; replacement: string }> = [
  { match: "if OpenCode honestly", replacement: "if the assistant honestly" },
  {
    match: "Here is some useful information about the environment you are running in:",
    replacement: "Environment context you are running in:",
  },
];

/**
 * Default pipeline shipped with the PR — matches the T4-200 fixture layout
 * proven against the live OmniRoute deployment (call log
 * f0c2fedb-b27a-4f1d-9ee6-0c88646a6d42).
 *
 * Layout after pipeline (system blocks):
 *   [0] x-anthropic-billing-header: cc_version=…; cc_entrypoint=sdk-cli; cch=…
 *   [1] You are a Claude agent, built on Anthropic's Claude Agent SDK.
 *   [2..] sanitized caller-supplied system blocks
 */
export const DEFAULT_CC_BRIDGE_PIPELINE: TransformOp[] = [
  // Sanitize caller-supplied system blocks first so dropped paragraphs do not
  // accidentally contain a stale billing header from a previous pass.
  {
    kind: "drop_paragraph_if_contains",
    needles: [...DEFAULT_PARAGRAPH_REMOVAL_ANCHORS],
  },
  {
    kind: "drop_paragraph_if_starts_with",
    prefixes: [...DEFAULT_IDENTITY_PREFIXES],
  },
  ...DEFAULT_TEXT_REPLACEMENTS.map<ReplaceTextOp>((r) => ({
    kind: "replace_text",
    match: r.match,
    replacement: r.replacement,
    allOccurrences: true,
  })),
  // Then prepend the SDK identity (becomes block[1] after billing prepend).
  {
    kind: "prepend_system_block",
    text: CLAUDE_AGENT_SDK_IDENTITY,
    idempotencyKey: "claude-agent-sdk-identity",
  },
  // Billing header always lands at block[0] — matches T4-200 fixture layout.
  {
    kind: "inject_billing_header",
    entrypoint: "sdk-cli",
    versionFormat: "ex-machina",
    cchAlgo: "sha256-first-user",
  },
];

export const DEFAULT_CC_BRIDGE_TRANSFORMS_CONFIG: CcBridgeTransformsConfig = {
  enabled: true,
  pipeline: DEFAULT_CC_BRIDGE_PIPELINE,
};

// ────────────────────────────────────────────────────────────────────────────
// Billing header value
// ────────────────────────────────────────────────────────────────────────────

interface SystemBlock {
  type: string;
  text?: string;
  [key: string]: unknown;
}

interface MessageContentBlock {
  type?: string;
  text?: string;
  [key: string]: unknown;
}

interface Message {
  role?: string;
  content?: string | MessageContentBlock[];
  [key: string]: unknown;
}

/**
 * Pull the textual content of the first user message in the request.
 * Returns "" when no user message has text content.
 */
export function extractFirstUserMessageText(messages: Message[]): string {
  if (!Array.isArray(messages)) return "";

  for (const msg of messages) {
    if (msg?.role !== "user") continue;
    if (typeof msg.content === "string") return msg.content;
    if (Array.isArray(msg.content)) {
      for (const block of msg.content) {
        if (block?.type === "text" && typeof block.text === "string") {
          return block.text;
        }
      }
    }
  }
  return "";
}

function sha256Hex(input: string): string {
  return createHash("sha256").update(input, "utf8").digest("hex");
}

function pickCharsAtPositions(text: string, positions: readonly number[]): string {
  return positions.map((p) => (typeof text[p] === "string" ? text[p] : "\0")).join("");
}

/**
 * Compute the `cc_version` suffix per the ex-machina algorithm.
 *
 * `sha256(CCH_SALT + chars-at-CCH_POSITIONS(firstUserMessage) + version).slice(0, 3)`
 */
export function computeExMachinaVersionSuffix(firstUserText: string, version: string): string {
  const picks = pickCharsAtPositions(firstUserText, CCH_POSITIONS);
  return sha256Hex(`${CCH_SALT}${picks}${version}`).slice(0, 3);
}

/**
 * Compute the `cc_version` suffix per the OmniRoute native-OAuth algorithm:
 * sha256(YYYY-MM-DD + version).slice(0,3). Stable per UTC day.
 */
export function computeDaystampVersionSuffix(version: string, now: Date = new Date()): string {
  const dayStamp = now.toISOString().slice(0, 10);
  return sha256Hex(`${dayStamp}${version}`).slice(0, 3);
}

/**
 * Compute the `cch=` attestation value per ex-machina algorithm:
 * sha256(firstUserMessage).slice(0,5).
 */
export function computeCchSha256FirstUser(firstUserText: string): string {
  return sha256Hex(firstUserText).slice(0, 5);
}

interface BuildBillingHeaderOptions {
  entrypoint: string;
  versionFormat: "ex-machina" | "omniroute-daystamp";
  cchAlgo: "sha256-first-user" | "xxhash64-body" | "static-zero";
  version?: string;
  now?: Date;
}

/**
 * Build the `x-anthropic-billing-header: …` string injected as system block[0].
 *
 * `xxhash64-body` and `static-zero` both emit `cch=00000` here because the
 * actual body-level CCH attestation is computed later by
 * `claudeCodeCCH.signRequestBody()` and replaces a 00000 placeholder in the
 * serialized JSON. ex-machina's `sha256-first-user` value lives in the
 * header itself.
 */
export function buildBillingHeaderValue(
  messages: Message[],
  options: BuildBillingHeaderOptions
): string {
  const version = options.version || DEFAULT_CLAUDE_CODE_VERSION;
  const firstUserText = extractFirstUserMessageText(messages);

  const suffix =
    options.versionFormat === "omniroute-daystamp"
      ? computeDaystampVersionSuffix(version, options.now)
      : computeExMachinaVersionSuffix(firstUserText, version);

  let cch: string;
  switch (options.cchAlgo) {
    case "sha256-first-user":
      cch = computeCchSha256FirstUser(firstUserText);
      break;
    case "xxhash64-body":
    case "static-zero":
    default:
      cch = "00000";
      break;
  }

  return `x-anthropic-billing-header: cc_version=${version}.${suffix}; cc_entrypoint=${options.entrypoint}; cch=${cch};`;
}

// ────────────────────────────────────────────────────────────────────────────
// Body shape helpers
// ────────────────────────────────────────────────────────────────────────────

interface RequestBody {
  system?: string | SystemBlock[];
  messages?: Message[];
  [key: string]: unknown;
}

function normalizeSystemToBlocks(system: unknown): SystemBlock[] {
  if (system === null || system === undefined) return [];
  if (typeof system === "string") {
    return system.length > 0 ? [{ type: "text", text: system }] : [];
  }
  if (Array.isArray(system)) {
    return system
      .filter((b): b is SystemBlock => !!b && typeof b === "object")
      .map((b) => ({ ...b }));
  }
  if (typeof system === "object") {
    const block = system as SystemBlock;
    return block && typeof block.text === "string" ? [{ ...block }] : [];
  }
  return [];
}

function isTextBlock(block: SystemBlock): block is SystemBlock & { text: string } {
  return block.type === "text" && typeof block.text === "string";
}

function containsString(haystack: string, needle: string, caseSensitive: boolean): boolean {
  if (caseSensitive) return haystack.includes(needle);
  return haystack.toLowerCase().includes(needle.toLowerCase());
}

function startsWithString(haystack: string, prefix: string, caseSensitive: boolean): boolean {
  if (caseSensitive) return haystack.startsWith(prefix);
  return haystack.toLowerCase().startsWith(prefix.toLowerCase());
}

// ────────────────────────────────────────────────────────────────────────────
// Op executors
// ────────────────────────────────────────────────────────────────────────────

function applyDropParagraphIfContains(
  blocks: SystemBlock[],
  op: DropParagraphIfContainsOp
): SystemBlock[] {
  const caseSensitive = op.caseSensitive !== false;
  const needles = op.needles || [];
  if (needles.length === 0) return blocks;

  return blocks.map((block) => {
    if (!isTextBlock(block)) return block;
    const paragraphs = block.text.split(/\n\n+/);
    const filtered = paragraphs.filter(
      (p) => !needles.some((n) => containsString(p, n, caseSensitive))
    );
    return { ...block, text: filtered.join("\n\n") };
  });
}

function applyDropParagraphIfStartsWith(
  blocks: SystemBlock[],
  op: DropParagraphIfStartsWithOp
): SystemBlock[] {
  const caseSensitive = op.caseSensitive !== false;
  const prefixes = op.prefixes || [];
  if (prefixes.length === 0) return blocks;

  return blocks.map((block) => {
    if (!isTextBlock(block)) return block;
    const paragraphs = block.text.split(/\n\n+/);
    const filtered = paragraphs.filter(
      (p) => !prefixes.some((prefix) => startsWithString(p.trimStart(), prefix, caseSensitive))
    );
    return { ...block, text: filtered.join("\n\n") };
  });
}

function applyReplaceText(blocks: SystemBlock[], op: ReplaceTextOp): SystemBlock[] {
  if (!op.match) return blocks;
  return blocks.map((block) => {
    if (!isTextBlock(block)) return block;
    if (!block.text.includes(op.match)) return block;
    let next = block.text;
    if (op.allOccurrences) {
      next = next.split(op.match).join(op.replacement);
    } else {
      next = next.replace(op.match, op.replacement);
    }
    return { ...block, text: next };
  });
}

function applyReplaceRegex(blocks: SystemBlock[], op: ReplaceRegexOp): SystemBlock[] {
  if (!op.pattern) return blocks;
  let regex: RegExp;
  try {
    regex = new RegExp(op.pattern, op.flags ?? "u");
  } catch {
    return blocks;
  }
  return blocks.map((block) => {
    if (!isTextBlock(block)) return block;
    return { ...block, text: block.text.replace(regex, op.replacement) };
  });
}

function applyDropBlockIfContains(blocks: SystemBlock[], op: DropBlockIfContainsOp): SystemBlock[] {
  const needles = op.needles || [];
  if (needles.length === 0) return blocks;
  return blocks.filter((block) => {
    if (!isTextBlock(block)) return true;
    return !needles.some((n) => block.text.includes(n));
  });
}

function applyPrependSystemBlock(blocks: SystemBlock[], op: PrependSystemBlockOp): SystemBlock[] {
  if (!op.text) return blocks;
  // Idempotency: skip if any text block already starts with idempotencyKey (when
  // set) or with op.text itself (default). Scans ALL blocks, not just the first.
  const prefix = op.idempotencyKey ?? op.text;
  const alreadyPresent = blocks.some((b) => isTextBlock(b) && b.text.startsWith(prefix));
  if (alreadyPresent) return blocks;
  return [{ type: "text", text: op.text }, ...blocks];
}

function applyAppendSystemBlock(blocks: SystemBlock[], op: AppendSystemBlockOp): SystemBlock[] {
  if (!op.text) return blocks;
  // Idempotency: skip if any text block already starts with idempotencyKey (when
  // set) or is an exact match of op.text (default). Scans ALL blocks.
  const prefix = op.idempotencyKey;
  const alreadyPresent = prefix
    ? blocks.some((b) => isTextBlock(b) && b.text.startsWith(prefix))
    : blocks.some((b) => isTextBlock(b) && b.text === op.text);
  if (alreadyPresent) return blocks;
  return [...blocks, { type: "text", text: op.text }];
}

function applyInjectBillingHeader(
  body: RequestBody,
  blocks: SystemBlock[],
  op: InjectBillingHeaderOp
): SystemBlock[] {
  // No user message → no billing header (ex-machina parity, transform.ts:340).
  const messages = Array.isArray(body.messages) ? body.messages : [];
  const hasUser = messages.some((m) => m?.role === "user");
  if (!hasUser) return blocks;

  const headerValue = buildBillingHeaderValue(messages, {
    entrypoint: op.entrypoint,
    versionFormat: op.versionFormat,
    cchAlgo: op.cchAlgo,
    version: op.version,
  });

  // Idempotency: replace any existing billing header block (ex-machina + native
  // OAuth path both rebuild on retry; see executors/base.ts issue #1712).
  const headerPrefix = "x-anthropic-billing-header:";
  const filtered = blocks.filter((b) => !(isTextBlock(b) && b.text.startsWith(headerPrefix)));
  return [{ type: "text", text: headerValue }, ...filtered];
}

// ────────────────────────────────────────────────────────────────────────────
// Pipeline executor
// ────────────────────────────────────────────────────────────────────────────

export interface ApplyPipelineResult {
  body: RequestBody;
  appliedOpKinds: string[];
}

/**
 * Run the configured transform pipeline against a request body.
 *
 * The body is mutated in place (its `system` field is replaced); returned for
 * chaining. `appliedOpKinds` lists the ops that ran (omitting no-ops when
 * config is disabled). When `config.enabled === false`, the body is returned
 * unchanged and `appliedOpKinds` is empty.
 */
export function applyCcBridgeTransformPipeline(
  body: RequestBody,
  config: CcBridgeTransformsConfig = getCcBridgeTransformsConfig()
): ApplyPipelineResult {
  if (!body || typeof body !== "object") {
    return { body, appliedOpKinds: [] };
  }
  if (!config.enabled || !Array.isArray(config.pipeline) || config.pipeline.length === 0) {
    return { body, appliedOpKinds: [] };
  }

  let blocks = normalizeSystemToBlocks(body.system);
  const appliedOpKinds: string[] = [];

  for (const op of config.pipeline) {
    switch (op.kind) {
      case "drop_paragraph_if_contains":
        blocks = applyDropParagraphIfContains(blocks, op);
        break;
      case "drop_paragraph_if_starts_with":
        blocks = applyDropParagraphIfStartsWith(blocks, op);
        break;
      case "replace_text":
        blocks = applyReplaceText(blocks, op);
        break;
      case "replace_regex":
        blocks = applyReplaceRegex(blocks, op);
        break;
      case "drop_block_if_contains":
        blocks = applyDropBlockIfContains(blocks, op);
        break;
      case "prepend_system_block":
        blocks = applyPrependSystemBlock(blocks, op);
        break;
      case "append_system_block":
        blocks = applyAppendSystemBlock(blocks, op);
        break;
      case "inject_billing_header":
        blocks = applyInjectBillingHeader(body, blocks, op);
        break;
      default: {
        // Unknown op kind — skip silently to keep forward compatibility.
        continue;
      }
    }
    appliedOpKinds.push(op.kind);
  }

  // Drop empty text blocks left behind by paragraph removal (matches
  // ex-machina sanitizeSystemText trim semantics).
  blocks = blocks.filter((b) => !isTextBlock(b) || b.text.length > 0);

  body.system = blocks;
  return { body, appliedOpKinds };
}

// ────────────────────────────────────────────────────────────────────────────
// Runtime singleton (mirrors cliFingerprints `_cliCompatProviders` pattern).
// ────────────────────────────────────────────────────────────────────────────

let _runtimeConfig: CcBridgeTransformsConfig = DEFAULT_CC_BRIDGE_TRANSFORMS_CONFIG;

/**
 * Replace the active CC bridge transforms config. Called from
 * `runtimeSettings.applyCcBridgeTransformsSection()` when the Settings UI
 * saves a new pipeline.
 */
export function setCcBridgeTransformsConfig(config: CcBridgeTransformsConfig | null): void {
  if (!config) {
    _runtimeConfig = DEFAULT_CC_BRIDGE_TRANSFORMS_CONFIG;
    return;
  }
  _runtimeConfig = {
    enabled: config.enabled !== false,
    pipeline: Array.isArray(config.pipeline) ? config.pipeline : DEFAULT_CC_BRIDGE_PIPELINE,
  };
}

/**
 * Read the currently active config (defaults to DEFAULT_CC_BRIDGE_PIPELINE).
 */
export function getCcBridgeTransformsConfig(): CcBridgeTransformsConfig {
  return _runtimeConfig;
}

/**
 * Reset to defaults — exposed for tests and the Settings UI "Reset" button.
 */
export function resetCcBridgeTransformsConfig(): void {
  _runtimeConfig = DEFAULT_CC_BRIDGE_TRANSFORMS_CONFIG;
}
