/**
 * UI ports (gateways) — interfaces describing *what the UI needs*, independent
 * of transport (ARCHITECTURE §1.3). React components depend on these, never on
 * `@tauri-apps/api` directly. Implemented by the Tauri adapters and by mocks.
 *
 * Signatures are intentionally minimal/skeletal for L1; later lots flesh out
 * each gateway as their use cases land. They are aligned with the planned use
 * cases (ARCHITECTURE §6) so the shape is stable.
 */

import type {
  Agent,
  AgentDrift,
  AgentProfile,
  AppExitWorkGuardState,
  CustomProviderConfig,
  DomainEvent,
  EffortSelection,
  EmbedderEngines,
  EmbedderProfile,
  EmbeddedServerStatus,
  FirstRunState,
  GitBranches,
  GitCommit,
  GitFileStatus,
  GraphCommit,
  HealthReport,
  JsonValue,
  LayoutKind,
  LayoutList,
  LayoutOperation,
  LayoutTree,
  PluginLayoutOrigin,
  LocalModelServerConfig,
  ModelServerCommandPreview,
  Memory,
  MemoryIndexEntry,
  MemoryLink,
  MemoryType,
  McpToolPolicy,
  OpenCodeConfig,
  OpenCodeProviderCatalogEntry,
  ProfileModelCatalog,
  PairedDevice,
  PairingCode,
  PermissionSet,
  PluginAdmin,
  PluginCommandTask,
  PluginConfigDocument,
  PluginConfigDocumentFormat,
  PluginConfigDocumentWriteResult,
  PluginConfigUpdateMode,
  PluginEventBatch,
  PluginEventSubscription,
  PluginPublicEventType,
  PluginExpectedFileKind,
  PluginInstallResult,
  PluginToolchainDiagnostic,
  PluginProjectStructure,
  PluginReview,
  PluginRuntimeContributionCatalog,
  PluginSourceKind,
  PluginUninstallResult,
  PluginWorkspaceBinaryFile,
  PluginWorkspaceDirectoryListing,
  PluginWorkspaceStat,
  PluginWorkspaceTextFile,
  ProjectMcpToolPermissions,
  PageDirection,
  Project,
  ProjectPermissions,
  ProjectWorkState,
  ProjectSystemPermissions,
  ProfileAvailability,
  ResumableAgent,
  ResolvedAgentPermissions,
  ResolvedAgentSystemPermissions,
  ReplyChunk,
  ServerExposurePreview,
  ServerExposureSettings,
  Skill,
  SkillKind,
  SkillScope,
  SystemPermissionSet,
  Sprint,
  Template,
  TerminalSession,
  Ticket,
  TicketBulkResult,
  TicketAttachmentContent,
  TicketChat,
  TicketCarnet,
  TicketLinkKind,
  TicketList,
  TicketPriority,
  TicketStatus,
  TurnPage,
  Unsubscribe,
} from "@/domain";

/** System-level gateway: health/ping + global domain-event subscription. */
export interface SystemGateway {
  /** Calls the backend `health` command (smoke test of the whole pipeline). */
  health(note?: string): Promise<HealthReport>;
  /** Subscribes to relayed domain events. */
  onDomainEvent(handler: (event: DomainEvent) => void): Promise<Unsubscribe>;
  /**
   * Opens a native folder picker and returns the chosen path, or `null` if the
   * user cancelled. This is the only sanctioned way to pick a folder — all call
   * sites go through this port; the Tauri plugin is only imported in the adapter.
   */
  pickFolder(): Promise<string | null>;
  /**
   * Opens a native file picker for a single local archive (plugin install from
   * archive, carnet §1.4/§9) and returns the chosen path, or `null` if the user
   * cancelled. Same sanctioned-picker rule as {@link pickFolder}.
   */
  pickArchiveFile(): Promise<string | null>;
  /**
   * Opens a native file picker for one local file and returns its path, or
   * `null` when cancelled. Used for ticket attachments; no file bytes cross the
   * frontend boundary.
   */
  pickFile(): Promise<string | null>;
  /**
   * Subscribes to the app-exit work-in-progress guard (ticket #83): fired when
   * closing the main window is intercepted because it would interrupt active
   * agents/background tasks. Desktop-only — the web transport returns an inert
   * unsubscribe (never fires; there is no interceptable window to guard).
   */
  onAppExitWorkGuard(
    handler: (state: AppExitWorkGuardState) => void,
  ): Promise<Unsubscribe>;
  /**
   * Bypasses the guard once and requests the main window to close for real
   * (ticket #83) — the user chose "Quitter quand même".
   */
  confirmAppExit(): Promise<void>;
}

/** Input for {@link AgentGateway.createAgent}. */
export interface CreateAgentInput {
  name: string;
  profileId: string;
  initialContent?: string;
}

/**
 * Best-effort enriched details about a CLI conversation (T7), used to enrich the
 * resume popup. Both fields are optional: a missing inspector or a missing
 * transcript yields an empty object — the popup degrades to the status alone.
 */
export interface ConversationDetails {
  /** A short, best-effort label for the conversation (last topic). */
  lastTopic?: string;
  /** A best-effort cumulative token count. */
  tokenCount?: number;
}

/** Agents: create, list, read/update context, delete, launch (L6). */
export interface AgentGateway {
  /** Lists all agents belonging to the given project. */
  listAgents(projectId: string): Promise<Agent[]>;
  /**
   * Lists the agents that can be **resumed** when the project is (re)opened
   * (ARCHITECTURE §15.2): a read-only inventory of agent cells that were running
   * and/or carry a persisted conversation id at close time. Drives the
   * `ResumeProjectPanel` shown once on open; an empty list ⇒ no panel. Pure
   * inventory: no PTY is spawned by this call.
   */
  listResumableAgents(projectId: string): Promise<ResumableAgent[]>;
  /**
   * Lists the agents that currently own a live session and the cell hosting
   * each. Used to disable an agent already running in another cell (it cannot be
   * launched a second time — one live session per agent).
   */
  listLiveAgents(projectId: string): Promise<LiveAgent[]>;
  /**
   * Rebinds an already-live agent session to another visible layout cell without
   * respawning the CLI process. Used when a background agent is opened in a cell,
   * or when a live session is moved from a now-detached/closed cell.
   *
   * Backends that do not yet support agent-session rebinding may omit this
   * method; the UI will show the session as running elsewhere but cannot attach
   * it from a different cell.
   */
  attachLiveAgent?(
    projectId: string,
    agentId: string,
    nodeId: string,
  ): Promise<LiveAgent>;
  /**
   * Stops an already-live agent session without going through terminal teardown
   * commands from the Work panel. The backend owns the live-agent lifecycle.
   */
  stopLiveAgent?(projectId: string, agentId: string): Promise<StoppedLiveAgent>;
  /** Creates a new agent from scratch; returns the created agent. */
  createAgent(projectId: string, input: CreateAgentInput): Promise<Agent>;
  /**
   * Hot-swaps an agent's runtime AI profile (Chantier A). The conversation
   * history is abandoned (the product decision is "start fresh"); the agent's
   * context `.md` and project memory are preserved. When the agent has a live
   * session it is relaunched on the new profile and returned as
   * {@link TerminalSession} so the hosting cell can rebind; otherwise
   * `relaunchedSession` is absent.
   */
  changeAgentProfile(
    projectId: string,
    agentId: string,
    profileId: string,
    rows: number,
    cols: number,
  ): Promise<{ agent: Agent; relaunchedSession?: TerminalSession }>;
  /**
   * Sets or clears one agent's effort override. `null` clears the override and
   * lets launch fall back to the profile default.
   */
  updateAgentEffort(
    projectId: string,
    agentId: string,
    effort: EffortSelection | null,
  ): Promise<Agent>;
  /** Reads an agent's `.md` context by agent id. */
  readContext(projectId: string, agentId: string): Promise<string>;
  /** Overwrites an agent's `.md` context. */
  updateContext(projectId: string, agentId: string, content: string): Promise<void>;
  /** Removes an agent from the project. */
  deleteAgent(projectId: string, agentId: string): Promise<void>;
  /**
   * Launches the agent: opens a PTY, spawns the CLI, and wires the output stream.
   * Mirrors {@link TerminalGateway.openTerminal} but invokes `launch_agent`.
   *
   * The returned handle carries an optional {@link TerminalHandle.assignedConversationId}:
   * when the agent's profile assigns a fresh CLI conversation id on this first
   * launch (the cell had none yet), it is surfaced here so the caller can persist
   * it on the hosting leaf (`setCellConversation`) and resume on the next open.
   * Pass the leaf's current `conversationId` via {@link OpenTerminalOptions} to
   * resume an existing conversation instead.
   */
  launchAgent(
    projectId: string,
    agentId: string,
    options: OpenTerminalOptions,
    onData: (bytes: Uint8Array) => void,
  ): Promise<TerminalHandle>;
  /**
   * Re-attaches to an agent's already-running PTY (same backend mechanism as
   * {@link TerminalGateway.reattach}; agent sessions share the session-based
   * terminal commands). Used when an agent cell's view re-mounts after a
   * navigation/layout change, so the agent is never killed.
   */
  reattach(
    sessionId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<ReattachResult>;
  /**
   * Reads best-effort {@link ConversationDetails} for an agent's conversation
   * (T7), to enrich the resume popup with the last topic + a token indicator.
   * Best-effort by contract: a missing/unsupported inspector or a missing
   * transcript resolves to an empty object (never rejects for "no details").
   */
  inspectConversation(
    projectId: string,
    agentId: string,
    conversationId: string,
  ): Promise<ConversationDetails>;
}

/** Options for opening a terminal. */
export interface OpenTerminalOptions {
  /** Working directory (typically the project root). */
  cwd: string;
  /** Initial terminal height in rows. */
  rows: number;
  /** Initial terminal width in columns. */
  cols: number;
  /**
   * Persistent CLI conversation id recorded on the hosting cell, if any. When
   * present, an agent launch **resumes** that conversation; when absent the
   * launch may *assign* a fresh id (surfaced on the returned handle). Only
   * meaningful for {@link AgentGateway.launchAgent}; ignored for plain terminals.
   */
  conversationId?: string;
  /**
   * The layout leaf (node) hosting this launch. Drives the "one live session per
   * agent" invariant backend-side: launching an agent already running in a
   * *different* node is refused (`AGENT_ALREADY_RUNNING`); the same node is
   * idempotent. Only meaningful for {@link AgentGateway.launchAgent}.
   */
  nodeId?: string;
}

/** One currently-live agent and the cell hosting it (see {@link AgentGateway.listLiveAgents}). */
export interface LiveAgent {
  /** The live agent's id. */
  agentId: string;
  /** The node (layout leaf) hosting the agent's live session. */
  nodeId: string;
  /**
   * The live PTY session id, when the backend exposes it. Required for opening
   * a background/running agent in a new visible cell without respawning it.
   */
  sessionId: string;
  /** Runtime kind backing the live agent. */
  kind: "pty" | "structured";
}

/** Result returned when the backend stops a live agent. */
export interface StoppedLiveAgent {
  agentId: string;
  sessionId: string;
  kind: "pty" | "structured";
}

/**
 * A live terminal handle returned by {@link TerminalGateway.openTerminal}.
 *
 * The output stream is delivered to the `onData` callback passed at open time
 * (over a Tauri Channel in the real adapter); the handle exposes the input/
 * control operations and a `close` that also tears the stream down.
 */
export interface TerminalHandle {
  /** Stable session id (UUID) used by the backend for this PTY. */
  readonly sessionId: string;
  /**
   * Conversation id **assigned** by an agent launch when the profile minted a
   * fresh one (the cell had none yet). Present only on handles returned by
   * {@link AgentGateway.launchAgent}; `undefined` for plain terminals, resumes,
   * or profiles without a session block. The caller persists it on the hosting
   * leaf so the next open resumes instead of re-assigning.
   */
  readonly assignedConversationId?: string;
  /** Sends bytes (xterm keystrokes) to the PTY. */
  write(data: Uint8Array): Promise<void>;
  /** Resizes the PTY. */
  resize(rows: number, cols: number): Promise<void>;
  /**
   * Detaches the **view** from the PTY without killing it: stops the local
   * output subscription so a torn-down view (navigation / layout change) stops
   * receiving bytes, while the backend PTY keeps running. The session can later
   * be re-attached via {@link TerminalGateway.reattach}.
   *
   * This is the lifecycle the view's cleanup must use — never {@link close}.
   */
  detach(): void;
  /**
   * Kills the PTY and stops the output stream. Reserved for an **explicit** user
   * action (closing the terminal); navigation must never call this.
   */
  close(): Promise<void>;
}

/**
 * The write-portal contract an agent-cell terminal talks to (ARCHITECTURE §20).
 * The portal owns the *human line* counter, the suspension flag (raised while it
 * injects a delegation) and — once the view has a live PTY — the handle it
 * writes through. The terminal view is the only effective PTY writer
 * (single-writer invariant): it relays human keystrokes and the portal injects
 * via the same handle, never a second physical writer.
 */
export interface WritePortal {
  /**
   * Reports a raw human keystroke chunk (`term.onData`) so the portal can keep
   * its line counter. Called for **every** keystroke in agent mode, including
   * while suspended (the portal decides what to count).
   */
  onHumanData(data: string): void;
  /**
   * Whether the keystroke relay to the PTY is currently suspended (the portal
   * is injecting a delegation). When `true`, the view drops keystrokes so they
   * do not race the injected text.
   */
  isSuspended(): boolean;
  /** Binds the live PTY handle so the portal can inject through it. */
  bindHandle(handle: TerminalHandle): void;
  /** Drops the handle reference (view torn down). */
  unbindHandle(): void;
}

/**
 * Terminals (L3): open a PTY with a per-session output stream, then write/
 * resize/close it through the returned {@link TerminalHandle}.
 */
export interface TerminalGateway {
  /**
   * Opens a terminal. `onData` receives every chunk of PTY output (bytes) as it
   * arrives. Resolves once the PTY is spawned and the stream is wired.
   */
  openTerminal(
    options: OpenTerminalOptions,
    onData: (bytes: Uint8Array) => void,
  ): Promise<TerminalHandle>;
  /**
   * Re-attaches to an **already-running** PTY identified by `sessionId` (after a
   * view was torn down by navigation/layout change). Returns the live handle and
   * the retained scrollback, which the caller repaints into xterm before the new
   * output stream (`onData`) starts delivering subsequent bytes. Does NOT
   * re-spawn the process.
   *
   * Rejects if the session is no longer alive (the caller then opens fresh).
   */
  reattach(
    sessionId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<ReattachResult>;
  /**
   * Kills a live PTY by its session id, independently of any view-held handle.
   * Used when a cell's agent changes: the old PTY must be torn down even though
   * the owning {@link TerminalHandle} is private to its (unmounting) view, whose
   * cleanup only ever {@link TerminalHandle.detach}es. Best-effort: resolves even
   * if the session is already gone.
   *
   * This is the only sanctioned way to kill a PTY outside
   * {@link TerminalHandle.close}.
   */
  closeTerminal(sessionId: string): Promise<void>;
}

/** The outcome of {@link TerminalGateway.reattach}. */
export interface ReattachResult {
  /** The live terminal handle for the re-attached session. */
  handle: TerminalHandle;
  /** The retained scrollback bytes to repaint before the live stream resumes. */
  scrollback: Uint8Array;
}

/** A UI subscription attached to one background task output stream. */
export interface BackgroundTaskAttachment {
  /** Attached task id, echoed by the backend. */
  taskId: string;
  /** Retained bytes to repaint immediately before live chunks arrive. */
  scrollback: Uint8Array;
  /** Whether subsequent live bytes are expected on the supplied callback. */
  live: boolean;
  /** Detaches the local UI subscriber without cancelling the task. */
  detach(): void;
}

/** Projects: create/open/close/list (L2). */
export interface ProjectGateway {
  /** Lists the projects known to the registry. */
  listProjects(): Promise<Project[]>;
  /** Creates a project from a root; returns the created project. */
  createProject(name: string, root: string): Promise<Project>;
  /** Opens a project by id; returns the opened project. */
  openProject(projectId: string): Promise<Project>;
  /** Closes a project by id. */
  closeProject(projectId: string): Promise<void>;
  /** Reads the shared project context stored at `.ideai/CONTEXT.md`. */
  readProjectContext(projectId: string): Promise<string>;
  /** Overwrites the shared project context stored at `.ideai/CONTEXT.md`. */
  updateProjectContext(projectId: string, content: string): Promise<void>;
}

/** Layout: load the terminal grid tree and apply mutating operations (L4). */
export interface LayoutGateway {
  /** Loads a project's layout tree (defaults to a single cell if none persisted).
   * When `layoutId` is omitted, the active layout for the project is used. */
  loadLayout(projectId: string, layoutId?: string): Promise<LayoutTree>;
  /**
   * Applies a split/merge/resize/move/setSession/setCellAgent operation; the backend persists
   * `.ideai/layout.json` and returns the resulting tree.
   * When `layoutId` is omitted, the active layout for the project is used.
   */
  mutateLayout(
    projectId: string,
    operation: LayoutOperation,
    layoutId?: string,
  ): Promise<LayoutTree>;
  /** Lists all named layouts for a project, with the current active id. */
  listLayouts(projectId: string): Promise<LayoutList>;
  /** Creates a new named layout for a project; returns the new layout id. */
  createLayout(
    projectId: string,
    name: string,
    kind?: LayoutKind,
    pluginOrigin?: PluginLayoutOrigin,
  ): Promise<{ layoutId: string }>;
  /** Renames a layout. */
  renameLayout(projectId: string, layoutId: string, name: string): Promise<void>;
  /** Deletes a layout; returns the new active layout id. */
  deleteLayout(projectId: string, layoutId: string): Promise<{ activeId: string }>;
  /**
   * Sets the active layout for a project. Returns the layout id the backend
   * *actually* activated — authoritative (invariant I4): it equals the requested
   * id when valid, else the unchanged current active id (self-healing fallback
   * when the requested id was stale, e.g. after an external overwrite of
   * `layouts.json`). Callers must adopt this id rather than the one they asked for.
   */
  setActiveLayout(projectId: string, layoutId: string): Promise<{ activeId: string }>;
}

/** Git: status/commit/checkout/… (L8). */
export interface GitGateway {
  status(projectId: string): Promise<GitFileStatus[]>;
  stage(projectId: string, path: string): Promise<void>;
  unstage(projectId: string, path: string): Promise<void>;
  commit(projectId: string, message: string): Promise<GitCommit>;
  branches(projectId: string): Promise<GitBranches>;
  checkout(projectId: string, branch: string): Promise<void>;
  log(projectId: string, limit: number): Promise<GitCommit[]>;
  init(projectId: string): Promise<void>;
  /** Returns the full commit DAG for the git-graph layout. */
  graph(projectId: string, limit: number): Promise<GraphCommit[]>;
}

/** Remote (SSH/WSL) connection management (L9). */
export interface RemoteGateway {
  connect(projectId: string): Promise<void>;
}

/** Input for {@link TemplateGateway.createTemplate}. */
export interface CreateTemplateInput {
  name: string;
  content: string;
  defaultProfileId: string;
}

/**
 * Templates (L7): CRUD for agent templates, creation of agents from templates,
 * drift detection and synchronisation.
 */
export interface TemplateGateway {
  /** Lists all templates. */
  listTemplates(): Promise<Template[]>;
  /** Creates a new template; returns the created template. */
  createTemplate(input: CreateTemplateInput): Promise<Template>;
  /** Updates a template's content; increments its version; returns the updated template. */
  updateTemplate(templateId: string, content: string): Promise<Template>;
  /** Deletes a template by id. */
  deleteTemplate(templateId: string): Promise<void>;
  /**
   * Creates an agent in `projectId` based on the given template.
   * The agent's origin will be `fromTemplate`; its context will be the template's `contentMd`.
   */
  createAgentFromTemplate(
    projectId: string,
    templateId: string,
    opts?: { name?: string; synchronized?: boolean },
  ): Promise<Agent>;
  /** Returns the list of synchronized agents in `projectId` whose template has been updated. */
  detectDrift(projectId: string): Promise<AgentDrift[]>;
  /**
   * Syncs a single agent to the current version of its template.
   * Returns `{ synced: true, version }` on success, `{ synced: false, version: null }` for
   * scratch / non-synchronized agents.
   */
  syncAgent(
    projectId: string,
    agentId: string,
  ): Promise<{ synced: boolean; version: number | null }>;
}

/** Input for {@link SkillGateway.createSkill}. */
export interface CreateSkillInput {
  /** Owning project (resolved to a root; ignored on disk for `global`). */
  projectId: string;
  name: string;
  description?: string | null;
  kind?: SkillKind;
  content: string;
  scope: SkillScope;
}

/**
 * Skills (L12): CRUD for reusable, model-agnostic workflows in either scope, and
 * agent↔skill assignment. Assigned skills are injected into the agent's
 * convention file at activation (handled backend-side, ARCHITECTURE §14.2).
 */
export interface SkillGateway {
  /** Lists the skills in one scope for the given project. */
  listSkills(projectId: string, scope: SkillScope): Promise<Skill[]>;
  /** Creates a new skill; returns the created skill. */
  createSkill(input: CreateSkillInput): Promise<Skill>;
  /** Updates a skill's content; returns the updated skill. */
  updateSkill(
    projectId: string,
    scope: SkillScope,
    skillId: string,
    content: string,
  ): Promise<Skill>;
  /** Deletes a skill by id from its scope's store. */
  deleteSkill(
    projectId: string,
    scope: SkillScope,
    skillId: string,
  ): Promise<void>;
  /** Assigns a skill to an agent (idempotent). */
  assignSkill(
    projectId: string,
    agentId: string,
    skillId: string,
    scope: SkillScope,
  ): Promise<void>;
  /** Unassigns a skill from an agent (idempotent). */
  unassignSkill(
    projectId: string,
    agentId: string,
    skillId: string,
  ): Promise<void>;
}

/** Input for {@link MemoryGateway.createMemory}. */
export interface CreateMemoryInput {
  /** Owning project (resolved to its `.ideai/` memory store). */
  projectId: string;
  /** Human title; also the source of the note's slug identity. */
  name: string;
  description: string;
  type: MemoryType;
  content: string;
}

/**
 * Memory (L14): CRUD over project memory notes + the recall-oriented index and
 * `[[wikilink]]` resolution. Identity is the **slug** (kebab-case), not a UUID;
 * a note's slug is immutable, so `updateMemory` never changes it. Mirrors the
 * seven backend memory commands (+ optional `recall`).
 */
export interface MemoryGateway {
  /** Lists every memory note of a project (full payloads). */
  listMemories(projectId: string): Promise<Memory[]>;
  /** Reads a single note by slug. */
  getMemory(projectId: string, slug: string): Promise<Memory>;
  /** Creates a new note; returns the created note. */
  createMemory(input: CreateMemoryInput): Promise<Memory>;
  /** Updates a note's mutable fields (slug is immutable); returns the updated note. */
  updateMemory(
    projectId: string,
    slug: string,
    description: string,
    type: MemoryType,
    content: string,
  ): Promise<Memory>;
  /** Deletes a note by slug. */
  deleteMemory(projectId: string, slug: string): Promise<void>;
  /** Reads the recall-oriented memory index. */
  readIndex(projectId: string): Promise<MemoryIndexEntry[]>;
  /** Resolves a note's `[[wikilinks]]` to the slugs of existing target notes. */
  resolveLinks(projectId: string, slug: string): Promise<MemoryLink[]>;
  /** Best-effort recall: the index entries most relevant to `text`, within a token budget. */
  recall(
    projectId: string,
    text: string,
    tokenBudget: number,
  ): Promise<MemoryIndexEntry[]>;
}

/**
 * Agent input control (ARCHITECTURE §20). The mediated-input strip is gone: the
 * agent cell is a **native terminal** and human keystrokes (Enter included) go
 * straight to the PTY. This port only carries the two out-of-band controls:
 *
 *  - **Interrompre** → `interrupt` (preempt the current turn — a control byte,
 *    not an enqueue).
 *  - **Ack** → `delegationDelivered`: the cell's write-portal confirms it has
 *    effectively written a delegation's text into the native PTY (closes the
 *    "the turn was delivered" loop for observability; the `ask` wake-up still
 *    rides on `idea_reply`).
 *
 * The component talks to this gateway via DI, never to `invoke()` directly.
 */
export interface InputGateway {
  /** Interrompre = preempt: signals the current turn to stop (not an enqueue). */
  interrupt(projectId: string, agentId: string): Promise<void>;
  /**
   * Ack: the cell's write-portal has written the delegation `ticket` into the
   * agent's native PTY (ARCHITECTURE §20.3). Best-effort; never changes the
   * ticket correlation.
   */
  delegationDelivered(
    projectId: string,
    agentId: string,
    ticket: string,
  ): Promise<void>;
  /**
   * Reports whether a **frontend terminal cell** is mounted for `agentId` (`true`
   * when the cell's write-portal binds its PTY handle, `false` on unmount). The
   * backend mediator needs this to deliver a turn to a **headless** agent — one
   * delegated in the background with no cell, where nobody would consume
   * `delegationReady`: it then writes the task into the PTY itself. An agent with a
   * mounted cell keeps the write-portal path. Best-effort; never throws into the UI.
   */
  setFrontAttached(agentId: string, attached: boolean): Promise<void>;
  /**
   * Cancels the **auto-resume** armed for a rate-limited agent (ARCHITECTURE §21):
   * the user clicked "Annuler la reprise" during the cancellable window. Disarms
   * the scheduled wake-up. Resolves to `true` iff a resume was effectively
   * cancelled (a wake-up was still pending); `false` if none was armed or it had
   * already fired (the resume then runs its course). The backend also emits
   * `agentResumeCancelled` on success, which clears the countdown in the UI.
   */
  cancelResume(agentId: string): Promise<boolean>;
  /**
   * Human net (level 3, ARCHITECTURE §21.1): arms a resume at a user-chosen
   * instant for an agent whose limit was *suspected* without a reliable reset
   * time. `resetsAtMs` is the chosen wake-up in epoch-milliseconds; the backend
   * clamps a past instant to "now" (⇒ immediate resume). This arms the same
   * cancellable resume as the automatic path and re-emits `agentResumeScheduled`,
   * so the UI flips on its own from "heure inconnue" to the nominal countdown.
   */
  setResumeAt(agentId: string, resetsAtMs: number): Promise<void>;
}

/**
 * AI profiles & first-run (L5). Drives the first-run wizard and profile
 * management: the pre-filled reference catalogue, detection of installed CLIs,
 * and CRUD/batch persistence of the chosen/edited/custom profiles.
 */
export interface ProfileGateway {
  /** First-run state: whether to show the wizard + the reference catalogue. */
  firstRunState(): Promise<FirstRunState>;
  /** The pre-filled, editable reference catalogue (Claude/Codex/Gemini/Aider). */
  referenceProfiles(): Promise<AgentProfile[]>;
  /** Probes each candidate's detection command; returns availability (✓/✗). */
  detectProfiles(candidates: AgentProfile[]): Promise<ProfileAvailability[]>;
  /** Lists the configured profiles. */
  listProfiles(): Promise<AgentProfile[]>;
  /** Creates or replaces (by id) a single profile; returns the saved profile. */
  saveProfile(profile: AgentProfile): Promise<AgentProfile>;
  /** Deletes a profile by id. */
  deleteProfile(profileId: string): Promise<void>;
  /**
   * Clones a persisted or reference profile seed and saves the fresh profile.
   * Used by Settings duplication for Codex/Claude/OpenCode identity copies.
   */
  cloneProfileFromSeed(input: CloneProfileFromSeedInput): Promise<AgentProfile>;
  /** Enriched Claude Code model catalogue. Manual model entry remains supported. */
  listClaudeModels(): Promise<ProfileModelCatalog>;
  /** Enriched Codex CLI model catalogue. Manual model entry remains supported. */
  listCodexModels(): Promise<ProfileModelCatalog>;
  /** Persists the batch of chosen profiles, closing the first run. */
  configureProfiles(profiles: AgentProfile[]): Promise<AgentProfile[]>;
  /**
   * Mints a new OpenCode profile from the canonical `opencode-llamacpp` seed
   * (F36 — several local OpenCode profiles per project, identity = id). `name`
   * overrides the seed display name; `opencode` overrides the seed config (used
   * by "Duplicate" to carry an existing row's endpoint). Returns the new,
   * freshly-id'd profile — it is not persisted until the batch is saved.
   */
  cloneOpenCodeProfileFromSeed(
    input?: CloneOpenCodeProfileFromSeedInput,
  ): Promise<AgentProfile>;
  /**
   * Static catalogue of OpenCode cloud providers (ticket #92), for the
   * provider/model pickers of the Cloud sub-form.
   */
  listOpenCodeProviders(): Promise<OpenCodeProviderCatalogEntry[]>;
  /**
   * Creates or replaces (by id) an OpenCode profile in **cloud** mode (ticket
   * #92). Unlike {@link saveProfile}, this takes the literal API key: the
   * backend seals it into the `SecretStore` and never returns it — the
   * returned profile's `opencodeProvider` only ever carries `providerId` +
   * `model` (+ the opaque `apiKeyRef`), never the literal key.
   */
  saveOpenCodeProviderProfile(
    input: SaveOpenCodeProviderProfileInput,
  ): Promise<AgentProfile>;
}

/** Input for {@link ProfileGateway.cloneOpenCodeProfileFromSeed}. */
export interface CloneOpenCodeProfileFromSeedInput {
  /** Optional display name for the new profile. */
  name?: string;
  /** Optional OpenCode config override; when omitted, the seed config is copied. */
  opencode?: OpenCodeConfig;
}

/** Input for {@link ProfileGateway.cloneProfileFromSeed}. */
export interface CloneProfileFromSeedInput {
  /** Id of the persisted or reference profile to clone. */
  seedProfileId: string;
  /** Optional display name for the new profile. */
  name?: string;
  /** Optional model override. When omitted, the seed model is copied. */
  model?: string;
}

/** Input for {@link ProfileGateway.saveOpenCodeProviderProfile}. */
export interface SaveOpenCodeProviderProfileInput {
  /** The profile to create or replace (by id). */
  profile: AgentProfile;
  /** Provider id — a catalogue entry, or a free-form id when {@link custom} is set. */
  providerId: string;
  /** Model name served by this provider. */
  model: string;
  /** Literal API key — sealed into the `SecretStore`, never persisted as-is. */
  apiKey: string;
  /**
   * Optional custom-provider configuration (ticket #92): an endpoint outside
   * the OpenCode registry. Absent/`undefined` = known catalogue provider
   * (unchanged behaviour).
   */
  custom?: CustomProviderConfig;
}

/**
 * Local model servers (F35). CRUD over the global registry of declared
 * `llama.cpp` servers an OpenCode profile can bind to via
 * `OpenCodeConfig.localModelServerId`. Mirrors the backend commands
 * (`list_model_servers`, `save_model_server`, `delete_model_server`,
 * `preview_model_server_command`, `delete_model_artifact`).
 */
export interface ModelServerGateway {
  /** Lists the declared local model servers. */
  listModelServers(): Promise<LocalModelServerConfig[]>;
  /**
   * Upserts a server config (create when `id` is a fresh client-minted UUID,
   * update otherwise); returns the persisted config.
   */
  saveModelServer(config: LocalModelServerConfig): Promise<LocalModelServerConfig>;
  /**
   * Deletes a server by id. Rejects with a `GatewayError` whose `code` is
   * `model_server_in_use` when a profile still references it.
   */
  deleteModelServer(serverId: string): Promise<void>;
  /**
   * Deletes the IdeA-managed downloaded model artifact for a server while
   * keeping the server config. Rejects with `invalid` for local `.gguf` paths,
   * `model_server_in_use` while downloading or used by a live agent, and
   * `not_configured` when the server no longer exists.
   */
  deleteModelArtifact(serverId: string): Promise<void>;
  /**
   * Builds the `llama-server` command line the backend would launch for the
   * draft config, without persisting it. The backend is the sole authority on
   * the argv (never reconstruct it client-side). Rejects when the config is
   * invalid (e.g. `modelSource` missing).
   */
  previewModelServerCommand(
    config: LocalModelServerConfig,
  ): Promise<ModelServerCommandPreview>;
}

/**
 * Embedded web server lifecycle + exposure settings (ticket #68). Backs the
 * desktop `Settings → Deployment` surface, so the user can expose IdeA to
 * remote devices without a command line.
 *
 * **Desktop-only.** Only the Tauri transport implements it for real; the web
 * transport rejects (a web client is *served by* this server and must not
 * reconfigure it). See `desktop-only.test.ts`.
 *
 * The backend owns every network fact: LAN candidates and the proxy upstream
 * URL come from {@link previewExposure}, never from client-side derivation.
 */
export interface DesktopServerGateway {
  /** Reads the persisted exposure settings. */
  getExposureSettings(): Promise<ServerExposureSettings>;
  /**
   * Validates and persists exposure settings. Rejects with a `GatewayError`
   * (`INVALID`) when the config is inconsistent — e.g. a remote mode without an
   * `https://` `publicOrigin`, or `remoteProxyOtherMachine` without a concrete
   * `lanBindAddress` / a non-empty `trustedProxies`.
   */
  saveExposureSettings(settings: ServerExposureSettings): Promise<void>;
  /**
   * Derives LAN candidates, the upstream URL and non-fatal warnings for a
   * **draft** config, without persisting it. The single source of truth for the
   * addresses the UI displays.
   */
  previewExposure(
    settings: ServerExposureSettings,
  ): Promise<ServerExposurePreview>;
  /** Current server status. */
  status(): Promise<EmbeddedServerStatus>;
  /** Starts the server with the persisted settings; resolves with the new status. */
  start(): Promise<EmbeddedServerStatus>;
  /** Stops the server; resolves with the new status. */
  stop(): Promise<EmbeddedServerStatus>;
  /**
   * Observes status transitions. The backend emits no status event today, so
   * the Tauri adapter polls `embedded_server_status`; the schedule is a
   * transport detail owned by the adapter, and callers must not depend on it.
   */
  onStatusChanged(
    handler: (status: EmbeddedServerStatus) => void,
  ): Promise<Unsubscribe>;
}

/** Input for {@link EmbedderGateway.saveEmbedderProfile}. */
export interface SaveEmbedderProfileInput {
  profile: EmbedderProfile;
}

/**
 * Embedder configuration (L14 / lot C2). Drives the memory/embedder settings
 * panel: lists the configured embedder profiles, the available engines (with
 * build-time feature flags so unavailable strategies can be shown disabled),
 * and persists / removes profiles. Changing the active embedder takes effect at
 * the next app start. Mirrors the four backend embedder commands.
 */
export interface EmbedderGateway {
  /** Lists the configured embedder profiles (transparent: no secrets). */
  listEmbedderProfiles(): Promise<EmbedderProfile[]>;
  /** Creates or replaces (by id) a single profile; returns the saved profile. */
  saveEmbedderProfile(profile: EmbedderProfile): Promise<EmbedderProfile>;
  /** Deletes an embedder profile by id. */
  deleteEmbedderProfile(embedderId: string): Promise<void>;
  /** Describes the available engines + build feature flags + detection. */
  describeEmbedderEngines(): Promise<EmbedderEngines>;
}

/**
 * Paired-device management (ticket #77) — the access surface of the instance.
 *
 * Mounted identically on web and desktop, so it is a **port**, not a web
 * concern: the desktop adapter reaches the same use cases through the Tauri
 * composition root, the web adapter over HTTP. Revoking is authoritative
 * server-side; revoking the *current* device ends this session, which callers
 * detect from {@link PairedDevice.isCurrentDevice} before the call.
 */
export interface DeviceGateway {
  /** Lists every paired device, most recently active first (backend order). */
  listDevices(): Promise<PairedDevice[]>;
  /**
   * Generates a fresh single-use pairing code, invalidating any previous one.
   * The code exists only in server memory until it is consumed or expires.
   */
  createPairingCode(): Promise<PairingCode>;
  /** Renames one device (1-40 characters). */
  renameDevice(deviceId: string, name: string): Promise<void>;
  /** Revokes one device; its live WebSockets are closed server-side (B3). */
  revokeDevice(deviceId: string): Promise<void>;
  /** Revokes every device, the current one included. */
  revokeAllDevices(): Promise<void>;
}

/** Project and agent permission management (LP1). */
export interface PermissionGateway {
  /** Reads the full project permission document. */
  getProjectPermissions(projectId: string): Promise<ProjectPermissions>;
  /** Replaces or removes project-level default permissions. */
  updateProjectPermissions(
    projectId: string,
    permissions: PermissionSet | null,
  ): Promise<ProjectPermissions>;
  /** Replaces or removes one agent-specific override. */
  updateAgentPermissions(
    projectId: string,
    agentId: string,
    permissions: PermissionSet | null,
  ): Promise<ProjectPermissions>;
  /** Resolves effective permissions for one agent. */
  resolveAgentPermissions(
    projectId: string,
    agentId: string,
  ): Promise<ResolvedAgentPermissions>;
  /** Reads the full project system-permission document. */
  getProjectSystemPermissions(projectId: string): Promise<ProjectSystemPermissions>;
  /** Replaces or removes project-level default system permissions. */
  updateProjectSystemPermissions(
    projectId: string,
    permissions: SystemPermissionSet | null,
  ): Promise<ProjectSystemPermissions>;
  /** Replaces or removes one agent-specific system-permission override. */
  updateAgentSystemPermissions(
    projectId: string,
    agentId: string,
    permissions: SystemPermissionSet | null,
  ): Promise<ProjectSystemPermissions>;
  /** Resolves wanted/effective runtime-constrained system permissions. */
  resolveAgentSystemPermissions(
    projectId: string,
    agentId: string,
  ): Promise<ResolvedAgentSystemPermissions>;
  /**
   * Reads the project's durable MCP tool permission document plus the
   * backend-canonical catalogue classification (ticket #82). Distinct
   * document from the file/command permissions above.
   */
  getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions>;
  /** Replaces or removes the project-wide default MCP tool policy. */
  updateProjectMcpToolPermissions(
    projectId: string,
    policy: McpToolPolicy | null,
  ): Promise<ProjectMcpToolPermissions>;
  /** Replaces or removes one agent's MCP tool policy override. */
  updateAgentMcpToolPermissions(
    projectId: string,
    agentId: string,
    policy: McpToolPolicy | null,
  ): Promise<ProjectMcpToolPermissions>;
}

/** Read-only live work-state read-model for conversations/delegations. */
export interface WorkStateGateway {
  /** Reads the current per-agent live/offline and idle/busy state for a project. */
  getProjectWorkState(projectId: string): Promise<ProjectWorkState>;
  /**
   * Attaches a UI output subscriber to a background task. Running tasks replay
   * PTY scrollback and then stream live chunks; terminal tasks return only their
   * persisted output tail.
   */
  attachBackgroundTask(
    taskId: string,
    onData: (bytes: Uint8Array) => void,
  ): Promise<BackgroundTaskAttachment>;
  /**
   * Cancels a running/pending background task by id. The read-model refreshes
   * through the `backgroundTaskChanged` domain event.
   */
  cancelBackgroundTask(taskId: string): Promise<void>;
  /**
   * Re-runs a failed/cancelled background task under a **new** task id (the
   * original id is never reused).
   */
  retryBackgroundTask(taskId: string): Promise<void>;
}

/** One paginated page request over a conversation transcript (LS7). */
export interface ConversationPageRequest {
  /** Turn id to paginate around; omit to start from a thread end. */
  anchor?: string;
  /** Travel direction; defaults to `"backward"` (latest page first). */
  direction?: PageDirection;
  /** Requested page size (clamped by the backend to `[1, 200]`). */
  limit?: number;
}

/** Read-only, paginated access to a conversation's full transcript (LS7). */
export interface ConversationGateway {
  /** Reads one page of a conversation's transcript (full text, never truncated). */
  readPage(
    projectId: string,
    conversationId: string,
    request?: ConversationPageRequest,
  ): Promise<TurnPage>;
}

/**
 * Filter/search criteria for {@link TicketGateway.list} (ticket #12).
 *
 * Status and priority are **multi-select**: `statuses`/`priorities` are sets of
 * accepted values with OR semantics *within* a facet and AND *across* facets. An
 * empty or absent array means "no constraint on this facet" (all values pass).
 * `sort` is optional; when absent, the backend preserves its historical order.
 */
export interface TicketListQuery {
  statuses?: TicketStatus[];
  priorities?: TicketPriority[];
  assignedAgentId?: string;
  createdBy?: TicketCreatorFilter;
  /** Free-text match over title/description. */
  text?: string;
  sort?: TicketListSort;
  limit?: number;
  cursor?: string;
}

export type TicketCreatorFilter =
  | { kind: "user" }
  | { kind: "agent"; agentId: string };

export type TicketListSortField = "number" | "priority" | "status" | "title";

export type TicketListSortDirection = "asc" | "desc";

export interface TicketListSort {
  field: TicketListSortField;
  direction: TicketListSortDirection;
}

/** Input for {@link TicketGateway.create}. */
export interface CreateTicketInput {
  title: string;
  description?: string;
  priority?: TicketPriority;
  status?: TicketStatus;
  assignedAgentIds?: string[];
}

/**
 * Fields to change on {@link TicketGateway.update}. Only the provided keys are
 * touched; `expectedVersion` is mandatory (optimistic concurrency — a stale
 * value surfaces a `versionConflict` {@link GatewayError}).
 */
export interface UpdateTicketInput {
  title?: string;
  description?: string;
  status?: TicketStatus;
  priority?: TicketPriority;
  assignedAgentIds?: string[];
  expectedVersion: number;
}

/**
 * Ticket gateway (public wire name for the backend `Issue` model). The only
 * frontend place that knows the `ticket_*` command names; features consume this
 * port through DI. Every mutation carries an `expectedVersion` and rejects with
 * a `versionConflict` {@link GatewayError} when the ticket moved underneath.
 */
export interface TicketGateway {
  /** Creates a ticket and returns it. */
  create(projectId: string, input: CreateTicketInput): Promise<Ticket>;
  /** Reads one ticket, optionally including its carnet body. */
  read(
    projectId: string,
    ref: string,
    includeCarnet?: boolean,
  ): Promise<Ticket>;
  /** Lists tickets matching the (optional) filter/search query. */
  list(projectId: string, query?: TicketListQuery): Promise<TicketList>;
  /** Applies partial edits (title/description/status/priority/assignees). */
  update(
    projectId: string,
    ref: string,
    input: UpdateTicketInput,
  ): Promise<Ticket>;
  /**
   * Deletes a ticket (ticket #6). Rejects with a `notFound` {@link GatewayError}
   * when the ref does not exist. On success the backend emits an `issueDeleted`
   * domain event (payload `{ projectId, issueRef, freedSprint }`) — the single
   * source of truth that drives the ticket's removal from lists and the closing
   * of an open detail view. Callers must not mutate local state imperatively.
   */
  delete(projectId: string, ref: string): Promise<void>;
  /** Updates several tickets' status in one command; partial failures are per item. */
  bulkUpdateStatus(
    projectId: string,
    refs: string[],
    status: TicketStatus,
  ): Promise<TicketBulkResult>;
  /** Updates several tickets' priority in one command; partial failures are per item. */
  bulkUpdatePriority(
    projectId: string,
    refs: string[],
    priority: TicketPriority,
  ): Promise<TicketBulkResult>;
  /** Deletes several tickets in one command; partial failures are per item. */
  bulkDelete(projectId: string, refs: string[]): Promise<TicketBulkResult>;
  /** Reads the ticket-scoped Markdown carnet. */
  readCarnet(projectId: string, ref: string): Promise<TicketCarnet>;
  /** Replaces (not appends) the ticket-scoped carnet body. */
  updateCarnet(
    projectId: string,
    ref: string,
    carnet: string,
    expectedVersion: number,
  ): Promise<Ticket>;
  /** Adds a local file path as a ticket attachment and returns the updated ticket. */
  addAttachment(
    projectId: string,
    ref: string,
    path: string,
    expectedVersion: number,
    mime?: string | null,
  ): Promise<Ticket>;
  /** Reads raw attachment content as base64. Not used by the minimal v1 UI. */
  readAttachment(
    projectId: string,
    ref: string,
    attachmentId: string,
  ): Promise<TicketAttachmentContent>;
  /** Marks an attachment as summarized in the ticket carnet. */
  markAttachmentSummarized(
    projectId: string,
    ref: string,
    attachmentId: string,
    expectedVersion: number,
  ): Promise<Ticket>;
  /** Links this ticket to another `#id` with the given relationship kind. */
  link(
    projectId: string,
    ref: string,
    targetRef: string,
    kind: TicketLinkKind,
    expectedVersion: number,
  ): Promise<Ticket>;
  /** Removes a link (optionally scoped to a single kind). */
  unlink(
    projectId: string,
    ref: string,
    targetRef: string,
    expectedVersion: number,
    kind?: TicketLinkKind,
  ): Promise<Ticket>;
  /** Assigns/unassigns an agent to the ticket. */
  assign(
    projectId: string,
    ref: string,
    agentId: string,
    assigned: boolean,
    expectedVersion: number,
  ): Promise<Ticket>;
  /** Lists the project's sprints, ordered by `order` (ticket #10). */
  listSprints(projectId: string): Promise<Sprint[]>;
  /**
   * Sets (or clears with `sprintId === null`) a ticket's sprint membership
   * (ticket #10). Optimistic concurrency: `expectedVersion` must match.
   */
  setTicketSprint(
    projectId: string,
    ref: string,
    sprintId: string | null,
    expectedVersion: number,
  ): Promise<Ticket>;
  /** Creates a sprint (name required by the backend) and returns it (#11). */
  createSprint(projectId: string, name: string): Promise<Sprint>;
  /**
   * Renames a sprint. Optimistic concurrency: `expectedVersion` must match the
   * sprint's current version (#11).
   */
  renameSprint(
    projectId: string,
    sprintId: string,
    name: string,
    expectedVersion: number,
  ): Promise<Sprint>;
  /**
   * Reorders sprints to the given full list of ids (execution order). Returns
   * the reordered sprint list (#11).
   */
  reorderSprints(projectId: string, orderedIds: string[]): Promise<Sprint[]>;
  /**
   * Deletes a sprint. The backend unassigns its tickets (it does NOT delete
   * them); they fall back to the "no sprint" bucket (#11).
   */
  deleteSprint(projectId: string, sprintId: string): Promise<void>;
  /**
   * Opens an AI assistant chat bound to a ticket (ticket #8). Spawns a
   * structured assistant session driven by `profileId`; returns the live
   * session handle. One session per ticket — reopening supersedes.
   */
  openTicketChat(
    projectId: string,
    issueRef: string,
    profileId: string,
  ): Promise<TicketChat>;
  /** Closes (disposes) a ticket's assistant chat session (ticket #8). */
  closeTicketChat(projectId: string, issueRef: string): Promise<void>;
  /**
   * Sends a message on an open assistant session and streams the reply back as
   * {@link ReplyChunk}s via `onChunk` (ticket #8). Resolves once the turn has
   * started; the turn ends at the `final` chunk. Reuses the shared `agent_send`
   * chat transport (the same as the structured chat cell).
   */
  sendTicketChat(
    sessionId: string,
    message: string,
    onChunk: (chunk: ReplyChunk) => void,
  ): Promise<void>;
}

/**
 * Payload of the OS window-lifecycle event that fires when a detached view
 * window closes (ticket #23). Lets the main window re-toggle the view's
 * placement out of `"detached"`.
 *
 * Ticket #47: a detached window is now **panel-only** and follows the main
 * window's focused project rather than embedding a project id, so the close
 * event no longer carries a `projectId` — the placement is keyed by `panel`.
 */
export interface ViewWindowClosed {
  /** The detached panel id (a `PanelId`, kept as `string` at the port seam). */
  panel: string;
}

/**
 * A snapshot of one detached view window currently open at the OS level
 * (ticket #50). Enumerated at startup so the main window can reconcile its
 * `placements` state with windows that were already open before it mounted (a
 * panel-only window restored by the OS shows as "detached", not "closed").
 *
 * `panel` is a `PanelId`-shaped string (kept as `string` at the port seam);
 * `visible` distinguishes a truly-open window from a tracked-but-hidden one.
 */
export interface ViewWindowSnapshot {
  panel: string;
  label: string;
  visible: boolean;
}

export interface PluginLayoutWindowOpenInput {
  pluginId: string;
  layoutType: string;
  state?: JsonValue;
}

export interface PluginLayoutWindowOpenResult {
  label: string;
  url: string;
  alreadyOpen: boolean;
  providerPluginDisplayName: string;
  layoutLabel: string;
  surface: {
    pluginId: string;
    layoutType: string;
    state: JsonValue;
  };
}

/**
 * Detaching a View into its own OS window (ticket #23, reworked in #47).
 *
 * The backend owns the Tauri `WebviewWindow` lifecycle (create/focus/close,
 * anti-duplicate registry). This port is the UI's only door to it — components
 * never call `invoke()` directly. `panel` is a `PanelId`-shaped string; it is
 * typed as `string` here so ports need not depend on the `features/` layer.
 *
 * A detached window is **panel-only**: it no longer carries a project id and
 * instead follows the focused project published through
 * {@link FocusedProjectGateway}. Windows are therefore keyed by `panel` alone.
 */
export interface WindowGateway {
  /**
   * Opens — or focuses, if already open — a separate OS window rendering only
   * `panel` (the backend's `open_view_window` command). The window follows the
   * main window's focused project; no project id is passed.
   */
  openViewWindow(panel: string): Promise<void>;
  /**
   * Opens — or focuses, if already open — a separate OS window rendering one
   * plugin layout contribution declared in the provider plugin's
   * `contributes.layouts`. The backend validates the provider is runtime-active
   * and the layout type is declared before creating the window.
   */
  openPluginLayoutWindow(
    input: PluginLayoutWindowOpenInput,
  ): Promise<PluginLayoutWindowOpenResult>;
  /** Closes the detached window for `panel` if one is open. */
  closeViewWindow(panel: string): Promise<void>;
  /**
   * Lists the detached view windows currently open at the OS level (ticket #50).
   * Called once at main-window mount to reconcile `placements` with windows that
   * survived a restart / were opened before mount, so a panel-only window shows
   * as "detached" rather than "closed" in the Panneaux menu. Best-effort: callers
   * treat a rejection as "no reconciliation".
   */
  listOpenViewWindows(): Promise<ViewWindowSnapshot[]>;
  /**
   * Subscribes to detached-window close events (OS close or programmatic). The
   * handler receives the `{ panel }` that closed. Returns an unsubscribe.
   */
  onViewWindowClosed(
    handler: (event: ViewWindowClosed) => void,
  ): Promise<Unsubscribe>;
}

/**
 * A focused-project snapshot (ticket #47). Mirrors the backend
 * `FocusedProjectDto` — the minimal identity a panel-only window needs to mount
 * a project-scoped view (id for the read-models, name for chrome, root as the
 * agents panel's cwd base).
 */
export interface FocusedProject {
  id: string;
  name: string;
  root: string;
}

/**
 * The focused-project channel between the main window and detached panel-only
 * windows (ticket #47).
 *
 * The main window is the single writer: it publishes the currently active
 * project (or `null` when none is open) via {@link setFocusedProject}. Detached
 * windows are readers: they read the current focus at mount
 * ({@link getFocusedProject}) then track changes via
 * {@link onFocusedProjectChanged}. This decouples a restored panel window from
 * any stale embedded project id — with no focus it shows an "open a project"
 * shell and mounts no project-scoped panel.
 */
export interface FocusedProjectGateway {
  /** Publishes the focused project (or `null` when no project is active). */
  setFocusedProject(project: FocusedProject | null): Promise<void>;
  /** Reads the current focused project; `null` when none is focused. */
  getFocusedProject(): Promise<FocusedProject | null>;
  /**
   * Subscribes to focused-project changes. The handler receives the new focus
   * (or `null`). Returns an unsubscribe.
   */
  onFocusedProjectChanged(
    handler: (project: FocusedProject | null) => void,
  ): Promise<Unsubscribe>;
}

/**
 * Thin UI-preferences port (ticket #29). Persists small, per-surface pieces of
 * UI state (like the ticket filters) so they survive a restart / reopen. This is
 * **frontend-owned** state: the backend stays the source of tickets, never of UI
 * preferences. Values are opaque JSON blobs addressed by an explicit string key;
 * the adapter serialises/parses them. Reads are best-effort — an absent or
 * corrupt entry yields `null` so callers fall back to their default state, and
 * writes never throw (a full/blocked store degrades to "not persisted").
 *
 * Deliberately synchronous: hydration happens during a component's first render,
 * so the persisted state is applied without a flash of default filters. The
 * localStorage adapter is synchronous; a future async backend (Tauri store)
 * would front this with an in-memory cache to keep the contract.
 */
export interface UiPreferencesGateway {
  /** Reads and JSON-parses the blob at `key`; `null` when absent or invalid. */
  read(key: string): unknown;
  /** JSON-serialises and persists `value` at `key`. Best-effort (never throws). */
  write(key: string, value: unknown): void;
  /** Removes any persisted blob at `key`. Best-effort (never throws). */
  remove(key: string): void;
}

/**
 * The full set of gateways the app depends on, injected via the DI provider.
 * The composition (real vs mock) is chosen in `app/`.
 */
/** Input to `reviewPackage` — a candidate package not yet committed to the store. */
export interface ReviewPluginPackageInput {
  sourceKind: PluginSourceKind;
  path: string;
}

/**
 * Plugin system gateway (ticket #43, F1) — admin CRUD + the bootstrap catalog
 * the runtime loader consumes. Mirrors the Tauri commands in carnet §5
 * (`plugin_*`); no DTO shape is re-derived here beyond what the carnet froze.
 */
export interface PluginGateway {
  listPlugins(): Promise<PluginAdmin[]>;
  reviewPackage(input: ReviewPluginPackageInput): Promise<PluginReview>;
  installFromArchive(path: string): Promise<PluginInstallResult>;
  installFromDirectory(path: string): Promise<PluginInstallResult>;
  setEnabled(pluginId: string, enabled: boolean): Promise<PluginAdmin>;
  uninstall(pluginId: string): Promise<PluginUninstallResult>;
  listRuntimeContributions(): Promise<PluginRuntimeContributionCatalog>;
  openPluginsFolder(pluginId?: string): Promise<void>;
}

export interface PluginWorkspacePathInput {
  projectId: string;
  path: string;
}

export interface PluginWorkspaceWriteTextInput extends PluginWorkspacePathInput {
  content: string;
}

export interface PluginWorkspaceWriteBinaryInput extends PluginWorkspacePathInput {
  bytes: Uint8Array;
}

export interface PluginProjectStructureQuery {
  projectId: string;
  path?: string;
  maxDepth?: number;
  maxEntries?: number;
}

export interface PluginWorkspaceGateway {
  readText(input: PluginWorkspacePathInput): Promise<PluginWorkspaceTextFile>;
  readBinary(input: PluginWorkspacePathInput): Promise<PluginWorkspaceBinaryFile>;
  writeText(input: PluginWorkspaceWriteTextInput): Promise<void>;
  writeBinary(input: PluginWorkspaceWriteBinaryInput): Promise<void>;
  listDir(input: PluginWorkspacePathInput): Promise<PluginWorkspaceDirectoryListing>;
  stat(input: PluginWorkspacePathInput): Promise<PluginWorkspaceStat>;
  queryProjectStructure(input: PluginProjectStructureQuery): Promise<PluginProjectStructure>;
}

export interface PluginRunCommandInput {
  projectId: string;
  ownerAgentId: string;
  label: string;
  command: string;
  args?: string[];
  cwd?: string;
  env?: Array<[string, string]>;
  recordOnly?: boolean;
  deadlineMs?: number;
}

export interface PluginTaskStatusInput {
  taskId: string;
}

export interface PluginTaskGateway {
  runCommand(input: PluginRunCommandInput): Promise<PluginCommandTask>;
  getStatus(input: PluginTaskStatusInput): Promise<PluginCommandTask | null>;
}

export interface PluginToolRequirementInput {
  id: string;
  executable: string;
  versionArgs?: string[];
  required?: boolean;
  env?: Array<[string, string]>;
}

export interface PluginEnvRequirementInput {
  name: string;
  required?: boolean;
  equals?: string;
}

export interface PluginFileRequirementInput {
  path: string;
  required?: boolean;
  kind?: PluginExpectedFileKind;
}

export interface PluginToolchainDiagnosticRequest {
  projectId: string;
  cwd?: string;
  tools?: PluginToolRequirementInput[];
  env?: PluginEnvRequirementInput[];
  files?: PluginFileRequirementInput[];
}

export interface PluginToolchainGateway {
  diagnose(input: PluginToolchainDiagnosticRequest): Promise<PluginToolchainDiagnostic>;
}

export interface PluginEventSubscribeInput {
  projectId: string;
  eventTypes?: PluginPublicEventType[];
  capacity?: number;
}

export interface PluginEventPollInput {
  subscriptionId: string;
  maxEvents?: number;
}

export interface PluginEventUnsubscribeInput {
  subscriptionId: string;
}

export interface PluginEventGateway {
  subscribe(input: PluginEventSubscribeInput): Promise<PluginEventSubscription>;
  poll(input: PluginEventPollInput): Promise<PluginEventBatch>;
  unsubscribe(input: PluginEventUnsubscribeInput): Promise<PluginEventSubscription>;
}

export interface PluginConfigDocumentReadInput {
  projectId: string;
  path: string;
  format?: PluginConfigDocumentFormat;
}

export interface PluginConfigDocumentUpdateInput extends PluginConfigDocumentReadInput {
  mode?: PluginConfigUpdateMode;
  value: PluginConfigDocument["value"];
}

export interface PluginConfigGateway {
  readDocument(input: PluginConfigDocumentReadInput): Promise<PluginConfigDocument>;
  updateDocument(input: PluginConfigDocumentUpdateInput): Promise<PluginConfigDocumentWriteResult>;
}

export interface PluginStorageGetInput {
  pluginId: string;
  key: string;
}

export interface PluginStorageSetInput extends PluginStorageGetInput {
  value: JsonValue;
}

export interface PluginStorageGateway {
  get(input: PluginStorageGetInput): Promise<JsonValue | null>;
  set(input: PluginStorageSetInput): Promise<void>;
  delete(input: PluginStorageGetInput): Promise<boolean>;
}

export interface Gateways {
  system: SystemGateway;
  agent: AgentGateway;
  input: InputGateway;
  terminal: TerminalGateway;
  project: ProjectGateway;
  layout: LayoutGateway;
  git: GitGateway;
  remote: RemoteGateway;
  profile: ProfileGateway;
  modelServer: ModelServerGateway;
  desktopServer: DesktopServerGateway;
  template: TemplateGateway;
  skill: SkillGateway;
  memory: MemoryGateway;
  embedder: EmbedderGateway;
  device: DeviceGateway;
  permission: PermissionGateway;
  workState: WorkStateGateway;
  conversation: ConversationGateway;
  ticket: TicketGateway;
  window: WindowGateway;
  focusedProject: FocusedProjectGateway;
  uiPreferences: UiPreferencesGateway;
  plugin: PluginGateway;
  pluginWorkspace: PluginWorkspaceGateway;
  pluginTask: PluginTaskGateway;
  pluginToolchain: PluginToolchainGateway;
  pluginEvents: PluginEventGateway;
  pluginConfig: PluginConfigGateway;
  pluginStorage: PluginStorageGateway;
}
