// Protocole WebSocket Arboretum — une connexion multiplexée par client.
// Messages de contrôle : frames TEXTE JSON. Sortie terminal : frames BINAIRES
// (un chunk PTY peut couper un caractère UTF-8 en frontière de frame ; le
// décodage incombe à xterm.write(Uint8Array) côté client, jamais au transport).

export const PROTOCOL_VERSION = 1;

// ---- Frames binaires ----
// Layout : [type u8][channel u32le][payload...]
export const BINARY_FRAME = {
  HEADER_BYTES: 5,
  /** serveur → client : sortie terminal */
  OUTPUT: 0x01,
  /** serveur → client : resync — le client doit reset son terminal avant d'écrire le payload */
  RESYNC: 0x02,
} as const;

export function encodeBinaryFrame(type: number, channel: number, payload: Uint8Array): Uint8Array {
  const frame = new Uint8Array(BINARY_FRAME.HEADER_BYTES + payload.byteLength);
  const view = new DataView(frame.buffer);
  view.setUint8(0, type);
  view.setUint32(1, channel, true);
  frame.set(payload, BINARY_FRAME.HEADER_BYTES);
  return frame;
}

export function decodeBinaryFrame(data: Uint8Array): { type: number; channel: number; payload: Uint8Array } {
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  return {
    type: view.getUint8(0),
    channel: view.getUint32(1, true),
    payload: data.subarray(BINARY_FRAME.HEADER_BYTES),
  };
}

// ---- Flow control (pattern officiel xterm.js, watermarks du design) ----
// INVARIANT anti-deadlock : ACK_EVERY_BYTES <= LOW_WATERMARK. Le client n'ACK
// qu'à réception de données ; si le serveur pouvait se mettre en pause avec un
// reliquat non-ACKé > LOW mais < pas d'ACK, plus aucun des deux ne progresserait.
// Avec ACK_EVERY <= LOW, une fois tout le flux en vol traité, le reliquat est
// < LOW et le serveur reprend toujours. (Complété côté client par un ACK
// traînant débouncé.)
export const FLOW = {
  /** le client ACK tous les N octets réellement traités par xterm.write */
  ACK_EVERY_BYTES: 64 * 1024,
  /** pause du PTY quand TOUS les clients interactifs dépassent HIGH */
  HIGH_WATERMARK: 384 * 1024,
  /** reprise quand le min repasse sous LOW */
  LOW_WATERMARK: 128 * 1024,
  /** au-delà : client marqué lagging, flux coupé, resync au rattrapage */
  LAGGING_BYTES: 2 * 1024 * 1024,
} as const;

/** Replay à l'attach : reset terminal + queue du ring (l'écran TUI se repeint en continu) */
export const REPLAY_TAIL_BYTES = 256 * 1024;

// ---- États de session (sous-ensemble P1 ; étendu en P3) ----
export type SessionRuntimeStatus =
  | 'starting'
  | 'running' // P1 : pas encore de distinction busy/waiting/idle (P3 via claude-adapter)
  | 'exited';

/** Origine d'une session listée par Arboretum. */
export type SessionSource =
  | 'managed' // PTY lancé par Arboretum (vit dans le PtyManager)
  | 'discovered'; // session Claude externe lue sur disque (~/.claude)

/** Statut brut tel qu'écrit par le CLI dans ~/.claude/sessions (interprété finement en P3). */
export type SessionRegistryStatus = 'busy' | 'idle' | 'waiting';

// ---- États fins de session (P3-B, claude-adapter) ----
/** busy = Claude traite ; waiting = bloqué sur un dialogue ; idle = prêt pour une instruction. */
export type SessionActivity = 'busy' | 'waiting' | 'idle';
export type DialogKind = 'trust' | 'permission' | 'question' | 'plan';
export interface DialogOption {
  n: number;
  label: string;
  selected: boolean;
}
export interface SessionDialog {
  kind: DialogKind;
  waitingFor: string | null;
  options: DialogOption[];
}

export interface SessionSummary {
  id: string;
  cwd: string;
  command: string;
  title: string | null;
  status: SessionRuntimeStatus;
  live: boolean;
  createdAt: string;
  endedAt: string | null;
  exitCode: number | null;
  clients: number;
  // ---- P2 : découverte & reprise (additif) ----
  /** 'managed' = PtyManager ; 'discovered' = lue sur disque. */
  source: SessionSource;
  /** ID interne du CLI claude (clé de corrélation managé↔découvert) ; null pour bash ou claude pas encore prêt. */
  claudeSessionId: string | null;
  /** pid du process claude (découvertes vivantes & managées vivantes) ; null sinon. */
  pid: number | null;
  /** true si un `--resume` direct est sûr (session morte) ; false pour une vivante (corruption). */
  resumable: boolean;
  /** true si Arboretum tient le PTY (managée vivante) ; jamais pour une découverte. */
  attachable: boolean;
  /** statut brut du registre ~/.claude/sessions (P2) ; interprété finement en P3 (claude-adapter). */
  registryStatus: SessionRegistryStatus | null;
  // ---- P3-B : états fins (optionnels, remplis par le claude-adapter) ----
  /** absent/null = inconnu (bash, démarrage, historique). */
  activity?: SessionActivity | null;
  waitingFor?: string | null;
  /** dialogue typé en cours (présent quand activity === 'waiting'). */
  dialog?: SessionDialog | null;
}

// ---- Worktrees & repos (P3) ----
/** Hook lancé après création d'un worktree (commande shell exécutée dans le nouveau worktree). */
export interface PostCreateHook {
  id: string;
  label: string;
  run: string;
  enabled: boolean;
}

export interface RepoSummary {
  id: string;
  /** chemin absolu de la racine du repo (main worktree). */
  path: string;
  label: string;
  defaultBranch: string | null;
  postCreateHooks: PostCreateHook[];
  /** pré-écrire hasTrustDialogAccepted dans ~/.claude.json à la création d'un worktree. */
  preTrust: boolean;
  createdAt: string;
  /** false si le chemin n'est plus un repo git accessible. */
  valid: boolean;
  /** true = masqué du dashboard (conservé en DB → non ré-ajouté au re-scan). */
  hidden: boolean;
}

export interface WorktreeGitStatus {
  ahead: number;
  behind: number;
  dirtyCount: number;
  upstream: string | null;
}

export interface WorktreeSummary {
  repoId: string;
  /** chemin absolu du worktree (clé de corrélation avec le cwd des sessions). */
  path: string;
  branch: string | null;
  head: string;
  detached: boolean;
  locked: boolean;
  prunable: boolean;
  isMain: boolean;
  git: WorktreeGitStatus;
  /** sessions corrélées par cwd (managées + découvertes) ; leur `activity` est remplie en P3-B. */
  sessions: SessionSummary[];
}

// ---- Groupes de travail (P5) ----
// Un groupe regroupe plusieurs repos pour piloter des sessions Claude en simultané sur
// plusieurs worktrees. Membership légère : seule la liste d'ids de repos est persistée ;
// les repos/worktrees/sessions du groupe sont dérivés par filtrage sur `repoId` côté client.
export interface GroupSummary {
  id: string;
  label: string;
  description: string | null;
  /** couleur d'accent UI (hex `#rrggbb`) ou null. */
  color: string | null;
  /** ids de repos membres, ordonnés par leur position dans le groupe. */
  repoIds: string[];
  createdAt: string;
  updatedAt: string;
}

// ---- Messages client → serveur ----
export type ClientMessage =
  | { type: 'hello'; protocol: number }
  | { type: 'attach'; sessionId: string; mode: 'interactive' | 'observer'; cols: number; rows: number }
  | { type: 'detach'; channel: number }
  | { type: 'stdin'; channel: number; data: string }
  // P4-A : répondre à un dialogue Claude sans clavier. Le serveur traduit l'intention
  // en keystrokes (chiffre+Entrée pour 'select', Entrée pour 'confirm', Esc pour 'deny')
  // et valide l'option contre le dialogue courant (anti-frappe fantôme mobile).
  | { type: 'answer'; channel: number; action: 'select' | 'confirm' | 'deny'; optionN?: number }
  | { type: 'resize'; channel: number; cols: number; rows: number }
  | { type: 'ack'; channel: number; bytes: number }
  | { type: 'sub'; topics: Array<'sessions' | 'worktrees' | 'groups'> }
  | { type: 'ping' };

// ---- Messages serveur → client ----
export type ServerMessage =
  | { type: 'hello_ok'; protocol: number; serverVersion: string }
  | { type: 'attached'; channel: number; sessionId: string; mode: 'interactive' | 'observer'; controlling: boolean }
  | { type: 'detached'; channel: number; reason: 'client' | 'session_exit' | 'replaced' }
  | { type: 'control_changed'; channel: number; controlling: boolean }
  | { type: 'session_update'; session: SessionSummary }
  | { type: 'session_exit'; sessionId: string; exitCode: number | null; signal: number | null }
  | { type: 'repo_update'; repo: RepoSummary }
  | { type: 'repo_removed'; repoId: string }
  | { type: 'worktree_update'; repoId: string; worktree: WorktreeSummary }
  | { type: 'worktree_removed'; repoId: string; path: string }
  | { type: 'group_update'; group: GroupSummary }
  | { type: 'group_removed'; groupId: string }
  | { type: 'error'; code: ErrorCode; message: string; channel?: number }
  | { type: 'pong' };

export type ErrorCode =
  | 'BAD_PROTOCOL'
  | 'BAD_MESSAGE'
  | 'NOT_FOUND'
  | 'NOT_ATTACHED'
  | 'NOT_CONTROLLING'
  | 'SESSION_EXITED'
  | 'INVALID_ANSWER'
  | 'INTERNAL';

export function parseClientMessage(raw: string): ClientMessage | null {
  let obj: unknown;
  try {
    obj = JSON.parse(raw);
  } catch {
    return null;
  }
  if (typeof obj !== 'object' || obj === null || typeof (obj as { type?: unknown }).type !== 'string') return null;
  const m = obj as Record<string, unknown>;
  const isU32 = (v: unknown): v is number => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0 && v <= 0xffffffff;
  const isCount = (v: unknown): v is number => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0;
  const isDim = (v: unknown): v is number => typeof v === 'number' && Number.isSafeInteger(v) && v >= 2 && v <= 1000;
  switch (m.type) {
    case 'hello':
      return isCount(m.protocol) ? { type: 'hello', protocol: m.protocol } : null;
    case 'attach':
      return typeof m.sessionId === 'string' && (m.mode === 'interactive' || m.mode === 'observer') && isDim(m.cols) && isDim(m.rows)
        ? { type: 'attach', sessionId: m.sessionId, mode: m.mode, cols: m.cols, rows: m.rows }
        : null;
    case 'detach':
      return isU32(m.channel) ? { type: 'detach', channel: m.channel } : null;
    case 'stdin':
      return isU32(m.channel) && typeof m.data === 'string' && m.data.length <= 65536
        ? { type: 'stdin', channel: m.channel, data: m.data }
        : null;
    case 'answer': {
      if (!isU32(m.channel)) return null;
      if (m.action === 'select')
        return isCount(m.optionN) && (m.optionN as number) >= 1 && (m.optionN as number) <= 99
          ? { type: 'answer', channel: m.channel, action: 'select', optionN: m.optionN as number }
          : null;
      return m.action === 'confirm' || m.action === 'deny'
        ? { type: 'answer', channel: m.channel, action: m.action }
        : null;
    }
    case 'resize':
      return isU32(m.channel) && isDim(m.cols) && isDim(m.rows)
        ? { type: 'resize', channel: m.channel, cols: m.cols, rows: m.rows }
        : null;
    case 'ack':
      return isU32(m.channel) && isCount(m.bytes)
        ? { type: 'ack', channel: m.channel, bytes: m.bytes }
        : null;
    case 'sub':
      return Array.isArray(m.topics) && m.topics.every((t) => t === 'sessions' || t === 'worktrees' || t === 'groups')
        ? { type: 'sub', topics: m.topics as Array<'sessions' | 'worktrees' | 'groups'> }
        : null;
    case 'ping':
      return { type: 'ping' };
    default:
      return null;
  }
}
