import type { MessageMetadata } from '@boocode/contracts/message-metadata';
import type { ArenaState } from '@boocode/contracts/arena';

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

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

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

// 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 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;
  // null = inherit from project.default_web_search_enabled.
  web_search_enabled: boolean | null;
  // 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;
  // 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[];
}

export interface Chat {
  id: string;
  session_id: string;
  name: string | null;
  model: 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;
  // 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 interface ToolCall {
  id: string;
  name: string;
  args: Record<string, unknown>;
}

export interface ToolResult {
  tool_call_id: string;
  output: unknown;
  truncated: boolean;
  error?: string;
  // unified diff snippet for write-tool results. Present when the tool
  // modified files (edit_file, create_file, etc.) and the backend computed a
  // diff. Rendered inline by DiffSnippet.
  diff?: string;
}

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;
  cache_tokens: number | null;
  reasoning_tokens: 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;
  // per-message metadata; see MessageMetadata. null for the vast majority of
  // messages.
  metadata: MessageMetadata | null;
  // 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;
  // compare group id. Set when the message is part of a multi-model compare
  // response. All assistant messages in the same compare group share this id,
  // keyed to the user message that triggered the compare.
  compare_group_id?: string;
  // 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;
}

// provider-grouped model catalog (W2, D-4).
export interface ModelCatalogProvider {
  id: string;
  label: string;
  models: ModelInfo[];
}

export interface ModelCatalogResponse {
  providers: ModelCatalogProvider[];
}

// 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';

// '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().
// 'markdown_artifact' + 'html_artifact' 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';

// per-pane artifact payloads. Optional + namespaced so older saved pane rows
// (without these fields) deserialize unchanged.
// 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: 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';
}

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;
  // 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[];
}
