export type Role = "user" | "assistant" | "tool" | "system";

/** "trace" rows are intermediate agent breadcrumbs (tool-call hints,
 * progress pings) that should not be rendered as conversational replies. */
export type MessageKind = "message" | "trace";

/** One image attached to a UIMessage.
 *
 * ``url`` can arrive in three different shapes, which the bubble renders
 * identically:
 * - A ``data:image/...;base64,...`` URL generated by the Composer for the
 *   optimistic preview of an in-flight user turn. Self-contained, no
 *   lifecycle.
 * - A signed ``/api/media/...`` URL attached to a historical user turn by
 *   the backend on session replay. Safe to drop into an ``<img src>``.
 * - Absent. The backend couldn't resolve a stored path (file moved,
 *   deleted, or pre-media-persistence session). The bubble shows a
 *   placeholder tile with ``name`` as the label.
 */
export interface UIImage {
  url?: string;
  name?: string;
}

export type UIMediaKind = "image" | "video" | "file";

export interface UIMediaAttachment {
  kind: UIMediaKind;
  url?: string;
  name?: string;
}

export interface UIMessage {
  id: string;
  role: Role;
  content: string;
  kind?: MessageKind;
  isStreaming?: boolean;
  createdAt: number;
  /** For trace rows: each individual hint line, so consecutive hints can
   * render as a single collapsible group. */
  traces?: string[];
  /** User turn: optimistic blob URLs for preview. Replay: placeholder chips. */
  images?: UIImage[];
  /** Signed or local UI-renderable media attachments. */
  media?: UIMediaAttachment[];
  /** Assistant turn: accumulated model reasoning / thinking text. Built up
   * incrementally from ``reasoning_delta`` frames; finalized when
   * ``reasoning_end`` arrives. */
  reasoning?: string;
  /** True while ``reasoning_delta`` frames are still arriving for this turn.
   * Drives the shimmer header on ``ReasoningBubble``. */
  reasoningStreaming?: boolean;
}

export interface ChatSummary {
  /** Server-side session key, e.g. ``websocket:abcd-...``. */
  key: string;
  /** Local channel + chat_id parts derived from ``key`` for convenience. */
  channel: string;
  chatId: string;
  createdAt: string | null;
  updatedAt: string | null;
  title?: string;
  preview: string;
}

export interface BootstrapResponse {
  token: string;
  ws_path: string;
  expires_in: number;
  model_name?: string | null;
}

export interface SettingsPayload {
  agent: {
    model: string;
    provider: string;
    resolved_provider: string | null;
    has_api_key: boolean;
  };
  providers: Array<{
    name: string;
    label: string;
    configured: boolean;
    api_key_hint?: string | null;
    api_base?: string | null;
    default_api_base?: string | null;
  }>;
  web_search: {
    provider: string;
    api_key_hint?: string | null;
    base_url?: string | null;
    providers: Array<{
      name: string;
      label: string;
      credential: "none" | "api_key" | "base_url";
    }>;
  };
  runtime: {
    config_path: string;
  };
  requires_restart: boolean;
}

export interface SettingsUpdate {
  model?: string;
  provider?: string;
}

export interface ProviderSettingsUpdate {
  provider: string;
  apiKey?: string;
  apiBase?: string;
}

export interface WebSearchSettingsUpdate {
  provider: string;
  apiKey?: string;
  baseUrl?: string;
}

export interface SlashCommand {
  command: string;
  title: string;
  description: string;
  icon: string;
  argHint?: string;
}

export type ConnectionStatus =
  | "idle"
  | "connecting"
  | "open"
  | "reconnecting"
  | "closed"
  | "error";

export type InboundEvent =
  | { event: "ready"; chat_id: string; client_id: string }
  | { event: "attached"; chat_id: string }
  | {
      event: "message";
      chat_id: string;
      text: string;
      reply_to?: string;
      media?: string[];
      media_urls?: Array<{ url: string; name?: string }>;
      /** Present when the frame is an agent breadcrumb (e.g. tool hint,
       * generic progress line) rather than a conversational reply. */
      kind?: "tool_hint" | "progress" | "reasoning";
    }
  | {
      event: "delta";
      chat_id: string;
      text: string;
      stream_id?: string;
    }
  | {
      event: "stream_end";
      chat_id: string;
      stream_id?: string;
    }
  | {
      event: "reasoning_delta";
      chat_id: string;
      text: string;
      stream_id?: string;
    }
  | {
      event: "reasoning_end";
      chat_id: string;
      stream_id?: string;
    }
  | {
      event: "runtime_model_updated";
      model_name: string;
      model_preset?: string | null;
    }
  | { event: "turn_end"; chat_id: string }
  | { event: "session_updated"; chat_id: string }
  | { event: "error"; chat_id?: string; detail?: string };

/** Base64-encoded image attached to an outbound ``message`` envelope.
 *
 * ``data_url`` must be a ``data:image/<png|jpeg|webp|gif>;base64,...`` string
 * — the server whitelists those MIME types and rejects everything else
 * (including SVG, to avoid an XSS surface). ``name`` is advisory: it's
 * preserved for the file on disk and surfaced as the placeholder label when
 * the session is replayed.
 */
export interface OutboundMedia {
  data_url: string;
  name?: string;
}

export interface OutboundImageGeneration {
  enabled: true;
  aspect_ratio?: string | null;
}

export type Outbound =
  | { type: "new_chat" }
  | { type: "attach"; chat_id: string }
  | {
      type: "message";
      chat_id: string;
      content: string;
      media?: OutboundMedia[];
      image_generation?: OutboundImageGeneration;
      /** Marks messages sent by the embedded WebUI, without changing the
       * generic websocket protocol for other clients. */
      webui?: true;
    };
