// src/layer/adapters/mcp-truncate-large.ts
//
// ToolResultAdapter that compresses large MCP tool outputs into a head+tail
// preview, while indexing the full content into the per-session KB.
//
// Match condition: toolName starts with "mcp__" AND byteLength(rawContent) >= 2048.
//
// Determinism guarantees:
//   - No Date.now(), no Math.random(), no Map/Set iteration.
//   - JSON key picking uses Object.keys(obj).sort() (alphabetical, V8/Bun stable).
//   - Regex constructed fresh per call (no /g flag with shared lastIndex state).
//   - Preview format bytes are EXACT and STABLE — cache-hash safe, matches
//     bash-truncate-large header strings for consistent kb-search.
//
// See ./types.ts for load-bearing invariants (Rule A: frozen bytes,
// Rule B: latest-turn only, determinism).

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

const HEAD_LINES = 20;
const TAIL_LINES = 20;
const BYTE_THRESHOLD = 2048;
const MCP_PREFIX = "mcp__";

/**
 * Derive a stable, deterministic KB query hint for MCP results.
 * Priority:
 *   1. Parse first non-empty line as JSON. If valid object, pick the first
 *      STRING-valued key (alphabetically sorted for determinism), truncated
 *      to 32 chars: "<key>:<value>".
 *   2. MCP tool name with "mcp__" prefix stripped (e.g. mcp__github__get_pr
 *      → github__get_pr).
 *   3. Fallback: first 8 chars of toolCallId.
 *
 * IMPORTANT: wrap JSON.parse in try/catch; treat failure as "no JSON object".
 * IMPORTANT: no /g flag — regexes created fresh each call, no lastIndex side effect.
 */
function deriveQueryHint(
  lines: string[],
  toolName: string,
  toolCallId: string
): string {
  // Strategy 1: try parsing first non-empty line as JSON object
  for (const line of lines) {
    const trimmed = line.trim();
    if (trimmed.length === 0) continue;
    try {
      const parsed: unknown = JSON.parse(trimmed);
      if (
        parsed !== null &&
        typeof parsed === "object" &&
        !Array.isArray(parsed)
      ) {
        const obj = parsed as Record<string, unknown>;
        const sortedKeys = Object.keys(obj).sort();
        for (const key of sortedKeys) {
          const val = obj[key];
          if (typeof val === "string") {
            const truncatedVal = val.slice(0, 32);
            return `${key}:${truncatedVal}`;
          }
        }
      }
    } catch {
      // JSON parse failed — fall through
    }
    // Only check the first non-empty line
    break;
  }

  // Strategy 2: strip mcp__ prefix from tool name
  if (typeof toolName === "string" && toolName.startsWith(MCP_PREFIX)) {
    const stripped = toolName.slice(MCP_PREFIX.length);
    if (stripped.length > 0) {
      return stripped;
    }
  }

  // Strategy 3: fallback to toolCallId prefix
  return toolCallId.slice(0, 8);
}

export const mcpTruncateLarge: ToolResultAdapter = {
  id: "mcp-truncate-large",

  match(r: ToolResult): boolean {
    if (typeof r.toolName !== "string") return false;
    if (!r.toolName.startsWith(MCP_PREFIX)) return false;
    const raw: string =
      typeof r.content === "string" ? r.content : JSON.stringify(r.content);
    return Buffer.byteLength(raw, "utf8") >= BYTE_THRESHOLD;
  },

  compress(r: ToolResult): CompressedResult {
    const rawContent: string =
      typeof r.content === "string" ? r.content : JSON.stringify(r.content);

    const lines = rawContent.split("\n");
    const totalLines = lines.length;
    const truncatedLines = Math.max(0, totalLines - (HEAD_LINES + TAIL_LINES));

    // Defensive: if content fits within head+tail window, pass through unchanged.
    if (totalLines <= HEAD_LINES + TAIL_LINES) {
      return {
        preview: rawContent,
        kbEntries: [],
        bytesSaved: 0,
      };
    }

    const head = lines.slice(0, HEAD_LINES);
    const tail = lines.slice(totalLines - TAIL_LINES);

    const suggestedQ = deriveQueryHint(lines, r.toolName ?? "", r.toolCallId);

    // STABLE preview format — EXACT byte strings, no clocks, no random.
    // Header strings are byte-identical to bash-truncate-large for kb-search compatibility.
    const preview =
      `--- preview (head ${HEAD_LINES} of ${totalLines} lines) ---\n` +
      head.join("\n") +
      `\n--- ...(${truncatedLines} lines truncated)... ---\n` +
      `--- preview (tail ${TAIL_LINES} of ${totalLines} lines) ---\n` +
      tail.join("\n") +
      `\n--- end ---\n` +
      `Full content indexed. Use the kb tool with q='${suggestedQ}' to retrieve.`;

    return {
      preview,
      kbEntries: [{ section: "full-output", content: rawContent }],
      bytesSaved:
        Buffer.byteLength(rawContent, "utf8") -
        Buffer.byteLength(preview, "utf8"),
    };
  },
};
