/**
 * Typed client for the @void-nav/api orchestrator backend.
 *
 * The orchestrator turns the implementation board into an agentic system: each
 * run hands a kanban card to a `pi` subprocess (running inside an isolated git
 * worktree) and streams its progress back to the UI. The agent drives the board
 * and the documentation through the server's internal endpoints, so the board
 * stays fully interactive and the docs remain the central store of truth.
 *
 * Keep these types in sync with `apps/api/src/types.ts`.
 */

export type RunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'stopped';

export interface AgentRun {
  id: string;
  cardId: string;
  status: RunStatus;
  useWorktree: boolean;
  /** Whether this run created (and therefore owns/cleans up) its worktree. */
  ownsWorktree: boolean;
  branch: string | null;
  worktreePath: string | null;
  /** pi session JSONL path persisted for this run (resumable by refinements). */
  sessionFile: string | null;
  prompt: string;
  summary: string | null;
  commitSha: string | null;
  error: string | null;
  createdAt: string;
  startedAt: string | null;
  finishedAt: string | null;
}

export type RunEventType =
  | 'status'
  | 'text'
  | 'tool_start'
  | 'tool_end'
  | 'log'
  | 'bevy'
  | 'done'
  | 'error';

export interface RunEvent {
  seq?: number;
  type: RunEventType;
  data: Record<string, unknown>;
  createdAt?: string;
}

const BASE = '/api/orchestrator';

class ApiError extends Error {
  constructor(message: string, readonly status: number) {
    super(message);
    this.name = 'ApiError';
  }
}

async function req<T>(path: string, init?: RequestInit): Promise<T> {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
  });
  if (!res.ok) {
    let detail = `${res.status} ${res.statusText}`;
    try {
      const body = await res.json();
      if (body?.error) detail = body.error;
    } catch {
      /* keep status text */
    }
    throw new ApiError(detail, res.status);
  }
  if (res.status === 204) return undefined as T;
  return (await res.json()) as T;
}

export interface StartRunInput {
  cardId: string;
  prompt?: string;
  useWorktree?: boolean;
  cleanupOnFinish?: boolean;
  /**
   * If set, start a refinement run that continues this prior run's branch with
   * `prompt` as the operator's refinement feedback.
   */
  refineRunId?: string;
}

/** A commit on a run's branch that is not yet on main. */
export interface DiffCommit {
  sha: string;
  subject: string;
}

/** A run's branch diff vs main. */
export interface DiffResult {
  branch: string;
  commits: DiffCommit[];
  stat: string;
  patch: string;
  truncated: boolean;
}

/** Outcome of merging a run's branch into the main worktree. */
export interface MergeResult {
  ok: boolean;
  alreadyMerged: boolean;
  target: string;
  branch: string;
  output: string;
}

/** One branch's outcome in a batch (merge-all) merge. */
export interface BatchMergeItem extends MergeResult {
  /** The run this branch belonged to. */
  runId: string;
  /** True if this branch conflicted (whether or not resolution succeeded). */
  conflict: boolean;
  /** When an agent resolved a conflict for this branch, its run id; else null. */
  resolutionRunId: string | null;
  /** True if the branch no longer resolves (cleaned up but the row lingered) —
   * skipped, batch continues. */
  skipped: boolean;
}

/** Result of merging several branches sequentially into the main branch. */
export interface BatchMergeResult {
  target: string;
  /** One entry per candidate branch, in the order processed. */
  items: BatchMergeItem[];
  /** The branch that conflicted and stopped the batch, if any. */
  haltedOn: string | null;
  /** True if the main worktree was dirty and nothing was attempted. */
  aborted: boolean;
  /** Human-readable reason when aborted or halted. */
  reason: string | null;
  /** Branch ref snapshotting HEAD before the batch, so the operator can reset
   * the whole batch (`git reset --hard <recoveryRef>`) if a resolution misfires. */
  recoveryRef: string | null;
}

export const orchestratorApi = {
  listRuns: (cardId?: string) =>
    req<{ runs: AgentRun[] }>(`/runs${cardId ? `?cardId=${encodeURIComponent(cardId)}` : ''}`),

  getRun: (id: string, since = 0) =>
    req<{ run: AgentRun; events: RunEvent[] }>(`/runs/${id}?since=${since}`),

  startRun: (input: StartRunInput) =>
    req<{ run: AgentRun }>(`/runs`, { method: 'POST', body: JSON.stringify(input) }),

  messageRun: (id: string, text: string, mode: 'steer' | 'followUp') =>
    req<{ ok: boolean }>(`/runs/${id}/message`, {
      method: 'POST',
      body: JSON.stringify({ text, mode }),
    }),

  stopRun: (id: string) =>
    req<{ ok: boolean }>(`/runs/${id}/stop`, { method: 'POST' }),

  deleteRun: (id: string) => req<void>(`/runs/${id}`, { method: 'DELETE' }),

  // --- worktree review & playtesting ---

  getDiff: (id: string) => req<DiffResult>(`/runs/${id}/diff`),

  mergeRun: (id: string) =>
    req<MergeResult>(`/runs/${id}/merge`, { method: 'POST' }),

  /** Merge all reviewable agent branches into the main branch sequentially. */
  mergeAll: () => req<BatchMergeResult>('/runs/merge-all', { method: 'POST' }),

  bevyStatus: (id: string) => req<{ running: boolean }>(`/runs/${id}/bevy`),

  startBevy: (id: string) => req<{ ok: boolean }>(`/runs/${id}/bevy`, { method: 'POST' }),

  stopBevy: (id: string) =>
    req<{ ok: boolean }>(`/runs/${id}/bevy/stop`, { method: 'POST' }),
};
