// ═══════════════════════════════════════════════════════════════════
// YOU WRITE THIS FILE. These four functions shape the bot's entire
// behavior. Defaults below are safe and intentionally conservative —
// redline to taste. Each function is ~5-10 lines.
// ═══════════════════════════════════════════════════════════════════

import type { CycleContext, CycleResult, Gap, Proposal } from "./types.ts";

// Should this cycle even fire right now?
// Called AFTER the pause-file check + cost-cap check have already passed.
// Use this for judgment calls (cooldown, shared-resource conflicts).
export function shouldRunCycle(ctx: CycleContext): { run: boolean; reason: string } {
  if (ctx.workingTreeDirty) {
    return { run: false, reason: "dirty working tree — commit or stash your work first" };
  }
  if (ctx.lastCycleAt) {
    const minsAgo = (Date.now() - new Date(ctx.lastCycleAt).getTime()) / 60000;
    if (minsAgo < 10) return { run: false, reason: `last cycle ${minsAgo.toFixed(1)}min ago (<10min cooldown)` };
  }
  if (ctx.autotuneBusy) {
    return { run: false, reason: "autotune agent is running — avoid Ollama contention" };
  }
  return { run: true, reason: "ready" };
}

// Which [bot-eligible] gap from the PRD does this cycle attack?
// Default: first in document order. Alternatives: random for exploration,
// smallest-context, tag-priority (e.g. [bot-eligible:high] wins).
export function pickGap(gaps: Gap[]): { gap: Gap | null; reason: string } {
  if (gaps.length === 0) return { gap: null, reason: "no [bot-eligible] gaps in PRD" };
  return { gap: gaps[0], reason: "first eligible gap in document order" };
}

// Cheap gate BEFORE burning test cycles. Reject if proposal smells wrong.
// This catches runaway cloud proposals before we apply+test them.
export function scoreProposal(p: Proposal): { accept: boolean; reason: string } {
  if (p.files.length === 0) return { accept: false, reason: "empty proposal" };
  if (p.files.length > 5) return { accept: false, reason: `>5 files (${p.files.length})` };
  if (p.estimated_loc > 200) return { accept: false, reason: `>200 LOC (${p.estimated_loc})` };

  // Paths the bot is NEVER allowed to touch. Extend cautiously.
  const forbidden = [
    ".git/", "secrets", "lakehouse.toml",
    "docs/ADR-", "docs/DECISIONS.md", "docs/PRD.md",
    "/etc/", "/root/", "Cargo.lock",
  ];
  for (const f of p.files) {
    const hit = forbidden.find(prefix => f.path.includes(prefix));
    if (hit) return { accept: false, reason: `touches forbidden path (${hit}): ${f.path}` };
  }
  return { accept: true, reason: "passes size + path filters" };
}

// Tests are green and changes applied. Do we actually open a PR?
// Mem0-aligned: the cycle already short-circuited with `cycle_noop` if
// every file was identical to current state, so by the time we're here
// at least one file was actually added or updated.
//
// Stricter options below — uncomment to require a test file touched, etc.
export function shouldOpenPR(c: CycleResult): { open: boolean; reason: string } {
  if (!c.testsGreen) return { open: false, reason: "tests red" };
  const realChanges = c.filesAdded.length + c.filesUpdated.length;
  if (realChanges === 0) return { open: false, reason: "nothing actually changed on disk" };

  // Stricter option — require at least one test file in the diff:
  // const hasTest = [...c.filesAdded, ...c.filesUpdated].some(f => /test|_tests/i.test(f));
  // if (!hasTest) return { open: false, reason: "no test file touched" };

  return { open: true, reason: `green + ${c.filesAdded.length} add / ${c.filesUpdated.length} update` };
}
