// TauriAdapter — implements BackendAdapter for Tauri 2.x backend
// Wraps existing invoke() and listen() calls from @tauri-apps/api

import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import type {
  BackendAdapter, BackendCapabilities, UnsubscribeFn, AgentStartOptions,
  AgentMessage, AgentStatus, FileEntry, FileContent, PtyCreateOptions,
  SettingsMap, GroupsFile, 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,
  SessionId, ProjectId, GroupId, AgentId,
} from '@agor/types';

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

interface TauriDirEntry {
  name: string; path: string; is_dir: boolean; size: number; ext: string;
}

export class TauriAdapter implements BackendAdapter {
  readonly capabilities = TAURI_CAPABILITIES;
  private unlisteners: UnlistenFn[] = [];

  async init(): Promise<void> { /* Tauri IPC ready on webview load */ }

  async destroy(): Promise<void> {
    for (const fn of this.unlisteners) fn();
    this.unlisteners = [];
  }

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

  async getSetting(key: string): Promise<string | null> {
    return invoke<string | null>('settings_get', { key });
  }

  async setSetting(key: string, value: string): Promise<void> {
    return invoke('settings_set', { key, value });
  }

  async getAllSettings(): Promise<SettingsMap> {
    const pairs = await invoke<[string, string][]>('settings_list');
    const map: SettingsMap = {};
    for (const [k, v] of pairs) map[k] = v;
    return map;
  }

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

  async loadGroups(): Promise<GroupsFile> {
    return invoke<GroupsFile>('groups_load');
  }

  async saveGroups(groups: GroupsFile): Promise<void> {
    return invoke('groups_save', { config: groups });
  }

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

  async startAgent(options: AgentStartOptions): Promise<{ ok: boolean; error?: string }> {
    const tauriOpts = {
      provider: options.provider ?? 'claude',
      session_id: options.sessionId,
      prompt: options.prompt,
      cwd: options.cwd,
      max_turns: options.maxTurns,
      max_budget_usd: options.maxBudgetUsd,
      resume_session_id: undefined,
      permission_mode: options.permissionMode,
      setting_sources: options.settingSources,
      system_prompt: options.systemPrompt,
      model: options.model,
      claude_config_dir: options.claudeConfigDir,
      additional_directories: options.additionalDirectories,
      worktree_name: options.worktreeName,
      provider_config: options.providerConfig,
      extra_env: options.extraEnv,
    };
    try {
      await invoke('agent_query', { options: tauriOpts });
      return { ok: true };
    } catch (err: unknown) {
      return { ok: false, error: String(err) };
    }
  }

  async stopAgent(sessionId: string): Promise<{ ok: boolean; error?: string }> {
    try {
      await invoke('agent_stop', { sessionId });
      return { ok: true };
    } catch (err: unknown) {
      return { ok: false, error: String(err) };
    }
  }

  async sendPrompt(sessionId: string, prompt: string): Promise<{ ok: boolean; error?: string }> {
    try {
      await invoke('agent_query', {
        options: { session_id: sessionId, prompt, resume_session_id: sessionId },
      });
      return { ok: true };
    } catch (err: unknown) {
      return { ok: false, error: String(err) };
    }
  }

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

  async createPty(options: PtyCreateOptions): Promise<string> {
    return invoke<string>('pty_spawn', {
      options: { shell: options.shell, cwd: options.cwd, args: options.args, cols: options.cols, rows: options.rows },
    });
  }

  async writePty(sessionId: string, data: string): Promise<void> {
    return invoke('pty_write', { id: sessionId, data });
  }

  async resizePty(sessionId: string, cols: number, rows: number): Promise<void> {
    return invoke('pty_resize', { id: sessionId, cols, rows });
  }

  async closePty(sessionId: string): Promise<void> {
    return invoke('pty_kill', { id: sessionId });
  }

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

  async listDirectory(path: string): Promise<FileEntry[]> {
    const entries = await invoke<TauriDirEntry[]>('list_directory_children', { path });
    return entries.map((e) => ({
      name: e.name, path: e.path, isDir: e.is_dir, size: e.size, ext: e.ext || undefined,
    }));
  }

  async readFile(path: string): Promise<FileContent> {
    return invoke<FileContent>('read_file_content', { path });
  }

  async writeFile(path: string, content: string): Promise<void> {
    return invoke('write_file_content', { path, content });
  }

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

  async listSessions(): Promise<PersistedSession[]> {
    return invoke('session_list');
  }

  async saveSession(session: PersistedSession): Promise<void> {
    return invoke('session_save', { session });
  }

  async deleteSession(id: string): Promise<void> {
    return invoke('session_delete', { id });
  }

  async updateSessionTitle(id: string, title: string): Promise<void> {
    return invoke('session_update_title', { id, title });
  }

  async touchSession(id: string): Promise<void> {
    return invoke('session_touch', { id });
  }

  async updateSessionGroup(id: string, groupName: string): Promise<void> {
    return invoke('session_update_group', { id, group_name: groupName });
  }

  async saveLayout(layout: PersistedLayout): Promise<void> {
    return invoke('layout_save', { layout });
  }

  async loadLayout(): Promise<PersistedLayout> {
    return invoke('layout_load');
  }

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

  async saveAgentMessages(
    sessionId: SessionId, projectId: ProjectId, sdkSessionId: string | undefined,
    messages: AgentMessageRecord[],
  ): Promise<void> {
    return invoke('agent_messages_save', {
      sessionId, projectId, sdkSessionId: sdkSessionId ?? null, messages,
    });
  }

  async loadAgentMessages(projectId: ProjectId): Promise<AgentMessageRecord[]> {
    return invoke('agent_messages_load', { projectId });
  }

  async saveProjectAgentState(state: ProjectAgentState): Promise<void> {
    return invoke('project_agent_state_save', { state });
  }

  async loadProjectAgentState(projectId: ProjectId): Promise<ProjectAgentState | null> {
    return invoke('project_agent_state_load', { projectId });
  }

  async saveSessionMetric(metric: Omit<SessionMetricRecord, 'id'>): Promise<void> {
    return invoke('session_metric_save', { metric: { id: 0, ...metric } });
  }

  async loadSessionMetrics(projectId: ProjectId, limit = 20): Promise<SessionMetricRecord[]> {
    return invoke('session_metrics_load', { projectId, limit });
  }

  async getCliGroup(): Promise<string | null> {
    return invoke('cli_get_group');
  }

  async discoverMarkdownFiles(cwd: string): Promise<MdFileEntry[]> {
    return invoke('discover_markdown_files', { cwd });
  }

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

  async btmsgGetAgents(groupId: GroupId): Promise<BtmsgAgent[]> {
    return invoke('btmsg_get_agents', { groupId });
  }

  async btmsgUnreadCount(agentId: AgentId): Promise<number> {
    return invoke('btmsg_unread_count', { agentId });
  }

  async btmsgUnreadMessages(agentId: AgentId): Promise<BtmsgMessage[]> {
    return invoke('btmsg_unread_messages', { agentId });
  }

  async btmsgHistory(agentId: AgentId, otherId: AgentId, limit = 20): Promise<BtmsgMessage[]> {
    return invoke('btmsg_history', { agentId, otherId, limit });
  }

  async btmsgSend(fromAgent: AgentId, toAgent: AgentId, content: string): Promise<string> {
    return invoke('btmsg_send', { fromAgent, toAgent, content });
  }

  async btmsgSetStatus(agentId: AgentId, status: string): Promise<void> {
    return invoke('btmsg_set_status', { agentId, status });
  }

  async btmsgEnsureAdmin(groupId: GroupId): Promise<void> {
    return invoke('btmsg_ensure_admin', { groupId });
  }

  async btmsgAllFeed(groupId: GroupId, limit = 100): Promise<BtmsgFeedMessage[]> {
    return invoke('btmsg_all_feed', { groupId, limit });
  }

  async btmsgMarkRead(readerId: AgentId, senderId: AgentId): Promise<void> {
    return invoke('btmsg_mark_read', { readerId, senderId });
  }

  async btmsgGetChannels(groupId: GroupId): Promise<BtmsgChannel[]> {
    return invoke('btmsg_get_channels', { groupId });
  }

  async btmsgChannelMessages(channelId: string, limit = 100): Promise<BtmsgChannelMessage[]> {
    return invoke('btmsg_channel_messages', { channelId, limit });
  }

  async btmsgChannelSend(channelId: string, fromAgent: AgentId, content: string): Promise<string> {
    return invoke('btmsg_channel_send', { channelId, fromAgent, content });
  }

  async btmsgCreateChannel(name: string, groupId: GroupId, createdBy: AgentId): Promise<string> {
    return invoke('btmsg_create_channel', { name, groupId, createdBy });
  }

  async btmsgAddChannelMember(channelId: string, agentId: AgentId): Promise<void> {
    return invoke('btmsg_add_channel_member', { channelId, agentId });
  }

  async btmsgRegisterAgents(config: GroupsFile): Promise<void> {
    return invoke('btmsg_register_agents', { config });
  }

  async btmsgUnseenMessages(agentId: AgentId, sessionId: string): Promise<BtmsgMessage[]> {
    return invoke('btmsg_unseen_messages', { agentId, sessionId });
  }

  async btmsgMarkSeen(sessionId: string, messageIds: string[]): Promise<void> {
    return invoke('btmsg_mark_seen', { sessionId, messageIds });
  }

  async btmsgPruneSeen(): Promise<number> {
    return invoke('btmsg_prune_seen');
  }

  async btmsgRecordHeartbeat(agentId: AgentId): Promise<void> {
    return invoke('btmsg_record_heartbeat', { agentId });
  }

  async btmsgGetStaleAgents(groupId: GroupId, thresholdSecs = 300): Promise<string[]> {
    return invoke('btmsg_get_stale_agents', { groupId, thresholdSecs });
  }

  async btmsgGetDeadLetters(groupId: GroupId, limit = 50): Promise<DeadLetterEntry[]> {
    return invoke('btmsg_get_dead_letters', { groupId, limit });
  }

  async btmsgClearDeadLetters(groupId: GroupId): Promise<void> {
    return invoke('btmsg_clear_dead_letters', { groupId });
  }

  async btmsgClearAllComms(groupId: GroupId): Promise<void> {
    return invoke('btmsg_clear_all_comms', { groupId });
  }

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

  async bttaskList(groupId: GroupId): Promise<Task[]> {
    return invoke<Task[]>('bttask_list', { groupId });
  }

  async bttaskComments(taskId: string): Promise<TaskComment[]> {
    return invoke<TaskComment[]>('bttask_comments', { taskId });
  }

  async bttaskUpdateStatus(taskId: string, status: string, version: number): Promise<number> {
    return invoke<number>('bttask_update_status', { taskId, status, version });
  }

  async bttaskAddComment(taskId: string, agentId: AgentId, content: string): Promise<string> {
    return invoke<string>('bttask_add_comment', { taskId, agentId, content });
  }

  async bttaskCreate(
    title: string, description: string, priority: string,
    groupId: GroupId, createdBy: AgentId, assignedTo?: AgentId,
  ): Promise<string> {
    return invoke<string>('bttask_create', { title, description, priority, groupId, createdBy, assignedTo });
  }

  async bttaskDelete(taskId: string): Promise<void> {
    return invoke('bttask_delete', { taskId });
  }

  async bttaskReviewQueueCount(groupId: GroupId): Promise<number> {
    return invoke<number>('bttask_review_queue_count', { groupId });
  }

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

  async saveSessionAnchors(anchors: SessionAnchorRecord[]): Promise<void> {
    return invoke('session_anchors_save', { anchors });
  }

  async loadSessionAnchors(projectId: string): Promise<SessionAnchorRecord[]> {
    return invoke('session_anchors_load', { projectId });
  }

  async deleteSessionAnchor(id: string): Promise<void> {
    return invoke('session_anchor_delete', { id });
  }

  async clearProjectAnchors(projectId: string): Promise<void> {
    return invoke('session_anchors_clear', { projectId });
  }

  async updateAnchorType(id: string, anchorType: string): Promise<void> {
    return invoke('session_anchor_update_type', { id, anchorType });
  }

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

  async searchInit(): Promise<void> {
    return invoke('search_init');
  }

  async searchAll(query: string, limit = 20): Promise<SearchResult[]> {
    return invoke<SearchResult[]>('search_query', { query, limit });
  }

  async searchRebuild(): Promise<void> {
    return invoke('search_rebuild');
  }

  async searchIndexMessage(sessionId: string, role: string, content: string): Promise<void> {
    return invoke('search_index_message', { sessionId, role, content });
  }

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

  async logAuditEvent(agentId: AgentId, eventType: AuditEventType, detail: string): Promise<void> {
    return invoke('audit_log_event', { agentId, eventType, detail });
  }

  async getAuditLog(groupId: GroupId, limit = 200, offset = 0): Promise<AuditEntry[]> {
    return invoke('audit_log_list', { groupId, limit, offset });
  }

  async getAuditLogForAgent(agentId: AgentId, limit = 50): Promise<AuditEntry[]> {
    return invoke('audit_log_for_agent', { agentId, limit });
  }

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

  sendDesktopNotification(title: string, body: string, urgency: NotificationUrgency = 'normal'): void {
    invoke('notify_desktop', { title, body, urgency }).catch((_e: unknown) => {
      // eslint-disable-next-line no-console
      console.warn('[TauriAdapter] Desktop notification failed:', _e);
    });
  }

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

  telemetryLog(level: TelemetryLevel, message: string, context?: Record<string, unknown>): void {
    invoke('frontend_log', { level, message, context: context ?? null }).catch((_e: unknown) => {
      // eslint-disable-next-line no-console
      console.warn('[TauriAdapter] Telemetry IPC failed:', _e);
    });
  }

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

  async storeSecret(key: string, value: string): Promise<void> {
    return invoke('secrets_store', { key, value });
  }

  async getSecret(key: string): Promise<string | null> {
    return invoke('secrets_get', { key });
  }

  async deleteSecret(key: string): Promise<void> {
    return invoke('secrets_delete', { key });
  }

  async listSecrets(): Promise<string[]> {
    return invoke('secrets_list');
  }

  async hasKeyring(): Promise<boolean> {
    return invoke('secrets_has_keyring');
  }

  async knownSecretKeys(): Promise<string[]> {
    return invoke('secrets_known_keys');
  }

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

  async fsWatchProject(projectId: string, cwd: string): Promise<void> {
    return invoke('fs_watch_project', { projectId, cwd });
  }

  async fsUnwatchProject(projectId: string): Promise<void> {
    return invoke('fs_unwatch_project', { projectId });
  }

  onFsWriteDetected(callback: (event: FsWriteEvent) => void): UnsubscribeFn {
    return this.listenTauri<FsWriteEvent>('fs-write-detected', callback);
  }

  async fsWatcherStatus(): Promise<FsWatcherStatus> {
    return invoke('fs_watcher_status');
  }

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

  async ctxInitDb(): Promise<void> {
    return invoke('ctx_init_db');
  }

  async ctxRegisterProject(name: string, description: string, workDir?: string): Promise<void> {
    return invoke('ctx_register_project', { name, description, workDir: workDir ?? null });
  }

  async ctxGetContext(project: string): Promise<CtxEntry[]> {
    return invoke('ctx_get_context', { project });
  }

  async ctxGetShared(): Promise<CtxEntry[]> {
    return invoke('ctx_get_shared');
  }

  async ctxGetSummaries(project: string, limit = 5): Promise<CtxSummary[]> {
    return invoke('ctx_get_summaries', { project, limit });
  }

  async ctxSearch(query: string): Promise<CtxEntry[]> {
    return invoke('ctx_search', { query });
  }

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

  async memoraAvailable(): Promise<boolean> {
    return invoke<boolean>('memora_available');
  }

  async memoraList(options?: { tags?: string[]; limit?: number; offset?: number }): Promise<MemoraSearchResult> {
    return invoke<MemoraSearchResult>('memora_list', {
      tags: options?.tags ?? null,
      limit: options?.limit ?? 50,
      offset: options?.offset ?? 0,
    });
  }

  async memoraSearch(query: string, options?: { tags?: string[]; limit?: number }): Promise<MemoraSearchResult> {
    return invoke<MemoraSearchResult>('memora_search', {
      query,
      tags: options?.tags ?? null,
      limit: options?.limit ?? 50,
    });
  }

  async memoraGet(id: number): Promise<MemoraNode | null> {
    return invoke<MemoraNode | null>('memora_get', { id });
  }

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

  async listSshSessions(): Promise<SshSessionRecord[]> {
    return invoke('ssh_session_list');
  }

  async saveSshSession(session: SshSessionRecord): Promise<void> {
    return invoke('ssh_session_save', { session });
  }

  async deleteSshSession(id: string): Promise<void> {
    return invoke('ssh_session_delete', { id });
  }

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

  async discoverPlugins(): Promise<PluginMeta[]> {
    return invoke<PluginMeta[]>('plugins_discover');
  }

  async readPluginFile(pluginId: string, filename: string): Promise<string> {
    return invoke<string>('plugin_read_file', { pluginId, filename });
  }

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

  async listProfiles(): Promise<ClaudeProfile[]> {
    return invoke<ClaudeProfile[]>('claude_list_profiles');
  }

  async listSkills(): Promise<ClaudeSkill[]> {
    return invoke<ClaudeSkill[]>('claude_list_skills');
  }

  async readSkill(path: string): Promise<string> {
    return invoke<string>('claude_read_skill', { path });
  }

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

  async listRemoteMachines(): Promise<RemoteMachineInfo[]> {
    return invoke('remote_list');
  }

  async addRemoteMachine(config: RemoteMachineConfig): Promise<string> {
    return invoke('remote_add', { config });
  }

  async removeRemoteMachine(machineId: string): Promise<void> {
    return invoke('remote_remove', { machineId });
  }

  async connectRemoteMachine(machineId: string): Promise<void> {
    return invoke('remote_connect', { machineId });
  }

  async disconnectRemoteMachine(machineId: string): Promise<void> {
    return invoke('remote_disconnect', { machineId });
  }

  async probeSpki(url: string): Promise<string> {
    return invoke('remote_probe_spki', { url });
  }

  async addSpkiPin(machineId: string, pin: string): Promise<void> {
    return invoke('remote_add_pin', { machineId, pin });
  }

  async removeSpkiPin(machineId: string, pin: string): Promise<void> {
    return invoke('remote_remove_pin', { machineId, pin });
  }

  onRemoteSidecarMessage(callback: (msg: RemoteSidecarMessage) => void): UnsubscribeFn {
    return this.listenTauri<RemoteSidecarMessage>('remote-sidecar-message', callback);
  }

  onRemotePtyData(callback: (msg: RemotePtyData) => void): UnsubscribeFn {
    return this.listenTauri<RemotePtyData>('remote-pty-data', callback);
  }

  onRemotePtyExit(callback: (msg: RemotePtyExit) => void): UnsubscribeFn {
    return this.listenTauri<RemotePtyExit>('remote-pty-exit', callback);
  }

  onRemoteMachineReady(callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn {
    return this.listenTauri<RemoteMachineEvent>('remote-machine-ready', callback);
  }

  onRemoteMachineDisconnected(callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn {
    return this.listenTauri<RemoteMachineEvent>('remote-machine-disconnected', callback);
  }

  onRemoteStateSync(callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn {
    return this.listenTauri<RemoteMachineEvent>('remote-state-sync', callback);
  }

  onRemoteError(callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn {
    return this.listenTauri<RemoteMachineEvent>('remote-error', callback);
  }

  onRemoteMachineReconnecting(callback: (msg: RemoteReconnectingEvent) => void): UnsubscribeFn {
    return this.listenTauri<RemoteReconnectingEvent>('remote-machine-reconnecting', callback);
  }

  onRemoteMachineReconnectReady(callback: (msg: RemoteMachineEvent) => void): UnsubscribeFn {
    return this.listenTauri<RemoteMachineEvent>('remote-machine-reconnect-ready', callback);
  }

  onRemoteSpkiTofu(callback: (msg: RemoteSpkiTofuEvent) => void): UnsubscribeFn {
    return this.listenTauri<RemoteSpkiTofuEvent>('remote-spki-tofu', callback);
  }

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

  async watchFile(paneId: string, path: string): Promise<string> {
    return invoke('file_watch', { paneId, path });
  }

  async unwatchFile(paneId: string): Promise<void> {
    return invoke('file_unwatch', { paneId });
  }

  async readWatchedFile(path: string): Promise<string> {
    return invoke('file_read', { path });
  }

  onFileChanged(callback: (payload: FileChangedPayload) => void): UnsubscribeFn {
    return this.listenTauri<FileChangedPayload>('file-changed', callback);
  }

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

  async queryAgent(options: AgentQueryOptions): Promise<void> {
    if (options.remote_machine_id) {
      const { remote_machine_id: machineId, ...agentOptions } = options;
      return invoke('remote_agent_query', { machineId, options: agentOptions });
    }
    return invoke('agent_query', { options });
  }

  async stopAgentDirect(sessionId: string, remoteMachineId?: string): Promise<void> {
    if (remoteMachineId) {
      return invoke('remote_agent_stop', { machineId: remoteMachineId, sessionId });
    }
    return invoke('agent_stop', { sessionId });
  }

  async isAgentReady(): Promise<boolean> {
    return invoke<boolean>('agent_ready');
  }

  async restartAgentSidecar(): Promise<void> {
    return invoke('agent_restart');
  }

  async setSandbox(projectCwds: string[], worktreeRoots: string[], enabled: boolean): Promise<void> {
    return invoke('agent_set_sandbox', { projectCwds, worktreeRoots, enabled });
  }

  onSidecarMessage(callback: (msg: SidecarMessagePayload) => void): UnsubscribeFn {
    return this.listenTauri<SidecarMessagePayload>('sidecar-message', (payload) => {
      if (typeof payload !== 'object' || payload === null) return;
      callback(payload);
    });
  }

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

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

  async spawnPty(options: PtySpawnOptions): Promise<string> {
    if (options.remote_machine_id) {
      const { remote_machine_id: machineId, ...ptyOptions } = options;
      return invoke<string>('remote_pty_spawn', { machineId, options: ptyOptions });
    }
    return invoke<string>('pty_spawn', { options });
  }

  async writePtyDirect(id: string, data: string, remoteMachineId?: string): Promise<void> {
    if (remoteMachineId) {
      return invoke('remote_pty_write', { machineId: remoteMachineId, id, data });
    }
    return invoke('pty_write', { id, data });
  }

  async resizePtyDirect(id: string, cols: number, rows: number, remoteMachineId?: string): Promise<void> {
    if (remoteMachineId) {
      return invoke('remote_pty_resize', { machineId: remoteMachineId, id, cols, rows });
    }
    return invoke('pty_resize', { id, cols, rows });
  }

  async killPty(id: string, remoteMachineId?: string): Promise<void> {
    if (remoteMachineId) {
      return invoke('remote_pty_kill', { machineId: remoteMachineId, id });
    }
    return invoke('pty_kill', { id });
  }

  onPtyData(id: string, callback: (data: string) => void): UnsubscribeFn {
    return this.listenTauri<string>(`pty-data-${id}`, callback);
  }

  onPtyExit(id: string, callback: () => void): UnsubscribeFn {
    return this.listenTauri(`pty-exit-${id}`, () => callback());
  }

  // ── Events (simplified — kept for backward compat) ──────────────────────

  /** Subscribe to a Tauri event; tracks unlisten for cleanup. */
  private listenTauri<T>(event: string, handler: (payload: T) => void): UnsubscribeFn {
    let unlisten: UnlistenFn | null = null;
    listen<T>(event, (e) => handler(e.payload)).then((fn) => {
      unlisten = fn;
      this.unlisteners.push(fn);
    });
    return () => {
      if (unlisten) {
        unlisten();
        this.unlisteners = this.unlisteners.filter((f) => f !== unlisten);
      }
    };
  }

  onAgentMessage(callback: (sessionId: string, messages: AgentMessage[]) => void): UnsubscribeFn {
    return this.listenTauri<SidecarMessagePayload>('sidecar-message', (msg) => {
      if (msg.type !== 'message' || !msg.sessionId || !msg.event) return;
      const agentMsg: AgentMessage = {
        id: String(msg.event['id'] ?? Date.now()),
        role: mapSidecarRole(String(msg.event['type'] ?? '')),
        content: String(msg.event['content'] ?? ''),
        toolName: msg.event['toolName'] as string | undefined,
        toolInput: msg.event['toolInput'] as string | undefined,
        timestamp: Number(msg.event['timestamp'] ?? Date.now()),
      };
      callback(msg.sessionId, [agentMsg]);
    });
  }

  onAgentStatus(callback: (sessionId: string, status: AgentStatus, error?: string) => void): UnsubscribeFn {
    return this.listenTauri<SidecarMessagePayload>('sidecar-message', (msg) => {
      if (msg.type !== 'status' || !msg.sessionId) return;
      callback(msg.sessionId, normalizeStatus(String(msg.event?.['status'] ?? 'idle')), msg.message);
    });
  }

  onAgentCost(
    callback: (sessionId: string, costUsd: number, inputTokens: number, outputTokens: number) => void,
  ): UnsubscribeFn {
    return this.listenTauri<SidecarMessagePayload>('sidecar-message', (msg) => {
      if (msg.type !== 'cost' || !msg.sessionId || !msg.event) return;
      callback(msg.sessionId, Number(msg.event['totalCostUsd'] ?? 0), Number(msg.event['inputTokens'] ?? 0), Number(msg.event['outputTokens'] ?? 0));
    });
  }

  onPtyOutput(_callback: (sessionId: string, data: string) => void): UnsubscribeFn {
    // Tauri PTY uses per-session event channels (pty-data-{id}).
    // Use onPtyData(id, callback) for per-session PTY output.
    return () => {};
  }

  onPtyClosed(_callback: (sessionId: string, exitCode: number | null) => void): UnsubscribeFn {
    return () => {};
  }
}

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

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

function mapSidecarRole(type: string): MsgRole {
  switch (type) {
    case 'text': return 'assistant';
    case 'thinking': return 'thinking';
    case 'tool_call': return 'tool-call';
    case 'tool_result': return 'tool-result';
    case 'init': case 'error': case 'status': return 'system';
    default: return 'assistant';
  }
}

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