import type { MessageCost, PromptImage, TurnMessage } from '../api/chat/types';

// ── Envelope ──

export type SidecarMessage = SidecarCommand | SidecarEvent;

/**
 * A session the agent is holding in memory right now — the ground truth about what is alive.
 *
 * `pendingTasks` is background work started but not yet notified, and it is the field worth having:
 * together with `isGenerating` it is exactly what the agent's own idle GC consults before deciding a
 * session may be collected, so a caller can tell "busy" from "merely open" the same way it does.
 */
export type LiveClaudeSession = {
  sessionKey: string;
  /**
   * Claude's own session id — the transcript's filename — or null before the first turn reports one.
   *
   * NOT the same as `sessionKey`, which is officer's handle for the conversation. Only the agent holds
   * the mapping (`setClaudeSession`/`getClaudeSession`), so anything that wants to find the transcript
   * has to be told it here. Assuming the two were equal made every live row unnameable and unlinkable.
   */
  claudeSessionId: string | null;
  isGenerating: boolean;
  pendingTasks: number;
};

/**
 * An OpenCode turn in flight. Only ever the generating ones, which is why it carries neither
 * `isGenerating` (always true) nor `pendingTasks` (no such concept on this harness).
 *
 * It deliberately carries no id for OpenCode's own `ses_…` session, and does not need to: the sidecar
 * only ever knows its own `sessionKey`, while the `ses_…` is reported separately over `opencode:session`
 * and recorded by officer in `api/chat/opencode/state.ts`. `/chat/live` joins the two on officer's side,
 * which is where both halves already are — widening this type would mean sending the sidecar a fact it
 * had told officer in the first place.
 */
export type LiveOpenCodeSession = {
  sessionKey: string;
};

// ── Commands (API server → sidecar) ──

export type SidecarCommand =
  | { type: 'ping'; id: string }
  | { type: 'state:sync'; id: string }
  // Proxy
  | { type: 'proxy:secret'; id: string }
  // Claude Code
  | { type: 'claude:spawn'; id: string; params: ClaudeSpawnParams }
  | { type: 'claude:spawn-streaming'; id: string; params: ClaudeSpawnStreamingParams }
  // Every one of these carries `userId` for the same reason `ClaudeSpawnStreamingParams.member` does: the
  // sidecar's session map is global and `sessionKey` arrives in a client message, so a command without an
  // identity is a command that acts on whoever's session happens to match. Resolved by the platform from the
  // authenticated request, never taken from the client.
  | { type: 'claude:kill'; id: string; sessionKey: string; userId: number }
  | { type: 'claude:interrupt'; id: string; sessionKey: string; userId: number }
  | { type: 'claude:clear-session'; id: string; sessionKey: string; userId: number }
  // Is a turn still running for this session? Only the process that owns the session can say, which is
  // exactly why it is asked over the wire — see `isClaudeGenerating` in sidecar-registry.
  | { type: 'claude:is-generating'; id: string; sessionKey: string; userId: number }
  // Which session key owns this transcript? The map lives on the agent's disk, so only it can answer —
  // see `findClaudeSessionKey` in sidecar-registry, and `attach` in the chat socket for why it is asked.
  | { type: 'claude:find-session'; id: string; claudeSessionId: string; userId: number }
  // Everything the agent is holding right now. `claude:is-generating` answers for a session you can
  // already name; this is for the case where officer has forgotten every name it had — its session
  // records are in memory and die with `pm2 restart officer`, while the agent keeps running. Without it
  // a live session is invisible until a browser happens to reconnect to it by id.
  | { type: 'claude:list'; id: string; userId: number }
  // OpenCode — drive a turn through the serve (POST /api/session/{id}/prompt), anchored to the chat cwd
  // by a per-request location header. Was an `opencode run` subprocess until 2026-08-10.
  | { type: 'opencode:run-streaming'; id: string; params: OpenCodeRunParams }
  | { type: 'opencode:kill'; id: string; sessionKey: string }
  // Which OpenCode turns are running right now. The counterpart of `claude:list`, and thinner for a
  // reason: OpenCode has no warm session between turns, so there is nothing to report but the running
  // ones — see `listRunningServeTurns`.
  | { type: 'opencode:list'; id: string }
  // VNC
  | { type: 'vnc:start'; id: string; params: VncStartParams }
  // Provision the VNC password without starting a server — the UI needs it before it can connect
  | { type: 'vnc:ensure-password'; id: string; email: string }
  | { type: 'vnc:stop'; id: string; email: string };

// ── Responses/Events (sidecar → API server) ──

export type SidecarEvent =
  | { type: 'pong'; id: string }
  | { type: 'state:sync'; id: string; state: ClaudeState }
  | { type: 'proxy:secret'; id: string; secret: string }
  // Claude Code
  | { type: 'claude:spawned'; id: string; sessionKey: string }
  // A finished, browser-facing turn message. The agent has already committed it to chat_session_events
  // and `seq` is its cursor id there; officer relays it verbatim. No `seq` means it is not durable —
  // an `assistant:delta` (superseded by the text that follows) or a message whose write failed.
  | { type: 'claude:message'; sessionKey: string; msg: TurnMessage; seq?: number }
  | { type: 'claude:result'; id: string; result: ClaudeCodeResult }
  | { type: 'claude:error'; id: string; error: string }
  | { type: 'claude:killed'; id: string }
  | { type: 'claude:interrupted'; id: string }
  | { type: 'claude:session-cleared'; id: string }
  | { type: 'claude:generating'; id: string; generating: boolean }
  | { type: 'claude:session-key'; id: string; sessionKey: string | null }
  | { type: 'claude:sessions'; id: string; sessions: LiveClaudeSession[] }
  | { type: 'opencode:sessions'; id: string; sessions: LiveOpenCodeSession[] }
  // VNC
  | { type: 'vnc:started'; id: string; port: number; display: number }
  | { type: 'vnc:password'; id: string; password: string }
  | { type: 'vnc:stopped'; id: string }
  | { type: 'vnc:error'; id: string; error: string }
  // Email — new mail no longer crosses this socket; the sidecar owns the SSE stream and pushes directly
  // OpenCode — the sidecar reports where its `opencode serve` is listening (random port) on connect
  | { type: 'opencode:server'; port: number }
  // OpenCode turn streaming (analog of claude:*): spawned ack, per-message stream, session id report
  | { type: 'opencode:spawned'; id: string; sessionKey: string }
  // Same contract as `claude:message`: a finished turn message the sidecar has already committed to
  // chat_session_events, plus the cursor id it landed on. Officer relays it and folds it into its
  // in-memory transcript; it does not translate or persist.
  | { type: 'opencode:message'; sessionKey: string; msg: TurnMessage; seq?: number }
  | { type: 'opencode:session'; sessionKey: string; sessionId: string }
  | { type: 'opencode:error'; id: string; error: string }
  // Music — the sidecar reports where its audio-streaming HTTP server is listening (random port) on connect
  | { type: 'music:server'; port: number }
  // Vault — the sidecar reports where its Vaultwarden reverse-proxy HTTP/WS server is listening on connect
  | { type: 'vault:server'; port: number }
  // slskd — the sidecar reports where its slskd reverse-proxy HTTP server is listening (random port) on connect
  | { type: 'slskd:server'; port: number }
  // Headscale — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'headscale:server'; port: number }
  // Transmission — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'transmission:server'; port: number }
  // InvoiceShelf — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'invoiceshelf:server'; port: number }
  // Jellyfin — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'jellyfin:server'; port: number }
  // Photos (Immich) — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'photos:server'; port: number }
  // Memos — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'memos:server'; port: number }
  // Gitea — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'gitea:server'; port: number }
  // CalDAV/CardDAV — the sidecar reports where its HTTP server is listening (random port) on connect.
  // One port serves both doors: /dav (forwarded to Radicale) and /_officer (JSON for Officer's UI).
  | { type: 'caldav:server'; port: number }
  // Wallet — the sidecar reports where its HTTP server is listening (random port) on connect
  | { type: 'wallet:server'; port: number }
  // PTY — the sidecar reports where its terminal HTTP/WS server is listening (random port) on connect
  | { type: 'pty:server'; port: number }
  // Email — the sidecar reports where its mail HTTP server is listening (random port) on connect
  | { type: 'email:server'; port: number }
  // Notify — the sidecar reports where its notification HTTP server is listening (random port) on connect
  | { type: 'notify:server'; port: number }
  // Generic
  | { type: 'error'; id?: string; error: string };

// ── Claude sidecar state ──

export type ClaudeState = {
  proxySecret: string;
  claudeSessions: Record<string, { userId: number; claudeSessionId: string }>; // sessionKey → whose, and which transcript
};

// ── Param types ──

export type ClaudeSpawnParams = {
  userId: number;
  email: string;
  username: string;
  prompt: string;
  sessionKey: string;
  model?: string;
  cwd?: string;
};

export type ClaudeSpawnStreamingParams = {
  userId: number;
  email: string;
  username: string;
  prompt: string;
  images?: PromptImage[]; // sent as native image content blocks alongside the prompt text
  sessionKey: string;
  cwd?: string;
  model?: string;
  resumeSessionId?: string; // resume this Claude session uuid (from the /chat session list)
  // Whether turn output should be committed to chat_session_events (default true). A chat session wants
  // it — that is what survives an officer restart. A pipeline step does not: its sessionKey is a throwaway
  // uuid no browser will ever replay, and the job's own event log is its record.
  durable?: boolean;
  // Run this turn as a member's own Linux account, with their `claude` and their credential.
  //
  // Absent means the owner, which is every caller today: `api/chat/chat.ts` and the socket in `server.tsx`
  // both refuse non-owners, so nothing can currently populate this. That is deliberate — the field exists so
  // the sidecar side is testable and reviewable before the gates come off, not because anything sets it yet.
  //
  // Resolved by the platform from `resolveHomeDir`, NEVER taken from a client. Same rule as the pty sidecar,
  // where `server.tsx` deletes any client-supplied `osUser`/`home` from the query string before setting its
  // own — a member's identity is not a parameter they get to choose.
  member?: { osUser: string; home: string };
};

export type ClaudeCodeResult = {
  text: string;
  sessionId: string;
  model: string;
  cost: MessageCost;
};

// ── OpenCode turn params ──

export type OpenCodeRunParams = {
  sessionKey: string;
  prompt: string;
  cwd?: string; // passed to `opencode run --dir` — hard-re-anchors tools to this directory
  model?: string; // `providerID/modelID` (e.g. opencode/claude-haiku-4-5); passed to --model verbatim
  resumeSessionId?: string; // OpenCode `ses_…` id to continue (`--session`)
  /**
   * Images for this turn, base64 as the browser sent them.
   *
   * `opencode run` takes attachments as PATHS (`-f`), not inline data, so the sidecar writes each one to
   * a temp file and deletes it when the turn ends. They travel as data because that is what crosses the
   * websocket from the browser, and because officer and the sidecar are not guaranteed to share a disk.
   */
  images?: PromptImage[];
  durable?: boolean; // commit turn output to chat_session_events (default true) — see ClaudeSpawnStreamingParams
};

// ── VNC types ──

export type VncStartParams = {
  email: string;
  username: string;
  resolution?: string;
};

export type VncSessionInfo = {
  email: string;
  display: number;
  port: number;
  pid: number;
  alive: boolean;
};

// ── PTY ──
//
// Nothing but a port crosses this socket now. The pty sidecar serves its own HTTP + WebSocket listener and
// the browser reaches it through a byte relay, so there is no command vocabulary left: pty:init, :input,
// :resize, :close and :list all lived here until the sidecar owned its own transport, and officer filtered
// one global output stream per session to feed them. The port arrives as the shared `pty:server` event
// that createSidecarProxy already listens for.
