/**
 * Pure UI-domain types — TS mirrors of the backend DTOs (camelCase wire format,
 * ARCHITECTURE §1.3). No React, no Tauri here: these are plain data shapes the
 * ports speak in, so view logic and gateways stay testable in isolation.
 */

/** Health report returned by the `health` gateway/command. */
export interface HealthReport {
  version: string;
  alive: boolean;
  timeMillis: number;
  correlationId: string;
  note: string | null;
}

// ---------------------------------------------------------------------------
// App-exit work guard (ticket #83) — confirmation before quitting IdeA while
// agents/background tasks are active. Mirrors the backend
// `AppExitWorkGuardStateDto` (main window only; detached windows don't carry
// this guard).
// ---------------------------------------------------------------------------

/** One active work item contributing to {@link AppExitWorkGuardState}. */
export type AppExitWorkGuardDetail =
  | {
      kind: "busyAgent";
      projectId: string;
      projectName: string;
      agentId: string;
      agentName: string;
      ticketId: string | null;
    }
  | {
      kind: "activeBackgroundTask";
      projectId: string;
      projectName: string;
      agentId: string;
      agentName: string;
      taskId: string;
      state: string;
      taskKind: string;
    };

/** App-wide shutdown guard read model, carried by the `app-exit-work-guard` event. */
export interface AppExitWorkGuardState {
  hasWorkInProgress: boolean;
  busyAgentCount: number;
  activeBackgroundTaskCount: number;
  totalWorkCount: number;
  details: AppExitWorkGuardDetail[];
}

/**
 * Lifecycle status of a local model server during an agent launch (F35, mirror
 * of the backend `ModelServerStatusDto`, tagged on `state`, camelCase wire).
 *
 * Contract note — the backend event carries the `serverId` on the *envelope*
 * ({@link DomainEvent} `modelServerStatusChanged`), not inside the status, and it
 * does NOT emit `baseURL`/`model` in the status payload: the UI reads what the
 * backend actually sends. `reused` (on `ready`) tells apart a freshly-started
 * server from a reused running one.
 */
export type ModelServerStatus =
  | { state: "notConfigured" }
  | { state: "probing" }
  | { state: "starting" }
  | {
      /**
       * The server is downloading/preparing a remote model. Progress fields are
       * optional (all `null` in the F1 MVP; the bar/% display is the F2 stretch):
       * the UI only needs `downloading` to raise the cell overlay.
       */
      state: "downloading";
      downloadedBytes: number | null;
      totalBytes: number | null;
      percent: number | null;
      source: string | null;
    }
  | { state: "ready"; reused: boolean }
  | { state: "failed"; code: string; message: string };

/**
 * Stop policy of a managed local model server (mirror of the backend
 * `StopPolicyDto`, camelCase wire):
 * - `keepAlive`: leave the process running,
 * - `stopOnAppExit`: stop when IdeA exits (the effective default),
 * - `stopWhenUnused`: stop when no profile references it.
 */
export type StopPolicy = "keepAlive" | "stopOnAppExit" | "stopWhenUnused";

/**
 * Where the served `.gguf` model comes from (F35 V2, mirror of the backend
 * `ModelSourceDto`, camelCase wire, tagged on `type`):
 * - `localPath`: an absolute `.gguf` path already present on disk (`--model`),
 * - `huggingFace`: a `namespace/repo[:quant]` reference llama.cpp downloads and
 *   caches automatically (`-hf`).
 */
export type ModelSource =
  | { type: "localPath"; path: string }
  | { type: "huggingFace"; repo: string };

/**
 * A declared local model server (F35, mirror of the backend flat
 * `LocalModelServerConfigDto`, camelCase wire). Global to IdeA (not project
 * scoped). Referenced by an OpenCode profile through
 * `OpenCodeConfig.localModelServerId` (= this `id`).
 *
 * Contract notes:
 * - `id` is a UUID **minted client-side** on create (the backend requires a valid
 *   UUID; it does not generate the server id, only an internal model id it hides).
 * - `servedModelName` is what the user configures and what is sent to OpenCode as
 *   the `model` — the backend's internal `model.id`/`model.label` are not exposed.
 * - `modelSource` (V2) replaces the former `modelPath`; it is required by the
 *   backend when `autoStart` is `true`.
 */
export interface LocalModelServerConfig {
  id: string;
  /** Only `llamaCpp` is supported today (camelCase, not "llamacpp"). */
  kind: "llamaCpp";
  name: string;
  /** OpenAI-compatible base URL served by `llama-server` (normalised to `/v1`). */
  baseURL: string;
  port: number;
  /** Explicit model source (`.gguf` path or Hugging Face ref). Required for auto-start. */
  modelSource?: ModelSource;
  /** Model name exposed by the server and sent to OpenCode as `model`. */
  servedModelName: string;
  /** Explicit `llama-server` executable path/command (optional). */
  binaryPath?: string;
  /** llama.cpp `--host` (default `127.0.0.1`). */
  host: string;
  /** llama.cpp `-ngl` GPU layers (optional; `0` = CPU only). */
  gpuLayers?: number;
  /** llama.cpp `-c` context size (optional). */
  contextSize?: number;
  /** Whether to pass `--jinja` (Jinja chat template). */
  jinja: boolean;
  args: string[];
  autoStart: boolean;
  stopPolicy: StopPolicy;
}

/**
 * A previewed `llama-server` command line (F35 V2, mirror of the backend
 * `PreviewModelServerCommandDto`). Built by the backend from a draft config —
 * never reconstructed client-side.
 */
export interface ModelServerCommandPreview {
  /** Resolved executable (e.g. `llama-server` or an absolute path). */
  command: string;
  /** Argument vector, unescaped. */
  args: string[];
  /** Human-readable, shell-escaped command line. */
  display: string;
}

/**
 * How the embedded web server is exposed (ticket #68, mirror of the backend
 * `ServerExposureMode`).
 *
 * - `localOnly` — binds loopback, no remote access.
 * - `remoteProxyLocal` — binds loopback; an HTTPS reverse proxy runs on *this*
 *   machine and reaches IdeA over loopback.
 * - `remoteProxyOtherMachine` — binds a concrete LAN address so a proxy on
 *   *another* host can reach it; that proxy must be declared in
 *   `trustedProxies`.
 */
export type ServerExposureMode =
  | "localOnly"
  | "remoteProxyLocal"
  | "remoteProxyOtherMachine";

/**
 * Persisted embedded-server exposure settings (mirror of the backend
 * `ServerExposureSettingsDto`).
 *
 * The backend validates every field on save/start; the UI never derives network
 * facts (LAN addresses, upstream URL) from these — it asks `previewExposure`.
 */
export interface ServerExposureSettings {
  mode: ServerExposureMode;
  /** TCP port to bind. `0` asks the OS for an ephemeral port. */
  port: number;
  /** Public HTTPS origin, required by both remote modes. */
  publicOrigin?: string;
  /**
   * Peers allowed to contact IdeA, as IPs or CIDRs. **Not** a listen address.
   * Required (non-empty) by `remoteProxyOtherMachine`.
   */
  trustedProxies: string[];
  /**
   * Concrete LAN address to bind, required by `remoteProxyOtherMachine`. Must
   * come from {@link ServerExposurePreview.candidateLanAddresses} — the UI is
   * never allowed to invent one.
   */
  lanBindAddress?: string;
}

/** A non-fatal diagnostic from the exposure preview (never blocks a start). */
export interface DiagnosticWarning {
  /** Stable warning code (e.g. `missingTrustedProxy`). */
  code: string;
  /** Human-readable, actionable message. */
  message: string;
}

/**
 * Backend-derived preview of a draft exposure config (mirror of
 * `ServerExposurePreviewDto`). The backend is the sole authority on LAN
 * addresses and the proxy upstream URL.
 */
export interface ServerExposurePreview {
  /** LAN addresses the backend discovered on this host. */
  candidateLanAddresses: string[];
  /** URL the reverse proxy should target; absent in `localOnly`. */
  upstreamUrl?: string;
  /** Non-fatal diagnostics to surface alongside the form. */
  warnings: DiagnosticWarning[];
}

/** Embedded-server lifecycle state (mirror of `EmbeddedServerStatusStateDto`). */
export type EmbeddedServerState =
  | "stopped"
  | "starting"
  | "running"
  | "stopping"
  | "failed";

/**
 * Embedded-server status (mirror of `EmbeddedServerStatusDto`).
 *
 * Carries **no pairing code** (#77): a code no longer exists while the server
 * runs, only when someone asks for one. Generating is a device-management action
 * ({@link PairingCode}), not a property of the server's status.
 */
export interface EmbeddedServerStatus {
  state: EmbeddedServerState;
  /** Local URL, when running. */
  localUrl?: string;
  /** Public URL, when a remote mode is configured. */
  publicUrl?: string;
  /** Upstream URL to hand to the reverse proxy. */
  upstreamUrl?: string;
  /** Last failure, when `state` is `failed`. */
  error?: GatewayError;
}

/** A domain event relayed from the backend (tagged union on `type`). */
export type DomainEvent =
  | { type: "projectCreated"; projectId: string }
  | { type: "agentLaunched"; agentId: string; sessionId: string }
  | { type: "agentExited"; agentId: string; code: number }
  | {
      /**
       * A local model server changed lifecycle state while (re)launching an
       * agent whose OpenCode profile binds `localModelServerId` (F35). Keyed by
       * `serverId`; the frontend correlates it back to the agent through the
       * profile's `opencode.localModelServerId`.
       */
      type: "modelServerStatusChanged";
      serverId: string;
      status: ModelServerStatus;
    }
  | {
      /**
       * An agent launch failed before a runtime session was created (F35). Unlike
       * a generic "agent failed", this carries an actionable `message` and a
       * stable `code`; `cause` namespaces the failure (e.g. `"model_server"`).
       */
      type: "agentLaunchFailed";
      agentId: string;
      cause: string;
      code: string;
      message: string;
    }
  | { type: "agentProfileChanged"; agentId: string; profileId: string }
  | { type: "agentBusyChanged"; agentId: string; busy: boolean }
  | {
      type: "backgroundTaskChanged";
      projectId: string;
      taskId: string;
      agentId: string;
      state: string;
    }
  | {
      type: "agentInboxChanged";
      agentId: string;
      depth: number;
      action: string;
    }
  | {
      type: "agentWakeChanged";
      projectId: string;
      agentId: string;
      action: string;
      reason?: string;
    }
  | {
      /**
       * A delegation is ready to be injected into the agent's native terminal
       * (ARCHITECTURE §20). The backend is the queue/busy authority but no longer
       * PTY-writes the turn: the frontend write-portal runs the handshake (b→e)
       * and writes `text` + `submitSequence`. `submitSequence`/`submitDelayMs`
       * come from the target's profile; absent ⇒ the portal applies its defaults
       * (`"\r"`, ~350 ms).
       */
      type: "delegationReady";
      agentId: string;
      ticket: string;
      text: string;
      submitSequence?: string;
      submitDelayMs?: number;
    }
  | {
      /**
       * An agent entered a session/rate limit (ARCHITECTURE §21). Low-frequency,
       * model-agnostic. `resetsAtMs` is the reset instant in epoch-milliseconds;
       * absent ⇒ unknown (no reliable time → no auto-resume). The frontend badges
       * "limité jusqu'à HH:MM" when known, "limité" otherwise.
       */
      type: "agentRateLimited";
      agentId: string;
      resetsAtMs?: number;
    }
  | {
      /**
       * An auto-resume was armed for a rate-limited agent (ARCHITECTURE §21).
       * `fireAtMs` is the wake-up deadline in epoch-milliseconds. The frontend
       * shows the countdown + the "Annuler la reprise" button (cancellable window).
       */
      type: "agentResumeScheduled";
      agentId: string;
      fireAtMs: number;
    }
  | {
      /**
       * An agent's auto-resume was cancelled (ARCHITECTURE §21): the user clicked
       * "Annuler la reprise". The frontend removes the countdown but keeps the
       * "limité" state (no auto-resume will fire).
       */
      type: "agentResumeCancelled";
      agentId: string;
    }
  | {
      /**
       * An agent was effectively resumed after a limit (ARCHITECTURE §21): the
       * wake-up fired (or an immediate resume). The frontend clears all limit state.
       */
      type: "agentResumed";
      agentId: string;
    }
  | {
      /**
       * Human net (level 3): a session limit is suspected without any reliable
       * reset time (ARCHITECTURE §21.1). IdeA never resumes blind: this asks the
       * frontend to surface an "heure inconnue" state and prompt the user.
       * `resetsAtMs` carries an estimate when one exists, else absent.
       */
      type: "agentRateLimitSuspected";
      agentId: string;
      resetsAtMs?: number;
    }
  | { type: "templateUpdated"; templateId: string; version: number }
  | { type: "agentDriftDetected"; agentId: string; from: number; to: number }
  | { type: "agentSynced"; agentId: string; to: number }
  | { type: "layoutChanged"; projectId: string }
  | { type: "remoteConnected"; projectId: string }
  | { type: "gitStateChanged"; projectId: string }
  | { type: "memorySaved"; slug: string }
  | {
      type: "orchestratorRequestProcessed";
      requesterId: string;
      action: string;
      ok: boolean;
      /**
       * Which entry door the delegation arrived through: `"mcp"` (MCP server) or
       * `"file"` (`.ideai/requests` watcher). Optional for backward-compatibility:
       * an event relayed without it simply yields no source badge.
       */
      source?: "mcp" | "file";
    }
  | { type: "issueCreated"; issueId: string; issueRef: string }
  | { type: "issueUpdated"; issueRef: string; version: number }
  | {
      /**
       * A ticket was deleted (ticket #6). Mirror of the backend
       * `DomainEventDto::IssueDeleted`. `issueRef` is the deleted `#N`;
       * `freedSprint` is the sprint id it was released from, when any. Starts with
       * `issue`, so {@link isTicketEvent} matches it and ticket lists refresh
       * automatically; an open detail on the same `issueRef` closes.
       */
      type: "issueDeleted";
      projectId: string;
      issueRef: string;
      freedSprint: string | null;
    }
  | {
      type: "issueStatusChanged";
      issueRef: string;
      status: TicketStatus;
      version: number;
    }
  | {
      type: "issuePriorityChanged";
      issueRef: string;
      priority: TicketPriority;
      version: number;
    }
  | { type: "issueCarnetUpdated"; issueRef: string; version: number }
  | {
      type: "issueLinked";
      issueRef: string;
      target: string;
      kind: TicketLinkKind;
      version: number;
    }
  | {
      type: "issueUnlinked";
      issueRef: string;
      target: string;
      kind: TicketLinkKind;
      version: number;
    }
  | {
      type: "issueAgentAssigned";
      issueRef: string;
      agentId: string;
      version: number;
    }
  | {
      type: "issueAgentUnassigned";
      issueRef: string;
      agentId: string;
      version: number;
    }
  | {
      /**
       * A ticket changed sprint membership (ticket #10). Mirror of the backend
       * `DomainEventDto::IssueSprintChanged`. `from`/`to` are sprint ids (`null`
       * ⇒ no sprint). Starts with `issue`, so {@link isTicketEvent} matches it
       * and ticket lists refresh automatically.
       */
      type: "issueSprintChanged";
      issueRef: string;
      from: string | null;
      to: string | null;
      version: number;
    }
  | { type: "sprintCreated"; sprintId: string; order: number }
  | { type: "sprintRenamed"; sprintId: string; name: string; version: number }
  | {
      type: "sprintReordered";
      sprintId: string;
      order: number;
      version: number;
    }
  | { type: "sprintDeleted"; sprintId: string }
  | {
      /**
       * A ticket AI assistant chat was opened (ticket #8). Mirror of the backend
       * `DomainEventDto::TicketAssistantOpened`. `issueRef` is the bound ticket,
       * `profileId` the runtime AI profile driving the assistant.
       */
      type: "ticketAssistantOpened";
      issueRef: string;
      profileId: string;
    }
  | {
      /** A ticket AI assistant chat was closed/disposed (ticket #8). */
      type: "ticketAssistantClosed";
      issueRef: string;
    }
  | {
      /**
       * An intermediate assistant announcement emitted **during** an inter-agent
       * structured turn (ticket #4, "annonces inter-agent"). Mirror of the backend
       * `DomainEventDto::AgentAnnouncement`. Ephemeral (never persisted): the front
       * folds these into a live, bounded, per-`(target, ticketId)` index that feeds
       * the target-cell overlay (F3) and the requester-cell preview (F2).
       *
       * `requester` is `"user"` or the requesting agent's id; `target` is the agent
       * being contacted; `text` is the human-readable réflexion/annonce.
       */
      type: "agentAnnouncement";
      projectId: string;
      requester: string;
      target: string;
      ticketId: string;
      text: string;
      atMs: number;
    }
  | { type: "ptyOutput"; sessionId: string; bytes: number[] };

/**
 * Whether a domain event is one of the ticket (`Issue*`) events. A small helper
 * so ticket view-models refresh on any ticket mutation without re-enumerating
 * every variant at each call site.
 */
export function isTicketEvent(
  event: DomainEvent,
): event is Extract<DomainEvent, { type: `issue${string}` }> {
  return event.type.startsWith("issue");
}

/**
 * Whether a domain event is a sprint lifecycle event (`sprint*`). Lets the
 * ticket view-model re-fetch the sprint list when sprints are created, renamed,
 * reordered or deleted (ticket #10).
 */
export function isSprintEvent(
  event: DomainEvent,
): event is Extract<DomainEvent, { type: `sprint${string}` }> {
  return event.type.startsWith("sprint");
}

/** Where a project physically lives (mirror of the backend `RemoteRef`). */
export type RemoteRef =
  | { kind: "local" }
  | {
      kind: "ssh";
      host: string;
      port: number;
      user: string;
      auth: unknown;
      remoteRoot: string;
    }
  | { kind: "wsl"; distro: string };

/** A project as returned by the `project` gateway (mirror of `ProjectDto`). */
export interface Project {
  id: string;
  name: string;
  root: string;
  remote: RemoteRef;
  createdAt: number;
}

/** Stable error shape mirrored from the backend `ErrorDto`. */
export interface GatewayError {
  code: string;
  message: string;
}

// ---------------------------------------------------------------------------
// Work state (UX conversations/delegations live read-model)
// ---------------------------------------------------------------------------

/** Live session currently associated with an agent in the work-state read-model. */
export interface LiveWorkSession {
  nodeId: string;
  sessionId: string;
  kind: "pty" | "structured";
}

/** Busy/idle status for an agent in the work-state read-model. */
export type WorkBusyState =
  | { state: "idle" }
  | { state: "busy"; ticket: string; sinceMs: number };

/** FIFO status for an input/ticket currently tracked by the backend queue. */
export type TicketWorkStatus = "inProgress" | "queued";

/** Who produced an input/ticket in the work-state read-model. */
export type TicketWorkSource =
  | { kind: "human" }
  | { kind: "agent"; agentId: string };

/** One queued or in-progress input/ticket for an agent. */
export interface AgentTicketState {
  ticketId: string;
  conversationId: string;
  position: number;
  status: TicketWorkStatus;
  source: TicketWorkSource;
  requesterLabel: string;
  taskPreview: string;
  taskLen: number;
}

/** One first-class background task visible in the work-state read-model. */
export interface BackgroundCompletion {
  taskId: string;
  ownerAgentId: string;
  projectId: string;
  kind: string;
  status: "running" | "completed" | "failed" | "cancelled" | "pending" | "delivered";
  exitCode: number | null;
  /** Human-readable summary / error / reason of a terminal result (ticket #5). */
  summary: string | null;
  stdoutTail: string | null;
  stderrTail: string | null;
  /** Last-update timestamp (epoch ms); chronological ordering key. */
  updatedAtMs: number;
}

/** One item currently pending in an agent's inbox. */
export interface InboxItem {
  id: string;
  agentId: string;
  source: string;
  kind: string;
  body: string;
  createdAtMs: number;
  correlationId?: string;
  priority: number;
}

/** Availability of a compact conversation summary in the work-state read-model. */
export type ConversationPreviewStatus =
  | "ready"
  | "missing"
  | "partial"
  | "unavailable";

/** Recent turn preview attached to a conversation summary. */
export interface ConversationTurnWorkPreview {
  role: "prompt" | "response" | "toolActivity";
  source: TicketWorkSource;
  atMs: number;
  textPreview: string;
  textLen: number;
}

/** Compact thread context joined to tickets by conversation id. */
export interface ConversationWorkSummary {
  conversationId: string;
  status: ConversationPreviewStatus;
  objectivePreview: string | null;
  summaryPreview: string | null;
  summaryLen: number;
  upTo: string | null;
  recentTurns: ConversationTurnWorkPreview[];
}

/** One agent row in the project work-state read-model. */
export interface AgentWorkState {
  agentId: string;
  name: string;
  profileId: string;
  live?: LiveWorkSession;
  busy: WorkBusyState;
  tickets: AgentTicketState[];
  inboxDepth?: number;
  inbox?: InboxItem[];
  backgroundTasks?: BackgroundCompletion[];
}

/** Minimal read-only live-state surface for a project. */
export interface ProjectWorkState {
  agents: AgentWorkState[];
  conversations: ConversationWorkSummary[];
}

// ---------------------------------------------------------------------------
// Conversation transcript (LS7 — human thread-per-pair viewer, mirror of LS6 DTO)
// ---------------------------------------------------------------------------

/** Nature of a transcript turn (mirror of the backend `TurnRole`). */
export type TurnRole = "prompt" | "response" | "toolActivity";

/** Origin of a transcript turn (mirror of the backend `TurnSource`). */
export type TurnSource = { kind: "human" } | { kind: "agent"; agentId: string };

/** One turn of the human transcript — full text, never truncated. */
export interface TurnView {
  id: string;
  atMs: number;
  role: TurnRole;
  source: TurnSource;
  text: string;
  textLen: number;
}

/** Pagination travel direction (`"backward"` = towards older turns). */
export type PageDirection = "forward" | "backward";

/** A page of the human transcript, oldest-to-newest. */
export interface TurnPage {
  turns: TurnView[];
  hasMore: boolean;
  nextAnchor?: string;
}

// ---------------------------------------------------------------------------
// Permissions (LP1)
// ---------------------------------------------------------------------------

/** Agent capability governed by IdeA permissions. */
export type Capability = "read" | "write" | "delete" | "executeBash";

/** Permission rule verdict. */
export type PermissionEffect = "allow" | "deny";

/** Fallback stance when no permission rule matches. */
export type PermissionPosture = "ask" | "allow" | "deny";

/** Glob path scope, relative to the project root. */
export type PathScope = string[];

/** Shell command matcher. */
export type CommandMatcher =
  | { kind: "exact"; value: string }
  | { kind: "prefix"; value: string }
  | { kind: "glob"; value: string };

/** Per-command bash verdict. */
export interface CommandRule {
  matcher: CommandMatcher;
  effect: PermissionEffect;
}

/** One permission rule. */
export interface PermissionRule {
  capability: Capability;
  effect: PermissionEffect;
  paths?: PathScope;
  commands?: CommandRule[];
}

/** Permission policy bundle. */
export interface PermissionSet {
  rules: PermissionRule[];
  fallback: PermissionPosture;
}

/** One sparse agent override in `.ideai/permissions.json`. */
export interface AgentPermissionOverride {
  agentId: string;
  permissions: PermissionSet;
}

/** Full project permission document. */
export interface ProjectPermissions {
  version: number;
  projectDefaults?: PermissionSet;
  agents?: AgentPermissionOverride[];
}

/** Effective project+agent policy. */
export interface EffectivePermissions {
  rules: PermissionRule[];
  fallback: PermissionPosture;
}

// ---------------------------------------------------------------------------
// MCP tool permissions (ticket #82) — distinct from the file/command
// permissions above: an allowlist of exact MCP tool names, applied by the MCP
// server/bridge before dispatch, not by Landlock/sandbox.
// ---------------------------------------------------------------------------

/** Allowlist-based MCP tool policy: exact tool names permitted. */
export interface McpToolPolicy {
  allowedTools: string[];
}

/** One agent's MCP tool policy override, replacing the project default entirely. */
export interface AgentMcpToolPolicyOverride {
  agentId: string;
  policy: McpToolPolicy;
}

/**
 * Backend-canonical classification of the MCP tool catalogue. The frontend
 * must treat this as the source of truth for read vs write/action — never
 * hardcode the split locally (ticket #82 acceptance criteria).
 */
export interface McpToolCatalogue {
  readOnlyTools: string[];
  writeActionTools: string[];
}

/** Full per-project MCP tool permission document, mirroring the backend DTO. */
export interface ProjectMcpToolPermissions {
  version: number;
  catalogue: McpToolCatalogue;
  projectDefault: McpToolPolicy | null;
  agents: AgentMcpToolPolicyOverride[];
}

// ---------------------------------------------------------------------------
// Layout (L4) — mirror of the domain `LayoutTree` (ARCHITECTURE §3, §7).
// ---------------------------------------------------------------------------

/** Split direction: `row` = columns (left→right), `column` = rows (top→bottom). */
export type Direction = "row" | "column";

/** A terminal-hosting leaf cell. `session` is the hosted SessionId, if any.
 * `agent` is the agent id if an agent is pinned to this cell (absent = plain terminal).
 * `conversationId` is the persistent CLI conversation id (survives PTY close/reopen,
 * lets the agent resume); omitted when absent (mirrors the backend `skip_serializing_if`).
 * `agentWasRunning` records whether the cell's agent process was running at close time;
 * omitted when `false`. */
export interface LeafCell {
  id: string;
  session?: string | null;
  agent?: string;
  conversationId?: string;
  agentWasRunning?: boolean;
}

/** A weighted child of a split. `weight` is a relative (`> 0`) share. */
export interface WeightedChild {
  node: LayoutNode;
  weight: number;
}

/** A weighted n-ary split (rows or columns). */
export interface SplitContainer {
  id: string;
  direction: Direction;
  children: WeightedChild[];
}

/** A spreadsheet grid cell placement with spans. */
export interface GridCell {
  node: LayoutNode;
  row: number;
  col: number;
  rowSpan: number;
  colSpan: number;
}

/** A spreadsheet-style grid with per-row/col weights and span-based merging. */
export interface GridContainer {
  id: string;
  colWeights: number[];
  rowWeights: number[];
  cells: GridCell[];
}

/**
 * A node in the layout tree. Tagged on `type` with the payload under `node`,
 * matching the backend `#[serde(tag = "type", content = "node")]`.
 */
export type LayoutNode =
  | { type: "leaf"; node: LeafCell }
  | { type: "split"; node: SplitContainer }
  | { type: "grid"; node: GridContainer };

/** The root of a tab's terminal layout. */
export interface LayoutTree {
  root: LayoutNode;
}

/**
 * A layout mutation (mirror of the backend `LayoutOperationDto`, tagged on
 * `type`). Node/session ids are UUID strings.
 */
export type LayoutOperation =
  | {
      type: "split";
      target: string;
      direction: Direction;
      newLeaf: string;
      container: string;
    }
  | { type: "merge"; container: string; keepIndex: number }
  | { type: "resize"; container: string; weights: number[] }
  | { type: "move"; from: string; to: string }
  | { type: "setSession"; target: string; session?: string | null }
  | { type: "setCellAgent"; target: string; agent: string | null }
  | { type: "setCellConversation"; target: string; conversationId: string | null }
  | { type: "setAgentRunning"; target: string; running: boolean };

/** The kind of a named layout. */
export type LayoutKind = "terminal" | "gitGraph";

/** Named layout entry returned by `listLayouts`. */
export interface LayoutInfo {
  id: string;
  name: string;
  kind: LayoutKind;
}

/**
 * A commit node in the full git graph (DAG). `refs` carries short branch/tag
 * names (e.g. `"main"`, `"tag: v1.0"`). `timestamp` is seconds since Unix epoch.
 */
export interface GraphCommit {
  hash: string;
  summary: string;
  parents: string[];
  refs: string[];
  author: string;
  timestamp: number;
}

/** Response of `listLayouts`. */
export interface LayoutList {
  layouts: LayoutInfo[];
  activeId: string;
}

/** Unsubscribe handle returned by event subscriptions. */
export type Unsubscribe = () => void;

// ---------------------------------------------------------------------------
// AI profiles & first-run (L5) — mirror of the domain `AgentProfile` /
// `ContextInjection` (CONTEXT §9, ARCHITECTURE §3).
// ---------------------------------------------------------------------------

/**
 * Context-injection strategy (tagged on `strategy`, mirror of the backend
 * `ContextInjection`):
 * - `conventionFile`: write the `.md` to a conventional file (e.g. `CLAUDE.md`),
 * - `flag`: pass the context path through a CLI flag,
 * - `stdin`: pipe the content on stdin,
 * - `env`: pass the context via an environment variable.
 */
export type ContextInjection =
  | { strategy: "conventionFile"; target: string }
  | { strategy: "flag"; flag: string }
  | { strategy: "stdin" }
  | { strategy: "env"; var: string };

/** The four injection strategy discriminants. */
export type InjectionStrategy = ContextInjection["strategy"];

/**
 * Structured-execution adapter of a profile (mirror of the backend
 * `StructuredAdapter`, camelCase wire format). Absent (`structuredAdapter`
 * omitted) ⇒ the profile is a plain TUI/PTY agent (historical behaviour);
 * present ⇒ the profile is selectable as a structured AI agent:
 * - `claude` / `codex` / `openCode`: driven by a spawned CLI binary,
 * - `openAiCompatible`: driven by IdeA's native HTTP adapter against an
 *   OpenAI-compatible chat server (Ollama, llama.cpp, a LAN runtime) — see
 *   {@link HttpChatConfig}.
 */
export type StructuredAdapter = "claude" | "codex" | "openCode" | "openAiCompatible";

/**
 * HTTP configuration of an OpenAI-compatible chat server (mirror of the backend
 * `HttpChatConfig`, camelCase wire format). Carried by a profile whose
 * {@link StructuredAdapter} is `openAiCompatible`.
 *
 * Transparent by design: secrets are never stored here — `apiKeyEnv` is the
 * *name* of an environment variable, never the key itself (the backend resolves
 * it at call time).
 */
export interface HttpChatConfig {
  /** Root or `/chat/completions` endpoint of the HTTP server (http/https). */
  endpoint: string;
  /** Model name sent in the `/chat/completions` payload. */
  model: string;
  /** Name of the env var holding the API key (optional). Never the key. */
  apiKeyEnv?: string;
  /** Per-turn request timeout, in milliseconds. */
  requestTimeoutMs?: number;
  /** Connection timeout, in milliseconds. */
  connectTimeoutMs?: number;
  /** Tool-calling re-loop guard for one turn (defaults to ~16 backend-side). */
  maxToolIterations?: number;
}

/**
 * Configuration for an OpenCode process-backed profile pointed at its own
 * llama.cpp (OpenAI-compatible) endpoint. Mirror of the backend `OpenCodeConfig`
 * (camelCase wire format: `baseURL`, `apiKey?`, `model`, `reasoning?`,
 * `attachment?`). OpenCode is a locally-piloted CLI, distinct from the in-process
 * {@link HttpChatConfig} adapter.
 */
export interface OpenCodeConfig {
  /**
   * OpenAI-compatible base URL served by `llama-server` (http/https), for
   * example `http://localhost:8080/v1`.
   */
  baseURL: string;
  /**
   * Optional API key forwarded to the provider — the key *itself*, not an env
   * var name (a local llama.cpp typically needs none / a placeholder). Omitted
   * when blank (mirrors the backend `skip_serializing_if`).
   */
  apiKey?: string;
  /** Model name served by `llama-server`, for example `qwen3-coder-30b`. */
  model: string;
  /** Enables model reasoning. Effective default: `true`. */
  reasoning?: boolean;
  /** Enables attachments. Effective default: `false`. */
  attachment?: boolean;
  /**
   * Optional id of a managed local model server (F35) this profile binds to.
   * When set, IdeA can start/stop the referenced `llama-server` for the profile;
   * omitted when the profile points at an externally-managed endpoint (mirrors
   * the backend `skip_serializing_if`). Opaque string id on the wire.
   */
  localModelServerId?: string;
}

/**
 * A declarative AI-CLI profile (mirror of the backend `AgentProfile`). `id` is a
 * UUID string; `detect` is the optional detection command line.
 */
export interface AgentProfile {
  id: string;
  name: string;
  command: string;
  args: string[];
  contextInjection: ContextInjection;
  detect: string | null;
  cwdTemplate: string;
  /** Optional CLI flags for agent-session continuity: `assignFlag` to bind a
   * conversation id at launch, `resumeFlag` to resume an existing conversation. */
  session?: { assignFlag?: string; resumeFlag: string };
  /**
   * Structured-execution adapter (mirror of the backend `structured_adapter`,
   * `skip_serializing_if = Option::is_none`). Absent ⇒ plain TUI/PTY profile;
   * present ⇒ selectable structured AI agent. Additive: existing Claude/Codex
   * profiles are unaffected.
   */
  structuredAdapter?: StructuredAdapter;
  /**
   * HTTP config for a `openAiCompatible` structured adapter (mirror of the
   * backend `chat_http`, `skip_serializing_if = Option::is_none`). Absent for
   * every historical profile and every non-HTTP adapter.
   */
  chatHttp?: HttpChatConfig;
  /** OpenCode process-backed config. Present for `structuredAdapter: "openCode"`. */
  opencode?: OpenCodeConfig;
}

/** Availability of a candidate profile after detection (mirror of the DTO). */
export interface ProfileAvailability {
  profile: AgentProfile;
  available: boolean;
}

/** First-run state (mirror of `FirstRunStateDto`). */
export interface FirstRunState {
  isFirstRun: boolean;
  referenceProfiles: AgentProfile[];
}

// ---------------------------------------------------------------------------
// Agents (L6) — mirror of the domain `Agent` / `AgentOrigin` (ARCHITECTURE §6).
// ---------------------------------------------------------------------------

/**
 * Origin of an agent (tagged on `type`, mirror of the backend `AgentOrigin`):
 * - `scratch`: created from scratch, no template link,
 * - `fromTemplate`: derived from a template, tracking the last synced version.
 */
export type AgentOrigin =
  | { type: "scratch" }
  | { type: "fromTemplate"; templateId: string; syncedTemplateVersion: number };

/**
 * A project-scoped agent (mirror of the backend `Agent` DTO, camelCase wire
 * format). `contextPath` is the relative path of the agent's `.md` within
 * `.ideai/` (e.g. `agents/foo.md`).
 */
export interface Agent {
  id: string;
  name: string;
  contextPath: string;
  profileId: string;
  origin: AgentOrigin;
  synchronized: boolean;
  /** Skills assigned to this agent (injected into its convention file). */
  skills: SkillRef[];
}

/**
 * A terminal/PTY session as returned by the backend (mirror of
 * `TerminalSessionDto`, camelCase wire format). Surfaced by `changeAgentProfile`
 * as the freshly relaunched session when a live agent was hot-swapped.
 */
export interface TerminalSession {
  /** Stable session id (UUID) — used for write/resize/close + the output channel. */
  sessionId: string;
  /** Working directory the shell runs in. */
  cwd: string;
  /** Current rows. */
  rows: number;
  /** Current cols. */
  cols: number;
  /**
   * Conversation id assigned by this (re)launch, when the profile supports
   * session assignment and the hosting cell had none yet; absent otherwise.
   */
  assignedConversationId?: string;
}

/**
 * An agent that can be resumed when its project is (re)opened (mirror of the
 * backend `ResumableAgentDto`, camelCase wire format; ARCHITECTURE §15.2). It is
 * a read-only inventory entry computed at open time — never a spawn:
 * - `nodeId` is the layout leaf hosting the agent (where to relaunch),
 * - `conversationId` is the persisted CLI conversation id; absent ⇒ relaunch fresh,
 * - `wasRunning` is `agentWasRunning` frozen at close time (drives the status label),
 * - `resumeSupported` reflects whether the agent's profile exposes a usable
 *   session strategy; when `false`, "Reprendre" relaunches fresh ("relance à neuf").
 */
export interface ResumableAgent {
  agentId: string;
  name: string;
  nodeId: string;
  conversationId?: string;
  wasRunning: boolean;
  resumeSupported: boolean;
}

// ---------------------------------------------------------------------------
// Skills (L12) — mirror of the domain `Skill` / `SkillRef`.
// ---------------------------------------------------------------------------

/**
 * Where a skill lives (selects its backing store): `global` skills are reusable
 * across projects; `project` skills are specific to one project's `.ideai/`.
 */
export type SkillScope = "global" | "project";

/**
 * A reusable, model-agnostic workflow assignable to agents (mirror of the
 * backend `Skill` DTO, camelCase wire format).
 */
export interface Skill {
  id: string;
  name: string;
  contentMd: string;
  scope: SkillScope;
}

/** A reference from an agent to one assigned skill (mirror of `SkillRef`). */
export interface SkillRef {
  skillId: string;
  scope: SkillScope;
}

// ---------------------------------------------------------------------------
// Memory (L14) — mirror of the backend `MemoryDto` / `MemoryIndexEntryDto`.
// ---------------------------------------------------------------------------

/**
 * The category of a memory note (selects how it is recalled/injected). Mirrors
 * the backend `type` field; identity of a note is its **slug** (kebab-case).
 */
export type MemoryType = "user" | "feedback" | "project" | "reference";

/**
 * A memory note (mirror of the backend `Memory` DTO, camelCase wire format).
 * `name` doubles as the human title and the source of the slug identity.
 */
export interface Memory {
  name: string;
  description: string;
  type: MemoryType;
  content: string;
}

/**
 * One entry of the memory index (mirror of the backend `MemoryIndexEntry`):
 * a lightweight, recall-oriented projection of a note.
 */
export interface MemoryIndexEntry {
  slug: string;
  title: string;
  hook: string;
  type: MemoryType;
}

/** A resolved `[[wikilink]]` target — the slug of another note. */
export type MemoryLink = string;

// ---------------------------------------------------------------------------
// Embedder (L14 / lot C2) — mirror of the backend `EmbedderProfileDto` and
// `EmbedderEnginesDto`. Drives the memory/embedder settings panel. Identity of
// a profile is its `id`; changing the active embedder takes effect at the next
// app start (the UI says so explicitly).
// ---------------------------------------------------------------------------

/**
 * Embedding strategy of an embedder profile (mirror of the backend `strategy`).
 * `none` ⇒ no vector tier (naïve recall); the other strategies select where the
 * embeddings come from.
 */
export type EmbedderStrategy = "localOnnx" | "localServer" | "api" | "none";

/**
 * A declarative embedder profile (mirror of the backend `EmbedderProfile` DTO,
 * camelCase wire format). Transparent by design: secrets are never stored here —
 * `apiKeyEnv` is the *name* of an environment variable, never the key itself.
 */
export interface EmbedderProfile {
  id: string;
  name: string;
  strategy: EmbedderStrategy;
  /** Model id/name (ONNX model, server/api model). Omitted for `none`. */
  model?: string;
  /** Server/API endpoint URL. Omitted for `localOnnx` / `none`. */
  endpoint?: string;
  /** Name of the env var holding the API key (api strategy). Never the key. */
  apiKeyEnv?: string;
  /** Embedding vector dimension. */
  dimension: number;
}

/**
 * A recommended local ONNX engine (mirror of the backend `recommendedOnnx`
 * entry). `e5-small` is the curated default (`recommended: true`).
 */
export interface RecommendedOnnxEngine {
  id: string;
  displayName: string;
  dimension: number;
  approxSizeMb: number;
  recommended: boolean;
}

/**
 * Capabilities/availability snapshot of the embedder engines on this build
 * (mirror of the backend `describe_embedder_engines`). The `vector*Enabled`
 * flags reflect Cargo feature flags compiled into the build: a strategy whose
 * flag is `false` is shown disabled ("not available in this build").
 */
export interface EmbedderEngines {
  recommendedOnnx: RecommendedOnnxEngine[];
  ollamaDetected: boolean;
  onnxCachedModels: string[];
  vectorHttpEnabled: boolean;
  vectorOnnxEnabled: boolean;
}

// ---------------------------------------------------------------------------
// Templates (L7) — mirror of the domain `Template` / `AgentDrift`.
// ---------------------------------------------------------------------------

/**
 * A reusable agent template (mirror of the backend `Template` DTO).
 * `version` is incremented on each `updateTemplate` call.
 */
export interface Template {
  id: string;
  name: string;
  contentMd: string;
  version: number;
  defaultProfileId: string;
}

/**
 * Drift between a synchronized agent's last-synced template version and the
 * template's current version (mirror of `AgentDrift` backend DTO).
 */
export interface AgentDrift {
  agentId: string;
  from: number;
  to: number;
}

// ---------------------------------------------------------------------------
// Git (L8) — UI-domain types for the git feature.
// ---------------------------------------------------------------------------

/** A file in the working tree with its staging state. */
export interface GitFileStatus {
  path: string;
  staged: boolean;
}

/** A git commit summary (short representation). */
export interface GitCommit {
  hash: string;
  summary: string;
}

/** The list of branches and the currently checked-out branch. */
export interface GitBranches {
  branches: string[];
  current: string | null;
}

// ---------------------------------------------------------------------------
// Tickets (public wire name for the backend `Issue` model — "ticket" is the
// user-visible term; all commands/DTOs/events speak `ticket`/`issue`). Mirrors
// the `Ticket*Dto` shapes in `crates/app-tauri/src/tickets.rs` (camelCase).
// ---------------------------------------------------------------------------

/** A ticket reference on the wire, always `#<n>` (e.g. `"#42"`). */
export type TicketRef = string;

/** Lifecycle status of a ticket. */
export type TicketStatus = "open" | "inProgress" | "QA" | "closed";

/** Priority of a ticket. */
export type TicketPriority = "low" | "medium" | "high" | "critical";

/** The kind of relationship between two tickets. */
export type TicketLinkKind =
  | "relatesTo"
  | "blocks"
  | "blockedBy"
  | "duplicates"
  | "dependsOn";

/** Who performed a ticket mutation (mirror of `TicketActorDto`, tagged `kind`). */
export type TicketActor =
  | { kind: "user" }
  | { kind: "agent"; agentId: string }
  | { kind: "system" };

/** One directed link from a ticket to another (mirror of `TicketLinkDto`). */
export interface TicketLink {
  targetRef: TicketRef;
  kind: TicketLinkKind;
}

/** Lifecycle status of a sprint (mirror of the backend `SprintStatus`). */
export type SprintStatus = "planned" | "active" | "done";

/** A sprint bucket tickets can belong to (mirror of `SprintDto`). */
export interface Sprint {
  id: string;
  /** Reorderable position; sections are displayed by ascending `order`. */
  order: number;
  name: string;
  status: SprintStatus;
  /** How many tickets currently belong to this sprint. */
  ticketCount: number;
  /** Optimistic-concurrency version. */
  version: number;
}

/** The full ticket, as returned by `ticket_read`/`ticket_create`/… . */
export interface Ticket {
  id: string;
  ref: TicketRef;
  number: number;
  title: string;
  description: string;
  status: TicketStatus;
  priority: TicketPriority;
  /** Sprint membership (`null` ⇒ no sprint). Additive (ticket #10). */
  sprintId?: string | null;
  /** Present only when the ticket was read with `includeCarnet`. */
  carnet?: string;
  links: TicketLink[];
  assignedAgentIds: string[];
  createdBy: TicketActor;
  updatedBy: TicketActor;
  createdAt: number;
  updatedAt: number;
  /** Optimistic-concurrency version; echoed back as `expectedVersion` on writes. */
  version: number;
}

/** A ticket summary row from `ticket_list` (mirror of `TicketSummaryDto`). */
export interface TicketSummary {
  ref: TicketRef;
  path: string;
  title: string;
  status: TicketStatus;
  priority: TicketPriority;
  /** Sprint membership (`null` ⇒ no sprint). Additive (ticket #10). */
  sprintId?: string | null;
  assignedAgentIds: string[];
  updatedAt: number;
}

/** One page of ticket summaries (mirror of `TicketListDto`). */
export interface TicketList {
  items: TicketSummary[];
  /** Opaque cursor for the next page, absent when the list is exhausted. */
  nextCursor?: string;
}

/** The scoped-to-a-ticket Markdown carnet (mirror of `TicketCarnetDto`). */
export interface TicketCarnet {
  ref: TicketRef;
  carnet: string;
  version: number;
}

/**
 * A ticket AI assistant chat session (mirror of the backend `TicketChatDto`,
 * ticket #8). `sessionId` is the live structured session driving the assistant;
 * `requester` is the assistant agent id; `issueRef` the bound ticket.
 */
export interface TicketChat {
  sessionId: string;
  requester: string;
  issueRef: TicketRef;
}

/**
 * One streamed chunk of an assistant turn (mirror of the backend `ReplyChunk`,
 * tagged on `kind`). `textDelta` is an incremental text fragment; `toolActivity`
 * a best-effort activity badge; `final` the deterministic end-of-turn chunk
 * carrying the aggregated content — after it the turn is frozen. `error` is a
 * visible terminal fallback (ticket #60): the model produced no usable answer,
 * so the turn ends with a human-readable explanation instead of a silent hang.
 */
export type ReplyChunk =
  | { kind: "textDelta"; text: string }
  | { kind: "toolActivity"; label: string }
  | { kind: "final"; content: string }
  | { kind: "error"; message: string };

// ---------------------------------------------------------------------------
// Paired devices + pairing code (ticket #77)
// ---------------------------------------------------------------------------

/**
 * One device paired with this IdeA instance (mirror of the backend device DTO).
 *
 * Deliberately carries **no IP and no User-Agent**: the design is mono-user and
 * the list is an access-management surface, not a forensics log. `name` is the
 * human label typed on the device itself at pairing time.
 */
export interface PairedDevice {
  deviceId: string;
  name: string;
  /** Epoch milliseconds the device was paired. */
  pairedAtMs: number;
  /** Epoch milliseconds of the device's last authenticated request. */
  lastSeenAtMs: number;
  /** True for the device rendering this list (never true on desktop). */
  isCurrentDevice: boolean;
}

/**
 * A freshly generated, single-use pairing code (mirror of the backend DTO).
 *
 * `code` is the **canonical** value — uppercase hex, no separator. Any grouping
 * shown to the user is presentation only; see {@link normalizePairingCode}.
 */
export interface PairingCode {
  code: string;
  /** Epoch milliseconds the code stops being accepted. */
  expiresAtMs: number;
  /** Lifetime granted at generation (600 s today). */
  ttlSeconds: number;
}

/**
 * Canonical form of a pairing code as the server compares it: uppercase, with
 * spaces **and dashes** removed.
 *
 * Both separators matter. The UI groups the code visually (`AB12 CD34`) and a
 * user may retype it with a dash out of habit, so a code copied by eye must
 * still pair. The server normalises too (#76) — this keeps the client honest
 * rather than being the only line of defence.
 */
export function normalizePairingCode(raw: string): string {
  return raw.replace(/[\s-]+/g, "").toUpperCase();
}
