// Bot-local knowledge base. Every finished cycle already persists a
// CycleResult to data/_bot/cycles/{id}.json — that IS the outcome log.
// KB here just reads that dir, filters to prior cycles on the same gap,
// and produces a short summary the cloud model can condition on.
//
// No separate jsonl, no new write path, no embedding calls. The bot's
// "memory" is the same primary artifact that the observer consumes.
//
// Future: embedding-based neighbor matching across gaps (cheap once
// sidecar is local), cross-pollination with scenario KB's
// pathway_recommendations. Not required for the feedback loop to work
// on a single gap — that's the floor we're building first.

import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import type { CycleResult } from "./types.ts";

const CYCLES_DIR = "/home/profit/lakehouse/data/_bot/cycles";

export interface HistoryEntry {
  cycle_id: string;
  ended_at: string;
  outcome: string;
  reason: string;
  pr_url: string | null;
  tests_green: boolean | null;
  files_added: string[];
  files_updated: string[];
  tokens_used: number;
}

export async function loadHistory(gap_id: string, max: number = 5): Promise<HistoryEntry[]> {
  let entries: string[] = [];
  try {
    entries = await readdir(CYCLES_DIR);
  } catch {
    return [];
  }
  const matches: HistoryEntry[] = [];
  for (const e of entries) {
    if (!e.endsWith(".json")) continue;
    try {
      const raw = await readFile(join(CYCLES_DIR, e), "utf8");
      const r = JSON.parse(raw) as CycleResult;
      if (r.gap?.id !== gap_id) continue;
      matches.push({
        cycle_id: r.cycle_id,
        ended_at: r.ended_at,
        outcome: r.outcome,
        reason: r.reason,
        pr_url: r.prUrl,
        tests_green: r.testsGreen,
        files_added: r.filesAdded ?? [],
        files_updated: r.filesUpdated ?? [],
        tokens_used: r.tokens_used,
      });
    } catch {
      // Skip unreadable / malformed cycle files. Don't fail the current
      // cycle because an old one is corrupt.
    }
  }
  matches.sort((a, b) => b.ended_at.localeCompare(a.ended_at));
  return matches.slice(0, max);
}

// Compact prompt-ready summary. Empty string when there's no history —
// caller can skip the "prior attempts" block entirely.
export function summarizeHistory(h: HistoryEntry[]): string {
  if (h.length === 0) return "";
  const lines = h.map(e => {
    const when = e.ended_at.slice(0, 16).replace("T", " ");
    const files = [...e.files_added, ...e.files_updated];
    const filesStr = files.length > 0 ? ` touched: ${files.join(", ")}` : "";
    const prStr = e.pr_url ? ` PR: ${e.pr_url}` : "";
    return `- ${when} UTC — ${e.outcome}${prStr}${filesStr}\n    reason: ${e.reason}`;
  });
  return [
    `Prior attempts on this gap (${h.length} most recent):`,
    ...lines,
    "",
    "Learn from these: build on what worked, avoid paths that failed.",
  ].join("\n");
}

// Aggregate stats for telemetry — lets the bot expose "% of cycles on
// this gap that landed a PR" without re-parsing the raw history.
export function statsFor(h: HistoryEntry[]): {
  attempts: number;
  pr_opened: number;
  tests_failed: number;
  proposal_rejected: number;
  noop: number;
} {
  return {
    attempts: h.length,
    pr_opened: h.filter(e => e.outcome === "ok").length,
    tests_failed: h.filter(e => e.outcome === "tests_failed").length,
    proposal_rejected: h.filter(e => e.outcome === "proposal_rejected").length,
    noop: h.filter(e => e.outcome === "cycle_noop").length,
  };
}
