import { createHash, randomUUID } from "node:crypto";
import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, join, resolve } from "node:path";
import type { GoalControllerDecision } from "./goal-controller.js";

export type GoalRunStatus =
  | "draft"
  | "blocked"
  | "ready"
  | "running"
  | "verifying"
  | "passed"
  | "failed"
  | "paused";

export type GoalTaskStatus = "pending" | "running" | "verifying" | "done" | "failed" | "blocked";

export type GoalPrerequisiteStatus = "unknown" | "met" | "missing";

export type GoalEvidenceKind = "log" | "command" | "screenshot" | "file" | "summary";

export type GoalVerificationStatus = "pass" | "fail" | "unknown";

export interface GoalVerificationResult {
  status: GoalVerificationStatus;
  summary: string;
  command?: string;
  exitCode?: number;
  outputPath?: string;
  checkedAt: string;
}

export interface GoalPrerequisite {
  id: string;
  label: string;
  status: GoalPrerequisiteStatus;
  checkCommand?: string;
  instructions?: string;
  evidence?: string;
}

export interface GoalHarnessItem {
  id: string;
  label: string;
  command?: string;
  path?: string;
  description?: string;
}

export type GoalEvidenceMechanism =
  | "command"
  | "test"
  | "script"
  | "fixture"
  | "log"
  | "screenshot"
  | "video"
  | "browser"
  | "device"
  | "source"
  | "manual";

export interface GoalEvidencePlan {
  id: string;
  label: string;
  mechanism: GoalEvidenceMechanism;
  description: string;
  status: "planned" | "ready" | "blocked";
  command?: string;
  path?: string;
  instructions?: string;
  evidence?: string;
}

export interface GoalTask {
  id: string;
  title: string;
  prompt: string;
  status: GoalTaskStatus;
  workerId?: string;
  attempts: number;
  verification?: GoalVerificationResult;
  lastSummary?: string;
}

export interface GoalEvidence {
  id: string;
  kind: GoalEvidenceKind;
  label: string;
  path?: string;
  content?: string;
  createdAt: string;
}

export interface GoalVerifier {
  description: string;
  command?: string;
  lastResult?: GoalVerificationResult;
}

export interface GoalRun {
  id: string;
  title: string;
  goal: string;
  status: GoalRunStatus;
  createdAt: string;
  updatedAt: string;
  projectPath: string;
  successCriteria: string[];
  prerequisites: GoalPrerequisite[];
  harness: GoalHarnessItem[];
  evidencePlan: GoalEvidencePlan[];
  tasks: GoalTask[];
  evidence: GoalEvidence[];
  verifier?: GoalVerifier;
  blockers: string[];
  activeWorkerId?: string;
  continueRequestedAt?: string;
}

export interface GoalCounts {
  total: number;
  active: number;
  blocked: number;
  pending: number;
  running: number;
  passed: number;
  failed: number;
}

export interface ReconcileActiveGoalRunsOptions {
  isWorkerActive?: (workerId: string, run: GoalRun) => boolean;
  isVerifierActive?: (run: GoalRun) => boolean;
}

export interface GoalReconciliationResult {
  runs: GoalRun[];
  repairedRunIds: string[];
  evidenceCount: number;
}

export interface GoalRunInput {
  id?: string;
  title: string;
  goal: string;
  status?: GoalRunStatus;
  successCriteria?: string[];
  prerequisites?: GoalPrerequisite[];
  harness?: GoalHarnessItem[];
  evidencePlan?: GoalEvidencePlan[];
  tasks?: GoalTask[];
  evidence?: GoalEvidence[];
  verifier?: GoalVerifier;
  blockers?: string[];
  activeWorkerId?: string;
  continueRequestedAt?: string;
}

export interface GoalTaskInput {
  id?: string;
  title: string;
  prompt: string;
  status?: GoalTaskStatus;
  workerId?: string;
  attempts?: number;
  verification?: GoalVerificationResult;
  lastSummary?: string;
}

export interface GoalEvidenceInput {
  id?: string;
  kind: GoalEvidenceKind;
  label: string;
  path?: string;
  content?: string;
  createdAt?: string;
}

const GOALS_BASE_ENV = "GG_GOALS_BASE";
const DEFAULT_PROJECT_DIR_NAME = "projects";

let writeQueue: Promise<void> = Promise.resolve();

function goalsBaseDir(): string {
  return process.env[GOALS_BASE_ENV] ?? join(homedir(), ".gg", "goals", DEFAULT_PROJECT_DIR_NAME);
}

export function normalizeProjectPath(cwd: string): string {
  return resolve(cwd);
}

function nowIso(): string {
  return new Date().toISOString();
}

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function stringArray(value: unknown): string[] {
  return Array.isArray(value)
    ? value.filter((item): item is string => typeof item === "string")
    : [];
}

function optionalString(value: unknown): string | undefined {
  return typeof value === "string" && value.length > 0 ? value : undefined;
}

function isRunStatus(value: unknown): value is GoalRunStatus {
  return (
    value === "draft" ||
    value === "blocked" ||
    value === "ready" ||
    value === "running" ||
    value === "verifying" ||
    value === "passed" ||
    value === "failed" ||
    value === "paused"
  );
}

function isTaskStatus(value: unknown): value is GoalTaskStatus {
  return (
    value === "pending" ||
    value === "running" ||
    value === "verifying" ||
    value === "done" ||
    value === "failed" ||
    value === "blocked"
  );
}

function isPrerequisiteStatus(value: unknown): value is GoalPrerequisiteStatus {
  return value === "unknown" || value === "met" || value === "missing";
}

function isEvidenceKind(value: unknown): value is GoalEvidenceKind {
  return (
    value === "log" ||
    value === "command" ||
    value === "screenshot" ||
    value === "file" ||
    value === "summary"
  );
}

function isEvidenceMechanism(value: unknown): value is GoalEvidenceMechanism {
  return (
    value === "command" ||
    value === "test" ||
    value === "script" ||
    value === "fixture" ||
    value === "log" ||
    value === "screenshot" ||
    value === "video" ||
    value === "browser" ||
    value === "device" ||
    value === "source" ||
    value === "manual"
  );
}

function isEvidencePlanStatus(value: unknown): value is GoalEvidencePlan["status"] {
  return value === "planned" || value === "ready" || value === "blocked";
}

function isVerificationStatus(value: unknown): value is GoalVerificationStatus {
  return value === "pass" || value === "fail" || value === "unknown";
}

function normalizeVerification(value: unknown): GoalVerificationResult | undefined {
  if (!isObject(value)) return undefined;
  return {
    status: isVerificationStatus(value.status) ? value.status : "unknown",
    summary: typeof value.summary === "string" ? value.summary : "",
    ...(optionalString(value.command) ? { command: optionalString(value.command) } : {}),
    ...(typeof value.exitCode === "number" ? { exitCode: value.exitCode } : {}),
    ...(optionalString(value.outputPath) ? { outputPath: optionalString(value.outputPath) } : {}),
    checkedAt: typeof value.checkedAt === "string" ? value.checkedAt : nowIso(),
  };
}

function normalizePrerequisite(value: unknown): GoalPrerequisite | null {
  if (!isObject(value)) return null;
  const label = typeof value.label === "string" ? value.label : "Prerequisite";
  return {
    id: typeof value.id === "string" ? value.id : randomUUID(),
    label,
    status: isPrerequisiteStatus(value.status) ? value.status : "unknown",
    ...(optionalString(value.checkCommand)
      ? { checkCommand: optionalString(value.checkCommand) }
      : {}),
    ...(optionalString(value.instructions)
      ? { instructions: optionalString(value.instructions) }
      : {}),
    ...(optionalString(value.evidence) ? { evidence: optionalString(value.evidence) } : {}),
  };
}

function normalizeHarnessItem(value: unknown): GoalHarnessItem | null {
  if (!isObject(value)) return null;
  const label = typeof value.label === "string" ? value.label : "Harness";
  return {
    id: typeof value.id === "string" ? value.id : randomUUID(),
    label,
    ...(optionalString(value.command) ? { command: optionalString(value.command) } : {}),
    ...(optionalString(value.path) ? { path: optionalString(value.path) } : {}),
    ...(optionalString(value.description)
      ? { description: optionalString(value.description) }
      : {}),
  };
}

function normalizeEvidencePlanItem(value: unknown): GoalEvidencePlan | null {
  if (!isObject(value)) return null;
  const label = typeof value.label === "string" ? value.label : "Evidence path";
  const description = typeof value.description === "string" ? value.description : label;
  return {
    id: typeof value.id === "string" ? value.id : randomUUID(),
    label,
    mechanism: isEvidenceMechanism(value.mechanism) ? value.mechanism : "command",
    description,
    status: isEvidencePlanStatus(value.status) ? value.status : "planned",
    ...(optionalString(value.command) ? { command: optionalString(value.command) } : {}),
    ...(optionalString(value.path) ? { path: optionalString(value.path) } : {}),
    ...(optionalString(value.instructions)
      ? { instructions: optionalString(value.instructions) }
      : {}),
    ...(optionalString(value.evidence) ? { evidence: optionalString(value.evidence) } : {}),
  };
}

function normalizeTask(value: unknown): GoalTask | null {
  if (!isObject(value)) return null;
  const title = typeof value.title === "string" ? value.title : "Goal task";
  const prompt = typeof value.prompt === "string" ? value.prompt : title;
  return {
    id: typeof value.id === "string" ? value.id : randomUUID(),
    title,
    prompt,
    status: isTaskStatus(value.status) ? value.status : "pending",
    ...(optionalString(value.workerId) ? { workerId: optionalString(value.workerId) } : {}),
    attempts: typeof value.attempts === "number" && value.attempts >= 0 ? value.attempts : 0,
    ...(normalizeVerification(value.verification)
      ? { verification: normalizeVerification(value.verification) }
      : {}),
    ...(optionalString(value.lastSummary)
      ? { lastSummary: optionalString(value.lastSummary) }
      : {}),
  };
}

function normalizeEvidence(value: unknown): GoalEvidence | null {
  if (!isObject(value)) return null;
  const label = typeof value.label === "string" ? value.label : "Evidence";
  return {
    id: typeof value.id === "string" ? value.id : randomUUID(),
    kind: isEvidenceKind(value.kind) ? value.kind : "summary",
    label,
    ...(optionalString(value.path) ? { path: optionalString(value.path) } : {}),
    ...(optionalString(value.content) ? { content: optionalString(value.content) } : {}),
    createdAt: typeof value.createdAt === "string" ? value.createdAt : nowIso(),
  };
}

function normalizeVerifier(value: unknown): GoalVerifier | undefined {
  if (!isObject(value)) return undefined;
  const description = typeof value.description === "string" ? value.description : "Goal verifier";
  return {
    description,
    ...(optionalString(value.command) ? { command: optionalString(value.command) } : {}),
    ...(normalizeVerification(value.lastResult)
      ? { lastResult: normalizeVerification(value.lastResult) }
      : {}),
  };
}

function normalizeRun(value: unknown, fallbackProjectPath: string): GoalRun | null {
  if (!isObject(value)) return null;
  const title = typeof value.title === "string" ? value.title : "Untitled goal";
  const goal = typeof value.goal === "string" ? value.goal : title;
  const createdAt = typeof value.createdAt === "string" ? value.createdAt : nowIso();
  const projectPath =
    typeof value.projectPath === "string" ? value.projectPath : fallbackProjectPath;
  const prerequisites = Array.isArray(value.prerequisites)
    ? value.prerequisites
        .map(normalizePrerequisite)
        .filter((item): item is GoalPrerequisite => !!item)
    : [];
  const tasks = Array.isArray(value.tasks)
    ? value.tasks.map(normalizeTask).filter((item): item is GoalTask => !!item)
    : [];
  const computedStatus = deriveRunnableStatus(
    isRunStatus(value.status) ? value.status : "draft",
    prerequisites,
  );

  return {
    id: typeof value.id === "string" ? value.id : randomUUID(),
    title,
    goal,
    status: computedStatus,
    createdAt,
    updatedAt: typeof value.updatedAt === "string" ? value.updatedAt : createdAt,
    projectPath,
    successCriteria: stringArray(value.successCriteria),
    prerequisites,
    harness: Array.isArray(value.harness)
      ? value.harness.map(normalizeHarnessItem).filter((item): item is GoalHarnessItem => !!item)
      : [],
    evidencePlan: Array.isArray(value.evidencePlan)
      ? value.evidencePlan
          .map(normalizeEvidencePlanItem)
          .filter((item): item is GoalEvidencePlan => !!item)
      : [],
    tasks,
    evidence: Array.isArray(value.evidence)
      ? value.evidence.map(normalizeEvidence).filter((item): item is GoalEvidence => !!item)
      : [],
    ...(normalizeVerifier(value.verifier) ? { verifier: normalizeVerifier(value.verifier) } : {}),
    blockers: stringArray(value.blockers),
    ...(optionalString(value.activeWorkerId)
      ? { activeWorkerId: optionalString(value.activeWorkerId) }
      : {}),
    ...(optionalString(value.continueRequestedAt)
      ? { continueRequestedAt: optionalString(value.continueRequestedAt) }
      : {}),
  };
}

function sortNewestFirst(runs: GoalRun[]): GoalRun[] {
  return [...runs].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
}

function isActiveGoalRun(run: GoalRun): boolean {
  return (
    run.status === "running" ||
    run.status === "verifying" ||
    run.activeWorkerId !== undefined ||
    run.tasks.some((task) => task.status === "running" || task.status === "verifying")
  );
}

function wouldEraseActiveGoalRuns(
  previousRuns: readonly GoalRun[],
  nextRuns: readonly GoalRun[],
): boolean {
  if (nextRuns.length > 0) return false;
  return previousRuns.some(isActiveGoalRun);
}

function deriveRunnableStatus(
  requestedStatus: GoalRunStatus,
  prerequisites: readonly GoalPrerequisite[],
): GoalRunStatus {
  if (
    requestedStatus === "passed" ||
    requestedStatus === "failed" ||
    requestedStatus === "paused"
  ) {
    return requestedStatus;
  }
  if (hasBlockingGoalPrerequisites(prerequisites)) return "blocked";
  return requestedStatus;
}

function enqueueWrite<T>(fn: () => Promise<T>): Promise<T> {
  const result = writeQueue.then(fn);
  writeQueue = result.then(
    () => {},
    () => {},
  );
  return result;
}

async function writeGoalRunsFile(cwd: string, runs: readonly GoalRun[]): Promise<void> {
  const normalizedCwd = normalizeProjectPath(cwd);
  const dir = projectDir(normalizedCwd);
  await mkdir(dir, { recursive: true });
  const goalsPath = join(dir, "goals.json");
  const existingRuns = await readGoalRunsFile(normalizedCwd);
  if (wouldEraseActiveGoalRuns(existingRuns, runs)) {
    const timestamp = nowIso();
    const repairedRuns = existingRuns.map((run) => ({
      ...run,
      evidence: [
        ...run.evidence,
        createGoalEvidence({
          kind: "summary",
          label: "Goal store write rejected",
          content:
            "Rejected an attempted empty Goal overwrite while active work was present; preserving existing durable state.",
          createdAt: timestamp,
        }),
      ],
      updatedAt: timestamp,
    }));
    await atomicWriteJson(goalsPath, sortNewestFirst(repairedRuns));
    await Promise.all(
      repairedRuns.map((run) => writeGoalProgressJournalFromRun(normalizedCwd, run)),
    );
    return;
  }
  const sorted = sortNewestFirst([...runs]);
  await atomicWriteJson(goalsPath, sorted);
  await atomicWriteJson(join(dir, "meta.json"), {
    path: normalizedCwd,
    name: basename(normalizedCwd),
  });
  await Promise.all(sorted.map((run) => writeGoalProgressJournalFromRun(normalizedCwd, run)));
}

async function atomicWriteJson(path: string, value: unknown): Promise<void> {
  const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
  await writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf-8");
  await rename(tmpPath, path);
}

async function readGoalRunsFile(cwd: string): Promise<GoalRun[]> {
  const normalizedCwd = normalizeProjectPath(cwd);
  try {
    const data = await readFile(join(projectDir(normalizedCwd), "goals.json"), "utf-8");
    const parsed: unknown = JSON.parse(data);
    if (!Array.isArray(parsed)) return [];
    return sortNewestFirst(
      parsed
        .map((item) => normalizeRun(item, normalizedCwd))
        .filter((run): run is GoalRun => run !== null),
    );
  } catch {
    return [];
  }
}

export function hashPath(cwd: string): string {
  return createHash("sha256").update(normalizeProjectPath(cwd)).digest("hex").slice(0, 16);
}

export function projectDir(cwd: string): string {
  return join(goalsBaseDir(), hashPath(cwd));
}

async function discoverGoalRunsById(id: string): Promise<GoalRun | null> {
  try {
    const entries = await readdir(goalsBaseDir(), { withFileTypes: true });
    const matches: GoalRun[] = [];
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      try {
        const dir = join(goalsBaseDir(), entry.name);
        const meta = await readProjectMeta(dir);
        const fallbackProjectPath = meta?.path ?? dir;
        const data = await readFile(join(dir, "goals.json"), "utf-8");
        const parsed: unknown = JSON.parse(data);
        if (!Array.isArray(parsed)) continue;
        for (const item of parsed) {
          const run = normalizeRun(item, fallbackProjectPath);
          if (run && (run.id === id || run.id.startsWith(id))) matches.push(run);
        }
      } catch {
        continue;
      }
    }
    return sortNewestFirst(matches)[0] ?? null;
  } catch {
    return null;
  }
}

async function readProjectMeta(dir: string): Promise<{ path?: string } | null> {
  try {
    const parsed: unknown = JSON.parse(await readFile(join(dir, "meta.json"), "utf-8"));
    return isObject(parsed) && typeof parsed.path === "string" ? { path: parsed.path } : null;
  } catch {
    return null;
  }
}

export function createGoalTask(input: GoalTaskInput): GoalTask {
  return {
    id: input.id ?? randomUUID(),
    title: input.title,
    prompt: input.prompt,
    status: input.status ?? "pending",
    ...(input.workerId ? { workerId: input.workerId } : {}),
    attempts: input.attempts ?? 0,
    ...(input.verification ? { verification: input.verification } : {}),
    ...(input.lastSummary ? { lastSummary: input.lastSummary } : {}),
  };
}

export function createGoalEvidence(input: GoalEvidenceInput): GoalEvidence {
  return {
    id: input.id ?? randomUUID(),
    kind: input.kind,
    label: input.label,
    ...(input.path ? { path: input.path } : {}),
    ...(input.content ? { content: input.content } : {}),
    createdAt: input.createdAt ?? nowIso(),
  };
}

export function createGoalRun(cwd: string, input: GoalRunInput): GoalRun {
  const normalizedCwd = normalizeProjectPath(cwd);
  const timestamp = nowIso();
  const prerequisites = input.prerequisites ?? [];
  const status = deriveRunnableStatus(input.status ?? "draft", prerequisites);
  return {
    id: input.id ?? randomUUID(),
    title: input.title,
    goal: input.goal,
    status,
    createdAt: timestamp,
    updatedAt: timestamp,
    projectPath: normalizedCwd,
    successCriteria: input.successCriteria ?? [],
    prerequisites,
    harness: input.harness ?? [],
    evidencePlan: input.evidencePlan ?? [],
    tasks: input.tasks ?? [],
    evidence: input.evidence ?? [],
    ...(input.verifier ? { verifier: input.verifier } : {}),
    blockers: input.blockers ?? [],
    ...(input.activeWorkerId ? { activeWorkerId: input.activeWorkerId } : {}),
    ...(input.continueRequestedAt ? { continueRequestedAt: input.continueRequestedAt } : {}),
  };
}

export async function loadGoalRuns(cwd: string): Promise<GoalRun[]> {
  return readGoalRunsFile(cwd);
}

export async function saveGoalRuns(cwd: string, runs: readonly GoalRun[]): Promise<void> {
  return enqueueWrite(() => writeGoalRunsFile(cwd, runs));
}

export async function reconcileActiveGoalRuns(
  cwd: string,
  options: ReconcileActiveGoalRunsOptions = {},
): Promise<GoalReconciliationResult> {
  return enqueueWrite(async () => {
    const runs = await loadGoalRuns(cwd);
    const timestamp = nowIso();
    const repairedRunIds: string[] = [];
    let evidenceCount = 0;
    const nextRuns = runs.map((run): GoalRun => {
      let next = run;
      const evidence: GoalEvidence[] = [];
      const blockers = new Set(run.blockers);
      let repaired = false;

      if (run.activeWorkerId && !options.isWorkerActive?.(run.activeWorkerId, run)) {
        const workerId = run.activeWorkerId;
        next = { ...next, activeWorkerId: undefined };
        evidence.push(
          createGoalEvidence({
            kind: "summary",
            label: "Goal worker reconciled",
            content: `Cleared stale activeWorkerId ${workerId}; no in-memory Goal worker was observed after startup/runtime reconciliation.`,
            createdAt: timestamp,
          }),
        );
        repaired = true;
      }

      const tasks = next.tasks.map((task) => {
        if (task.status !== "running" && task.status !== "verifying") return task;
        const workerId = task.workerId;
        if (workerId && options.isWorkerActive?.(workerId, run)) return task;
        repaired = true;
        evidence.push(
          createGoalEvidence({
            kind: "summary",
            label: "Goal task reconciled",
            content: `Reset stale task "${task.title}" (${task.id}) from ${task.status} to pending; no in-memory worker/verifier was observed.`,
            createdAt: timestamp,
          }),
        );
        return {
          ...task,
          status: "pending" as const,
          lastSummary: `Reset from stale ${task.status} state during Goal reconciliation.`,
        };
      });
      next = { ...next, tasks };

      if (next.status === "running") {
        const hasRunningWorker =
          next.activeWorkerId !== undefined &&
          options.isWorkerActive?.(next.activeWorkerId, run) === true;
        const hasActiveTask = next.tasks.some(
          (task) =>
            (task.status === "running" || task.status === "verifying") &&
            task.workerId !== undefined &&
            options.isWorkerActive?.(task.workerId, run) === true,
        );
        if (!hasRunningWorker && !hasActiveTask) {
          next = { ...next, status: "ready" };
          repaired = true;
        }
      } else if (next.status === "verifying" && !options.isVerifierActive?.(next)) {
        const blocker = "Verifier was interrupted; rerun or continue the Goal to verify again.";
        blockers.add(blocker);
        evidence.push(
          createGoalEvidence({
            kind: "summary",
            label: "Goal verifier reconciled",
            content:
              "Reset stale verifying state to ready because no in-memory verifier process was observed.",
            createdAt: timestamp,
          }),
        );
        next = { ...next, status: "ready" };
        repaired = true;
      }

      if (!repaired) return run;
      repairedRunIds.push(run.id);
      evidenceCount += evidence.length;
      return {
        ...next,
        evidence: [...next.evidence, ...evidence],
        blockers: [...blockers],
        updatedAt: timestamp,
      };
    });

    if (repairedRunIds.length > 0) await writeGoalRunsFile(cwd, nextRuns);
    return { runs: sortNewestFirst(nextRuns), repairedRunIds, evidenceCount };
  });
}

export async function upsertGoalRun(cwd: string, input: GoalRun | GoalRunInput): Promise<GoalRun> {
  return enqueueWrite(async () => {
    const runs = await loadGoalRuns(cwd);
    const existingIndex = input.id ? runs.findIndex((run) => run.id === input.id) : -1;
    const existing = existingIndex >= 0 ? runs[existingIndex] : undefined;
    const merged: GoalRun = existing
      ? {
          ...existing,
          ...input,
          id: existing.id,
          projectPath: normalizeProjectPath(cwd),
          createdAt: existing.createdAt,
          updatedAt: nowIso(),
          successCriteria: input.successCriteria ?? existing.successCriteria,
          prerequisites: input.prerequisites ?? existing.prerequisites,
          harness: input.harness ?? existing.harness,
          evidencePlan: input.evidencePlan ?? existing.evidencePlan,
          tasks: input.tasks ?? existing.tasks,
          evidence: input.evidence ?? existing.evidence,
          blockers: input.blockers ?? existing.blockers,
          status: deriveRunnableStatus(
            input.status ?? existing.status,
            input.prerequisites ?? existing.prerequisites,
          ),
        }
      : createGoalRun(cwd, input);

    const nextRuns = existingIndex >= 0 ? [...runs] : [merged, ...runs];
    if (existingIndex >= 0) nextRuns[existingIndex] = merged;
    await writeGoalRunsFile(cwd, nextRuns);
    return merged;
  });
}

export async function getGoalRun(cwd: string, id: string): Promise<GoalRun | null> {
  const runs = await loadGoalRuns(cwd);
  return (
    runs.find((run) => run.id === id || run.id.startsWith(id)) ?? (await discoverGoalRunsById(id))
  );
}

export async function getActiveGoalRun(cwd: string): Promise<GoalRun | null> {
  const runs = await loadGoalRuns(cwd);
  return (
    runs.find((run) => run.status === "running" || run.status === "verifying") ??
    runs.find((run) => run.status === "ready" || run.status === "blocked") ??
    runs.find((run) => run.status === "draft" || run.status === "paused") ??
    runs[0] ??
    null
  );
}

export async function appendGoalDecision(
  cwd: string,
  runId: string,
  decision: GoalControllerDecision | { kind: string; reason?: string; content?: string },
): Promise<GoalRun | null> {
  const parts = [`kind=${decision.kind}`];
  if ("reason" in decision && decision.reason) parts.push(`reason=${decision.reason}`);
  if ("content" in decision && decision.content) parts.push(decision.content);
  if ("task" in decision && decision.task) {
    const task = decision.task as GoalTask;
    parts.push(`task=${task.id}`, `title=${task.title}`);
    if (task.workerId) parts.push(`worker=${task.workerId}`);
  }
  if ("attempts" in decision && typeof decision.attempts === "number")
    parts.push(`attempts=${decision.attempts}`);
  if ("workerId" in decision && decision.workerId) parts.push(`worker=${decision.workerId}`);
  if ("command" in decision && decision.command) parts.push(`verifier=${decision.command}`);
  if ("status" in decision && decision.status) parts.push(`status=${decision.status}`);
  parts.push(`timestamp=${nowIso()}`);
  return appendGoalEvidence(cwd, runId, {
    kind: "summary",
    label: `Goal decision: ${decision.kind}`,
    content: parts.join("; "),
  });
}

export async function appendGoalEvidence(
  cwd: string,
  runId: string,
  input: GoalEvidenceInput,
): Promise<GoalRun | null> {
  const discovered = await getGoalRun(cwd, runId);
  const writeCwd = discovered?.projectPath ?? cwd;
  return enqueueWrite(async () => {
    const runs = await loadGoalRuns(writeCwd);
    const index = runs.findIndex((run) => run.id === runId || run.id.startsWith(runId));
    if (index === -1) return null;
    const run = runs[index];
    const updated: GoalRun = {
      ...run,
      evidence: [...run.evidence, createGoalEvidence(input)],
      updatedAt: nowIso(),
    };
    const nextRuns = [...runs];
    nextRuns[index] = updated;
    await writeGoalRunsFile(writeCwd, nextRuns);
    return updated;
  });
}

export async function updateGoalTask(
  cwd: string,
  runId: string,
  taskId: string,
  patch: Partial<GoalTask> | GoalTaskInput,
): Promise<GoalRun | null> {
  const discovered = await getGoalRun(cwd, runId);
  const writeCwd = discovered?.projectPath ?? cwd;
  return enqueueWrite(async () => {
    const runs = await loadGoalRuns(writeCwd);
    const runIndex = runs.findIndex((run) => run.id === runId || run.id.startsWith(runId));
    if (runIndex === -1) return null;
    const run = runs[runIndex];
    const taskIndex = run.tasks.findIndex(
      (task) => task.id === taskId || task.id.startsWith(taskId),
    );
    const tasks = [...run.tasks];
    if (taskIndex === -1) {
      if ("title" in patch && "prompt" in patch && patch.title && patch.prompt) {
        tasks.push(
          createGoalTask({
            ...patch,
            title: patch.title,
            prompt: patch.prompt,
          }),
        );
      } else {
        return null;
      }
    } else {
      const existingTask = tasks[taskIndex];
      tasks[taskIndex] = {
        ...existingTask,
        ...patch,
        id: existingTask.id,
        title: patch.title ?? existingTask.title,
        prompt: patch.prompt ?? existingTask.prompt,
      };
    }

    const updated: GoalRun = { ...run, tasks, updatedAt: nowIso() };
    const nextRuns = [...runs];
    nextRuns[runIndex] = updated;
    await writeGoalRunsFile(writeCwd, nextRuns);
    return updated;
  });
}

export function isBlockingGoalPrerequisite(item: GoalPrerequisite): boolean {
  return item.status === "missing" || (item.status === "unknown" && !item.evidence);
}

export function hasBlockingGoalPrerequisites(prerequisites: readonly GoalPrerequisite[]): boolean {
  return prerequisites.some(isBlockingGoalPrerequisite);
}

export function goalHasBlockingPrerequisites(run: GoalRun): boolean {
  return hasBlockingGoalPrerequisites(run.prerequisites);
}

export function formatGoalPrerequisiteInstruction(item: GoalPrerequisite): string {
  return item.instructions?.trim() || "User must provide this prerequisite.";
}

export function formatGoalBlockingPrerequisiteList(
  prerequisites: readonly GoalPrerequisite[],
): string {
  const missing = prerequisites.filter(isBlockingGoalPrerequisite);
  if (missing.length === 0) return "Goal has no missing user prerequisites.";
  return missing
    .map((item) => `${item.label}: ${formatGoalPrerequisiteInstruction(item)}`)
    .join("; ");
}

export function formatGoalBlockingPrerequisites(run: GoalRun): string {
  return formatGoalBlockingPrerequisiteList(run.prerequisites);
}

export function summarizeGoalCountsFromRuns(runs: readonly GoalRun[]): GoalCounts {
  const counts: GoalCounts = {
    total: runs.length,
    active: 0,
    blocked: 0,
    pending: 0,
    running: 0,
    passed: 0,
    failed: 0,
  };

  for (const run of runs) {
    if (run.status === "blocked") counts.blocked++;
    if (run.status === "running" || run.status === "verifying") counts.running++;
    if (run.status === "passed") counts.passed++;
    if (run.status === "failed") counts.failed++;
    if (run.status === "draft" || run.status === "ready" || run.status === "paused")
      counts.pending++;
    if (run.status !== "passed" && run.status !== "failed") counts.active++;
  }

  return counts;
}

export async function summarizeGoalCounts(cwd: string): Promise<GoalCounts> {
  return summarizeGoalCountsFromRuns(await loadGoalRuns(cwd));
}

async function writeGoalProgressJournalFromRun(cwd: string, run: GoalRun): Promise<string> {
  const dir = join(projectDir(cwd), "journals");
  await mkdir(dir, { recursive: true });
  const path = join(dir, `${run.id}.md`);
  const lines = [
    `# ${run.title}`,
    "",
    `Status: ${run.status}`,
    `Goal: ${run.goal}`,
    "",
    "## Success criteria",
    ...(run.successCriteria.length
      ? run.successCriteria.map((item) => `- ${item}`)
      : ["- none recorded"]),
    "",
    "## Prerequisites",
    ...(run.prerequisites.length
      ? run.prerequisites.map(
          (item) => `- [${item.status}] ${item.label}${item.evidence ? ` — ${item.evidence}` : ""}`,
        )
      : ["- none"]),
    "",
    "## Tasks",
    ...(run.tasks.length
      ? run.tasks.map(
          (task) =>
            `- [${task.status}] ${task.title} (attempts: ${task.attempts})${task.lastSummary ? ` — ${task.lastSummary}` : ""}`,
        )
      : ["- none"]),
    "",
    "## Verifier",
    run.verifier?.lastResult
      ? `- ${run.verifier.lastResult.status}: ${run.verifier.lastResult.summary}${run.verifier.lastResult.outputPath ? ` (${run.verifier.lastResult.outputPath})` : ""}`
      : `- ${run.verifier?.command ?? "none"}`,
    "",
    "## Blockers",
    ...(run.blockers.length ? run.blockers.map((item) => `- ${item}`) : ["- none"]),
    "",
    "## Recent evidence",
    ...run.evidence
      .slice(-10)
      .map(
        (item) =>
          `- ${item.createdAt} [${item.kind}] ${item.label}${item.path ? ` (${item.path})` : ""}${item.content ? ` — ${item.content}` : ""}`,
      ),
    "",
  ];
  await writeFile(path, lines.join("\n"), "utf-8");
  return path;
}

export async function writeGoalProgressJournal(cwd: string, runId: string): Promise<string | null> {
  const run = await getGoalRun(cwd, runId);
  if (!run) return null;
  return writeGoalProgressJournalFromRun(run.projectPath, run);
}
