export const PROJECT_STATUSES = ['open', 'archived'] as const;
export type ProjectStatus = typeof PROJECT_STATUSES[number];

// v1.13.10: per-tool cost rolling-window stat. Returned by
// GET /api/tools/cost_stats — one entry per tool with mean prompt/completion
// tokens over the last 100 invocations. AgentPicker sums across an agent's
// whitelisted tools for per-agent cost hints.
export interface ToolCostStat {
  tool_name: string;
  mean_prompt_tokens: number;
  mean_completion_tokens: number;
  n_calls: number;
  updated_at: string;
}

export interface Project {
  id: string;
  name: string;
  path: string;
  added_at: string;
  last_session_id: string | null;
  status: ProjectStatus;
  gitea_remote: string | null;
  // v1.9: per-project defaults. Empty string on default_system_prompt means
  // "no override" — inference falls through to the base system prompt.
  default_system_prompt: string;
  default_web_search_enabled: boolean;
}

export interface AvailableProject {
  path: string;
  name: string;
}

export type SessionStatus = 'open' | 'archived';

// WorktreeRiskReport single-sourced in @boocode/contracts — edit the package, not here.
export type { WorktreeRiskReport } from '@boocode/contracts/worktree-risk';

export interface Session {
  id: string;
  project_id: string;
  name: string;
  model: string;
  system_prompt: string;
  status: SessionStatus;
  created_at: string;
  updated_at: string;
  agent_id: string | null;
  // v1.9: null = inherit from project.default_web_search_enabled.
  web_search_enabled: boolean | null;
  // v1.12.1: server-authoritative pane layout, replaces localStorage.
  // A value may be the legacy bare WorkspacePane[] (older rows) OR the new
  // WorkspaceState envelope (panes + tab numbering + reopen stack). Normalize
  // on read via useWorkspacePanes' toWorkspaceState.
  workspace_panes: WorkspacePane[] | WorkspaceState;
  // v1.13.17: paths the agent has been granted read access to via the
  // request_read_access tool. Empty by default. Settings UI surfaces the
  // list with per-row revoke; the grant flow itself appends through the
  // dedicated POST /api/chats/:id/grant_read_access endpoint (not PATCH).
  allowed_read_paths: string[];
}

// v1.8.1: 'global' = /data/AGENTS.md (always-on), 'project' = per-project
// override at <root>/AGENTS.md. In-code builtins were retired; the seed file
// lives at /data/AGENTS.md.
export type AgentSource = 'global' | 'project';

export interface Agent {
  id: string;
  name: string;
  description: string;
  system_prompt: string;
  temperature: number;
  tools: string[];
  model: string | null;
  source: AgentSource;
  // v1.8.2: per-agent tool-loop budget. null means resolve at runtime from
  // the agent's toolset (30 for all read-only, 10 otherwise) or 15 for raw
  // chat with no agent.
  max_tool_calls: number | null;
  // v1.14.0: per-agent step cap for the outer inference loop. null means
  // bounded only by MAX_STEPS (200). 0 means "no tool calls allowed."
  steps: number | null;
}

export interface AgentParseError {
  agent_name: string;
  reason: string;
}

export interface AgentsResponse {
  agents: Agent[];
  errors: AgentParseError[];
}

export const CHAT_STATUSES = ['open', 'archived'] as const;
export type ChatStatus = typeof CHAT_STATUSES[number];

export interface Chat {
  id: string;
  session_id: string;
  name: string | null;
  status: ChatStatus;
  created_at: string;
  updated_at: string;
  // Populated by GET /api/sessions/:id/chats only.
  message_count?: number;
  last_message_preview?: string | null;
  effective_context_tokens?: number | null;
  // v1.11.5: model's full context window from llama-swap /props. Used by
  // ContextBar to render the zero-state + auto-compaction threshold tooltip
  // before any assistant message exists in the chat. null when upstream
  // lookup failed (model unknown, llama-swap unreachable) — UI degrades
  // to a "model context unknown" placeholder.
  model_context_limit?: number | null;
}

export type MessageRole = 'user' | 'assistant' | 'tool' | 'system';
export type MessageStatus = 'streaming' | 'complete' | 'failed' | 'cancelled';
export type MessageKind = 'message' | 'compact';

export interface ToolCall {
  id: string;
  name: string;
  args: Record<string, unknown>;
}

export interface ToolResult {
  tool_call_id: string;
  output: unknown;
  truncated: boolean;
  error?: string;
}

// v1.8.2 / v1.11.6: ErrorReason + MessageMetadata single-sourced in
// @boocode/contracts — edit the package, not here.
import type { ErrorReason, MessageMetadata } from '@boocode/contracts/message-metadata';
export type { ErrorReason, MessageMetadata };

export interface Message {
  id: string;
  session_id: string;
  chat_id: string;
  role: MessageRole;
  content: string;
  kind: MessageKind;
  tool_calls: ToolCall[] | null;
  tool_results: ToolResult | null;
  status: MessageStatus;
  last_seq: number;
  tokens_used: number | null;
  ctx_used: number | null;
  ctx_max: number | null;
  // model-attribution: which model produced this assistant message (null for
  // user/system rows + pre-attribution messages). Rendered as a chip.
  model: string | null;
  started_at: string | null;
  finished_at: string | null;
  created_at: string;
  // v1.8.2: per-message metadata; see MessageMetadata. null for the vast
  // majority of messages.
  metadata: MessageMetadata | null;
  // v1.13.1-C: reasoning content captured from models that stream reasoning
  // tokens separately (qwen3.6 etc.) and from external agents over ACP
  // (agent_thought_chunk). Backend populates from message_parts; rendered by
  // MessageBubble as a collapsible "Thinking" block.
  reasoning_parts?: Array<{ text: string }> | null;
  // Coder wire shape pre-joins reasoning_parts into a single string
  // (CoderPane/CoderMessageList) and streams it live via reasoning_delta
  // frames. MessageBubble reads whichever of the two is present.
  reasoning_text?: string | null;
  // v1.11: anchored rolling compaction fields. Optional on the wire so that
  // older API responses (or test fixtures) parse without explicit nulls.
  //   summary       — true on the assistant row that holds the active
  //                   anchored summary. Render via SummaryCard.
  //   tail_start_id — first preserved tail message the summary covers up to
  //                   (exclusive). Diagnostic only on the client.
  //   compacted_at  — set on rows that are "behind the curtain" of the
  //                   current summary. Returned by the GET endpoint so the
  //                   UI can show history, but the server-side inference
  //                   assembly filters these out.
  summary?: boolean;
  tail_start_id?: string | null;
  compacted_at?: string | null;
}

export interface ModelInfo {
  id: string;
  [key: string]: unknown;
}

export type {
  ProviderModel,
  ProviderMode,
  ThinkingOption,
  ProviderSnapshotStatus,
  AgentCommand,
  ProviderSnapshotEntry,
} from '@boocode/contracts/provider-snapshot';

export type {
  ProviderOverride,
  CoderProvidersFile,
  ProviderConfigPatch,
} from '@boocode/contracts/provider-config';

// AgentSessionConfig single-sourced in @boocode/contracts — edit the package, not here.
export type { AgentSessionConfig } from '@boocode/contracts/message-metadata';

export type PermissionKind = 'tool' | 'question' | 'plan' | 'elicitation';

export interface PermissionPrompt {
  taskId: string;
  kind?: PermissionKind;
  toolTitle?: string;
  input?: Record<string, unknown>;
  options: Array<{ optionId: string; label: string }>;
}

export interface CoderSendMessageBody {
  content: string;
  pane_id: string;
  chat_id?: string;
  provider?: string;
  model?: string;
  mode_id?: string;
  thinking_option_id?: string;
}

export interface CoderSendMessageResponse {
  user_message_id?: string;
  assistant_message_id?: string;
  task_id?: string;
  dispatched?: boolean;
}

export interface CoderMessageWire {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  status?: 'streaming' | 'complete' | 'failed';
  // model-attribution: which model produced this coder assistant message.
  model?: string | null;
  reasoning_text?: string;
  // Context-window fill for the ContextBar (claude SDK turns set these from the
  // SDK's reported window; other agents omit them). Read via the Message cast.
  ctx_used?: number | null;
  ctx_max?: number | null;
  tool_calls?: Array<{
    id: string;
    function: { name: string; arguments: string };
  }>;
}

export interface CoderTaskDetail {
  id: string;
  state: 'pending' | 'running' | 'completed' | 'failed' | 'blocked' | 'cancelled';
  input: string;
  output_summary: string | null;
  agent: string | null;
  model: string | null;
  session_id: string | null;
}

export interface SidebarSession {
  id: string;
  name: string;
  model: string;
  updated_at: string;
  project_id: string;
}

export interface SidebarProject {
  id: string;
  name: string;
  path: string;
  gitea_remote: string | null;
  recent_sessions: SidebarSession[];
  total_sessions: number;
}

export interface SidebarResponse {
  projects: SidebarProject[];
}

export interface FileEntry {
  name: string;
  kind: 'file' | 'dir';
  size?: number;
}

export interface ListDirResult {
  entries: FileEntry[];
  truncated: boolean;
  total: number;
}

export interface ViewFileResult {
  content: string;
  truncated: boolean;
  total_bytes: number;
  bytes_returned: number;
}

// v1.8 mobile-tabs: shape returned by GET /api/projects/:id/git. Mirrors
// services/git_meta.ts on the server. branch=null means "not a git repo".
export interface GitMeta {
  branch: string | null;
  is_dirty: boolean;
  ahead: number;
  behind: number;
}

// git-diff-panel Phase 1: shapes returned by GET /api/projects/:id/git/diff.
export type GitDiffMode = 'uncommitted' | 'committed';
export type GitDiffChangeType = 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked';

export interface GitDiffFile {
  path: string;
  old_path: string | null;
  change_type: GitDiffChangeType;
  added_lines: number;
  removed_lines: number;
  staged: boolean;
  diff_body: string | null;
  is_binary: boolean;
  is_too_large: boolean;
}

export interface GitDiffResult {
  git_repo: boolean;
  mode: GitDiffMode;
  /** Server-computed mode based on dirty state — used for auto-select (FIX 1) and mode suggestion (FIX 4). */
  auto_mode?: GitDiffMode;
  base_label: string | null;
  in_progress_op: string | null;
  files: GitDiffFile[];
}

// git-diff-panel Phase 2: per-file info for the discard endpoint.
export interface GitDiscardFileInfo {
  path: string;
  change_type: GitDiffChangeType;
  staged: boolean;
}

// Batch 9.6: skill catalog row. Returned by GET /api/skills and consumed by
// the slash-command dropdown. `path` and `mtime` are exposed for debug surface
// (/api/skills) but the dropdown only renders name + description.
export interface Skill {
  name: string;
  description: string;
  path: string;
  mtime: number;
}

// Batch 9.7: ask_user_input shapes. The tool_call.args is { questions: AskUserQuestion[] }
// (1-3 entries); the eventual tool_result.output is { answers: AskUserAnswer[] } in the
// same order. AskUserInputCard renders questions and POSTs answers.
export type AskUserQuestionType = 'single_select' | 'multi_select';

export interface AskUserQuestion {
  question: string;
  type: AskUserQuestionType;
  options: string[];
}

export interface AskUserAnswer {
  question: string;
  selected_options: string[];
  free_text: string | null;
}

export interface AskUserAnswerSet {
  answers: AskUserAnswer[];
}

// v1.9: 'settings' is an ephemeral pane kind — never persisted, always
// singleton per workspace. The pane hook filters it out before writing to
// localStorage and dedupes on insertion via toggleSettingsPane().
// v1.14.x-html-artifact-panes: 'markdown_artifact' + 'html_artifact' added.
// Both carry payload state on the WorkspacePane row itself so
// useWorkspacePanes's JSON-string dedup + persisted jsonb stay self-contained
// — no extra fetch on rehydrate.
export type WorkspacePaneKind =
  | 'chat'
  | 'terminal'
  | 'coder'
  | 'empty'
  | 'settings'
  | 'markdown_artifact'
  | 'html_artifact'
  | 'orchestrator'
  | 'arena';

// Mixed tabs: a pane can hold tabs of different kinds (a BooChat tab next to a
// BooCode tab next to a Terminal tab). Each tab carries its own kind; the active
// tab's kind drives what the pane renders. `tabKinds` is parallel to `chatIds`.
export type WorkspaceTabKind = 'chat' | 'coder' | 'terminal';

// v1.14.x: per-pane artifact payloads. Optional + namespaced so older saved
// pane rows (without these fields) deserialize unchanged.
// v1.14.x: pane state is a reference only — the pane component fetches the
// actual content on mount. This keeps sessions.workspace_panes jsonb small and
// makes the message body / html_artifact part the single source of truth.
export interface MarkdownArtifactState {
  // chat_id is needed for the download endpoint
  // (POST /api/chats/:chat_id/messages/:msg_id/artifacts/download).
  chat_id: string;
  message_id: string;
  title: string;
}

export interface HtmlArtifactState {
  chat_id: string;
  message_id: string;
  title: string;
}

// Orchestrator pane state — carries run identity for fetch-on-mount + reopen.
export interface OrchestratorState {
  run_id: string;
  flow_name: string;
  band: 'small' | 'medium' | 'large';
}

// Arena pane state — single-sourced in @boocode/contracts; edit the package, not here.
import type { ArenaState, BattleShape, ContestantShape, CrossExaminationShape, BattleType, BattleStatus, ContestantStatus, ContestantLane } from '@boocode/contracts/arena';
export type { ArenaState, BattleShape, ContestantShape, CrossExaminationShape, BattleType, BattleStatus, ContestantStatus, ContestantLane };

// Orchestrator run API types (returned by GET /api/coder/runs/:id).
export interface FlowRunRow {
  id: string;
  project_id: string;
  flow_name: string;
  band: 'small' | 'medium' | 'large';
  model: string;
  status: 'running' | 'completed' | 'failed' | 'cancelled';
  input: { question: string; band?: string; [key: string]: unknown };
  report: string | null;
  error: string | null;
  created_at: string;
  updated_at: string;
}

export interface FlowStepRow {
  id: string;
  run_id: string;
  step_id: string;
  kind: 'agent' | 'code';
  agent: string | null;
  status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled';
  task_id: string | null;
  chat_id: string | null;
  session_id: string | null;
  input: string | null;
  output: string | null;
  error: string | null;
  created_at: string;
  updated_at: string;
}

export interface WorkspacePane {
  id: string;
  // For a tabbed pane (chat/coder/terminal) this mirrors the ACTIVE tab's kind,
  // so the existing render-by-pane.kind path renders the active tab. Special
  // panes (empty/settings/artifact) keep their own kind.
  kind: WorkspacePaneKind;
  chatId?: string;
  // Tab ids. For chat/coder tabs this is the chats-row id; for terminal tabs
  // it's a generated id used to key the tmux session. Parallel to tabKinds.
  chatIds: string[];
  // Per-tab kind, parallel to chatIds. Optional for legacy rows (back-filled on
  // load from pane.kind via normalizePaneKind).
  tabKinds?: WorkspaceTabKind[];
  activeChatIdx: number;
  // v1.14.x: populated only when kind === 'markdown_artifact' / 'html_artifact'.
  markdown_artifact_state?: MarkdownArtifactState;
  html_artifact_state?: HtmlArtifactState;
  // orchestrator pane: populated only when kind === 'orchestrator'.
  orchestrator_state?: OrchestratorState;
  // arena pane: populated only when kind === 'arena'.
  arena_state?: ArenaState;
}

// Reopen LIFO stack entry. Shape unchanged from the prior module-level stack;
// now persisted inside the WorkspaceState envelope so the reopen-pane stack
// survives a reload / cross-device sync.
export interface ClosedPaneEntry {
  kind: WorkspacePane['kind'];
  chatIds: string[];
  tabKinds?: WorkspaceTabKind[];
  activeChatIdx: number;
}

// Envelope persisted to sessions.workspace_panes. Supersedes the bare
// WorkspacePane[] shape (still accepted on read for legacy rows — see the
// migration in useWorkspacePanes.toWorkspaceState). The server accepts either
// shape; the frontend always emits this envelope going forward.
export interface WorkspaceState {
  panes: WorkspacePane[];
  // Stable, session-scoped tab number per chat id. Numbers only ever increase
  // and are never reused (retired entries are pruned on tab close).
  tabNumbers: { [chatId: string]: number };
  // Next number to hand out; starts at 1; ONLY increments.
  nextTabNumber: number;
  // Reopen LIFO stack, max 10, most-recent last.
  closedPaneStack: ClosedPaneEntry[];
}

export type WsFrame =
  | { type: 'snapshot'; messages: Message[] }
  | { type: 'message_started'; message_id: string; chat_id?: string; role: MessageRole }
  | { type: 'delta'; message_id: string; chat_id?: string; content: string }
  | { type: 'tool_call'; message_id: string; chat_id?: string; tool_call: ToolCall }
  | {
      type: 'tool_result';
      tool_message_id: string;
      chat_id?: string;
      tool_call_id: string;
      output: unknown;
      truncated: boolean;
      error?: string;
    }
  | {
      type: 'message_complete';
      message_id: string;
      chat_id?: string;
      tokens_used?: number | null;
      ctx_used?: number | null;
      ctx_max?: number | null;
      started_at?: string | null;
      finished_at?: string | null;
      // model-attribution: the model that produced this assistant message.
      model?: string | null;
      // v1.8.2: piggybacks the persisted metadata onto the terminal frame so
      // cap-hit sentinels (and any future stamped-on-complete metadata) flow
      // to the client without a refetch.
      metadata?: MessageMetadata | null;
      // F1 (D-8): terminal status of the assistant message. Absent on the normal
      // path (reducer defaults to 'complete'); the BooCoder dispatcher stamps it
      // 'cancelled' on a user Stop / stall and 'failed' on a thrown error so the
      // reducer renders a muted "Stopped" / failed state — no new frame type.
      status?: 'complete' | 'cancelled' | 'failed';
    }
  // v1.12.2: live throughput frame, published mid-stream every ~500ms with
  // the latest token + ctx counts so ChatThroughput can render tok/s and
  // ctx_used while the model is still generating.
  | {
      type: 'usage';
      message_id: string;
      chat_id?: string;
      completion_tokens: number | null;
      ctx_used: number | null;
      ctx_max: number | null;
    }
  | { type: 'messages_deleted'; message_ids: string[]; chat_id?: string }
  | { type: 'chat_renamed'; chat_id: string; name: string }
  // v1.11: published by services/compaction.ts after the new anchored
  // summary row lands. Carries the new summary row id for diagnostics; the
  // session-stream handler ignores the id and re-fetches the full message
  // list (the cohort of compacted_at-stamped rows changed too).
  | { type: 'compacted'; session_id: string; chat_id: string; summary_message_id: string }
  // v1.8.2: `reason` discriminates structured failures (the UI prefers it
  // over `error` text when present).
  | { type: 'error'; message_id?: string; chat_id?: string; error: string; reason?: ErrorReason }
  // agent-status-normalize (#10): BooCoder publishes a normalized per-(chat,agent)
  // lifecycle status for external coding agents on the per-session channel. The
  // CoderPane tracks the latest status per (chat_id, agent) and resets on chat
  // switch; AgentComposerBar renders the dot (distinct from the WS-liveness dot).
  | {
      type: 'agent_status_updated';
      chat_id: string;
      agent: string;
      status: 'working' | 'blocked' | 'idle' | 'error';
      reason?: string;
      at: string;
    }
  // orchestrator frames ([D-6]): run lifecycle. The per-agent content stream
  // reuses existing delta/tool_call/message_complete frames keyed by chat_id.
  | {
      type: 'flow_run_started';
      run_id: string;
      flow_name: string;
      band: 'small' | 'medium' | 'large';
      steps: Array<{
        step_id: string;
        agent: string;
        kind: 'agent' | 'code';
        chat_id: string;
        label: string;
      }>;
    }
  | {
      type: 'flow_run_step_updated';
      run_id: string;
      step_id: string;
      status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped' | 'cancelled';
      run_status?: 'running' | 'completed' | 'failed' | 'cancelled';
      report?: string;
    }
  // arena frames: battle lifecycle + per-contestant streaming
  | {
      type: 'battle_started';
      battle_id: string;
      battle_type: 'coding' | 'qa';
      prompt: string;
      contestants: Array<{ id: string; identity: string; model: string; lane: 'local' | 'cloud' }>;
    }
  | {
      type: 'contestant_updated';
      battle_id: string;
      contestant_id: string;
      status?: 'queued' | 'running' | 'done' | 'error';
      duration_ms?: number;
      tokens_per_sec?: number;
      battle_status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
      delta?: string;
      error?: string;
    }
  | {
      type: 'battle_updated';
      battle_id: string;
      status?: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
      winner_contestant_id?: string | null;
      analysis_ready?: boolean;
      cross_exam_id?: string;
    };
