import browser from 'webextension-polyfill';
import {
  ExecutorServerFrameSchema,
  PROTOCOL_VERSION,
  type ConversationState,
  type ExecutorCommand,
  type ExecutorServerFrame,
} from '@platform-modules/chatgpt-orchestrator-protocol';
import {
  findBindingByTabId,
  getJournal,
  getRuntimeStatus,
  loadBindings,
  loadContinuityViews,
  loadSettings,
  removeBinding,
  removeContinuityView,
  saveBinding,
  saveContinuityView,
  saveJournal,
  setRuntimeStatus,
  type JournalEntry,
  type ManagedBinding,
} from './storage.js';
import { AsyncSemaphore, PerKeySerialQueue } from './concurrency.js';
import { withContentReceiverReady } from './content-channel.js';
import { boundedSubmissionReadyWaitMs, submissionRecovery } from './submission-policy.js';
import { operatorActionFrame, parseContinuityControlServerFrame, type OperatorResultFrame } from './control-bridge.js';
import {
  OVERLAY_MESSAGE_KIND,
  UI_MESSAGE_KIND,
  localUiSnapshot,
  localWorkerView,
  type BackendContinuityView,
  type ContinuityUiSnapshot,
  type UiRequest,
} from './ui-model.js';

const CHATGPT_HOME = 'https://chatgpt.com/';
const CONTENT_KIND = 'chatgpt-orchestrator.content';
const CONTENT_STATE_KIND = 'chatgpt-orchestrator.content-state';
const CONTENT_READY_TIMEOUT_MS = 30_000;
const CONVERSATION_ID_TIMEOUT_MS = 45_000;
const EXECUTOR_WAKE_ALARM = 'chatgpt-orchestrator.executor-wake';

interface Inspection {
  state: 'loading' | 'ready' | 'generating' | 'idle' | 'error';
  conversationId: string | null;
  url: string;
  composerReady: boolean;
  generating: boolean;
  error: string | null;
  issue?: { code: string; detail: string; recoverable: boolean } | null;
  assistantOutputTail?: string | null;
  assistantOutputTruncated?: boolean;
}

interface ContentResponse {
  ok: boolean;
  inspection?: Inspection;
  code?: string;
  error?: string;
}

function now(): string { return new Date().toISOString(); }
function toConversationState(state: Inspection['state']): ConversationState { return state; }
function isChatGptUrl(url: string | undefined): boolean {
  if (!url) return false;
  try { return new URL(url).origin === 'https://chatgpt.com'; } catch { return false; }
}

class ExecutorClient {
  private socket: WebSocket | null = null;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  private stopping = false;
  private readonly processing = new Map<string, Promise<void>>();
  private readonly pendingOperatorResults = new Map<string, { workerId: string; resolve: (value: OperatorResultFrame) => void; reject: (reason?: unknown) => void; timer: ReturnType<typeof setTimeout> }>();
  private launchLimiter = new AsyncSemaphore(2);
  private readonly workerQueue = new PerKeySerialQueue();

  async start(): Promise<void> {
    this.stopping = false;
    if (this.socket && (this.socket.readyState === WebSocket.CONNECTING || this.socket.readyState === WebSocket.OPEN)) return;
    await this.connect();
  }

  stop(): void {
    this.stopping = true;
    if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
    if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
    this.reconnectTimer = null;
    this.heartbeatTimer = null;
    this.socket?.close(1000, 'extension reconnect');
    this.socket = null;
    for (const pending of this.pendingOperatorResults.values()) { clearTimeout(pending.timer); pending.reject(new Error('Executor connection closed before operator result.')); }
    this.pendingOperatorResults.clear();
  }

  async restart(): Promise<void> {
    this.stop();
    await this.start();
  }

  async observedTabState(tabId: number, inspection: Inspection): Promise<void> {
    const binding = await findBindingByTabId(tabId);
    if (!binding) return;
    const settings = await loadSettings();
    await this.updateBinding(settings.executorId, binding, inspection);
  }

  async reconcileManagedTab(tabId: number): Promise<void> {
    const binding = await findBindingByTabId(tabId);
    if (!binding) return;
    const settings = await loadSettings();
    const tab = await browser.tabs.get(tabId).catch(() => null);
    if (!tab || !isChatGptUrl(tab.url)) {
      await removeBinding(binding.workerId);
      await removeContinuityView(binding.workerId);
      this.sendConversationState(settings.executorId, { ...binding, state: 'closed', updatedAt: now() }, 'MANAGED_TAB_UNAVAILABLE: managed ChatGPT tab was closed or navigated away.');
      return;
    }
    try {
      const inspection = await this.inspectBoundTab({ ...binding, url: tab.url ?? binding.url });
      await this.updateBinding(settings.executorId, binding, inspection);
    } catch (error) {
      const detail = error instanceof Error ? error.message : 'Managed tab reconciliation failed.';
      const failed: ManagedBinding = { ...binding, url: tab.url ?? binding.url, state: 'error', updatedAt: now() };
      await saveBinding(failed);
      this.sendConversationState(settings.executorId, failed, `HOST_UI_CHANGED: ${detail}`);
      await this.syncOverlay(failed);
    }
  }

  async refreshOverlays(): Promise<void> {
    const bindings = await loadBindings();
    for (const binding of Object.values(bindings)) await this.syncOverlay(binding);
  }

  async uiSnapshot(currentTabId?: number): Promise<ContinuityUiSnapshot> {
    const [bindings, continuityViews, connection, settings] = await Promise.all([loadBindings(), loadContinuityViews(), getRuntimeStatus(), loadSettings()]);
    return localUiSnapshot(bindings, continuityViews, connection, settings.overlayVisible, currentTabId);
  }

  async handleUiAction(request: Exclude<UiRequest, { action: 'snapshot' }>): Promise<Record<string, unknown>> {
    const [bindings, continuityViews] = await Promise.all([loadBindings(), loadContinuityViews()]);
    const binding = bindings[request.workerId];
    if (!binding) return { ok: false, code: 'WORKER_NOT_MANAGED', error: 'The selected worker has no managed ChatGPT conversation in this browser.' };
    if (!continuityViews[request.workerId]) return { ok: false, code: 'CONTINUITY_VIEW_UNAVAILABLE', error: 'Backend continuity state has not been observed for this worker yet.' };
    if (this.socket?.readyState !== WebSocket.OPEN) return { ok: false, code: 'EXECUTOR_UNAVAILABLE', error: 'Backend continuity controls are unavailable while the executor connection is offline.' };
    const settings = await loadSettings();
    const requestId = `op_${crypto.randomUUID()}`;
    const resultPromise = new Promise<OperatorResultFrame>((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pendingOperatorResults.delete(requestId);
        reject(new Error('Timed out waiting for authoritative backend operator result.'));
      }, 10_000);
      this.pendingOperatorResults.set(requestId, { workerId: request.workerId, resolve, reject, timer });
    });
    this.send(operatorActionFrame(settings.executorId, requestId, request));
    try {
      const result = await resultPromise;
      if (result.view) await this.acceptContinuityView(result.view);
      return result.success
        ? { ok: true, view: result.view }
        : { ok: false, code: result.errorCode ?? 'OPERATOR_ACTION_FAILED', error: result.error ?? 'Backend rejected the continuity control.' };
    } catch (error) {
      return { ok: false, code: 'OPERATOR_RESULT_TIMEOUT', error: error instanceof Error ? error.message : 'Operator action failed.' };
    }
  }

  async managedTabRemoved(tabId: number): Promise<void> {
    const binding = await findBindingByTabId(tabId);
    if (!binding) return;
    await removeBinding(binding.workerId);
    await removeContinuityView(binding.workerId);
    const settings = await loadSettings();
    this.send({
      protocolVersion: PROTOCOL_VERSION,
      type: 'conversation.state',
      executorId: settings.executorId,
      workerId: binding.workerId,
      state: 'closed',
      tabId,
      ...(binding.conversationId ? { conversationId: binding.conversationId } : {}),
      url: binding.url,
    });
  }

  private async connect(): Promise<void> {
    const settings = await loadSettings();
    if (!settings.token) {
      await setRuntimeStatus('config-required', 'Set the executor token in extension options.');
      return;
    }
    let endpoint: URL;
    try {
      endpoint = new URL(settings.endpoint);
      if (!['ws:', 'wss:'].includes(endpoint.protocol)) throw new Error('Executor endpoint must use ws:// or wss://.');
      if (!['127.0.0.1', 'localhost', '::1'].includes(endpoint.hostname)) throw new Error('v0 executor endpoint must be loopback.');
    } catch (error) {
      await setRuntimeStatus('error', error instanceof Error ? error.message : 'Invalid executor endpoint.');
      return;
    }

    this.launchLimiter = new AsyncSemaphore(settings.maxConcurrentLaunches);
    await setRuntimeStatus('connecting', settings.endpoint);
    const socket = new WebSocket(settings.endpoint);
    this.socket = socket;
    const connectTimeout = setTimeout(() => {
      if (socket.readyState !== WebSocket.OPEN) {
        void setRuntimeStatus('disconnected', 'Executor connection timed out; retrying.');
        socket.close();
      }
    }, 8_000);

    socket.addEventListener('open', () => {
      clearTimeout(connectTimeout);
      socket.send(JSON.stringify({
        protocolVersion: PROTOCOL_VERSION,
        type: 'executor.hello',
        executorId: settings.executorId,
        token: settings.token,
        browser: __TARGET_BROWSER__,
        extensionVersion: browser.runtime.getManifest().version,
      }));
    });

    socket.addEventListener('message', (event) => {
      void this.handleServerFrame(event.data).catch(async (error) => {
        await setRuntimeStatus('error', error instanceof Error ? error.message : 'Executor frame handling failed.');
        socket.close(1011, 'frame handling failed');
      });
    });

    socket.addEventListener('close', () => {
      clearTimeout(connectTimeout);
      if (this.socket === socket) this.socket = null;
      if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
      if (!this.stopping) {
        void setRuntimeStatus('disconnected', 'Executor connection closed; retrying.');
        this.scheduleReconnect();
      }
    });

    socket.addEventListener('error', () => {
      clearTimeout(connectTimeout);
      void setRuntimeStatus('disconnected', 'Executor connection error; retrying.');
      if (socket.readyState !== WebSocket.CLOSED) socket.close();
    });
  }

  private scheduleReconnect(): void {
    if (this.reconnectTimer || this.stopping) return;
    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = null;
      void this.connect();
    }, 3_000);
  }

  private async handleServerFrame(raw: unknown): Promise<void> {
    const text = typeof raw === 'string' ? raw : raw instanceof Blob ? await raw.text() : String(raw);
    const parsed: unknown = JSON.parse(text);
    const controlFrame = parseContinuityControlServerFrame(parsed);
    if (controlFrame?.kind === 'operator-result') {
      const result = controlFrame.frame;
      const pending = this.pendingOperatorResults.get(result.requestId);
      if (!pending) return;
      this.pendingOperatorResults.delete(result.requestId);
      clearTimeout(pending.timer);
      if (pending.workerId !== result.workerId) {
        pending.reject(new Error('Backend operator result worker did not match the requested worker.'));
        return;
      }
      pending.resolve(result);
      return;
    }
    if (controlFrame?.kind === 'worker-continuity') {
      await this.acceptContinuityView(controlFrame.frame.view);
      return;
    }

    const frame = ExecutorServerFrameSchema.parse(parsed) as ExecutorServerFrame;
    if (frame.type === 'executor.ready') {
      await setRuntimeStatus('connected', `Connected as ${frame.executorId}.`);
      if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = setInterval(() => {
        this.send({
          protocolVersion: PROTOCOL_VERSION,
          type: 'executor.heartbeat',
          executorId: frame.executorId,
          timestamp: now(),
        });
      }, Math.max(5_000, Math.min(frame.heartbeatIntervalMs, 20_000)));
      await this.republishBindings(frame.executorId);
      return;
    }
    if (frame.type !== 'executor.command') return;

    const existing = this.processing.get(frame.command.commandId);
    if (existing) {
      await existing;
      return;
    }
    const task = this.handleCommand(frame.command).finally(() => {
      this.processing.delete(frame.command.commandId);
    });
    this.processing.set(frame.command.commandId, task);
    await task;
  }

  private send(frame: Record<string, unknown>): void {
    if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(frame));
  }

  private async republishBindings(executorId: string): Promise<void> {
    const bindings = await loadBindings();
    for (const binding of Object.values(bindings)) {
      const tab = await browser.tabs.get(binding.tabId).catch(() => null);
      if (!tab || !isChatGptUrl(tab.url)) {
        await removeBinding(binding.workerId);
        await removeContinuityView(binding.workerId);
        continue;
      }
      try {
        const inspection = await this.inspectBoundTab({ ...binding, url: tab.url ?? binding.url });
        await this.updateBinding(executorId, binding, inspection);
      } catch (error) {
        const detail = error instanceof Error ? error.message : 'Managed tab reconciliation failed.';
        const verified: ManagedBinding = { ...binding, url: tab.url ?? binding.url, state: 'error', updatedAt: now() };
        await saveBinding(verified);
        this.sendConversationState(executorId, verified, `HOST_UI_CHANGED: ${detail}`);
      }
    }
  }

  private async handleCommand(command: ExecutorCommand): Promise<void> {
    const settings = await loadSettings();
    this.send({
      protocolVersion: PROTOCOL_VERSION,
      type: 'command.ack',
      executorId: settings.executorId,
      commandId: command.commandId,
    });

    const previous = await getJournal(command.commandId);
    if (previous?.phase === 'completed') {
      this.sendResult(settings.executorId, command, true, previous.result ?? {});
      return;
    }
    if (previous?.phase === 'failed') {
      this.sendResult(settings.executorId, command, false, previous.result ?? {}, previous.error ?? 'Previously failed.');
      return;
    }

    if (Date.parse(command.deadline) <= Date.now()) {
      await this.failCommand(settings.executorId, command, 'Executor command deadline expired.');
      return;
    }

    try {
      const result = await this.executeCommand(settings.executorId, command, previous, settings.maxManagedWorkers);
      await saveJournal(command.commandId, { command, phase: 'completed', result, error: null });
      this.sendResult(settings.executorId, command, true, result);
    } catch (error) {
      const message = error instanceof Error ? error.message : 'Executor command failed.';
      await this.failCommand(settings.executorId, command, message);
    }
  }

  private async failCommand(executorId: string, command: ExecutorCommand, error: string): Promise<void> {
    await saveJournal(command.commandId, { command, phase: 'failed', result: {}, error });
    this.sendResult(executorId, command, false, {}, error);
  }

  private sendResult(executorId: string, command: ExecutorCommand, success: boolean, result: Record<string, unknown>, error?: string): void {
    this.send({
      protocolVersion: PROTOCOL_VERSION,
      type: 'command.result',
      executorId,
      commandId: command.commandId,
      success,
      result,
      ...(error ? { error } : {}),
    });
  }

  private async executeCommand(executorId: string, command: ExecutorCommand, previous: JournalEntry | null, maxManagedWorkers: number): Promise<Record<string, unknown>> {
    if (command.type === 'conversation.create') {
      return this.launchLimiter.run(async () => {
        const bindings = await loadBindings();
        if (!bindings[command.workerId] && Object.keys(bindings).length >= maxManagedWorkers) {
          throw new Error(`MANAGED_WORKER_LIMIT: refusing to exceed ${maxManagedWorkers} managed ChatGPT conversations.`);
        }
        return this.workerQueue.run(command.workerId, () => this.createConversation(executorId, command, previous));
      });
    }

    return this.workerQueue.run(command.workerId, async () => {
      switch (command.type) {
        case 'conversation.send': return this.sendPrompt(executorId, command, command.prompt, previous);
        case 'conversation.steer': return this.steerPrompt(executorId, command, previous);
        case 'conversation.inspect': return this.inspectConversation(executorId, command);
        case 'conversation.close': return this.closeConversation(executorId, command);
      }
    });
  }

  private async createConversation(executorId: string, command: Extract<ExecutorCommand, { type: 'conversation.create' }>, previous: JournalEntry | null): Promise<Record<string, unknown>> {
    let binding = (await loadBindings())[command.workerId] ?? null;

    const recovery = submissionRecovery(previous?.phase);
    if (recovery === 'ambiguous-no-resend' || recovery === 'submitted-no-resend') {
      if (!binding) throw new Error('AMBIGUOUS_SUBMISSION: extension restarted after prompt submission and no managed tab binding remains.');
      try {
        const inspection = await this.waitForConversationId(binding.tabId, command.deadline);
        binding = await this.updateBinding(executorId, binding, inspection);
        return this.bindingResult(binding);
      } catch (error) {
        const detail = error instanceof Error ? error.message : 'conversation identity was not observed';
        throw new Error(`AMBIGUOUS_SUBMISSION: bootstrap may already have been delivered; refusing to resend. ${detail}`);
      }
    }

    if (!binding) {
      await saveJournal(command.commandId, { command, phase: 'starting', result: null, error: null });
      const tab = await browser.tabs.create({ url: CHATGPT_HOME, active: false });
      if (tab.id === undefined) throw new Error('Browser did not return a tab ID for the managed ChatGPT conversation.');
      binding = {
        workerId: command.workerId,
        tabId: tab.id,
        conversationId: null,
        url: tab.url ?? CHATGPT_HOME,
        state: 'loading',
        updatedAt: now(),
      };
      await saveBinding(binding);
      await saveJournal(command.commandId, { command, phase: 'tab-created', result: null, error: null });
      this.sendConversationState(executorId, binding);
    }

    const before = await this.waitForContent(binding.tabId, CONTENT_READY_TIMEOUT_MS);
    binding = await this.updateBinding(executorId, binding, before);
    if (before.conversationId) return this.bindingResult(binding);

    await saveJournal(command.commandId, { command, phase: 'submission-started', result: null, error: null });
    await this.contentRequest(binding.tabId, { kind: CONTENT_KIND, action: 'submit', prompt: command.bootstrap });
    await saveJournal(command.commandId, { command, phase: 'submitted', result: null, error: null });

    const inspection = await this.waitForConversationId(binding.tabId, command.deadline);
    binding = await this.updateBinding(executorId, binding, inspection);
    return this.bindingResult(binding);
  }

  private async sendPrompt(executorId: string, command: Extract<ExecutorCommand, { type: 'conversation.send' }>, prompt: string, previous: JournalEntry | null): Promise<Record<string, unknown>> {
    const binding = await this.requireManagedBinding(command.workerId);
    const recovery = submissionRecovery(previous?.phase);
    if (recovery === 'ambiguous-no-resend') {
      throw new Error('AMBIGUOUS_SUBMISSION: extension restarted during follow-up submission; refusing to resend a possibly delivered prompt.');
    }
    if (recovery === 'submitted-no-resend') {
      const inspection = await this.inspectBoundTab(binding);
      const updated = await this.updateBinding(executorId, binding, inspection);
      return this.bindingResult(updated);
    }

    const inspection = await this.waitForIdle(binding.tabId, command.deadline);
    await this.updateBinding(executorId, binding, inspection);
    await saveJournal(command.commandId, { command, phase: 'submission-started', result: null, error: null });
    const response = await this.contentRequest(binding.tabId, { kind: CONTENT_KIND, action: 'submit', prompt });
    await saveJournal(command.commandId, { command, phase: 'submitted', result: null, error: null });
    const next = response.inspection ?? await this.inspectBoundTab(binding);
    const updated = await this.updateBinding(executorId, binding, next);
    return this.bindingResult(updated);
  }

  private async steerPrompt(executorId: string, command: Extract<ExecutorCommand, { type: 'conversation.steer' }>, previous: JournalEntry | null): Promise<Record<string, unknown>> {
    const binding = await this.requireManagedBinding(command.workerId);
    if (previous?.phase === 'submission-started') {
      throw new Error('AMBIGUOUS_SUBMISSION: extension restarted during mid-turn steering; refusing to resend a possibly delivered steering message.');
    }
    if (previous?.phase === 'submitted') {
      const inspection = await this.inspectBoundTab(binding);
      const updated = await this.updateBinding(executorId, binding, inspection);
      return { ...this.bindingResult(updated), steeringAccepted: true, reason: command.reason };
    }

    const before = await this.inspectBoundTab(binding);
    await this.updateBinding(executorId, binding, before);
    if (!before.generating) throw new Error('STEERING_UNAVAILABLE: managed ChatGPT conversation is not currently generating.');

    await saveJournal(command.commandId, { command, phase: 'submission-started', result: null, error: null });
    const response = await this.contentRequest(binding.tabId, { kind: CONTENT_KIND, action: 'steer', prompt: command.prompt });
    await saveJournal(command.commandId, { command, phase: 'submitted', result: null, error: null });
    const next = response.inspection ?? await this.inspectBoundTab(binding);
    const updated = await this.updateBinding(executorId, binding, next);
    return { ...this.bindingResult(updated), steeringAccepted: true, reason: command.reason };
  }

  private async inspectConversation(executorId: string, command: Extract<ExecutorCommand, { type: 'conversation.inspect' }>): Promise<Record<string, unknown>> {
    const binding = await this.requireManagedBinding(command.workerId);
    const inspection = await this.inspectBoundTab(binding);
    const updated = await this.updateBinding(executorId, binding, inspection);
    return { ...this.bindingResult(updated), inspection };
  }

  private async closeConversation(executorId: string, command: Extract<ExecutorCommand, { type: 'conversation.close' }>): Promise<Record<string, unknown>> {
    const binding = await this.requireManagedBinding(command.workerId);
    await browser.tabs.remove(binding.tabId);
    await removeBinding(command.workerId);
    await removeContinuityView(command.workerId);
    this.send({
      protocolVersion: PROTOCOL_VERSION,
      type: 'conversation.state',
      executorId,
      workerId: command.workerId,
      state: 'closed',
      tabId: binding.tabId,
      ...(binding.conversationId ? { conversationId: binding.conversationId } : {}),
      url: binding.url,
    });
    return { workerId: command.workerId, tabId: binding.tabId, closed: true };
  }

  private async requireManagedBinding(workerId: string): Promise<ManagedBinding> {
    const binding = (await loadBindings())[workerId];
    if (!binding) throw new Error(`Managed ChatGPT binding for ${workerId} does not exist.`);
    const tab = await browser.tabs.get(binding.tabId).catch(() => null);
    if (!tab || !isChatGptUrl(tab.url)) {
      await removeBinding(workerId);
      await removeContinuityView(workerId);
      throw new Error(`Managed tab for ${workerId} is missing or no longer on chatgpt.com.`);
    }
    return { ...binding, url: tab.url ?? binding.url };
  }

  private async waitForContent(tabId: number, timeoutMs: number): Promise<Inspection> {
    const response = await withContentReceiverReady(
      (remainingMs) => this.contentRequest(tabId, { kind: CONTENT_KIND, action: 'wait', condition: 'composer-ready', timeoutMs: remainingMs }),
      timeoutMs,
    );
    if (!response.inspection) throw new Error('ChatGPT content script returned no inspection after composer wait.');
    return response.inspection;
  }

  private async waitForConversationId(tabId: number, commandDeadline: string): Promise<Inspection> {
    const timeoutMs = Math.max(1, Math.min(CONVERSATION_ID_TIMEOUT_MS, Date.parse(commandDeadline) - Date.now()));
    const response = await this.contentRequest(tabId, { kind: CONTENT_KIND, action: 'wait', condition: 'conversation-id', timeoutMs });
    if (!response.inspection) throw new Error('ChatGPT content script returned no inspection after conversation-ID wait.');
    return response.inspection;
  }

  private async waitForIdle(tabId: number, commandDeadline: string): Promise<Inspection> {
    const timeoutMs = boundedSubmissionReadyWaitMs(commandDeadline);
    const response = await this.contentRequest(tabId, { kind: CONTENT_KIND, action: 'wait', condition: 'idle', timeoutMs });
    if (!response.inspection) throw new Error('ChatGPT content script returned no inspection after idle wait.');
    return response.inspection;
  }

  private async inspectBoundTab(binding: ManagedBinding): Promise<Inspection> {
    const tab = await browser.tabs.get(binding.tabId).catch(() => null);
    if (!tab || !isChatGptUrl(tab.url)) throw new Error('Managed ChatGPT tab is missing or navigated away.');
    const response = await this.contentRequest(binding.tabId, { kind: CONTENT_KIND, action: 'inspect' });
    if (!response.inspection) throw new Error('ChatGPT content script returned no inspection.');
    return response.inspection;
  }

  private async contentRequest(tabId: number, request: Record<string, unknown>): Promise<ContentResponse> {
    const response = await browser.tabs.sendMessage(tabId, request) as ContentResponse;
    if (!response?.ok) throw new Error(`${response?.code ?? 'CONTENT_ERROR'}: ${response?.error ?? 'ChatGPT content-script request failed.'}`);
    return response;
  }

  private async updateBinding(executorId: string, binding: ManagedBinding, inspection: Inspection): Promise<ManagedBinding> {
    const tab = await browser.tabs.get(binding.tabId).catch(() => null);
    const updated: ManagedBinding = {
      ...binding,
      conversationId: inspection.conversationId ?? binding.conversationId,
      url: tab?.url ?? inspection.url ?? binding.url,
      state: toConversationState(inspection.state),
      updatedAt: now(),
    };
    await saveBinding(updated);
    const error = inspection.issue ? `${inspection.issue.code}: ${inspection.issue.detail}` : inspection.error ?? undefined;
    this.sendConversationState(executorId, updated, error, inspection);
    await this.syncOverlay(updated);
    return updated;
  }

  private async acceptContinuityView(view: BackendContinuityView): Promise<void> {
    await saveContinuityView(view);
    const binding = (await loadBindings())[view.workerId];
    if (binding) await this.syncOverlay(binding, view);
  }

  private async syncOverlay(binding: ManagedBinding, suppliedView?: BackendContinuityView): Promise<void> {
    const [settings, views, connection] = await Promise.all([loadSettings(), suppliedView ? Promise.resolve(null) : loadContinuityViews(), getRuntimeStatus()]);
    const view = suppliedView ?? views?.[binding.workerId];
    await browser.tabs.sendMessage(binding.tabId, {
      kind: OVERLAY_MESSAGE_KIND,
      worker: localWorkerView(binding, binding.tabId, view, connection?.state === 'connected'),
      overlayVisible: settings.overlayVisible,
    }).catch(() => undefined);
  }

  private sendConversationState(executorId: string, binding: ManagedBinding, error?: string, inspection?: Inspection): void {
    this.send({
      protocolVersion: PROTOCOL_VERSION,
      type: 'conversation.state',
      executorId,
      workerId: binding.workerId,
      state: binding.state,
      tabId: binding.tabId,
      ...(binding.conversationId ? { conversationId: binding.conversationId } : {}),
      url: binding.url,
      ...(error ? { error } : {}),
      ...(inspection?.state === 'idle' && inspection.assistantOutputTail ? { assistantOutputTail: inspection.assistantOutputTail } : {}),
      ...(inspection?.state === 'idle' && inspection.assistantOutputTruncated !== undefined ? { assistantOutputTruncated: inspection.assistantOutputTruncated } : {}),
    });
  }

  private bindingResult(binding: ManagedBinding): Record<string, unknown> {
    return {
      workerId: binding.workerId,
      tabId: binding.tabId,
      conversationId: binding.conversationId,
      url: binding.url,
      state: binding.state,
    };
  }
}

const client = new ExecutorClient();
void browser.alarms.create(EXECUTOR_WAKE_ALARM, { periodInMinutes: 0.5 });
void client.start();

browser.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === EXECUTOR_WAKE_ALARM) void client.start();
});

browser.runtime.onMessage.addListener((message: unknown) => {
  if ((message as { kind?: string } | null)?.kind === 'chatgpt-orchestrator.wake') {
    void client.start();
  }
});

browser.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'local' && changes.settings) {
    void client.refreshOverlays();
    void client.restart();
  }
});

browser.tabs.onRemoved.addListener((tabId) => {
  void client.managedTabRemoved(tabId);
});

browser.tabs.onActivated.addListener(({ tabId }) => {
  void client.reconcileManagedTab(tabId);
});

browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
  if (changeInfo.status === 'complete' || changeInfo.url !== undefined) void client.reconcileManagedTab(tabId);
});


browser.runtime.onMessage.addListener((message: unknown, sender: browser.Runtime.MessageSender) => {
  const candidate = message as { kind?: string; inspection?: Inspection };
  if (candidate.kind === CONTENT_STATE_KIND && candidate.inspection && sender.tab?.id !== undefined) {
    void client.observedTabState(sender.tab.id, candidate.inspection);
    return undefined;
  }

  const ui = message as Partial<UiRequest>;
  if (ui.kind !== UI_MESSAGE_KIND) return undefined;
  if (ui.action === 'snapshot') return client.uiSnapshot(typeof ui.tabId === 'number' ? ui.tabId : sender.tab?.id);
  if ((ui.action === 'pause' || ui.action === 'continue-now' || ui.action === 'continue-anyway' || ui.action === 'stop') && typeof ui.workerId === 'string') {
    return client.handleUiAction(ui as Exclude<UiRequest, { action: 'snapshot' }>);
  }
  if (ui.action === 'set-auto-resume' && typeof ui.workerId === 'string' && typeof ui.enabled === 'boolean') {
    return client.handleUiAction(ui as Exclude<UiRequest, { action: 'snapshot' }>);
  }
  return Promise.resolve({ ok: false, code: 'INVALID_UI_REQUEST', error: 'Unsupported Execution Continuity UI request.' });
});
