import { BACKGROUND_MESSAGE_TYPES } from '@/common/message-types';
import {
  WEB_EDITOR_ACTIONS,
  type ElementChangeSummary,
  type WebEditorApplyBatchPayload,
  type WebEditorTxChangedPayload,
  type WebEditorHighlightElementPayload,
  type WebEditorRevertElementPayload,
  type WebEditorCancelExecutionPayload,
  type WebEditorCancelExecutionResponse,
} from '@/common/web-editor-types';
import { openAgentChatSidepanel } from '../utils/sidepanel';
import {
  requestAgentRpcFetch,
  subscribeAgentStream,
  unsubscribeAgentStream,
} from '../native-host';

const CONTEXT_MENU_ID = 'web_editor_toggle';
const COMMAND_KEY = 'toggle_web_editor';

/** Storage key prefix for TX change session data (per-tab isolation) */
const WEB_EDITOR_TX_CHANGED_SESSION_KEY_PREFIX = 'web-editor-tx-changed-';
const WEB_EDITOR_SELECTION_SESSION_KEY_PREFIX = 'web-editor-selection-';

/** Storage key prefix for excluded element keys (per-tab isolation, managed by sidepanel) */
const WEB_EDITOR_EXCLUDED_KEYS_SESSION_KEY_PREFIX = 'web-editor-excluded-keys-';

/** Storage key for AgentChat selected session ID */
const STORAGE_KEY_SELECTED_SESSION = 'agent-selected-session-id';

// In-memory execution status cache (per requestId)
interface ExecutionStatusEntry {
  status: string;
  message?: string;
  updatedAt: number;
  result?: { success: boolean; summary?: string; error?: string };
}
const executionStatusCache = new Map<string, ExecutionStatusEntry>();
const STATUS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

function cleanupExpiredStatuses(): void {
  const now = Date.now();
  for (const [key, entry] of executionStatusCache) {
    if (now - entry.updatedAt > STATUS_CACHE_TTL) {
      executionStatusCache.delete(key);
    }
  }
}

function setExecutionStatus(
  requestId: string,
  status: string,
  message?: string,
  result?: ExecutionStatusEntry['result'],
): void {
  executionStatusCache.set(requestId, {
    status,
    message,
    updatedAt: Date.now(),
    result,
  });
  // Periodic cleanup
  if (executionStatusCache.size > 100) {
    cleanupExpiredStatuses();
  }
}

function getExecutionStatus(requestId: string): ExecutionStatusEntry | undefined {
  return executionStatusCache.get(requestId);
}

// Stream subscriptions for status updates (per sessionId)
const sseConnections = new Map<
  string,
  { subscriptionId: string; lastRequestId: string; listener: (message: unknown) => void }
>();

async function closeSessionStatusSubscription(sessionId: string, requestId?: string): Promise<void> {
  const existing = sseConnections.get(sessionId);
  if (!existing) {
    return;
  }
  if (requestId && existing.lastRequestId !== requestId) {
    return;
  }

  chrome.runtime.onMessage.removeListener(existing.listener);
  await unsubscribeAgentStream(existing.subscriptionId).catch(() => {});
  sseConnections.delete(sessionId);
}

/**
 * Start SSE subscription for a session to receive status updates
 */
async function subscribeToSessionStatus(
  sessionId: string,
  requestId: string,
): Promise<void> {
  // Close existing subscription for this session if any
  const existing = sseConnections.get(sessionId);
  if (existing) {
    await closeSessionStatusSubscription(sessionId);
  }

  // Set initial status
  setExecutionStatus(requestId, 'starting', 'Connecting to Agent...');

  try {
    const subscription = await subscribeAgentStream(sessionId, {
      subscriptionId: `web-editor-${sessionId}-${requestId}`,
    });

    const onMessage = (message: unknown): void => {
      const msg = message as {
        type?: string;
        payload?: {
          subscriptionId?: string;
          event?: unknown;
        };
      };
      if (msg?.type !== BACKGROUND_MESSAGE_TYPES.AGENT_STREAM_EVENT) {
        return;
      }
      if (msg.payload?.subscriptionId !== subscription.subscriptionId || !msg.payload.event) {
        return;
      }
      handleSseEvent(requestId, msg.payload.event);
    };

    sseConnections.set(sessionId, {
      subscriptionId: subscription.subscriptionId,
      lastRequestId: requestId,
      listener: onMessage,
    });
    chrome.runtime.onMessage.addListener(onMessage);
    setExecutionStatus(requestId, 'running', 'Agent processing...');
  } catch (err) {
    const cached = getExecutionStatus(requestId);
    if (cached && !['completed', 'failed', 'cancelled'].includes(cached.status)) {
      setExecutionStatus(requestId, 'running', 'Agent processing (connection lost)...');
    }
  }
}

/**
 * Handle SSE event from Agent stream
 */
function handleSseEvent(requestId: string, event: unknown): void {
  if (!event || typeof event !== 'object') return;
  const e = event as Record<string, unknown>;
  const type = e.type;
  const data = e.data as Record<string, unknown> | undefined;

  // Check if this event is for our request
  const eventRequestId = data?.requestId as string | undefined;
  if (eventRequestId && eventRequestId !== requestId) return;

  if (type === 'status' && data) {
    const status = data.status as string;
    const message = data.message as string | undefined;
    const sessionId = typeof data.sessionId === 'string' ? data.sessionId : '';

    // Map Agent status to our status
    // - 'ready' -> 'running' (ready is a running sub-state)
    // - 'error' -> 'failed' (normalize server 'error' to UI 'failed')
    let mappedStatus = status;
    if (status === 'ready') mappedStatus = 'running';
    if (status === 'error') mappedStatus = 'failed';

    setExecutionStatus(requestId, mappedStatus, message);
    if (
      sessionId &&
      (mappedStatus === 'completed' || mappedStatus === 'failed' || mappedStatus === 'cancelled')
    ) {
      void closeSessionStatusSubscription(sessionId, requestId);
    }
  } else if (type === 'message' && data) {
    // Update status to show we're receiving messages
    const cached = getExecutionStatus(requestId);
    if (cached && cached.status === 'starting') {
      setExecutionStatus(requestId, 'running', 'Agent is working...');
    }

    // Check for completion indicators in message content
    const role = data.role as string | undefined;
    const isFinal = data.isFinal as boolean | undefined;
    if (role === 'assistant' && isFinal) {
      const content = data.content as string | undefined;
      setExecutionStatus(requestId, 'completed', 'Completed', {
        success: true,
        summary: content?.slice(0, 200),
      });
    }
  } else if (type === 'error') {
    const errorMsg = (e.error as string) || 'Unknown error';
    setExecutionStatus(requestId, 'failed', errorMsg, {
      success: false,
      error: errorMsg,
    });
  }
}

/** Script path for the active editor runtime (WXT unlisted script output). */
const WEB_EDITOR_SCRIPT_PATH = 'web-editor.js';

/** Script path for Phase 7 props agent (MAIN world) */
const PROPS_AGENT_SCRIPT_PATH = 'inject-scripts/props-agent.js';

type WebEditorInstructionType = 'update_text' | 'update_style';

interface WebEditorFingerprint {
  tag: string;
  id?: string;
  classes: string[];
  text?: string;
}

/** Debug source from framework fiber metadata (file, line, component name) */
interface DebugSource {
  file: string;
  line?: number;
  column?: number;
  componentName?: string;
}

/** Style operation details (before/after diff) */
interface StyleOperation {
  type: 'update_style';
  before: Record<string, string>;
  after: Record<string, string>;
  removed: string[];
}

interface WebEditorApplyPayload {
  pageUrl: string;
  targetFile?: string;
  fingerprint: WebEditorFingerprint;
  techStackHint?: string[];
  instruction: {
    type: WebEditorInstructionType;
    description: string;
    text?: string;
    style?: Record<string, string>;
  };

  // Extended fields (best-effort, optional)
  selectorCandidates?: string[];
  debugSource?: DebugSource;
  operation?: StyleOperation;
}

function normalizeString(value: unknown): string {
  return typeof value === 'string' ? value : '';
}

function normalizeStringArray(value: unknown): string[] {
  if (!Array.isArray(value)) return [];
  return value.map((item) => normalizeString(item)).filter(Boolean);
}

function normalizeStyleMap(value: unknown): Record<string, string> | undefined {
  if (!value || typeof value !== 'object') return undefined;
  const out: Record<string, string> = {};
  for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
    const key = normalizeString(k).trim();
    const val = normalizeString(v).trim();
    if (!key || !val) continue;
    out[key] = val;
  }
  return Object.keys(out).length ? out : undefined;
}

function normalizeStyleMapAllowEmpty(value: unknown): Record<string, string> | undefined {
  if (!value || typeof value !== 'object') return undefined;
  const out: Record<string, string> = {};
  for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
    const key = normalizeString(k).trim();
    if (!key) continue;
    // Allow empty values (represents removed styles)
    out[key] = normalizeString(v).trim();
  }
  return Object.keys(out).length ? out : undefined;
}

function normalizeDebugSource(value: unknown): DebugSource | undefined {
  if (!value || typeof value !== 'object') return undefined;
  const obj = value as Record<string, unknown>;
  const file = normalizeString(obj.file).trim();
  if (!file) return undefined;

  const source: DebugSource = { file };
  const line = Number(obj.line);
  if (Number.isFinite(line) && line > 0) source.line = line;
  const column = Number(obj.column);
  if (Number.isFinite(column) && column >= 0) source.column = column;
  const componentName = normalizeString(obj.componentName).trim();
  if (componentName) source.componentName = componentName;

  return source;
}

function normalizeOperation(value: unknown): StyleOperation | undefined {
  if (!value || typeof value !== 'object') return undefined;
  const obj = value as Record<string, unknown>;
  if (obj.type !== 'update_style') return undefined;

  const before = normalizeStyleMapAllowEmpty(obj.before);
  const after = normalizeStyleMapAllowEmpty(obj.after);
  const removed = normalizeStringArray(obj.removed);

  if (!before && !after && removed.length === 0) return undefined;

  return {
    type: 'update_style',
    before: before ?? {},
    after: after ?? {},
    removed,
  };
}

function normalizeApplyPayload(raw: unknown): WebEditorApplyPayload {
  const obj = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
  const pageUrl = normalizeString(obj.pageUrl).trim();
  const targetFile = normalizeString(obj.targetFile).trim() || undefined;
  const techStackHint = normalizeStringArray(obj.techStackHint);

  const fingerprintRaw = (
    obj.fingerprint && typeof obj.fingerprint === 'object' ? obj.fingerprint : {}
  ) as Record<string, unknown>;
  const fingerprint: WebEditorFingerprint = {
    tag: normalizeString(fingerprintRaw.tag).trim() || 'unknown',
    id: normalizeString(fingerprintRaw.id).trim() || undefined,
    classes: normalizeStringArray(fingerprintRaw.classes),
    text: normalizeString(fingerprintRaw.text).trim() || undefined,
  };

  const instructionRaw = (
    obj.instruction && typeof obj.instruction === 'object' ? obj.instruction : {}
  ) as Record<string, unknown>;
  const type = normalizeString(instructionRaw.type).trim() as WebEditorInstructionType;
  if (type !== 'update_text' && type !== 'update_style') {
    throw new Error('Invalid instruction.type');
  }

  const instruction = {
    type,
    description: normalizeString(instructionRaw.description).trim() || '',
    text: normalizeString(instructionRaw.text).trim() || undefined,
    style: normalizeStyleMap(instructionRaw.style),
  };

  if (!pageUrl) {
    throw new Error('pageUrl is required');
  }
  if (!instruction.description) {
    throw new Error('instruction.description is required');
  }

  // Extended fields (optional)
  const selectorCandidates = normalizeStringArray(obj.selectorCandidates);
  const debugSource = normalizeDebugSource(obj.debugSource);
  const operation = normalizeOperation(obj.operation);

  return {
    pageUrl,
    targetFile,
    fingerprint,
    techStackHint: techStackHint.length ? techStackHint : undefined,
    instruction,
    selectorCandidates: selectorCandidates.length ? selectorCandidates : undefined,
    debugSource,
    operation,
  };
}

/**
 * Normalize and validate batch apply payload.
 * Runtime validation for WebEditorApplyBatchPayload.
 */
function normalizeApplyBatchPayload(raw: unknown): WebEditorApplyBatchPayload {
  const obj = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;

  const tabIdRaw = Number(obj.tabId);
  const tabId = Number.isFinite(tabIdRaw) && tabIdRaw > 0 ? tabIdRaw : 0;

  const elements = Array.isArray(obj.elements) ? (obj.elements as ElementChangeSummary[]) : [];

  const excludedKeys = Array.isArray(obj.excludedKeys)
    ? obj.excludedKeys.map((k) => normalizeString(k).trim()).filter((k): k is string => Boolean(k))
    : [];

  const pageUrl = normalizeString(obj.pageUrl).trim() || undefined;

  return { tabId, elements, excludedKeys, pageUrl };
}

/**
 * Build a batch prompt for multiple element changes.
 * Designed for AgentChat integration to apply multiple visual edits at once.
 */
function buildAgentPromptBatch(elements: readonly ElementChangeSummary[], pageUrl: string): string {
  const lines: string[] = [];

  // Header
  lines.push('You are a senior frontend engineer working in a local codebase.');
  lines.push(
    'Goal: persist a batch of visual edits from the browser into the source code with minimal changes.',
  );
  lines.push('');

  // Page context
  lines.push(`Page URL: ${pageUrl}`);
  lines.push('');

  lines.push('## Batch Changes');
  lines.push(`Total elements: ${elements.length}`);
  lines.push('');
  lines.push(
    'For each element, prefer "source" (file/line/component) when available; otherwise use selectors/fingerprint to locate it.',
  );
  lines.push('');

  // Element details
  elements.forEach((element, index) => {
    const title = element.fullLabel || element.label || element.elementKey;
    lines.push(`### ${index + 1}. ${title}`);
    lines.push(`- elementKey: ${element.elementKey}`);
    lines.push(`- change type: ${element.type}`);

    // Debug source (high-confidence location)
    const ds = element.debugSource ?? element.locator?.debugSource;
    if (ds?.file) {
      const loc = ds.line ? `${ds.file}:${ds.line}${ds.column ? `:${ds.column}` : ''}` : ds.file;
      lines.push(`- source: ${loc}${ds.componentName ? ` (${ds.componentName})` : ''}`);
    }

    // Locator hints for fallback
    if (element.locator?.selectors?.length) {
      lines.push('- selectors:');
      for (const sel of element.locator.selectors.slice(0, 5)) {
        lines.push(`  - ${sel}`);
      }
    }
    if (element.locator?.fingerprint) {
      lines.push(`- fingerprint: ${element.locator.fingerprint}`);
    }
    if (Array.isArray(element.locator?.path) && element.locator.path.length > 0) {
      lines.push(`- path: ${JSON.stringify(element.locator.path)}`);
    }
    if (element.locator?.shadowHostChain?.length) {
      lines.push(`- shadowHostChain: ${JSON.stringify(element.locator.shadowHostChain)}`);
    }
    lines.push('');

    // Net effect details
    const net = element.netEffect;
    lines.push('#### Net Effect (apply these final values)');

    if (net.textChange) {
      lines.push('##### Text');
      lines.push(`- before: ${JSON.stringify(net.textChange.before)}`);
      lines.push(`- after: ${JSON.stringify(net.textChange.after)}`);
      lines.push('');
    }

    if (net.classChanges) {
      lines.push('##### Classes');
      lines.push(`- before: ${net.classChanges.before.join(' ')}`);
      lines.push(`- after: ${net.classChanges.after.join(' ')}`);
      lines.push('');
    }

    if (net.styleChanges) {
      lines.push('##### Styles (before → after)');
      const before = net.styleChanges.before ?? {};
      const after = net.styleChanges.after ?? {};
      const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]);
      for (const key of Array.from(allKeys).sort()) {
        const beforeVal = before[key] ?? '(unset)';
        const afterRaw = Object.prototype.hasOwnProperty.call(after, key) ? after[key] : '(unset)';
        const afterVal = afterRaw === '' ? '(removed)' : afterRaw;
        if (beforeVal !== afterVal) {
          lines.push(`- ${key}: "${beforeVal}" → "${afterVal}"`);
        }
      }
      lines.push('');
    }

    // Fallback message if no specific changes
    if (!net.textChange && !net.classChanges && !net.styleChanges) {
      lines.push(
        '- No net effect details available; use locator hints to inspect the element in code.',
      );
      lines.push('');
    }
  });

  // Instructions
  lines.push('## How to Apply');
  lines.push('1. Use "source" when available to go directly to the component file.');
  lines.push('2. Otherwise, use selectors/fingerprint/path to locate the element in the codebase.');
  lines.push('3. Apply the net effect with minimal changes and correct styling conventions.');
  lines.push('4. Avoid generated/bundled outputs; update source files only.');
  lines.push('');

  // Output format
  lines.push('## Constraints');
  lines.push('- Make the smallest safe edit possible for each element');
  lines.push(
    '- If Tailwind/CSS Modules/styled-components are used, update the correct styling source',
  );
  lines.push('- Do not change unrelated behavior or formatting');
  lines.push('');

  lines.push(
    '## Output\nApply all the changes in the repo, then reply with a short summary of what file(s) you modified and the exact changes made.',
  );

  return lines.join('\n');
}

function buildAgentPrompt(payload: WebEditorApplyPayload): string {
  const lines: string[] = [];

  // Header
  lines.push('You are a senior frontend engineer working in a local codebase.');
  lines.push(
    'Goal: persist a visual edit from the browser into the source code with minimal changes.',
  );
  lines.push('');

  // Page context
  lines.push(`Page URL: ${payload.pageUrl}`);
  lines.push('');

  // == Source Location (high-confidence if debugSource available) ==
  const ds = payload.debugSource;
  if (ds?.file) {
    lines.push('## Source Location (from framework debug info)');
    const loc = ds.line ? `${ds.file}:${ds.line}${ds.column ? `:${ds.column}` : ''}` : ds.file;
    lines.push(`- file: ${loc}`);
    if (ds.componentName) lines.push(`- component: ${ds.componentName}`);
    lines.push('');
    lines.push('This is high-confidence source location extracted from framework debug info.');
    lines.push('Start your search here. Only fall back to fingerprint if this file is invalid.');
    lines.push('');
  } else if (payload.targetFile) {
    lines.push(`## Target File (best-effort): ${payload.targetFile}`);
    lines.push(
      'If this path is invalid or points to node_modules, fall back to fingerprint search.',
    );
    lines.push('');
  }

  // == Element Fingerprint ==
  lines.push('## Element Fingerprint');
  lines.push(`- tag: ${payload.fingerprint.tag}`);
  if (payload.fingerprint.id) lines.push(`- id: ${payload.fingerprint.id}`);
  if (payload.fingerprint.classes?.length) {
    lines.push(`- classes: ${payload.fingerprint.classes.join(' ')}`);
  }
  if (payload.fingerprint.text) lines.push(`- text: ${payload.fingerprint.text}`);
  lines.push('');

  // == CSS Selectors (for precise matching) ==
  if (payload.selectorCandidates?.length) {
    lines.push('## CSS Selectors (ordered by specificity)');
    for (const sel of payload.selectorCandidates.slice(0, 5)) {
      lines.push(`- ${sel}`);
    }
    lines.push('');
    lines.push('Use these selectors to grep the codebase if file location is unavailable.');
    lines.push('');
  }

  // == Tech Stack ==
  if (payload.techStackHint?.length) {
    lines.push(`## Tech Stack: ${payload.techStackHint.join(', ')}`);
    lines.push('');
  }

  // == Requested Change ==
  lines.push('## Requested Change');
  lines.push(`- type: ${payload.instruction.type}`);
  lines.push(`- description: ${payload.instruction.description}`);

  if (payload.instruction.type === 'update_text' && payload.instruction.text !== undefined) {
    lines.push(`- new text: ${JSON.stringify(payload.instruction.text)}`);
  }

  // For style updates, show detailed before/after diff if available
  if (payload.instruction.type === 'update_style') {
    const op = payload.operation;
    if (op && (Object.keys(op.before).length > 0 || Object.keys(op.after).length > 0)) {
      lines.push('');
      lines.push('### Style Changes (before → after)');
      const allKeys = new Set([...Object.keys(op.before), ...Object.keys(op.after)]);
      for (const key of allKeys) {
        const before = op.before[key] ?? '(unset)';
        const after = op.after[key] ?? '(removed)';
        if (before !== after) {
          lines.push(`  ${key}: "${before}" → "${after}"`);
        }
      }
      if (op.removed.length > 0) {
        lines.push(`  [Removed]: ${op.removed.join(', ')}`);
      }
    } else if (payload.instruction.style) {
      lines.push(`- style map: ${JSON.stringify(payload.instruction.style, null, 2)}`);
    }
  }
  lines.push('');

  // == Instructions ==
  lines.push('## How to Apply');
  if (ds?.file) {
    lines.push(`1. Open ${ds.file}${ds.line ? ` around line ${ds.line}` : ''}`);
    if (ds.componentName) {
      lines.push(`2. Locate the "${ds.componentName}" component definition`);
    }
    lines.push(
      `3. Find the element matching tag="${payload.fingerprint.tag}"${payload.fingerprint.classes?.length ? ` with classes including "${payload.fingerprint.classes[0]}"` : ''}`,
    );
    lines.push('4. Apply the requested style/text change');
  } else if (payload.targetFile) {
    lines.push(`1. Open ${payload.targetFile}`);
    lines.push('2. Search for the element by matching fingerprint (tag, classes, text)');
    lines.push('3. If not found, use repo-wide search with selectors or class names');
    lines.push('4. Apply the requested change');
  } else {
    lines.push('1. Use repo-wide search (rg) with class names or text from fingerprint');
    if (payload.selectorCandidates?.length) {
      lines.push(`2. Try searching for: "${payload.selectorCandidates[0]}"`);
    }
    lines.push('3. Locate the component/template containing this element');
    lines.push('4. Apply the requested change');
  }
  lines.push('');

  // == Constraints ==
  lines.push('## Constraints');
  lines.push('- Make the smallest safe edit possible');
  if (payload.techStackHint?.includes('Tailwind')) {
    lines.push('- Tailwind detected: prefer updating className over inline styles');
  }
  if (payload.techStackHint?.includes('React')) {
    lines.push('- Update the component source, not generated/bundled code');
  }
  lines.push('- If CSS Modules or styled-components are used, update the correct styling source');
  lines.push('- Do not change unrelated behavior or formatting');
  lines.push('');

  // == Output ==
  lines.push(
    '## Output\nApply the change in the repo, then reply with a short summary of what file(s) you modified and the exact change made.',
  );

  return lines.join('\n');
}

async function ensureContextMenu(): Promise<void> {
  try {
    if (!(chrome as any).contextMenus?.create) return;
    try {
      await chrome.contextMenus.remove(CONTEXT_MENU_ID);
    } catch {}
    await chrome.contextMenus.create({
      id: CONTEXT_MENU_ID,
      title: 'Switch web page editing mode',
      contexts: ['all'],
    });
  } catch (error) {
    console.warn('[WebEditor] Failed to ensure context menu:', error);
  }
}

/**
 * Ensure the web editor script is injected into the tab
 */
async function ensureEditorInjected(tabId: number): Promise<void> {
  // Try to ping existing instance before injecting a second copy.
  try {
    const pong: { status?: string; version?: number } = await chrome.tabs.sendMessage(
      tabId,
      { action: WEB_EDITOR_ACTIONS.PING },
      { frameId: 0 },
    );

    if (pong?.status === 'pong') {
      return;
    }
  } catch {
    // No existing instance, fall through to inject.
  }

  try {
    await chrome.scripting.executeScript({
      target: { tabId },
      files: [WEB_EDITOR_SCRIPT_PATH],
      world: 'ISOLATED',
    });
    console.log('[WebEditor] Script injected successfully');
  } catch (error) {
    console.warn('[WebEditor] Failed to inject editor script:', error);
  }
}

/**
 * Inject props agent into MAIN world for Phase 7 Props editing
 */
async function ensurePropsAgentInjected(tabId: number): Promise<void> {
  try {
    await chrome.scripting.executeScript({
      target: { tabId },
      files: [PROPS_AGENT_SCRIPT_PATH],
      world: 'MAIN',
    });
  } catch (error) {
    // Best-effort: some pages (chrome://, extensions, PDF) block injection
    console.warn('[WebEditor] Failed to inject props agent:', error);
  }
}

/**
 * Send cleanup event to props agent
 */
async function sendPropsAgentCleanup(tabId: number): Promise<void> {
  try {
    // Dispatch cleanup event in ISOLATED world
    // CustomEvent crosses worlds and is observed by MAIN agent
    await chrome.scripting.executeScript({
      target: { tabId },
      func: () => {
        try {
          window.dispatchEvent(new CustomEvent('web-editor-props:cleanup'));
        } catch {
          // ignore
        }
      },
      world: 'ISOLATED',
    });
  } catch (error) {
    // Best-effort cleanup; ignore failures if tab is gone or injection blocked
    console.warn('[WebEditor] Failed to send props agent cleanup:', error);
  }
}

// =============================================================================
// Phase 7.1.6: Early Injection for Props Agent
// =============================================================================

/**
 * Content script ID prefix for early injection (document_start).
 * Registered scripts persist across sessions and survive browser restarts.
 */
const PROPS_AGENT_EARLY_INJECTION_ID_PREFIX = 'mcp_we_props_early';

/**
 * Result of early injection registration
 */
interface EarlyInjectionResult {
  id: string;
  host: string;
  matches: string[];
  alreadyRegistered: boolean;
}

/**
 * Sanitize a string for use in content script ID
 * Only allows alphanumeric, underscore, and hyphen
 */
function sanitizeContentScriptId(input: string): string {
  const cleaned = String(input ?? '')
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, '_')
    .replace(/^_+|_+$/g, '');
  return cleaned.slice(0, 80) || 'site';
}

/**
 * Build match patterns from tab URL for early injection.
 * Returns patterns for the specific host only (not all URLs).
 */
function buildEarlyInjectionPatterns(tabUrl: string): { host: string; matches: string[] } {
  let url: URL;
  try {
    url = new URL(tabUrl);
  } catch {
    throw new Error('Invalid tab URL');
  }

  if (url.protocol !== 'http:' && url.protocol !== 'https:') {
    throw new Error(`Early injection only supports http/https pages (got ${url.protocol})`);
  }

  const host = url.hostname.trim();
  if (!host) {
    throw new Error('Unable to derive host from tab URL');
  }

  // Match all paths on this host for both http and https
  return { host, matches: [`*://${host}/*`] };
}

/**
 * Register props agent for early injection (document_start, MAIN world).
 * This allows capturing React DevTools hook before React initializes.
 *
 * The registration is per-host and persists across sessions.
 */
async function registerPropsAgentEarlyInjection(tabUrl: string): Promise<EarlyInjectionResult> {
  const { host, matches } = buildEarlyInjectionPatterns(tabUrl);
  const id = `${PROPS_AGENT_EARLY_INJECTION_ID_PREFIX}_${sanitizeContentScriptId(host)}`;

  // Check if already registered (idempotent)
  let alreadyRegistered = false;
  try {
    const existing = await chrome.scripting.getRegisteredContentScripts({ ids: [id] });
    alreadyRegistered = existing.some((s) => s.id === id);
  } catch {
    // API might not support getRegisteredContentScripts in all contexts
    alreadyRegistered = false;
  }

  if (!alreadyRegistered) {
    await chrome.scripting.registerContentScripts([
      {
        id,
        js: [PROPS_AGENT_SCRIPT_PATH],
        matches,
        runAt: 'document_start',
        world: 'MAIN',
        allFrames: false,
        persistAcrossSessions: true,
      },
    ]);
    console.log(`[WebEditor] Registered early injection for ${host}`);
  }

  return { id, host, matches, alreadyRegistered };
}

async function toggleEditorInTab(tabId: number): Promise<{ active?: boolean }> {
  await ensureEditorInjected(tabId);

  try {
    const resp: { active?: boolean } = await chrome.tabs.sendMessage(
      tabId,
      { action: WEB_EDITOR_ACTIONS.TOGGLE },
      { frameId: 0 },
    );
    const active = typeof resp?.active === 'boolean' ? resp.active : undefined;

    // Phase 7: Inject props agent on start; cleanup on stop
    if (active === true) {
      await ensurePropsAgentInjected(tabId);
    } else if (active === false) {
      await sendPropsAgentCleanup(tabId);
    }

    return { active };
  } catch (error) {
    console.warn('[WebEditor] Failed to toggle editor in tab:', error);
    return {};
  }
}

async function getActiveTabId(): Promise<number | null> {
  try {
    const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const tabId = tabs?.[0]?.id;
    return typeof tabId === 'number' ? tabId : null;
  } catch {
    return null;
  }
}

export function initWebEditorListeners(): void {
  ensureContextMenu().catch(() => {});

  // Clean up session storage when tab is closed to avoid stale data
  chrome.tabs.onRemoved.addListener((tabId) => {
    try {
      const keys = [
        `${WEB_EDITOR_TX_CHANGED_SESSION_KEY_PREFIX}${tabId}`,
        `${WEB_EDITOR_SELECTION_SESSION_KEY_PREFIX}${tabId}`,
        `${WEB_EDITOR_EXCLUDED_KEYS_SESSION_KEY_PREFIX}${tabId}`,
      ];
      chrome.storage.session.remove(keys).catch(() => {});
    } catch {}
  });

  if ((chrome as any).contextMenus?.onClicked?.addListener) {
    chrome.contextMenus.onClicked.addListener(async (info, tab) => {
      try {
        if (info.menuItemId !== CONTEXT_MENU_ID) return;
        const tabId = tab?.id;
        if (typeof tabId !== 'number') return;
        await toggleEditorInTab(tabId);
      } catch {}
    });
  }

  chrome.commands.onCommand.addListener(async (command) => {
    try {
      if (command !== COMMAND_KEY) return;
      const tabId = await getActiveTabId();
      if (typeof tabId !== 'number') return;
      await toggleEditorInTab(tabId);
    } catch {}
  });

  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
    try {
      // Phase 7.1.6: Handle early injection registration request
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_PROPS_REGISTER_EARLY_INJECTION) {
        (async () => {
          const senderTab = (_sender as chrome.runtime.MessageSender)?.tab;
          const senderTabId = senderTab?.id;
          const senderTabUrl = senderTab?.url;

          if (typeof senderTabId !== 'number' || typeof senderTabUrl !== 'string') {
            return sendResponse({
              success: false,
              error: 'Sender tab information is required',
            });
          }

          try {
            const result = await registerPropsAgentEarlyInjection(senderTabUrl);

            // Respond first, then reload (to avoid message port closing during navigation)
            sendResponse({ success: true, ...result });

            // Small delay to ensure response is sent before navigation
            await new Promise((resolve) => setTimeout(resolve, 50));

            // Reload the tab so early injection takes effect
            try {
              await chrome.tabs.reload(senderTabId);
            } catch {
              // Best-effort: some tabs may block reload
            }
          } catch (err) {
            sendResponse({
              success: false,
              error: err instanceof Error ? err.message : String(err),
            });
          }
        })();
        return true; // Async response
      }

      // =====================================================================
      // WEB_EDITOR_OPEN_SOURCE: Open component source file in VSCode
      // =====================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_OPEN_SOURCE) {
        (async () => {
          try {
            const payload = message.payload as { debugSource?: unknown } | undefined;
            const debugSource = payload?.debugSource;

            if (!debugSource || typeof debugSource !== 'object') {
              return sendResponse({ success: false, error: 'debugSource is required' });
            }

            const rec = debugSource as Record<string, unknown>;
            const file = typeof rec.file === 'string' ? rec.file.trim() : '';
            if (!file) {
              return sendResponse({ success: false, error: 'debugSource.file is required' });
            }

            const stored = await chrome.storage.local.get(['agent-selected-project-id']);
            const projectId = stored['agent-selected-project-id'];

            if (!projectId || typeof projectId !== 'string') {
              return sendResponse({
                success: false,
                error: 'No project selected. Please select a project in AgentChat first.',
              });
            }

            // Prepare line/column
            const lineRaw = Number(rec.line);
            const columnRaw = Number(rec.column);
            const line = Number.isFinite(lineRaw) && lineRaw > 0 ? lineRaw : undefined;
            const column = Number.isFinite(columnRaw) && columnRaw > 0 ? columnRaw : undefined;

            // Call mcp-server to open file (server will validate project and path)
            const openResp = await requestAgentRpcFetch({
              operation: 'agent.projects.openFile',
              params: { projectId },
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                filePath: file,
                line,
                column,
              }),
            });

            // Try to parse JSON response for detailed error
            let result: { success: boolean; error?: string };
            try {
              result = (openResp.json || {}) as { success: boolean; error?: string };
            } catch {
              const text = openResp.body || '';
              result = {
                success: false,
                error: text || `HTTP ${openResp.statusCode}`,
              };
            }

            sendResponse(result);
          } catch (err) {
            sendResponse({
              success: false,
              error: err instanceof Error ? err.message : String(err),
            });
          }
        })();
        return true; // Async response
      }

      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_TOGGLE) {
        getActiveTabId()
          .then(async (tabId) => {
            if (typeof tabId !== 'number') return sendResponse({ success: false });
            const result = await toggleEditorInTab(tabId);
            sendResponse({ success: true, ...result });
          })
          .catch(() => sendResponse({ success: false }));
        return true;
      }

      // =======================================================================
      // Phase 1.5: Handle TX_CHANGED broadcast from web-editor
      // =======================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_TX_CHANGED) {
        (async () => {
          const senderTabId = (_sender as chrome.runtime.MessageSender)?.tab?.id;
          if (typeof senderTabId !== 'number') {
            sendResponse({ success: false, error: 'Sender tabId is required' });
            return;
          }

          const rawPayload = message.payload as WebEditorTxChangedPayload | undefined;
          if (!rawPayload || typeof rawPayload !== 'object') {
            sendResponse({ success: false, error: 'Invalid payload' });
            return;
          }

          // Hydrate payload with tabId from sender
          const payload: WebEditorTxChangedPayload = { ...rawPayload, tabId: senderTabId };
          const storageKey = `${WEB_EDITOR_TX_CHANGED_SESSION_KEY_PREFIX}${senderTabId}`;

          // Persist to session storage for cold-start recovery
          // Remove keys on clear to avoid stale data (rollback still has edits, so keep it)
          if (payload.action === 'clear') {
            // Clear TX state and excluded keys together
            const excludedKey = `${WEB_EDITOR_EXCLUDED_KEYS_SESSION_KEY_PREFIX}${senderTabId}`;
            await chrome.storage.session.remove([storageKey, excludedKey]);
          } else {
            await chrome.storage.session.set({ [storageKey]: payload });
          }

          // Broadcast to sidepanel (best-effort, ignore errors if sidepanel is closed)
          chrome.runtime
            .sendMessage({
              type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_TX_CHANGED,
              payload,
            })
            .catch(() => {
              // Ignore errors - sidepanel may be closed
            });

          sendResponse({ success: true });
        })().catch((error) => {
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          });
        });
        return true;
      }

      // =======================================================================
      // Selection sync: Handle SELECTION_CHANGED broadcast from web-editor
      // =======================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_SELECTION_CHANGED) {
        (async () => {
          const senderTabId = (_sender as chrome.runtime.MessageSender)?.tab?.id;
          if (typeof senderTabId !== 'number') {
            sendResponse({ success: false, error: 'Sender tabId is required' });
            return;
          }

          const rawPayload = message.payload as
            | import('@/common/web-editor-types').WebEditorSelectionChangedPayload
            | undefined;
          if (!rawPayload || typeof rawPayload !== 'object') {
            sendResponse({ success: false, error: 'Invalid payload' });
            return;
          }

          // Hydrate payload with tabId from sender
          const payload = { ...rawPayload, tabId: senderTabId };
          const storageKey = `${WEB_EDITOR_SELECTION_SESSION_KEY_PREFIX}${senderTabId}`;

          // Persist to session storage for cold-start recovery
          // Remove key on deselection to avoid stale data
          if (payload.selected === null) {
            await chrome.storage.session.remove(storageKey);
          } else {
            await chrome.storage.session.set({ [storageKey]: payload });
          }

          // Broadcast to sidepanel (best-effort, ignore errors if sidepanel is closed)
          chrome.runtime
            .sendMessage({
              type: BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_SELECTION_CHANGED,
              payload,
            })
            .catch(() => {
              // Ignore errors - sidepanel may be closed
            });

          sendResponse({ success: true });
        })().catch((error) => {
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          });
        });
        return true;
      }

      // =======================================================================
      // Clear selection: Handle CLEAR_SELECTION from sidepanel (after send)
      // =======================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_CLEAR_SELECTION) {
        (async () => {
          const payload = message.payload as { tabId?: number } | undefined;
          const targetTabId = payload?.tabId;

          if (typeof targetTabId !== 'number' || targetTabId <= 0) {
            sendResponse({ success: false, error: 'Invalid tabId' });
            return;
          }

          // Forward to content script (web-editor)
          try {
            await chrome.tabs.sendMessage(targetTabId, {
              action: WEB_EDITOR_ACTIONS.CLEAR_SELECTION,
            });
            sendResponse({ success: true });
          } catch (error) {
            // Tab may be closed or web-editor not active - this is expected
            sendResponse({
              success: false,
              error: error instanceof Error ? error.message : 'Failed to send to tab',
            });
          }
        })().catch((error) => {
          // Catch any unhandled errors in the async IIFE
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          });
        });
        return true;
      }

      // =======================================================================
      // Phase 1.5: Handle APPLY_BATCH from web-editor toolbar
      // =======================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_APPLY_BATCH) {
        const payload = normalizeApplyBatchPayload(message.payload);
        (async () => {
          const senderTabId = (_sender as chrome.runtime.MessageSender)?.tab?.id;
          const senderWindowId = (_sender as chrome.runtime.MessageSender)?.tab?.windowId;

          const stored = await chrome.storage.local.get([STORAGE_KEY_SELECTED_SESSION]);

          const sessionId = normalizeString(stored?.[STORAGE_KEY_SELECTED_SESSION]).trim();

          // Best-effort: open AgentChat sidepanel so user can see the session
          // Pass sessionId for deep linking directly to chat view
          if (typeof senderTabId === 'number') {
            openAgentChatSidepanel(senderTabId, senderWindowId, sessionId || undefined).catch(
              () => {},
            );
          }

          if (!sessionId) {
            // No session selected - sidepanel is already being opened (best-effort)
            // User needs to select or create a session manually
            sendResponse({
              success: false,
              error:
                'No Agent session selected. Please select or create a session in AgentChat, then try Apply again.',
            });
            return;
          }

          // Hydrate payload with tabId
          const hydratedPayload: WebEditorApplyBatchPayload =
            typeof senderTabId === 'number' ? { ...payload, tabId: senderTabId } : payload;

          // Read excluded keys from session storage (per-tab, managed by sidepanel)
          let sessionExcludedKeys: string[] = [];
          if (typeof senderTabId === 'number') {
            const excludedSessionKey = `${WEB_EDITOR_EXCLUDED_KEYS_SESSION_KEY_PREFIX}${senderTabId}`;
            try {
              if (chrome.storage?.session?.get) {
                const stored = (await chrome.storage.session.get(excludedSessionKey)) as Record<
                  string,
                  unknown
                >;
                const raw = stored?.[excludedSessionKey];
                sessionExcludedKeys = Array.isArray(raw)
                  ? raw.map((k) => normalizeString(k).trim()).filter(Boolean)
                  : [];
              }
            } catch {
              // Best-effort: ignore session storage failures
            }
          }

          // Filter out excluded elements (union: payload excludedKeys + session excludedKeys)
          const excluded = new Set([...hydratedPayload.excludedKeys, ...sessionExcludedKeys]);
          const elements = hydratedPayload.elements.filter((e) => !excluded.has(e.elementKey));
          if (elements.length === 0) {
            sendResponse({ success: false, error: 'No elements selected to apply.' });
            return;
          }

          // Build page URL from payload or sender tab
          const pageUrl =
            normalizeString(hydratedPayload.pageUrl).trim() ||
            normalizeString((_sender as chrome.runtime.MessageSender)?.tab?.url).trim() ||
            'unknown';

          // Build batch prompt and send to agent
          const instruction = buildAgentPromptBatch(elements, pageUrl);

          // Extract element labels for compact display
          const elementLabels = elements.slice(0, 5).map((e) => e.label);

          const resp = await requestAgentRpcFetch({
            operation: 'agent.chat.act',
            params: { sessionId },
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              instruction,
              // Pass dbSessionId so backend loads session-level configuration (engine, model, options)
              dbSessionId: sessionId,
              // Display text for UI (compact representation)
              displayText: `Apply ${elements.length} change${elements.length === 1 ? '' : 's'}`,
              // Client metadata for special message rendering
              clientMeta: {
                kind: 'web_editor_apply_batch',
                pageUrl,
                elementCount: elements.length,
                elementLabels,
              },
            }),
          });

          if (!resp.ok) {
            const text = resp.body || '';
            sendResponse({
              success: false,
              error: text || `HTTP ${resp.statusCode}`,
            });
            return;
          }

          const json: any = resp.json || {};
          const requestId = json?.requestId as string | undefined;

          if (requestId) {
            // Start SSE subscription for status updates (fire and forget)
            subscribeToSessionStatus(sessionId, requestId).catch(() => {});
          }

          sendResponse({ success: true, requestId, sessionId });
        })().catch((error) => {
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          });
        });
        return true;
      }

      // =======================================================================
      // Phase 1.8: Handle HIGHLIGHT_ELEMENT from sidepanel chips hover
      // =======================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_HIGHLIGHT_ELEMENT) {
        const payload = message.payload as WebEditorHighlightElementPayload | undefined;
        (async () => {
          // Validate payload
          const tabId = payload?.tabId;
          if (typeof tabId !== 'number' || !Number.isFinite(tabId) || tabId <= 0) {
            sendResponse({ success: false, error: 'Invalid tabId' });
            return;
          }

          const mode = payload?.mode;
          if (mode !== 'hover' && mode !== 'clear') {
            sendResponse({ success: false, error: 'Invalid mode' });
            return;
          }

          // Clear mode: forward directly without locator/selector validation
          // This prevents overlay residue when sidepanel unmounts
          if (mode === 'clear') {
            try {
              const response = await chrome.tabs.sendMessage(tabId, {
                action: WEB_EDITOR_ACTIONS.HIGHLIGHT_ELEMENT,
                mode: 'clear',
              });
              sendResponse({ success: true, response });
            } catch (error) {
              sendResponse({
                success: false,
                error: String(error instanceof Error ? error.message : error),
              });
            }
            return;
          }

          // Hover mode: validate and forward locator
          const locator = payload?.locator;
          if (!locator || typeof locator !== 'object') {
            sendResponse({ success: false, error: 'Invalid locator' });
            return;
          }

          // Extract best selector for fallback highlighting
          const selectors = Array.isArray(locator.selectors) ? locator.selectors : [];
          const primarySelector = selectors.find(
            (s): s is string => typeof s === 'string' && s.trim().length > 0,
          );

          if (!primarySelector) {
            sendResponse({ success: false, error: 'No valid selector in locator' });
            return;
          }

          // Forward to web-editor content script
          try {
            const response = await chrome.tabs.sendMessage(tabId, {
              action: WEB_EDITOR_ACTIONS.HIGHLIGHT_ELEMENT,
              locator, // Full locator for Shadow DOM/iframe support
              selector: primarySelector, // Backward compatibility fallback
              mode,
              elementKey: payload.elementKey,
            });

            sendResponse({ success: true, response });
          } catch (error) {
            // Content script might not be available
            sendResponse({
              success: false,
              error: String(error instanceof Error ? error.message : error),
            });
          }
        })().catch((error) => {
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          });
        });
        return true;
      }

      // =======================================================================
      // Phase 2: Handle REVERT_ELEMENT from sidepanel chips
      // =======================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_REVERT_ELEMENT) {
        const payload = message.payload as WebEditorRevertElementPayload | undefined;
        (async () => {
          // Validate payload
          const tabId = payload?.tabId;
          if (typeof tabId !== 'number' || !Number.isFinite(tabId) || tabId <= 0) {
            sendResponse({ success: false, error: 'Invalid tabId' });
            return;
          }

          const elementKey = payload?.elementKey;
          if (typeof elementKey !== 'string' || !elementKey.trim()) {
            sendResponse({ success: false, error: 'Invalid elementKey' });
            return;
          }

          // Forward to web-editor content script (frameId: 0 for main frame only)
          try {
            const response = await chrome.tabs.sendMessage(
              tabId,
              {
                action: WEB_EDITOR_ACTIONS.REVERT_ELEMENT,
                elementKey,
              },
              { frameId: 0 },
            );

            sendResponse({ success: true, ...response });
          } catch (error) {
            // Content script might not be available
            sendResponse({
              success: false,
              error: String(error instanceof Error ? error.message : error),
            });
          }
        })().catch((error) => {
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          });
        });
        return true;
      }

      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_APPLY) {
        const payload = normalizeApplyPayload(message.payload);
        (async () => {
          const senderTabId = (_sender as any)?.tab?.id;
          const senderWindowId = (_sender as any)?.tab?.windowId;

          const stored = await chrome.storage.local.get([STORAGE_KEY_SELECTED_SESSION]);
          const sessionId = normalizeString(stored?.[STORAGE_KEY_SELECTED_SESSION]).trim();

          if (typeof senderTabId === 'number') {
            openAgentChatSidepanel(senderTabId, senderWindowId, sessionId || undefined).catch(
              () => {},
            );
          }

          if (!sessionId) {
            return sendResponse({
              success: false,
              error:
                'No Agent session selected. Please select or create a session in AgentChat, then try Apply again.',
            });
          }

          const instruction = buildAgentPrompt(payload);

          const resp = await requestAgentRpcFetch({
            operation: 'agent.chat.act',
            params: { sessionId },
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              instruction,
              dbSessionId: sessionId,
            }),
          });

          if (!resp.ok) {
            const text = resp.body || '';
            return sendResponse({
              success: false,
              error: text || `HTTP ${resp.statusCode}`,
            });
          }

          const json: any = resp.json || {};
          const requestId = json?.requestId as string | undefined;

          if (requestId) {
            // Start SSE subscription for status updates (fire and forget)
            subscribeToSessionStatus(sessionId, requestId).catch(() => {});
          }

          return sendResponse({ success: true, requestId, sessionId });
        })().catch((error) => {
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          });
        });
        return true;
      }
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_STATUS_QUERY) {
        const { requestId } = message;
        if (!requestId || typeof requestId !== 'string') {
          sendResponse({ success: false, error: 'requestId is required' });
          return false;
        }

        const entry = getExecutionStatus(requestId);
        if (!entry) {
          // No status yet - likely still pending or not tracked
          sendResponse({ success: true, status: 'pending', message: 'Waiting for status...' });
        } else {
          sendResponse({
            success: true,
            status: entry.status,
            message: entry.message,
            result: entry.result,
          });
        }
        return false; // Synchronous response
      }

      // =======================================================================
      // Cancel Execution: Handle WEB_EDITOR_CANCEL_EXECUTION from toolbar/sidepanel
      // =======================================================================
      if (message?.type === BACKGROUND_MESSAGE_TYPES.WEB_EDITOR_CANCEL_EXECUTION) {
        const payload = message.payload as WebEditorCancelExecutionPayload | undefined;
        (async () => {
          // Validate payload
          const sessionId = payload?.sessionId?.trim();
          const requestId = payload?.requestId?.trim();

          if (!sessionId) {
            sendResponse({
              success: false,
              error: 'sessionId is required',
            } as WebEditorCancelExecutionResponse);
            return;
          }
          if (!requestId) {
            sendResponse({
              success: false,
              error: 'requestId is required',
            } as WebEditorCancelExecutionResponse);
            return;
          }

          try {
            // Call cancel API
            const response = await requestAgentRpcFetch({
              operation: 'agent.chat.cancelRequest',
              params: {
                sessionId,
                requestId,
              },
            });

            if (!response.ok) {
              const errorText = response.body || `HTTP ${response.statusCode}`;
              sendResponse({
                success: false,
                error: errorText,
              } as WebEditorCancelExecutionResponse);
              return;
            }

            // Update local execution status cache
            setExecutionStatus(requestId, 'cancelled', 'Execution cancelled by user');

            // Abort SSE connection for this session
            const sseConnection = sseConnections.get(sessionId);
            if (sseConnection && sseConnection.lastRequestId === requestId) {
              void closeSessionStatusSubscription(sessionId, requestId);
            }

            sendResponse({ success: true } as WebEditorCancelExecutionResponse);
          } catch (error) {
            sendResponse({
              success: false,
              error: String(error instanceof Error ? error.message : error),
            } as WebEditorCancelExecutionResponse);
          }
        })().catch((error) => {
          sendResponse({
            success: false,
            error: String(error instanceof Error ? error.message : error),
          } as WebEditorCancelExecutionResponse);
        });
        return true; // Will respond asynchronously
      }
    } catch (error) {
      sendResponse({
        success: false,
        error: String(error instanceof Error ? error.message : error),
      });
    }
    return false;
  });
}
