/**
 * src/types.ts — FROZEN shared contracts (T2).
 *
 * Single source of truth, imported READ-ONLY by every other module.
 * Transcribed from ARCHITECTURE §3. **Dependency-free on purpose**: no `ws`,
 * no `@types/node` ambient (`NodeJS.*`), no DOM — so BOTH the backend and the
 * browser frontend can import it without dragging in each other's type space.
 *
 * Need a new shared type? Change it HERE (a coordination point). Do not
 * redeclare it locally in another module.
 *
 * Three deliberate refinements vs ARCHITECTURE §3 (for the portability goal):
 *   1. `loadConfig` takes `EnvLike` instead of `NodeJS.ProcessEnv` (process.env is assignable).
 *   2. `Session.attachedWs` is `WebSocketLike` instead of `ws`'s `WebSocket` (a real ws socket
 *      is structurally assignable); `WS_OPEN` exported for the M5 readyState guard.
 *   3. `Config.wsPath` added (invariant 8 requires the WS path to be config, not hardcoded).
 */

/* ──────────────────────── config (§3.1) ──────────────────────── */

export interface Config {
  readonly port: number; // PORT, default 3000
  readonly bindHost: string; // BIND_HOST, default '0.0.0.0'
  readonly shellPath: string; // SHELL_PATH, default process.env.SHELL ?? '/bin/zsh'
  readonly homeDir: string; // PTY cwd, default os.homedir()
  readonly idleTtlMs: number; // IDLE_TTL, default 24h
  readonly scrollbackBytes: number; // ring buffer capacity, default 2MB
  readonly maxPayloadBytes: number; // max WS frame bytes, default 1MB (L5)
  readonly wsPath: string; // WS upgrade path, default '/term' (L3; invariant 8)
  readonly maxSessions: number; // cap on concurrent PTY sessions (DoS guard), default 50
  readonly maxMsgsPerSec: number; // per-connection WS message rate cap (DoS guard), default 2000
  readonly permTimeoutMs: number; // H3: how long a held PermissionRequest waits before fallback
  readonly reapIntervalMs: number; // idle-reaper sweep interval
  readonly previewBytes: number; // manage-page preview: bytes of scrollback tail rendered
  readonly useTmux: boolean; // H1: spawn the shell inside tmux so it survives a server restart
  readonly allowedOrigins: readonly string[]; // derived from NIC IPs, NOT bindHost (M1)
  // v0.6 Project Manager — project discovery (impl: src/config.ts T-PM3)
  readonly projectRoots: readonly string[]; // PROJECT_ROOTS, default [homeDir]
  readonly projectScanDepth: number; // PROJECT_SCAN_DEPTH, default 4
  readonly projectScanTtlMs: number; // PROJECT_SCAN_TTL, default 10000 (discovery cache)
  readonly projectDirtyCheck: boolean; // PROJECT_DIRTY_CHECK, default true (git status per repo)
}

/** process.env is structurally assignable to this; keeps the file free of `NodeJS.*`. */
export type EnvLike = Readonly<Record<string, string | undefined>>;

// impl anchor [src/config.ts]:
//   export function loadConfig(env: EnvLike): Config   // invalid values → throw (fail-fast)

/* ─────────────────────── protocol (§3.2) ─────────────────────── */

/** client → server. approve/reject (H3) resolve a held PermissionRequest.
 *  attach.cwd (M6) = spawn a new session in this directory ("new tab here"). */
export type ClientMessage =
  | { type: 'attach'; sessionId: string | null; cwd?: string }
  | { type: 'input'; data: string }
  | { type: 'resize'; cols: number; rows: number }
  | { type: 'approve' }
  | { type: 'reject' };

/** Claude Code activity, derived from hooks (H2). 'unknown' = no hook signal yet. */
export type ClaudeStatus = 'working' | 'waiting' | 'idle' | 'unknown';

/** server → client. exit.code = shell code; -1 when spawn never succeeded (M4).
 *  exit.reason optional normally, REQUIRED on spawn failure / abnormal exit.
 *  status (H2/H3) = Claude Code activity; `pending` true when a tool approval
 *  is held server-side and the client can approve/reject it. */
export type ServerMessage =
  | { type: 'attached'; sessionId: string }
  | { type: 'output'; data: string }
  | { type: 'exit'; code: number; reason?: string }
  | { type: 'status'; status: ClaudeStatus; detail?: string; pending?: boolean };

/** parseClientMessage result — never throws; errors flow here (§5.3). */
export type ParseResult =
  | { ok: true; message: ClientMessage }
  | { ok: false; error: string };

export interface Dims {
  cols: number;
  rows: number;
}

// impl anchors [src/protocol.ts]:
//   export const SESSION_ID_RE: RegExp                              // UUID v4 (M7)
//   export function parseClientMessage(raw: string): ParseResult    // NEVER throws
//   export function serialize(msg: ServerMessage): string

/* ──────────────────────── origin (§3.3) ──────────────────────── */

// impl anchor [src/http/origin.ts]:
//   export function isOriginAllowed(origin: string | undefined, allowed: readonly string[]): boolean
//   host+port must both match; undefined Origin rejected by default.

/* ─────────────────── PTY abstraction (§3.4) ──────────────────── */

export interface IDisposable {
  dispose(): void;
}

/** node-pty's IEvent shape: subscribe a listener, get a disposable back. */
export type PtyEvent<T> = (listener: (e: T) => void) => IDisposable;

/** Subset of node-pty's IPty that we use. A real node-pty IPty is assignable to
 *  this; test/helpers/mock-pty.ts (T3) implements exactly this. */
export interface IPty {
  readonly pid: number;
  readonly cols: number;
  readonly rows: number;
  onData: PtyEvent<string>;
  onExit: PtyEvent<{ exitCode: number; signal?: number }>;
  write(data: string): void;
  resize(cols: number, rows: number): void;
  kill(signal?: string): void;
}

/* ────────────────────── ring buffer (§3.4) ───────────────────── */

export interface RingBuffer {
  append(chunk: string): void;
  /** full buffer for replay; prepend `\x1b[0m` soft-reset as a safety net (M2). */
  snapshot(): string;
  /** last ~maxBytes of recent output, on whole-chunk boundaries (never splits an
   *  ANSI/UTF-8 sequence), for a read-only session preview. No soft-reset prefix. */
  tail(maxBytes: number): string;
}

// impl anchor [src/session/ring-buffer.ts]:
//   export function createRingBuffer(maxBytes: number): RingBuffer
//   M2: measure REAL bytes (Buffer), evict the whole oldest chunk — never split a
//   multi-byte UTF-8 codepoint or an ANSI escape sequence mid-way.

/* ──────────────────────── session (§3.4) ─────────────────────── */

/** The minimal WebSocket the server holds per session. A real `ws` WebSocket is
 *  structurally assignable; keeping this local makes types.ts portable. */
export interface WebSocketLike {
  readonly readyState: number;
  send(data: string): void;
  close(): void;
}

/** WebSocket.OPEN. Guard every send with `ws.readyState === WS_OPEN` (M5). */
export const WS_OPEN = 1;

/** Immutable snapshot, fixed at creation (§5.2). */
export interface SessionMeta {
  readonly id: string; // crypto.randomUUID() — UUID v4
  readonly createdAt: number; // caller-supplied timestamp (injectable for tests)
  readonly shellPath: string;
}

export interface Session {
  readonly meta: SessionMeta;
  readonly buffer: RingBuffer;
  /** All currently-attached clients (multi-device mirror sharing). Empty set =
   *  detached but PTY still alive (vibe-coding core). Output/exit/status are
   *  broadcast to every client; any client can send input (shared control). */
  readonly clients: Set<WebSocketLike>;
  /** Time the LAST client left (clients became empty); null while ≥1 attached. */
  detachedAt: number | null;
  /** last pty.onData timestamp; reapIdle liveness proxy (M3). */
  lastOutputAt: number;
  /** PTY exit time; null = alive. Once set: writeInput/resize are ignored (L4),
   *  and attach replays the buffer then re-sends `exit` (L1). */
  exitedAt: number | null;
  exitCode: number | null;
  /** Claude Code activity from hooks (H2); updated by manager.handleHookEvent. */
  claudeStatus: ClaudeStatus;
  /** Spawn directory, for the /live-sessions label; null when unknown. */
  cwd: string | null;
  /** tmux session name (H1) when running under tmux, else null. */
  readonly tmuxName: string | null;
  readonly pty: IPty;
}

// impl anchors [src/session/session.ts]:
//   createSession(cfg: Config, dims: Dims, now: number, onExit: (s: Session) => void): Session
//                                                  // spawn failure THROWS, not swallowed (M4)
//   attachWs(session: Session, ws: WebSocketLike): void             // adds a client, replays buffer (no size vote until it resizes)
//   detachWs(session: Session, ws: WebSocketLike, now: number): void // removes one client; never kills PTY
//   writeInput(session: Session, data: string): void                // no-op after exit (L4)
//   setClientDims(session: Session, ws: WebSocketLike, cols, rows): void  // PTY = latest-writer-wins (L4 no-op after exit)
//   kill(session: Session): void

/* ──────────────────────── manager (§3.5) ─────────────────────── */

/** A live (or just-exited) server session, for the /live-sessions discovery list. */
export interface LiveSessionInfo {
  id: string;
  createdAt: number;
  clientCount: number; // how many devices are currently attached
  status: ClaudeStatus;
  exited: boolean;
  cwd: string | null;
  cols: number; // current PTY size (for the manage page)
  rows: number;
}

/* ───────────────── project manager (v0.6, §4.3 FEATURE doc) ──────────────── */

/** One running session belonging to a project (live-sessions归并 by cwd).
 *  Mirrors LiveSessionInfo fields so buildProjects can map directly. */
export interface ProjectSessionRef {
  id: string;
  title?: string; // tab title / derived label (e.g. 'claude', 'shell'); optional
  status: ClaudeStatus; // reuse hook status (working|waiting|idle|unknown)
  clientCount: number; // mirror devices currently attached
  createdAt: number;
  exited: boolean;
}

/** A discovered project (git repo or recently-used cwd) for the Projects panel.
 *  impl: src/http/projects.ts buildProjects(cfg, liveSessions) — T-PM2. */
export interface ProjectInfo {
  name: string; // repo directory name
  path: string; // absolute path
  isGit: boolean;
  branch?: string; // current branch (git repos only)
  dirty?: boolean; // uncommitted changes (when projectDirtyCheck)
  lastActiveMs?: number; // newest ~/.claude/projects mtime for this cwd; sort key
  sessions: ProjectSessionRef[]; // running sessions in this project (1:N; may be empty)
}

export interface SessionManager {
  handleAttach(
    ws: WebSocketLike,
    sessionId: string | null,
    dims: Dims,
    now: number,
    cwd?: string,
  ): Session;
  get(id: string): Session | undefined;
  /** Live sessions for multi-device discovery (newest first). */
  list(): LiveSessionInfo[];
  /** Kill a session by id (manage page): close its clients, kill the PTY, drop it.
   *  Returns true if a session was found and killed. */
  killById(id: string): boolean;
  /** Set a session's Claude status (from a hook) and push it to the attached ws (H2/H3). */
  handleHookEvent(
    sessionId: string,
    status: ClaudeStatus,
    detail?: string,
    pending?: boolean,
  ): void;
  /** reclaim when now - max(detachedAt, lastOutputAt) > idleTtlMs (M3). Returns count. */
  reapIdle(now: number): number;
  shutdown(): void;
}

// impl anchor [src/session/manager.ts]:
//   export function createSessionManager(cfg: Config): SessionManager

/* ─────────────────────── frontend (§5/§6.3) ──────────────────── */

/** keybar.ts (T10) exports a mount fn of this shape; main.ts (T11) calls it. */
export type MountKeybar = (onSend: (data: string) => void) => void;
