// ElectrobunAdapter — implements BackendAdapter for Electrobun backend
// Wraps the Electrobun RPC pattern (request/response + message listeners)

import type {
  BackendAdapter, BackendCapabilities, UnsubscribeFn,
  AgentStartOptions, AgentMessage, AgentStatus,
  FileEntry, FileContent, PtyCreateOptions,
  SettingsMap, GroupsFile, GroupConfig, GroupId, AgentId, SessionId, ProjectId,
  SearchResult,
  PersistedSession, PersistedLayout,
  AgentMessageRecord, ProjectAgentState, SessionMetricRecord, MdFileEntry,
  BtmsgAgent, BtmsgMessage, BtmsgChannel, BtmsgChannelMessage, BtmsgFeedMessage,
  DeadLetterEntry, AuditEntry, AuditEventType,
  Task, TaskComment,
  SessionAnchorRecord,
  NotificationUrgency, TelemetryLevel,
  FsWriteEvent, FsWatcherStatus,
  CtxEntry, CtxSummary,
  MemoraNode, MemoraSearchResult,
  SshSessionRecord, PluginMeta,
  ClaudeProfile, ClaudeSkill,
  RemoteMachineConfig, RemoteMachineInfo,
  RemoteSidecarMessage, RemotePtyData, RemotePtyExit,
  RemoteMachineEvent, RemoteReconnectingEvent, RemoteSpkiTofuEvent,
  FileChangedPayload, AgentQueryOptions, SidecarMessagePayload, PtySpawnOptions,
} from '@agor/types';

// ── Capabilities ─────────────────────────────────────────────────────────────

const ELECTROBUN_CAPABILITIES: BackendCapabilities = {
  supportsPtyMultiplexing: true,
  supportsPluginSandbox: true,
  supportsNativeMenus: false,
  supportsOsKeychain: false,
  supportsFileDialogs: false,
  supportsAutoUpdater: false,
  supportsDesktopNotifications: false,
  supportsTelemetry: false,
};

// ── RPC handle type (matches ui-electrobun/src/mainview/rpc.ts) ──────────────

interface RpcRequestFns {
  [method: string]: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
}

interface RpcHandle {
  request: RpcRequestFns;
  addMessageListener: (event: string, handler: (payload: unknown) => void) => void;
  removeMessageListener?: (event: string, handler: (payload: unknown) => void) => void;
}

// ── Adapter ──────────────────────────────────────────────────────────────────

export class ElectrobunAdapter implements BackendAdapter {
  readonly capabilities = ELECTROBUN_CAPABILITIES;

  private rpc: RpcHandle | null = null;
  private messageHandlers: Array<{ event: string; handler: (payload: unknown) => void }> = [];

  /** Inject the Electrobun RPC handle (set from main.ts after Electroview.defineRPC()) */
  setRpc(rpc: RpcHandle): void {
    this.rpc = rpc;
  }

  private get r(): RpcHandle {
    if (!this.rpc) {
      throw new Error('[ElectrobunAdapter] RPC not initialized. Call setRpc() first.');
    }
    return this.rpc;
  }

  async init(): Promise<void> {
    // RPC handle is set externally via setRpc()
  }

  async destroy(): Promise<void> {
    if (this.rpc?.removeMessageListener) {
      for (const { event, handler } of this.messageHandlers) {
        this.rpc.removeMessageListener(event, handler);
      }
    }
    this.messageHandlers = [];
    this.rpc = null;
  }

  // ── Settings ─────────────────────────────────────────────────────────────

  async getSetting(key: string): Promise<string | null> {
    const res = await this.r.request['settings.get']({ key }) as { value: string | null };
    return res.value;
  }

  async setSetting(key: string, value: string): Promise<void> {
    await this.r.request['settings.set']({ key, value });
  }

  async getAllSettings(): Promise<SettingsMap> {
    const res = await this.r.request['settings.getAll']({}) as { settings: SettingsMap };
    return res.settings;
  }

  // ── Groups ───────────────────────────────────────────────────────────────

  async loadGroups(): Promise<GroupsFile> {
    const res = await this.r.request['groups.list']({}) as {
      groups: Array<{ id: string; name: string; icon: string; position: number }>;
    };

    const projectsRes = await this.r.request['settings.getProjects']({}) as {
      projects: Array<{ id: string; config: string }>;
    };

    const projectsByGroup = new Map<string, Array<Record<string, unknown>>>();
    for (const p of projectsRes.projects) {
      try {
        const config = JSON.parse(p.config) as Record<string, unknown>;
        const groupId = String(config['groupId'] ?? 'default');
        if (!projectsByGroup.has(groupId)) projectsByGroup.set(groupId, []);
        projectsByGroup.get(groupId)!.push(config);
      } catch { /* skip invalid */ }
    }

    const groups: GroupConfig[] = res.groups.map((g) => ({
      id: g.id as GroupId,
      name: g.name,
      projects: (projectsByGroup.get(g.id) ?? []) as unknown as GroupConfig['projects'],
    }));

    return {
      version: 1,
      groups,
      activeGroupId: (groups[0]?.id ?? 'default') as GroupId,
    };
  }

  async saveGroups(groupsFile: GroupsFile): Promise<void> {
    for (const group of groupsFile.groups) {
      await this.r.request['groups.create']({
        id: group.id, name: group.name, icon: '', position: groupsFile.groups.indexOf(group),
      });
    }
    for (const group of groupsFile.groups) {
      for (const project of group.projects) {
        await this.r.request['settings.setProject']({
          id: project.id, config: JSON.stringify({ ...project, groupId: group.id }),
        });
      }
    }
  }

  // ── Agent (simplified) ────────────────────────────────────────────────────

  async startAgent(options: AgentStartOptions): Promise<{ ok: boolean; error?: string }> {
    const res = await this.r.request['agent.start']({
      sessionId: options.sessionId, provider: options.provider ?? 'claude',
      prompt: options.prompt, cwd: options.cwd, model: options.model,
      systemPrompt: options.systemPrompt, maxTurns: options.maxTurns,
      permissionMode: options.permissionMode, claudeConfigDir: options.claudeConfigDir,
      extraEnv: options.extraEnv, additionalDirectories: options.additionalDirectories,
      worktreeName: options.worktreeName,
    }) as { ok: boolean; error?: string };
    return res;
  }

  async stopAgent(sessionId: string): Promise<{ ok: boolean; error?: string }> {
    const res = await this.r.request['agent.stop']({ sessionId }) as { ok: boolean; error?: string };
    return res;
  }

  async sendPrompt(sessionId: string, prompt: string): Promise<{ ok: boolean; error?: string }> {
    const res = await this.r.request['agent.prompt']({ sessionId, prompt }) as { ok: boolean; error?: string };
    return res;
  }

  // ── PTY (simplified) ──────────────────────────────────────────────────────

  async createPty(options: PtyCreateOptions): Promise<string> {
    const res = await this.r.request['pty.create']({
      sessionId: options.sessionId, cols: options.cols, rows: options.rows,
      cwd: options.cwd, shell: options.shell, args: options.args,
    }) as { ok: boolean; error?: string };
    if (!res.ok) throw new Error(res.error ?? 'PTY creation failed');
    return options.sessionId;
  }

  async writePty(sessionId: string, data: string): Promise<void> {
    await this.r.request['pty.write']({ sessionId, data });
  }

  async resizePty(sessionId: string, cols: number, rows: number): Promise<void> {
    await this.r.request['pty.resize']({ sessionId, cols, rows });
  }

  async closePty(sessionId: string): Promise<void> {
    await this.r.request['pty.close']({ sessionId });
  }

  // ── Files ────────────────────────────────────────────────────────────────

  async listDirectory(path: string): Promise<FileEntry[]> {
    const res = await this.r.request['files.list']({ path }) as {
      entries: Array<{ name: string; type: string; size: number }>; error?: string;
    };
    if (res.error) throw new Error(res.error);
    return res.entries.map((e) => ({
      name: e.name, path: `${path}/${e.name}`, isDir: e.type === 'dir', size: e.size,
    }));
  }

  async readFile(path: string): Promise<FileContent> {
    const res = await this.r.request['files.read']({ path }) as {
      content?: string; encoding: string; size: number; error?: string;
    };
    if (res.error) throw new Error(res.error);
    if (res.encoding === 'base64') return { type: 'Binary', message: `Binary file (${res.size} bytes)` };
    return { type: 'Text', content: res.content ?? '' };
  }

  async writeFile(path: string, content: string): Promise<void> {
    const res = await this.r.request['files.write']({ path, content }) as { ok: boolean; error?: string };
    if (!res.ok) throw new Error(res.error ?? 'Write failed');
  }

  // ── Session persistence ─────────────────────────────────────────────────

  async listSessions(): Promise<PersistedSession[]> {
    // TODO: wire to Electrobun RPC
    return [];
  }

  async saveSession(_session: PersistedSession): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async deleteSession(_id: string): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async updateSessionTitle(_id: string, _title: string): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async touchSession(_id: string): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async updateSessionGroup(_id: string, _groupName: string): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async saveLayout(_layout: PersistedLayout): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async loadLayout(): Promise<PersistedLayout> {
    // TODO: wire to Electrobun RPC
    return { preset: '1-col', pane_ids: [] };
  }

  // ── Agent persistence ───────────────────────────────────────────────────

  async saveAgentMessages(
    _sessionId: SessionId, _projectId: ProjectId, _sdkSessionId: string | undefined,
    _messages: AgentMessageRecord[],
  ): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async loadAgentMessages(_projectId: ProjectId): Promise<AgentMessageRecord[]> {
    // TODO: wire to Electrobun RPC
    return [];
  }

  async saveProjectAgentState(_state: ProjectAgentState): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async loadProjectAgentState(_projectId: ProjectId): Promise<ProjectAgentState | null> {
    // TODO: wire to Electrobun RPC
    return null;
  }

  async saveSessionMetric(_metric: Omit<SessionMetricRecord, 'id'>): Promise<void> {
    // TODO: wire to Electrobun RPC
  }

  async loadSessionMetrics(_projectId: ProjectId, _limit?: number): Promise<SessionMetricRecord[]> {
    // TODO: wire to Electrobun RPC
    return [];
  }

  async getCliGroup(): Promise<string | null> {
    // TODO: wire to Electrobun RPC
    return null;
  }

  async discoverMarkdownFiles(_cwd: string): Promise<MdFileEntry[]> {
    // TODO: wire to Electrobun RPC
    return [];
  }

  // ── Btmsg ───────────────────────────────────────────────────────────────

  async btmsgGetAgents(_groupId: GroupId): Promise<BtmsgAgent[]> {
    // TODO: wire to Electrobun RPC
    return [];
  }

  async btmsgUnreadCount(_agentId: AgentId): Promise<number> { return 0; }
  async btmsgUnreadMessages(_agentId: AgentId): Promise<BtmsgMessage[]> { return []; }
  async btmsgHistory(_agentId: AgentId, _otherId: AgentId, _limit?: number): Promise<BtmsgMessage[]> { return []; }
  async btmsgSend(_fromAgent: AgentId, _toAgent: AgentId, _content: string): Promise<string> { return ''; }
  async btmsgSetStatus(_agentId: AgentId, _status: string): Promise<void> { }
  async btmsgEnsureAdmin(_groupId: GroupId): Promise<void> { }
  async btmsgAllFeed(_groupId: GroupId, _limit?: number): Promise<BtmsgFeedMessage[]> { return []; }
  async btmsgMarkRead(_readerId: AgentId, _senderId: AgentId): Promise<void> { }
  async btmsgGetChannels(_groupId: GroupId): Promise<BtmsgChannel[]> { return []; }
  async btmsgChannelMessages(_channelId: string, _limit?: number): Promise<BtmsgChannelMessage[]> { return []; }
  async btmsgChannelSend(_channelId: string, _fromAgent: AgentId, _content: string): Promise<string> { return ''; }
  async btmsgCreateChannel(_name: string, _groupId: GroupId, _createdBy: AgentId): Promise<string> { return ''; }
  async btmsgAddChannelMember(_channelId: string, _agentId: AgentId): Promise<void> { }
  async btmsgRegisterAgents(_config: GroupsFile): Promise<void> { }
  async btmsgUnseenMessages(_agentId: AgentId, _sessionId: string): Promise<BtmsgMessage[]> { return []; }
  async btmsgMarkSeen(_sessionId: string, _messageIds: string[]): Promise<void> { }
  async btmsgPruneSeen(): Promise<number> { return 0; }
  async btmsgRecordHeartbeat(_agentId: AgentId): Promise<void> { }
  async btmsgGetStaleAgents(_groupId: GroupId, _thresholdSecs?: number): Promise<string[]> { return []; }
  async btmsgGetDeadLetters(_groupId: GroupId, _limit?: number): Promise<DeadLetterEntry[]> { return []; }
  async btmsgClearDeadLetters(_groupId: GroupId): Promise<void> { }
  async btmsgClearAllComms(_groupId: GroupId): Promise<void> { }

  // ── Bttask ──────────────────────────────────────────────────────────────

  async bttaskList(_groupId: GroupId): Promise<Task[]> { return []; }
  async bttaskComments(_taskId: string): Promise<TaskComment[]> { return []; }
  async bttaskUpdateStatus(_taskId: string, _status: string, _version: number): Promise<number> { return 0; }
  async bttaskAddComment(_taskId: string, _agentId: AgentId, _content: string): Promise<string> { return ''; }
  async bttaskCreate(
    _title: string, _description: string, _priority: string,
    _groupId: GroupId, _createdBy: AgentId, _assignedTo?: AgentId,
  ): Promise<string> { return ''; }
  async bttaskDelete(_taskId: string): Promise<void> { }
  async bttaskReviewQueueCount(_groupId: GroupId): Promise<number> { return 0; }

  // ── Anchors ─────────────────────────────────────────────────────────────

  async saveSessionAnchors(_anchors: SessionAnchorRecord[]): Promise<void> { }
  async loadSessionAnchors(_projectId: string): Promise<SessionAnchorRecord[]> { return []; }
  async deleteSessionAnchor(_id: string): Promise<void> { }
  async clearProjectAnchors(_projectId: string): Promise<void> { }
  async updateAnchorType(_id: string, _anchorType: string): Promise<void> { }

  // ── Search ──────────────────────────────────────────────────────────────

  async searchInit(): Promise<void> { }
  async searchAll(_query: string, _limit?: number): Promise<SearchResult[]> { return []; }
  async searchRebuild(): Promise<void> { }
  async searchIndexMessage(_sessionId: string, _role: string, _content: string): Promise<void> { }

  // ── Audit ───────────────────────────────────────────────────────────────

  async logAuditEvent(_agentId: AgentId, _eventType: AuditEventType, _detail: string): Promise<void> { }
  async getAuditLog(_groupId: GroupId, _limit?: number, _offset?: number): Promise<AuditEntry[]> { return []; }
  async getAuditLogForAgent(_agentId: AgentId, _limit?: number): Promise<AuditEntry[]> { return []; }

  // ── Notifications ───────────────────────────────────────────────────────

  sendDesktopNotification(_title: string, _body: string, _urgency?: NotificationUrgency): void {
    // Electrobun: in-app toasts only, no OS notifications
  }

  // ── Telemetry ───────────────────────────────────────────────────────────

  telemetryLog(_level: TelemetryLevel, _message: string, _context?: Record<string, unknown>): void {
    // Electrobun: no OTLP, console-only
  }

  // ── Secrets ─────────────────────────────────────────────────────────────

  async storeSecret(_key: string, _value: string): Promise<void> { }
  async getSecret(_key: string): Promise<string | null> { return null; }
  async deleteSecret(_key: string): Promise<void> { }
  async listSecrets(): Promise<string[]> { return []; }
  async hasKeyring(): Promise<boolean> { return false; }
  async knownSecretKeys(): Promise<string[]> { return []; }

  // ── Filesystem watcher ──────────────────────────────────────────────────

  async fsWatchProject(_projectId: string, _cwd: string): Promise<void> { }
  async fsUnwatchProject(_projectId: string): Promise<void> { }
  onFsWriteDetected(_callback: (event: FsWriteEvent) => void): UnsubscribeFn { return () => {}; }
  async fsWatcherStatus(): Promise<FsWatcherStatus> {
    return { max_watches: 0, estimated_watches: 0, usage_ratio: 0, active_projects: 0, warning: null };
  }

  // ── Ctx ─────────────────────────────────────────────────────────────────

  async ctxInitDb(): Promise<void> { }
  async ctxRegisterProject(_name: string, _description: string, _workDir?: string): Promise<void> { }
  async ctxGetContext(_project: string): Promise<CtxEntry[]> { return []; }
  async ctxGetShared(): Promise<CtxEntry[]> { return []; }
  async ctxGetSummaries(_project: string, _limit?: number): Promise<CtxSummary[]> { return []; }
  async ctxSearch(_query: string): Promise<CtxEntry[]> { return []; }

  // ── Memora ──────────────────────────────────────────────────────────────

  async memoraAvailable(): Promise<boolean> { return false; }
  async memoraList(_options?: { tags?: string[]; limit?: number; offset?: number }): Promise<MemoraSearchResult> {
    return { nodes: [], total: 0 };
  }
  async memoraSearch(_query: string, _options?: { tags?: string[]; limit?: number }): Promise<MemoraSearchResult> {
    return { nodes: [], total: 0 };
  }
  async memoraGet(_id: number): Promise<MemoraNode | null> { return null; }

  // ── SSH ─────────────────────────────────────────────────────────────────

  async listSshSessions(): Promise<SshSessionRecord[]> { return []; }
  async saveSshSession(_session: SshSessionRecord): Promise<void> { }
  async deleteSshSession(_id: string): Promise<void> { }

  // ── Plugins ─────────────────────────────────────────────────────────────

  async discoverPlugins(): Promise<PluginMeta[]> { return []; }
  async readPluginFile(_pluginId: string, _filename: string): Promise<string> { return ''; }

  // ── Claude provider ─────────────────────────────────────────────────────

  async listProfiles(): Promise<ClaudeProfile[]> { return []; }
  async listSkills(): Promise<ClaudeSkill[]> { return []; }
  async readSkill(_path: string): Promise<string> { return ''; }

  // ── Remote machines ─────────────────────────────────────────────────────

  async listRemoteMachines(): Promise<RemoteMachineInfo[]> { return []; }
  async addRemoteMachine(_config: RemoteMachineConfig): Promise<string> { return ''; }
  async removeRemoteMachine(_machineId: string): Promise<void> { }
  async connectRemoteMachine(_machineId: string): Promise<void> { }
  async disconnectRemoteMachine(_machineId: string): Promise<void> { }
  async probeSpki(_url: string): Promise<string> { return ''; }
  async addSpkiPin(_machineId: string, _pin: string): Promise<void> { }
  async removeSpkiPin(_machineId: string, _pin: string): Promise<void> { }
  onRemoteSidecarMessage(_callback: (msg: RemoteSidecarMessage) => void): UnsubscribeFn { return () => {}; }
  onRemotePtyData(_callback: (msg: RemotePtyData) => void): UnsubscribeFn { return () => {}; }
  onRemotePtyExit(_callback: (msg: RemotePtyExit) => void): UnsubscribeFn { return () => {}; }
  onRemoteMachineReady(_callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn { return () => {}; }
  onRemoteMachineDisconnected(_callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn { return () => {}; }
  onRemoteStateSync(_callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn { return () => {}; }
  onRemoteError(_callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn { return () => {}; }
  onRemoteMachineReconnecting(_callback: (msg: RemoteReconnectingEvent) => void): UnsubscribeFn { return () => {}; }
  onRemoteMachineReconnectReady(_callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn { return () => {}; }
  onRemoteSpkiTofu(_callback: (msg: RemoteSpkiTofuEvent) => void): UnsubscribeFn { return () => {}; }

  // ── File watcher ────────────────────────────────────────────────────────

  async watchFile(_paneId: string, _path: string): Promise<string> { return ''; }
  async unwatchFile(_paneId: string): Promise<void> { }
  async readWatchedFile(_path: string): Promise<string> { return ''; }
  onFileChanged(_callback: (payload: FileChangedPayload) => void): UnsubscribeFn { return () => {}; }

  // ── Agent bridge (direct IPC) ───────────────────────────────────────────

  async queryAgent(options: AgentQueryOptions): Promise<void> {
    await this.r.request['agent.start']({
      sessionId: options.session_id, provider: options.provider ?? 'claude',
      prompt: options.prompt, cwd: options.cwd, model: options.model,
      systemPrompt: options.system_prompt, maxTurns: options.max_turns,
      permissionMode: options.permission_mode, extraEnv: options.extra_env,
      resumeSessionId: options.resume_session_id,
    });
  }

  async stopAgentDirect(sessionId: string, _remoteMachineId?: string): Promise<void> {
    await this.r.request['agent.stop']({ sessionId });
  }

  async isAgentReady(): Promise<boolean> { return true; }
  async restartAgentSidecar(): Promise<void> { }

  async setSandbox(_projectCwds: string[], _worktreeRoots: string[], _enabled: boolean): Promise<void> { }

  onSidecarMessage(callback: (msg: SidecarMessagePayload) => void): UnsubscribeFn {
    return this.listenMsg<SidecarMessagePayload>('agent.sidecar', callback);
  }

  onSidecarExited(callback: () => void): UnsubscribeFn {
    return this.listenMsg('agent.sidecar.exited', () => callback());
  }

  // ── PTY bridge (per-session) ────────────────────────────────────────────

  async spawnPty(options: PtySpawnOptions): Promise<string> {
    const res = await this.r.request['pty.create']({
      sessionId: `pty-${Date.now()}`, cols: options.cols ?? 80, rows: options.rows ?? 24,
      cwd: options.cwd, shell: options.shell, args: options.args,
    }) as { ok: boolean; sessionId?: string; error?: string };
    if (!res.ok) throw new Error(res.error ?? 'PTY creation failed');
    return res.sessionId ?? '';
  }

  async writePtyDirect(id: string, data: string, _remoteMachineId?: string): Promise<void> {
    await this.r.request['pty.write']({ sessionId: id, data });
  }

  async resizePtyDirect(id: string, cols: number, rows: number, _remoteMachineId?: string): Promise<void> {
    await this.r.request['pty.resize']({ sessionId: id, cols, rows });
  }

  async killPty(id: string, _remoteMachineId?: string): Promise<void> {
    await this.r.request['pty.close']({ sessionId: id });
  }

  onPtyData(id: string, callback: (data: string) => void): UnsubscribeFn {
    return this.listenMsg<{ sessionId: string; data: string }>('pty.output', (p) => {
      if (p.sessionId === id) callback(p.data);
    });
  }

  onPtyExit(id: string, callback: () => void): UnsubscribeFn {
    return this.listenMsg<{ sessionId: string }>('pty.closed', (p) => {
      if (p.sessionId === id) callback();
    });
  }

  // ── Events ───────────────────────────────────────────────────────────────

  /** Subscribe to an RPC message event; returns unsubscribe function. */
  private listenMsg<T>(event: string, handler: (payload: T) => void): UnsubscribeFn {
    const wrapped = handler as (payload: unknown) => void;
    this.r.addMessageListener(event, wrapped);
    this.messageHandlers.push({ event, handler: wrapped });
    return () => {
      this.r.removeMessageListener?.(event, wrapped);
      this.messageHandlers = this.messageHandlers.filter((h) => h.handler !== wrapped);
    };
  }

  onAgentMessage(callback: (sessionId: string, messages: AgentMessage[]) => void): UnsubscribeFn {
    return this.listenMsg<{
      sessionId: string;
      messages: Array<{ id: string; type: string; parentId?: string; content: unknown; timestamp: number }>;
    }>('agent.message', (p) => {
      const converted = p.messages.map(convertWireMessage).filter((m): m is AgentMessage => m !== null);
      if (converted.length > 0) callback(p.sessionId, converted);
    });
  }

  onAgentStatus(callback: (sessionId: string, status: AgentStatus, error?: string) => void): UnsubscribeFn {
    return this.listenMsg<{ sessionId: string; status: string; error?: string }>('agent.status', (p) => {
      callback(p.sessionId, normalizeStatus(p.status), p.error);
    });
  }

  onAgentCost(
    callback: (sessionId: string, costUsd: number, inputTokens: number, outputTokens: number) => void,
  ): UnsubscribeFn {
    return this.listenMsg<{ sessionId: string; costUsd: number; inputTokens: number; outputTokens: number }>(
      'agent.cost', (p) => callback(p.sessionId, p.costUsd, p.inputTokens, p.outputTokens),
    );
  }

  onPtyOutput(callback: (sessionId: string, data: string) => void): UnsubscribeFn {
    return this.listenMsg<{ sessionId: string; data: string }>('pty.output', (p) => callback(p.sessionId, p.data));
  }

  onPtyClosed(callback: (sessionId: string, exitCode: number | null) => void): UnsubscribeFn {
    return this.listenMsg<{ sessionId: string; exitCode: number | null }>('pty.closed', (p) => callback(p.sessionId, p.exitCode));
  }
}

// ── Helpers ──────────────────────────────────────────────────────────────────

type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result' | 'thinking' | 'system';

function convertWireMessage(raw: {
  id: string; type: string; parentId?: string; content: unknown; timestamp: number;
}): AgentMessage | null {
  const c = raw.content as Record<string, unknown> | undefined;
  switch (raw.type) {
    case 'text':
      return { id: raw.id, role: 'assistant', content: String(c?.['text'] ?? ''), timestamp: raw.timestamp };
    case 'thinking':
      return { id: raw.id, role: 'thinking', content: String(c?.['text'] ?? ''), timestamp: raw.timestamp };
    case 'tool_call': {
      const name = String(c?.['name'] ?? 'Tool');
      const input = c?.['input'] as Record<string, unknown> | undefined;
      return {
        id: raw.id, role: 'tool-call', content: formatToolContent(name, input),
        toolName: name, toolInput: input ? JSON.stringify(input, null, 2) : undefined, timestamp: raw.timestamp,
      };
    }
    case 'tool_result': {
      const output = c?.['output'];
      return {
        id: raw.id, role: 'tool-result',
        content: typeof output === 'string' ? output : JSON.stringify(output, null, 2), timestamp: raw.timestamp,
      };
    }
    case 'init':
      return { id: raw.id, role: 'system', content: `Session initialized`, timestamp: raw.timestamp };
    case 'error':
      return { id: raw.id, role: 'system', content: `Error: ${String(c?.['message'] ?? 'Unknown')}`, timestamp: raw.timestamp };
    case 'cost': case 'status': case 'compaction':
      return null;
    default:
      return null;
  }
}

function formatToolContent(name: string, input: Record<string, unknown> | undefined): string {
  if (!input) return '';
  if (name === 'Bash' && typeof input['command'] === 'string') return input['command'] as string;
  if (typeof input['file_path'] === 'string') return input['file_path'] as string;
  return JSON.stringify(input, null, 2);
}

function normalizeStatus(status: string): AgentStatus {
  if (status === 'idle' || status === 'starting' || status === 'running' || status === 'done' || status === 'error') {
    return status;
  }
  return 'idle';
}
