import { existsSync, readFileSync } from "node:fs";
import { z } from "zod";
import type { Adapter } from "../adapter";
import type { AdapterResult, Item, Panel } from "../schema";

const DEFAULT_INTERVAL_MS = 2 * 60_000;

const GapRecordSchema = z.object({
  ts: z.string().min(1),
  sandboxId: z.string().min(1),
  image: z.string().min(1),
  tool: z.string().min(1),
  cwd: z.string().optional(),
});

const GapPanelDataSchema = z.object({
  image: z.string(),
  gapCount: z.number().int().nonnegative(),
  gaps: z.array(
    z.object({
      tool: z.string(),
      hits: z.number().int().positive(),
      sandboxes: z.array(z.string()),
      firstSeen: z.string(),
      lastSeen: z.string(),
    }),
  ),
});

export interface SandboxToolgapAdapterOptions {
  id?: string;
  interval?: number;
  /** JSONL of gap records pulled back from every sandbox host. */
  gapsPath: string;
  /** Image tag the sandboxes currently run; gaps from older images are already answered. */
  imageTagPath: string;
  readFileImpl?: (path: string) => string;
  existsImpl?: (path: string) => boolean;
}

interface Gap {
  tool: string;
  hits: number;
  sandboxes: Set<string>;
  firstSeen: string;
  lastSeen: string;
}

export function aggregateGaps(lines: string[], image: string): Gap[] {
  const byTool = new Map<string, Gap>();
  for (const line of lines) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    let parsed: unknown;
    try {
      parsed = JSON.parse(trimmed);
    } catch {
      continue;
    }
    const record = GapRecordSchema.safeParse(parsed);
    if (!record.success) continue;
    if (record.data.image !== image) continue;
    const existing = byTool.get(record.data.tool);
    if (existing) {
      existing.hits += 1;
      existing.sandboxes.add(record.data.sandboxId);
      if (record.data.ts < existing.firstSeen) existing.firstSeen = record.data.ts;
      if (record.data.ts > existing.lastSeen) existing.lastSeen = record.data.ts;
      continue;
    }
    byTool.set(record.data.tool, {
      tool: record.data.tool,
      hits: 1,
      sandboxes: new Set([record.data.sandboxId]),
      firstSeen: record.data.ts,
      lastSeen: record.data.ts,
    });
  }
  return [...byTool.values()].sort((a, b) => b.hits - a.hits || a.tool.localeCompare(b.tool));
}

/**
 * Surfaces tools the agent sandbox image does not carry. Identity is the tool name,
 * so a gap raises one item however many times it is hit; rebuilding the image with
 * the tool retires every gap recorded against the previous tag.
 */
export function createSandboxToolgapAdapter(options: SandboxToolgapAdapterOptions): Adapter {
  const id = options.id ?? "sandbox-toolgap";
  const readFileImpl = options.readFileImpl ?? ((path: string) => readFileSync(path, "utf8"));
  const existsImpl = options.existsImpl ?? existsSync;

  return {
    id,
    interval: options.interval ?? DEFAULT_INTERVAL_MS,
    async poll(): Promise<AdapterResult> {
      if (!existsImpl(options.imageTagPath)) {
        return { items: [], panels: [] };
      }
      const image = readFileImpl(options.imageTagPath).trim();
      if (!image) return { items: [], panels: [] };

      const lines = existsImpl(options.gapsPath) ? readFileImpl(options.gapsPath).split("\n") : [];
      const gaps = aggregateGaps(lines, image);

      const items: Item[] = gaps.map((gap) => ({
        id: `${id}:${gap.tool}`,
        source: id,
        severity: "warn",
        kind: "alert",
        title: `sandbox image is missing ${gap.tool}`,
        detail:
          `${gap.hits} call(s) in ${[...gap.sandboxes].sort().join(", ")} hit "${gap.tool}: command not found" ` +
          `inside ${image}. Add it to modules/sandbox/image/Containerfile and re-run sandbox-provision --rebuild.`,
        ts: gap.lastSeen,
        actions: [],
      }));

      const panelData: z.infer<typeof GapPanelDataSchema> = {
        image,
        gapCount: gaps.length,
        gaps: gaps.map((gap) => ({
          tool: gap.tool,
          hits: gap.hits,
          sandboxes: [...gap.sandboxes].sort(),
          firstSeen: gap.firstSeen,
          lastSeen: gap.lastSeen,
        })),
      };
      const panels: Panel[] = [
        { id, ts: new Date().toISOString(), data: GapPanelDataSchema.parse(panelData) },
      ];

      return { items, panels };
    },
  };
}
