// src/layer/adapters/json-compact-large.ts
//
// Deterministically compacts large JSON array-shaped tool results while
// preserving the full source in the per-session KB.

import { createHash } from "node:crypto";
import type { ToolResult } from "../../provider/canonical.ts";
import type { CompressedResult, ToolResultAdapter } from "./types.ts";

const BYTE_THRESHOLD = 4096;
const EDGE_ROWS = 12;
const MATCH_RE =
  /error|failed|exception|warning|missing|denied|unauthorized|timeout/i;

function rawContent(r: ToolResult): string {
  return typeof r.content === "string" ? r.content : JSON.stringify(r.content);
}

function stableId(raw: string): string {
  return createHash("sha256").update(raw).digest("hex").slice(0, 12);
}

function passThrough(raw: string): CompressedResult {
  return {
    preview: raw,
    kbEntries: [],
    bytesSaved: 0,
  };
}

function findArray(value: unknown): unknown[] | null {
  if (Array.isArray(value)) return value;
  if (value === null || typeof value !== "object") return null;

  const obj = value as Record<string, unknown>;
  const keys = Object.keys(obj).sort();
  for (const key of keys) {
    const child = obj[key];
    if (Array.isArray(child)) {
      const childRaw = JSON.stringify(child);
      if (Buffer.byteLength(childRaw, "utf8") >= BYTE_THRESHOLD) {
        return child;
      }
    }
  }

  for (const key of keys) {
    const found = findArray(obj[key]);
    if (found !== null) return found;
  }

  return null;
}

function selectedIndices(rows: unknown[]): { indices: number[]; matched: number } {
  const selected = new Array<boolean>(rows.length).fill(false);
  let matched = 0;

  for (let index = 0; index < Math.min(EDGE_ROWS, rows.length); index += 1) {
    selected[index] = true;
  }

  for (
    let index = Math.max(0, rows.length - EDGE_ROWS);
    index < rows.length;
    index += 1
  ) {
    selected[index] = true;
  }

  for (let index = 0; index < rows.length; index += 1) {
    const serialized = JSON.stringify(rows[index]);
    if (MATCH_RE.test(serialized)) {
      selected[index] = true;
      matched += 1;
    }
  }

  const indices: number[] = [];
  for (let index = 0; index < selected.length; index += 1) {
    if (selected[index]) indices.push(index);
  }
  return { indices, matched };
}

function fieldsFor(rows: unknown[]): string {
  const fields: string[] = [];
  for (const row of rows) {
    if (row === null || typeof row !== "object" || Array.isArray(row)) {
      continue;
    }
    for (const key of Object.keys(row as Record<string, unknown>)) {
      if (!fields.includes(key)) fields.push(key);
    }
  }
  return fields.length > 0 ? fields.join(",") : "(none)";
}

export const jsonCompactLarge: ToolResultAdapter = {
  id: "json-compact-large",

  match(r: ToolResult): boolean {
    const raw = rawContent(r);
    if (Buffer.byteLength(raw, "utf8") < BYTE_THRESHOLD) return false;

    try {
      return findArray(JSON.parse(raw)) !== null;
    } catch {
      return false;
    }
  },

  compress(r: ToolResult): CompressedResult {
    const raw = rawContent(r);
    if (Buffer.byteLength(raw, "utf8") < BYTE_THRESHOLD) {
      return passThrough(raw);
    }

    let parsed: unknown;
    let rows: unknown[] | null = null;
    try {
      parsed = JSON.parse(raw);
      rows = findArray(parsed);
    } catch {
      return passThrough(raw);
    }
    if (rows === null) return passThrough(raw);

    const { indices, matched } = selectedIndices(rows);
    const shownRows = indices.map((index) => rows[index]);
    const id = stableId(raw);
    const previewJson = JSON.stringify(shownRows, null, 2);
    const preview =
      `[fewtok compacted json-array: ${rows.length} rows, ${shownRows.length} shown, id=${id}]\n` +
      `Fields: ${fieldsFor(rows)}\n` +
      `Shown: first 12, last 12, matched/error ${matched}\n` +
      `${previewJson}\n` +
      `Use ftRetrieve with id='${id}' and q='<specific query>' for omitted rows.`;

    const rawBytes = Buffer.byteLength(raw, "utf8");
    const previewBytes = Buffer.byteLength(preview, "utf8");
    if (previewBytes >= rawBytes) return passThrough(raw);

    return {
      preview,
      kbEntries: [{ section: `json:${id}`, content: raw }],
      bytesSaved: rawBytes - previewBytes,
      meta: { contentKind: Array.isArray(parsed) ? "json-array" : "json-object" },
    };
  },
};
