// KEEP IN SYNC: apps/server/src/schema.sql projects_status_chk
export const PROJECT_STATUSES = ['open', 'archived'] as const;
export type ProjectStatus = typeof PROJECT_STATUSES[number];

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 inherited by new sessions. Empty string on
  // default_system_prompt means "no override" — the model gets the base
  // BooCode system prompt only. default_web_search_enabled is the inherited
  // value for sessions where web_search_enabled is null.
  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.
import type { WorktreeRiskReport } from '@boocode/contracts/worktree-risk';
export type { WorktreeRiskReport };

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: per-session override for web_search. null = inherit from
  // project.default_web_search_enabled. Plumbed but inert in v1.9 — the
  // actual web_search tool ships in Batch 8.
  web_search_enabled: boolean | null;
  // v1.12.1: server-side workspace pane layout. Replaces per-device
  // localStorage so all devices viewing the session see the same panes.
  workspace_panes: WorkspacePane[];
  // v1.13.17: absolute paths the agent has been granted read access to via
  // the request_read_access tool. Empty by default; populated only by the
  // grant_read_access endpoint's allow branch. Revoked via PATCH session.
  // path_guard's extraRoots check consults this list before refusing reads
  // outside the primary project root.
  allowed_read_paths: string[];
  // v[state-graph]: optional declarative state-graph engine. Default false
  // when the column is absent on existing DBs (ALTER TABLE .. DEFAULT FALSE).
  state_graph_enabled: boolean;
}

// v1.14.x-html-artifact-panes: 'markdown_artifact' + 'html_artifact' added.
// Optional payload state lives on the pane row itself so the jsonb survives
// a hard reload without needing a re-fetch.
export type WorkspacePaneKind =
  | 'chat'
  | 'terminal'
  | 'coder'
  | 'empty'
  | 'settings'
  | 'markdown_artifact'
  | 'html_artifact';

// v1.14.x: reference-only — the actual artifact body lives in the message
// row (markdown) or message_parts.payload (html_artifact). Pane components
// fetch on mount.
export interface MarkdownArtifactState {
  chat_id: string;
  message_id: string;
  title: string;
}

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

export interface WorkspacePane {
  id: string;
  kind: WorkspacePaneKind;
  chatId?: string;
  chatIds: string[];
  activeChatIdx: number;
  // v1.14.x: populated only when kind === 'markdown_artifact' / 'html_artifact'.
  markdown_artifact_state?: MarkdownArtifactState;
  html_artifact_state?: HtmlArtifactState;
}

// v1.8.1: agents come from two sources. 'global' = /data/AGENTS.md (always
// loaded inside the container), 'project' = per-project override at
// <root>/AGENTS.md. Project entries override global by name (case-sensitive).
export type AgentSource = 'global' | 'project';

export interface Agent {
  id: string;            // slug of name; stable handle stored in sessions.agent_id
  name: string;
  description: string;
  system_prompt: string;
  temperature: number;
  top_p: number | null;  // null means omit from request body
  top_k: number | null;  // null means omit from request body
  min_p: number | null;  // null means omit from request body
  presence_penalty: number | null;  // null means omit from request body
  // v2.6 sampling-streamjson-tokens (#11): llama.cpp sampler extensions.
  // null = omit from request body. top_n_sigma + the DRY repetition family
  // help the doom-loop-prone local model. All travel via the same
  // providerOptions.openaiCompatible extraBody channel as top_k/min_p.
  top_n_sigma: number | null;
  dry_multiplier: number | null;
  dry_base: number | null;
  dry_allowed_length: number | null;
  dry_penalty_last_n: number | null;
  tools: string[];       // whitelist of tool names; empty = no tools allowed
  model: string | null;  // null means "session.model wins"
  source: AgentSource;
  // v1.8.2: per-agent tool-loop budget. null means resolve at runtime from the
  // agent's toolset (30 if all tools are 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;
  // vDeepSeek: thinking/reasoning effort for DeepSeek V4 models.
  // Maps to DeepSeek's reasoning_effort API param.
  reasoning_effort: 'off' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null;
  // vDeepSeek: JSON output mode. When set, model outputs valid JSON object.
  response_format: { type: 'json_object' } | null;
  // vDeepSeek: prefix content for chat prefix completion. When set, injects
  // an assistant prefix message forcing the model to complete from it.
  prefix_content: string | null;
}

// One entry per malformed `## Name` block. Per-block errors don't fail the
// whole file — the loader returns parsed-successfully agents AND the list of
// skipped ones so the UI can show a non-blocking warning chip.
export interface AgentParseError {
  agent_name: string;
  reason: string;
}

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

// KEEP IN SYNC: apps/server/src/schema.sql chats_status_chk
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;
  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;
  // v1.11.5: model's full context window (from llama-swap props), threaded
  // to the frontend so ContextBar can render a zero-state + the auto-
  // compaction threshold tooltip before any assistant message lands.
  // Shared across all chats in a session (chats inherit session.model).
  // null when the upstream lookup failed (model unknown, llama-swap down).
  model_context_limit?: number | null;
}

// KEEP IN SYNC: apps/server/src/schema.sql messages_role_chk / messages_status_chk
export const MESSAGE_ROLES = ['user', 'assistant', 'system', 'tool'] as const;
export type MessageRole = typeof MESSAGE_ROLES[number];

export const MESSAGE_STATUSES = ['streaming', 'complete', 'failed', 'cancelled'] as const;
export type MessageStatus = typeof MESSAGE_STATUSES[number];

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;
  cache_tokens: number | null;
  reasoning_tokens: number | null;
  started_at: string | null;
  finished_at: string | null;
  created_at: string;
  // v1.8.2: per-message metadata. See MessageMetadata for the discriminated
  // shapes currently in use.
  metadata: MessageMetadata | null;
  // v1.13.1-C: reasoning content captured from the model's reasoning stream
  // (qwen3.6 etc.). Populated from message_parts via the messages_with_parts
  // view's reasoning_parts column. Optional — most rows have no reasoning
  // and the API may omit the field on legacy responses.
  // `signature` (Phase 2) carries the Anthropic thinking-block signature so it
  // can be replayed verbatim on the next turn. Absent for non-anthropic models.
  reasoning_parts?: Array<{ text: string; signature?: string }> | null;
  // v1.11: anchored rolling compaction. Optional so consumers that SELECT
  // the pre-v1.11 column set still type-check. See compaction.ts +
  // schema.sql for semantics.
  summary?: boolean;
  tail_start_id?: string | null;
  compacted_at?: string | null;
  // Per-assistant-turn model attribution (the chip). Read into the inference
  // payload so the active model can attribute prior turns when a chat mixes
  // models ("what did Opus say"). Optional — null/absent for user/tool rows
  // and pre-attribution assistant rows.
  model?: string | null;
}

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

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

export interface ModelCatalogResponse {
  providers: ModelCatalogProvider[];
}

export interface SidebarSession {
  id: string;
  project_id: string;
  name: string;
  model: string;
  updated_at: 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[];
}

// User-stream frames (broadcast on /ws/user channel)
export interface ProjectCreatedFrame {
  type: 'project_created';
  project: Project;
}
export interface ProjectDeletedFrame {
  type: 'project_deleted';
  project_id: string;
}
export interface SessionCreatedFrame {
  type: 'session_created';
  session: Session;
  project_id: string;
}
export interface SessionDeletedFrame {
  type: 'session_deleted';
  session_id: string;
  project_id: string;
}
export interface SessionUpdatedFrame {
  type: 'session_updated';
  session_id: string;
  project_id: string;
  name: string;
  updated_at: string;
}
export interface SessionRenamedFrame {
  type: 'session_renamed';
  session_id: string;
  name: string;
}
export interface SessionWorkspaceUpdatedFrame {
  type: 'session_workspace_updated';
  session_id: string;
  workspace_panes: WorkspacePane[];
}
export interface SessionArchivedFrame {
  type: 'session_archived';
  session_id: string;
  project_id: string;
}
export interface ChatCreatedFrame {
  type: 'chat_created';
  chat: Chat;
  session_id: string;
}
export interface ChatUpdatedFrame {
  type: 'chat_updated';
  chat_id: string;
  session_id: string;
  name: string | null;
  updated_at: string;
}
export interface ChatArchivedFrame {
  type: 'chat_archived';
  chat_id: string;
  session_id: string;
}
export interface ChatUnarchivedFrame {
  type: 'chat_unarchived';
  chat: Chat;
}
export interface ChatDeletedFrame {
  type: 'chat_deleted';
  chat_id: string;
  session_id: string;
}
export interface ProjectArchivedFrame {
  type: 'project_archived';
  project_id: string;
}
export interface ProjectUnarchivedFrame {
  type: 'project_unarchived';
  project: Project;
}
export interface ProjectUpdatedFrame {
  type: 'project_updated';
  project_id: string;
  name: string;
}
// v1.8 mobile-tabs: server can't know about client-side panes, so status
// is keyed by chat_id. Frontend dot derives pane status from pane.activeChatId.
// v1.8.2: optional `reason` carries a machine-readable code when status is
// 'error'. UI prefers reason; falls back to no detail when absent.
export interface ChatStatusFrame {
  type: 'chat_status';
  chat_id: string;
  status: 'streaming' | 'tool_running' | 'waiting_for_input' | 'idle' | 'error';
  at: string;
  reason?: ErrorReason;
}
export type UserStreamFrame =
  | ProjectCreatedFrame
  | ProjectDeletedFrame
  | SessionCreatedFrame
  | SessionDeletedFrame
  | SessionUpdatedFrame
  | SessionRenamedFrame
  | SessionWorkspaceUpdatedFrame
  | SessionArchivedFrame
  | ChatCreatedFrame
  | ChatUpdatedFrame
  | ChatArchivedFrame
  | ChatUnarchivedFrame
  | ChatDeletedFrame
  | ProjectArchivedFrame
  | ProjectUnarchivedFrame
  | ProjectUpdatedFrame
  | ChatStatusFrame;
