// src/layer/ToolResultAdapterLayer.ts
//
// Pipeline layer that processes tool_results from the LATEST user turn via
// the AdapterRegistry, freezes the produced bytes per toolCallId (cache safety
// rule A), and emits per-adapter SavingEvents.
//
// Rule A — frozen bytes: once a toolCallId has been compressed and sent
// upstream in this session, every later request MUST emit byte-identical
// content for the same toolCallId. Implemented via the per-session
// `frozenBytes` Map passed in by the handler (handler owns its lifetime).
// Rule B — latest-turn only: uses provider.extractLastTurnToolResults().
// Determinism: NO Date.now(), NO Math.random(); registry.dispatch() is
// deterministic by adapter contract.

import type { Layer, SavingEvent } from "./types.ts";
import type { Provider } from "../provider/types.ts";
import type { AdapterRegistry } from "./adapters/index.ts";
import type { KbStore } from "../kb/KbStore.ts";
import { decideToolResultLiveZone } from "./liveZone.ts";

export interface ToolResultAdapterLayerOpts {
  readonly sessionId: string;
  readonly provider: Provider;
  readonly store: KbStore;
  readonly registry: AdapterRegistry;
  readonly frozenBytes: Map<string, string>;
}

export class ToolResultAdapterLayer implements Layer {
  readonly id = "tool-result-adapter";
  private readonly opts: ToolResultAdapterLayerOpts;
  private readonly events: SavingEvent[] = [];

  constructor(opts: ToolResultAdapterLayerOpts) {
    this.opts = opts;
  }

  init(): void {}

  outbound(body: unknown): unknown {
    const { provider, registry, store, frozenBytes } = this.opts;
    let mutated: unknown = body;
    let parsed;
    try {
      parsed = provider.parseRequest(mutated, { kind: "messages", isStreaming: false });
    } catch {
      return mutated;
    }

    const results = provider.extractLastTurnToolResults(parsed);

    for (const r of results) {
      if (!r.toolCallId) continue;

      // Index raw content into KB before compression.
      const rawStr = typeof r.content === "string" ? r.content : JSON.stringify(r.content);
      const labelSnap = r.toolName ?? r.toolCallId;
      const sessionIdSnap = this.opts.sessionId;
      setImmediate(() => {
        try { store.insert(labelSnap, sessionIdSnap, rawStr); } catch { /* swallow */ }
      });

      const decision = decideToolResultLiveZone({
        providerSupportsTools: true,
        isLatestTurn: true,
        toolCallId: r.toolCallId,
        frozenBytes,
      });

      // Rule A: cached compressed bytes win
      const frozen = frozenBytes.get(r.toolCallId);
      if (!decision.mutable && frozen !== undefined) {
        try {
          mutated = provider.rewriteToolResult(mutated, r.toolCallId, frozen);
        } catch { /* leave raw */ }
        continue;
      }
      if (!decision.mutable) continue;

      let dispatched;
      try {
        dispatched = registry.dispatch({ ...r, content: rawStr });
      } catch {
        continue;
      }

      const preview = dispatched.compressed.preview;
      const rawBytes = Buffer.byteLength(dispatched.raw, "utf8");
      const sentBytes = Buffer.byteLength(preview, "utf8");

      const kbEntriesSnap = dispatched.compressed.kbEntries;
      const sidSnap = this.opts.sessionId;
      setImmediate(() => {
        for (const entry of kbEntriesSnap) {
          try { store.insert(entry.section, sidSnap, entry.content); } catch { /* best-effort */ }
        }
      });

      try {
        mutated = provider.rewriteToolResult(mutated, r.toolCallId, preview);
      } catch {
        continue;
      }
      frozenBytes.set(r.toolCallId, preview);

      this.events.push({
        layerId: this.id,
        kind: "adapter",
        rawBytes,
        sentBytes,
        meta: {
          adapterId: dispatched.adapterId,
          toolCallId: r.toolCallId,
          bytesSaved: rawBytes - sentBytes,
          ...dispatched.compressed.meta,
          liveZoneReason: decision.reason,
          rawBytes,
          sentBytes,
        },
      });
    }

    return mutated;
  }

  inbound(body: unknown): unknown {
    return body;
  }

  observe(): SavingEvent[] {
    const out = this.events.slice();
    this.events.length = 0;
    return out;
  }

  dispose(): void {
    /* store + frozenBytes lifetimes owned by handler */
  }
}
