import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'webpage-mcp-shared';
import { ERROR_MESSAGES, TIMEOUTS } from '@/common/constants';
import { TOOL_MESSAGE_TYPES } from '@/common/message-types';
import { clickTool, fillTool } from './interaction';
import { keyboardTool } from './keyboard';
import { screenshotTool } from './screenshot';
import {
  screenshotContextManager,
  scaleCoordinates,
} from '@/utils/screenshot-context';
import { cdpSessionManager } from '@/utils/cdp-session-manager';
import {
  captureFrameOnAction,
  isAutoCaptureActive,
  type ActionMetadata,
  type ActionType,
} from './gif-recorder';
import {
  getResolvedViewportCoordinates,
  isCompositeSelector,
  resolveFrameIdForMessageResult,
} from './target-resolution';

type MouseButton = 'left' | 'right' | 'middle';

interface Coordinates {
  x: number;
  y: number;
}

interface ZoomRegion {
  x0: number;
  y0: number;
  x1: number;
  y1: number;
}

interface Modifiers {
  altKey?: boolean;
  ctrlKey?: boolean;
  metaKey?: boolean;
  shiftKey?: boolean;
}

interface ComputerParams {
  action:
    | 'left_click'
    | 'right_click'
    | 'double_click'
    | 'triple_click'
    | 'left_click_drag'
    | 'scroll'
    | 'type'
    | 'key'
    | 'hover'
    | 'wait'
    | 'fill'
    | 'fill_form'
    | 'resize_page'
    | 'scroll_to'
    | 'zoom'
    | 'screenshot';
  // click/scroll coordinates in screenshot space (if screenshot context exists) or viewport space
  coordinates?: Coordinates; // for click/scroll; for drag, this is endCoordinates
  startCoordinates?: Coordinates; // for drag start
  // Optional element refs (from chrome_read_page) as alternative to coordinates
  ref?: string; // click target or drag end
  startRef?: string; // drag start
  scrollDirection?: 'up' | 'down' | 'left' | 'right';
  scrollAmount?: number;
  text?: string; // for type/key
  repeat?: number; // for key action (1-100)
  modifiers?: Modifiers; // for click actions
  region?: ZoomRegion; // for zoom action
  duration?: number; // seconds for wait
  // For fill
  selector?: string;
  selectorType?: 'css' | 'xpath'; // Type of selector (default: 'css')
  value?: string;
  frameId?: number; // Target frame for selector/ref resolution
  tabId?: number; // target existing tab id
  windowId?: number;
  background?: boolean; // avoid focusing/activating
}

function hasDisallowedPublicPageScheme(url: string): boolean {
  const match = url.trim().match(/^([a-zA-Z][a-zA-Z\d+.-]*):/);
  if (!match) {
    return false;
  }

  const protocol = match[1]?.toLowerCase();
  return protocol !== 'http' && protocol !== 'https';
}

function requiresPublicPageForAction(params: ComputerParams): boolean {
  if (params.action === 'resize_page') {
    return false;
  }

  if (params.action === 'wait') {
    return typeof params.text === 'string' && params.text.trim().length > 0;
  }

  return true;
}

function getNonPublicPageError(action: string): string {
  if (action === 'zoom' || action === 'screenshot') {
    return `Only http:// and https:// pages are supported by chrome_computer ${action}`;
  }

  return 'Only http:// and https:// pages are supported by chrome_computer';
}

// Minimal CDP helper encapsulated here to avoid scattering CDP code
class CDPHelper {
  static async attach(tabId: number): Promise<void> {
    await cdpSessionManager.attach(tabId, 'computer');
  }

  static async detach(tabId: number): Promise<void> {
    await cdpSessionManager.detach(tabId, 'computer');
  }

  static async send(
    tabId: number,
    method: string,
    params?: object,
  ): Promise<any> {
    return await cdpSessionManager.sendCommand(tabId, method, params);
  }

  static async dispatchMouseEvent(tabId: number, opts: any) {
    const params: any = {
      type: opts.type,
      x: Math.round(opts.x),
      y: Math.round(opts.y),
      modifiers: opts.modifiers || 0,
    };
    if (
      opts.type === 'mousePressed' ||
      opts.type === 'mouseReleased' ||
      opts.type === 'mouseMoved'
    ) {
      params.button = opts.button || 'none';
      if (opts.type === 'mousePressed' || opts.type === 'mouseReleased') {
        params.clickCount = opts.clickCount || 1;
      }
      // Per CDP: buttons is ignored for mouseWheel
      params.buttons = opts.buttons !== undefined ? opts.buttons : 0;
    }
    if (opts.type === 'mouseWheel') {
      params.deltaX = opts.deltaX || 0;
      params.deltaY = opts.deltaY || 0;
    }
    await this.send(tabId, 'Input.dispatchMouseEvent', params);
  }

  static async insertText(tabId: number, text: string) {
    await this.send(tabId, 'Input.insertText', { text });
  }

  static modifierMask(mods: string[]): number {
    const map: Record<string, number> = {
      alt: 1,
      ctrl: 2,
      control: 2,
      meta: 4,
      cmd: 4,
      command: 4,
      win: 4,
      windows: 4,
      shift: 8,
    };
    let mask = 0;
    for (const m of mods) mask |= map[m] || 0;
    return mask;
  }

  // Enhanced key mapping for common non-character keys
  private static KEY_ALIASES: Record<
    string,
    { key: string; code?: string; text?: string }
  > = {
    enter: { key: 'Enter', code: 'Enter' },
    return: { key: 'Enter', code: 'Enter' },
    backspace: { key: 'Backspace', code: 'Backspace' },
    delete: { key: 'Delete', code: 'Delete' },
    tab: { key: 'Tab', code: 'Tab' },
    escape: { key: 'Escape', code: 'Escape' },
    esc: { key: 'Escape', code: 'Escape' },
    space: { key: ' ', code: 'Space', text: ' ' },
    pageup: { key: 'PageUp', code: 'PageUp' },
    pagedown: { key: 'PageDown', code: 'PageDown' },
    home: { key: 'Home', code: 'Home' },
    end: { key: 'End', code: 'End' },
    arrowup: { key: 'ArrowUp', code: 'ArrowUp' },
    arrowdown: { key: 'ArrowDown', code: 'ArrowDown' },
    arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft' },
    arrowright: { key: 'ArrowRight', code: 'ArrowRight' },
  };

  private static resolveKeyDef(token: string): {
    key: string;
    code?: string;
    text?: string;
  } {
    const t = (token || '').toLowerCase();
    if (this.KEY_ALIASES[t]) return this.KEY_ALIASES[t];
    if (/^f([1-9]|1[0-2])$/.test(t)) {
      return { key: t.toUpperCase(), code: t.toUpperCase() };
    }
    if (t.length === 1) {
      const upper = t.toUpperCase();
      return { key: upper, code: `Key${upper}`, text: t };
    }
    return { key: token };
  }

  static async dispatchSimpleKey(tabId: number, token: string) {
    const def = this.resolveKeyDef(token);
    if (def.text && def.text.length === 1) {
      await this.insertText(tabId, def.text);
      return;
    }
    await this.send(tabId, 'Input.dispatchKeyEvent', {
      type: 'rawKeyDown',
      key: def.key,
      code: def.code,
    });
    await this.send(tabId, 'Input.dispatchKeyEvent', {
      type: 'keyUp',
      key: def.key,
      code: def.code,
    });
  }

  static async dispatchKeyChord(tabId: number, chord: string) {
    const parts = chord.split('+');
    const modifiers: string[] = [];
    let keyToken = '';
    for (const pRaw of parts) {
      const p = pRaw.trim().toLowerCase();
      if (
        [
          'ctrl',
          'control',
          'alt',
          'shift',
          'cmd',
          'meta',
          'command',
          'win',
          'windows',
        ].includes(p)
      )
        modifiers.push(p);
      else keyToken = pRaw.trim();
    }
    const mask = this.modifierMask(modifiers);
    const def = this.resolveKeyDef(keyToken);
    await this.send(tabId, 'Input.dispatchKeyEvent', {
      type: 'rawKeyDown',
      key: def.key,
      code: def.code,
      text: def.text,
      modifiers: mask,
    });
    await this.send(tabId, 'Input.dispatchKeyEvent', {
      type: 'keyUp',
      key: def.key,
      code: def.code,
      modifiers: mask,
    });
  }
}

class ComputerTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.COMPUTER;

  async execute(args: ComputerParams): Promise<ToolResult> {
    const params = args || ({} as ComputerParams);
    if (!params.action)
      return createErrorResponse('Action parameter is required');

    try {
      const explicit = await this.tryGetTab(args.tabId);
      const tab =
        explicit || (await this.getActiveTabOrThrowInWindow(args.windowId));
      if (!tab.id)
        return createErrorResponse(
          ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID',
        );
      if (
        requiresPublicPageForAction(params) &&
        hasDisallowedPublicPageScheme(String(tab.url || ''))
      ) {
        return createErrorResponse(getNonPublicPageError(params.action));
      }

      // Execute the action and capture frame on success
      const result = await this.executeAction(params, tab);

      // Trigger auto-capture on successful actions (except screenshot which is read-only)
      if (
        !result.isError &&
        params.action !== 'screenshot' &&
        params.action !== 'wait'
      ) {
        const actionType = this.mapActionToCapture(params.action);
        if (actionType) {
          // Convert to viewport-space coordinates for GIF overlays
          // params.coordinates may be screenshot-space when screenshot context exists
          const ctx = screenshotContextManager.getContext(tab.id);
          const toViewport = (
            c?: Coordinates,
          ): { x: number; y: number } | undefined => {
            if (!c) return undefined;
            if (!ctx) return { x: c.x, y: c.y };
            const scaled = scaleCoordinates(c.x, c.y, ctx);
            return { x: scaled.x, y: scaled.y };
          };

          const endCoords = toViewport(params.coordinates);
          const startCoords = toViewport(params.startCoordinates);

          await this.triggerAutoCapture(tab.id, actionType, {
            coordinateSpace: 'viewport',
            coordinates: endCoords,
            startCoordinates: startCoords,
            endCoordinates: actionType === 'drag' ? endCoords : undefined,
            text: params.text,
            ref: params.ref,
          });
        }
      }

      return result;
    } catch (error) {
      console.error('Error in computer tool:', error);
      return createErrorResponse(
        `Failed to execute action: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  private mapActionToCapture(action: string): ActionType | null {
    const mapping: Record<string, ActionType> = {
      left_click: 'click',
      right_click: 'right_click',
      double_click: 'double_click',
      triple_click: 'triple_click',
      left_click_drag: 'drag',
      scroll: 'scroll',
      type: 'type',
      key: 'key',
      hover: 'hover',
      fill: 'fill',
      fill_form: 'fill',
      resize_page: 'other',
      scroll_to: 'scroll',
      zoom: 'other',
    };
    return mapping[action] || null;
  }

  private async executeAction(
    params: ComputerParams,
    tab: chrome.tabs.Tab,
  ): Promise<ToolResult> {
    if (!tab.id) {
      return createErrorResponse(
        ERROR_MESSAGES.TAB_NOT_FOUND + ': Active tab has no ID',
      );
    }

    // Helper to project coordinates using screenshot context when available
    const project = (c?: Coordinates): Coordinates | undefined => {
      if (!c) return undefined;
      const ctx = screenshotContextManager.getContext(tab.id!);
      if (!ctx) return c;
      const scaled = scaleCoordinates(c.x, c.y, ctx);
      return { x: scaled.x, y: scaled.y };
    };

    const injectAccessibilityHelper = async (
      frameId?: number,
    ): Promise<void> => {
      await this.injectContentScript(
        tab.id!,
        ['inject-scripts/accessibility-tree-helper.js'],
        false,
        'ISOLATED',
        false,
        typeof frameId === 'number' ? [frameId] : undefined,
      );
    };

    const resolveRefPoint = async (
      ref: string,
      frameId?: number,
    ): Promise<Coordinates | undefined> => {
      await injectAccessibilityHelper(frameId);
      const resolved = await this.sendMessageToTab(
        tab.id!,
        {
          action: TOOL_MESSAGE_TYPES.RESOLVE_REF,
          ref,
        },
        frameId,
      );
      const point = getResolvedViewportCoordinates(resolved, frameId);
      return point ? project(point) : undefined;
    };

    const resolveSelectorPoint = async (
      selector: string,
      selectorType: 'css' | 'xpath',
      requestedFrameId?: number,
    ): Promise<
      | {
          point: Coordinates;
          frameId?: number;
          ref?: string;
        }
      | undefined
    > => {
      await injectAccessibilityHelper(requestedFrameId);
      const selectorFrameId = isCompositeSelector(selector)
        ? undefined
        : requestedFrameId;
      const ensured = await this.sendMessageToTab(
        tab.id!,
        {
          action: TOOL_MESSAGE_TYPES.ENSURE_REF_FOR_SELECTOR,
          selector,
          isXPath: selectorType === 'xpath',
        },
        selectorFrameId,
      );
      if (!ensured || !ensured.success) {
        return undefined;
      }
      const resolvedFrameId = await resolveFrameIdForMessageResult(
        tab.id!,
        requestedFrameId,
        ensured,
      );
      await injectAccessibilityHelper(resolvedFrameId);
      const resolvedRef =
        typeof ensured.ref === 'string' ? ensured.ref : undefined;
      if (resolvedRef) {
        try {
          await this.sendMessageToTab(
            tab.id!,
            { action: 'focusByRef', ref: resolvedRef },
            resolvedFrameId,
          );
        } catch {
          // Best effort - continue even if scroll fails
        }
        const point = await resolveRefPoint(resolvedRef, resolvedFrameId);
        if (point) {
          return { point, frameId: resolvedFrameId, ref: resolvedRef };
        }
      }
      const fallbackPoint = getResolvedViewportCoordinates(
        ensured,
        resolvedFrameId,
      );
      if (!fallbackPoint) {
        return undefined;
      }
      return {
        point: project(fallbackPoint)!,
        frameId: resolvedFrameId,
        ref: resolvedRef,
      };
    };

    switch (params.action) {
      case 'resize_page': {
        const width = Number(
          (params as any).coordinates?.x || (params as any).text,
        );
        const height = Number(
          (params as any).coordinates?.y || (params as any).value,
        );
        const w = Number((params as any).width ?? width);
        const h = Number((params as any).height ?? height);
        if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
          return createErrorResponse(
            'Provide width and height for resize_page (positive numbers)',
          );
        }
        try {
          // Prefer precise CDP emulation
          await CDPHelper.attach(tab.id);
          try {
            await CDPHelper.send(tab.id, 'Emulation.setDeviceMetricsOverride', {
              width: Math.round(w),
              height: Math.round(h),
              deviceScaleFactor: 0,
              mobile: false,
              screenWidth: Math.round(w),
              screenHeight: Math.round(h),
            });
          } finally {
            await CDPHelper.detach(tab.id);
          }
        } catch (e) {
          // Fallback: window resize
          if (tab.windowId !== undefined) {
            await chrome.windows.update(tab.windowId, {
              width: Math.round(w),
              height: Math.round(h),
            });
          } else {
            return createErrorResponse(
              `Failed to resize via CDP and cannot determine windowId: ${e instanceof Error ? e.message : String(e)}`,
            );
          }
        }
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                success: true,
                action: 'resize_page',
                width: w,
                height: h,
              }),
            },
          ],
          isError: false,
        };
      }
      case 'hover': {
        // Resolve target point from ref | selector | coordinates
        let coord: Coordinates | undefined = undefined;
        let resolvedBy: 'ref' | 'selector' | 'coordinates' | undefined;

        try {
          if (params.ref) {
            try {
              await injectAccessibilityHelper(params.frameId);
              await this.sendMessageToTab(
                tab.id,
                { action: 'focusByRef', ref: params.ref },
                params.frameId,
              );
            } catch {
              // Best effort - continue even if scroll fails
            }
            coord = await resolveRefPoint(params.ref, params.frameId);
            if (coord) {
              resolvedBy = 'ref';
            }
          } else if (params.selector) {
            const selectorResolution = await resolveSelectorPoint(
              params.selector,
              params.selectorType || 'css',
              params.frameId,
            );
            if (selectorResolution) {
              coord = selectorResolution.point;
              resolvedBy = 'selector';
            }
          } else if (params.coordinates) {
            coord = project(params.coordinates);
            resolvedBy = 'coordinates';
          }
        } catch (e) {
          // fall through to error handling below
        }

        if (!coord)
          return createErrorResponse(
            'Provide ref or selector or coordinates for hover, or failed to resolve target',
          );
        {
          const stale = ((): any => {
            if (!params.coordinates) return null;
            const getHostname = (url: string): string => {
              try {
                return new URL(url).hostname;
              } catch {
                return '';
              }
            };
            const currentHostname = getHostname(tab.url || '');
            const ctx = screenshotContextManager.getContext(tab.id!);
            const contextHostname = (ctx as any)?.hostname as
              | string
              | undefined;
            if (contextHostname && contextHostname !== currentHostname) {
              return createErrorResponse(
                `Security check failed: Domain changed since last screenshot (from ${contextHostname} to ${currentHostname}) during hover. Capture a new screenshot or use ref/selector.`,
              );
            }
            return null;
          })();
          if (stale) return stale;
        }

        try {
          await CDPHelper.attach(tab.id);
          try {
            // Move pointer to target. We can dispatch a single mouseMoved; browsers will generate mouseover/mouseenter as needed.
            await CDPHelper.dispatchMouseEvent(tab.id, {
              type: 'mouseMoved',
              x: coord.x,
              y: coord.y,
              button: 'none',
              buttons: 0,
            });
          } finally {
            await CDPHelper.detach(tab.id);
          }

          // Optional hold to allow UI (menus/tooltips) to appear
          const holdMs = Math.max(
            0,
            Math.min(params.duration ? params.duration * 1000 : 400, 5000),
          );
          if (holdMs > 0) await new Promise((r) => setTimeout(r, holdMs));

          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'hover',
                  coordinates: coord,
                  resolvedBy,
                  transport: 'cdp',
                }),
              },
            ],
            isError: false,
          };
        } catch (error) {
          console.warn(
            '[ComputerTool] CDP hover failed, attempting DOM fallback',
            error,
          );
          return await this.domHoverFallback(
            tab.id,
            coord,
            resolvedBy,
            params.ref,
          );
        }
      }
      case 'left_click':
      case 'right_click': {
        // Calculate CDP modifier mask for click events
        const modifiersMask = CDPHelper.modifierMask(
          [
            params.modifiers?.altKey ? 'alt' : undefined,
            params.modifiers?.ctrlKey ? 'ctrl' : undefined,
            params.modifiers?.metaKey ? 'meta' : undefined,
            params.modifiers?.shiftKey ? 'shift' : undefined,
          ].filter((v): v is string => typeof v === 'string'),
        );

        if (params.ref) {
          // Prefer DOM click via ref
          const domResult = await clickTool.execute({
            ref: params.ref,
            frameId: params.frameId,
            waitForNavigation: false,
            timeout: TIMEOUTS.DEFAULT_WAIT * 5,
            button: params.action === 'right_click' ? 'right' : 'left',
            modifiers: params.modifiers,
            tabId: tab.id,
            windowId: tab.windowId,
          });
          return domResult;
        }
        if (params.selector) {
          // Support selector-based click
          const domResult = await clickTool.execute({
            selector: params.selector,
            selectorType: params.selectorType,
            frameId: params.frameId,
            waitForNavigation: false,
            timeout: TIMEOUTS.DEFAULT_WAIT * 5,
            button: params.action === 'right_click' ? 'right' : 'left',
            modifiers: params.modifiers,
            tabId: tab.id,
            windowId: tab.windowId,
          });
          return domResult;
        }
        if (!params.coordinates)
          return createErrorResponse(
            'Provide ref, selector, or coordinates for click action',
          );
        {
          const stale = ((): any => {
            const getHostname = (url: string): string => {
              try {
                return new URL(url).hostname;
              } catch {
                return '';
              }
            };
            const currentHostname = getHostname(tab.url || '');
            const ctx = screenshotContextManager.getContext(tab.id!);
            const contextHostname = (ctx as any)?.hostname as
              | string
              | undefined;
            if (contextHostname && contextHostname !== currentHostname) {
              return createErrorResponse(
                `Security check failed: Domain changed since last screenshot (from ${contextHostname} to ${currentHostname}) during ${params.action}. Capture a new screenshot or use ref/selector.`,
              );
            }
            return null;
          })();
          if (stale) return stale;
        }
        const coord = project(params.coordinates)!;
        // Prefer DOM path via existing click tool
        const domResult = await clickTool.execute({
          coordinates: coord,
          waitForNavigation: false,
          timeout: TIMEOUTS.DEFAULT_WAIT * 5,
          button: params.action === 'right_click' ? 'right' : 'left',
          modifiers: params.modifiers,
          tabId: tab.id,
          windowId: tab.windowId,
        });
        if (!domResult.isError) {
          return domResult; // Standardized response from click tool
        }
        // Fallback to CDP if DOM failed
        try {
          await CDPHelper.attach(tab.id);
          const button: MouseButton =
            params.action === 'right_click' ? 'right' : 'left';
          const clickCount = 1;
          await CDPHelper.dispatchMouseEvent(tab.id, {
            type: 'mouseMoved',
            x: coord.x,
            y: coord.y,
            button: 'none',
            buttons: 0,
            modifiers: modifiersMask,
          });
          for (let i = 1; i <= clickCount; i++) {
            await CDPHelper.dispatchMouseEvent(tab.id, {
              type: 'mousePressed',
              x: coord.x,
              y: coord.y,
              button,
              buttons: button === 'left' ? 1 : 2,
              clickCount: i,
              modifiers: modifiersMask,
            });
            await CDPHelper.dispatchMouseEvent(tab.id, {
              type: 'mouseReleased',
              x: coord.x,
              y: coord.y,
              button,
              buttons: 0,
              clickCount: i,
              modifiers: modifiersMask,
            });
          }
          await CDPHelper.detach(tab.id);
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: params.action,
                  coordinates: coord,
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          await CDPHelper.detach(tab.id);
          return createErrorResponse(
            `CDP click failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
      }
      case 'double_click':
      case 'triple_click': {
        // Calculate CDP modifier mask for click events
        const modifiersMask = CDPHelper.modifierMask(
          [
            params.modifiers?.altKey ? 'alt' : undefined,
            params.modifiers?.ctrlKey ? 'ctrl' : undefined,
            params.modifiers?.metaKey ? 'meta' : undefined,
            params.modifiers?.shiftKey ? 'shift' : undefined,
          ].filter((v): v is string => typeof v === 'string'),
        );

        if (!params.coordinates && !params.ref && !params.selector)
          return createErrorResponse(
            'Provide ref, selector, or coordinates for double/triple click',
          );
        let coord = params.coordinates
          ? project(params.coordinates)!
          : (undefined as any);
        // If ref is provided, resolve center via accessibility helper
        if (params.ref) {
          try {
            coord =
              (await resolveRefPoint(params.ref, params.frameId)) || coord;
          } catch (e) {
            // ignore and use provided coordinates
          }
        } else if (params.selector) {
          // Support selector-based click
          try {
            const selectorResolution = await resolveSelectorPoint(
              params.selector,
              params.selectorType || 'css',
              params.frameId,
            );
            if (selectorResolution) {
              coord = selectorResolution.point;
            }
          } catch (e) {
            // ignore
          }
        }
        if (!coord)
          return createErrorResponse(
            'Failed to resolve coordinates from ref/selector',
          );
        {
          const stale = ((): any => {
            if (!params.coordinates) return null;
            const getHostname = (url: string): string => {
              try {
                return new URL(url).hostname;
              } catch {
                return '';
              }
            };
            const currentHostname = getHostname(tab.url || '');
            const ctx = screenshotContextManager.getContext(tab.id!);
            const contextHostname = (ctx as any)?.hostname as
              | string
              | undefined;
            if (contextHostname && contextHostname !== currentHostname) {
              return createErrorResponse(
                `Security check failed: Domain changed since last screenshot (from ${contextHostname} to ${currentHostname}) during ${params.action}. Capture a new screenshot or use ref/selector.`,
              );
            }
            return null;
          })();
          if (stale) return stale;
        }
        try {
          await CDPHelper.attach(tab.id);
          const button: MouseButton = 'left';
          const clickCount = params.action === 'double_click' ? 2 : 3;
          await CDPHelper.dispatchMouseEvent(tab.id, {
            type: 'mouseMoved',
            x: coord.x,
            y: coord.y,
            button: 'none',
            buttons: 0,
            modifiers: modifiersMask,
          });
          for (let i = 1; i <= clickCount; i++) {
            await CDPHelper.dispatchMouseEvent(tab.id, {
              type: 'mousePressed',
              x: coord.x,
              y: coord.y,
              button,
              buttons: 1,
              clickCount: i,
              modifiers: modifiersMask,
            });
            await CDPHelper.dispatchMouseEvent(tab.id, {
              type: 'mouseReleased',
              x: coord.x,
              y: coord.y,
              button,
              buttons: 0,
              clickCount: i,
              modifiers: modifiersMask,
            });
          }
          await CDPHelper.detach(tab.id);
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: params.action,
                  coordinates: coord,
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          await CDPHelper.detach(tab.id);
          return createErrorResponse(
            `CDP ${params.action} failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
      }
      case 'left_click_drag': {
        if (!params.startCoordinates && !params.startRef)
          return createErrorResponse(
            'Provide startRef or startCoordinates for drag',
          );
        if (!params.coordinates && !params.ref)
          return createErrorResponse('Provide ref or end coordinates for drag');
        let start = params.startCoordinates
          ? project(params.startCoordinates)!
          : (undefined as any);
        let end = params.coordinates
          ? project(params.coordinates)!
          : (undefined as any);
        {
          const stale = ((): any => {
            if (!params.startCoordinates && !params.coordinates) return null;
            const getHostname = (url: string): string => {
              try {
                return new URL(url).hostname;
              } catch {
                return '';
              }
            };
            const currentHostname = getHostname(tab.url || '');
            const ctx = screenshotContextManager.getContext(tab.id!);
            const contextHostname = (ctx as any)?.hostname as
              | string
              | undefined;
            if (contextHostname && contextHostname !== currentHostname) {
              return createErrorResponse(
                `Security check failed: Domain changed since last screenshot (from ${contextHostname} to ${currentHostname}) during left_click_drag. Capture a new screenshot or use ref/selector.`,
              );
            }
            return null;
          })();
          if (stale) return stale;
        }
        if (params.startRef || params.ref) {
          await injectAccessibilityHelper(params.frameId);
        }
        if (params.startRef) {
          try {
            start =
              (await resolveRefPoint(params.startRef, params.frameId)) || start;
          } catch {
            // ignore
          }
        }
        if (params.ref) {
          try {
            end = (await resolveRefPoint(params.ref, params.frameId)) || end;
          } catch {
            // ignore
          }
        }
        if (!start || !end)
          return createErrorResponse('Failed to resolve drag coordinates');
        try {
          await CDPHelper.attach(tab.id);
          await CDPHelper.dispatchMouseEvent(tab.id, {
            type: 'mouseMoved',
            x: start.x,
            y: start.y,
            button: 'none',
            buttons: 0,
          });
          await CDPHelper.dispatchMouseEvent(tab.id, {
            type: 'mousePressed',
            x: start.x,
            y: start.y,
            button: 'left',
            buttons: 1,
            clickCount: 1,
          });
          await CDPHelper.dispatchMouseEvent(tab.id, {
            type: 'mouseMoved',
            x: end.x,
            y: end.y,
            button: 'left',
            buttons: 1,
          });
          await CDPHelper.dispatchMouseEvent(tab.id, {
            type: 'mouseReleased',
            x: end.x,
            y: end.y,
            button: 'left',
            buttons: 0,
            clickCount: 1,
          });
          await CDPHelper.detach(tab.id);
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'left_click_drag',
                  start,
                  end,
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          await CDPHelper.detach(tab.id);
          return createErrorResponse(
            `Drag failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
      }
      case 'scroll': {
        if (!params.coordinates && !params.ref)
          return createErrorResponse('Provide ref or coordinates for scroll');
        let coord = params.coordinates
          ? project(params.coordinates)!
          : (undefined as any);
        if (params.ref) {
          try {
            coord =
              (await resolveRefPoint(params.ref, params.frameId)) || coord;
          } catch {
            // ignore
          }
        }
        if (!coord)
          return createErrorResponse('Failed to resolve scroll coordinates');
        {
          const stale = ((): any => {
            if (!params.coordinates) return null;
            const getHostname = (url: string): string => {
              try {
                return new URL(url).hostname;
              } catch {
                return '';
              }
            };
            const currentHostname = getHostname(tab.url || '');
            const ctx = screenshotContextManager.getContext(tab.id!);
            const contextHostname = (ctx as any)?.hostname as
              | string
              | undefined;
            if (contextHostname && contextHostname !== currentHostname) {
              return createErrorResponse(
                `Security check failed: Domain changed since last screenshot (from ${contextHostname} to ${currentHostname}) during scroll. Capture a new screenshot or use ref/selector.`,
              );
            }
            return null;
          })();
          if (stale) return stale;
        }
        const direction = params.scrollDirection || 'down';
        const amount = Math.max(1, Math.min(params.scrollAmount || 3, 10));
        // Convert to deltas (~100px per tick)
        const unit = 100;
        let deltaX = 0,
          deltaY = 0;
        if (direction === 'up') deltaY = -amount * unit;
        if (direction === 'down') deltaY = amount * unit;
        if (direction === 'left') deltaX = -amount * unit;
        if (direction === 'right') deltaX = amount * unit;
        try {
          await CDPHelper.attach(tab.id);
          await CDPHelper.dispatchMouseEvent(tab.id, {
            type: 'mouseWheel',
            x: coord.x,
            y: coord.y,
            deltaX,
            deltaY,
          });
          await CDPHelper.detach(tab.id);
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'scroll',
                  coordinates: coord,
                  deltaX,
                  deltaY,
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          await CDPHelper.detach(tab.id);
          return createErrorResponse(
            `Scroll failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
      }
      case 'type': {
        if (!params.text)
          return createErrorResponse(
            'Text parameter is required for type action',
          );
        try {
          // Optional focus via ref before typing
          if (params.ref) {
            await clickTool.execute({
              ref: params.ref,
              frameId: params.frameId,
              waitForNavigation: false,
              timeout: TIMEOUTS.DEFAULT_WAIT * 5,
              tabId: tab.id,
              windowId: tab.windowId,
            });
          }
          await CDPHelper.attach(tab.id);
          // Use CDP insertText to avoid complex KeyboardEvent emulation for long text
          await CDPHelper.insertText(tab.id, params.text);
          await CDPHelper.detach(tab.id);
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'type',
                  length: params.text.length,
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          await CDPHelper.detach(tab.id);
          // Fallback to DOM-based keyboard tool
          const res = await keyboardTool.execute({
            keys: params.text.split('').join(','),
            delay: 0,
            selector: undefined,
            tabId: tab.id,
            windowId: tab.windowId,
          });
          return res;
        }
      }
      case 'fill': {
        if (!params.ref && !params.selector) {
          return createErrorResponse(
            'Provide ref or selector and a value for fill',
          );
        }
        // Reuse existing fill tool to leverage robust DOM event behavior
        const res = await fillTool.execute({
          selector: params.selector as any,
          selectorType: params.selectorType as any,
          ref: params.ref as any,
          value: params.value as any,
          frameId: params.frameId,
          tabId: tab.id,
          windowId: tab.windowId,
        } as any);
        return res;
      }
      case 'fill_form': {
        const elements = (params as any).elements as Array<{
          ref: string;
          value: string | number | boolean;
        }>;
        if (!Array.isArray(elements) || elements.length === 0) {
          return createErrorResponse(
            'elements must be a non-empty array for fill_form',
          );
        }
        const results: Array<{ ref: string; ok: boolean; error?: string }> = [];
        for (const item of elements) {
          if (!item || !item.ref) {
            results.push({
              ref: String(item?.ref || ''),
              ok: false,
              error: 'missing ref',
            });
            continue;
          }
          try {
            const r = await fillTool.execute({
              ref: item.ref as any,
              value: item.value as any,
              frameId: params.frameId,
              tabId: tab.id,
              windowId: tab.windowId,
            } as any);
            const ok = !r.isError;
            results.push({
              ref: item.ref,
              ok,
              error: ok ? undefined : 'failed',
            });
          } catch (e) {
            results.push({
              ref: item.ref,
              ok: false,
              error: String(e instanceof Error ? e.message : e),
            });
          }
        }
        const successCount = results.filter((r) => r.ok).length;
        return {
          content: [
            {
              type: 'text',
              text: JSON.stringify({
                success: true,
                action: 'fill_form',
                filled: successCount,
                total: results.length,
                results,
              }),
            },
          ],
          isError: false,
        };
      }
      case 'key': {
        if (!params.text)
          return createErrorResponse(
            'text is required for key action (e.g., "Backspace Backspace Enter" or "cmd+a")',
          );
        const tokens = params.text.trim().split(/\s+/).filter(Boolean);
        const repeat = params.repeat ?? 1;
        if (!Number.isInteger(repeat) || repeat < 1 || repeat > 100) {
          return createErrorResponse(
            'repeat must be an integer between 1 and 100 for key action',
          );
        }
        try {
          // Optional focus via ref before key events
          if (params.ref) {
            await clickTool.execute({
              ref: params.ref,
              frameId: params.frameId,
              waitForNavigation: false,
              timeout: TIMEOUTS.DEFAULT_WAIT * 5,
              tabId: tab.id,
              windowId: tab.windowId,
            });
          }
          await CDPHelper.attach(tab.id);
          for (let i = 0; i < repeat; i++) {
            for (const t of tokens) {
              if (t.includes('+')) await CDPHelper.dispatchKeyChord(tab.id, t);
              else await CDPHelper.dispatchSimpleKey(tab.id, t);
            }
          }
          await CDPHelper.detach(tab.id);
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'key',
                  keys: tokens,
                  repeat,
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          await CDPHelper.detach(tab.id);
          // Fallback to DOM keyboard simulation (comma-separated combinations)
          const keysStr = tokens.join(',');
          const repeatedKeys =
            repeat === 1
              ? keysStr
              : Array.from({ length: repeat }, () => keysStr).join(',');
          const res = await keyboardTool.execute({
            keys: repeatedKeys,
            frameId: params.frameId,
            tabId: tab.id,
            windowId: tab.windowId,
          });
          return res;
        }
      }
      case 'wait': {
        const hasTextCondition =
          typeof (params as any).text === 'string' &&
          (params as any).text.trim().length > 0;
        if (hasTextCondition) {
          try {
            // Conditional wait for text appearance/disappearance using content script
            await this.injectContentScript(
              tab.id,
              ['inject-scripts/wait-helper.js'],
              false,
              'ISOLATED',
              true,
            );
            const appear = (params as any).appear !== false; // default to true
            const timeoutMs = Math.max(
              0,
              Math.min(((params as any).timeout as number) || 10000, 120000),
            );
            const resp = await this.sendMessageToTab(tab.id, {
              action: TOOL_MESSAGE_TYPES.WAIT_FOR_TEXT,
              text: (params as any).text,
              appear,
              timeout: timeoutMs,
            });
            if (!resp || resp.success !== true) {
              return createErrorResponse(
                resp && resp.reason === 'timeout'
                  ? `wait_for timed out after ${timeoutMs}ms for text: ${(params as any).text}`
                  : `wait_for failed: ${resp && resp.error ? resp.error : 'unknown error'}`,
              );
            }
            return {
              content: [
                {
                  type: 'text',
                  text: JSON.stringify({
                    success: true,
                    action: 'wait_for',
                    appear,
                    text: (params as any).text,
                    matched: resp.matched || null,
                    tookMs: resp.tookMs,
                  }),
                },
              ],
              isError: false,
            };
          } catch (e) {
            return createErrorResponse(
              `wait_for failed: ${e instanceof Error ? e.message : String(e)}`,
            );
          }
        } else {
          const seconds = Math.max(
            0,
            Math.min((params as any).duration || 0, 30),
          );
          if (!seconds)
            return createErrorResponse(
              'Duration parameter is required and must be > 0',
            );
          await new Promise((r) => setTimeout(r, seconds * 1000));
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'wait',
                  duration: seconds,
                }),
              },
            ],
            isError: false,
          };
        }
      }
      case 'scroll_to': {
        if (!params.ref) {
          return createErrorResponse('ref is required for scroll_to action');
        }
        try {
          await this.injectContentScript(tab.id, [
            'inject-scripts/accessibility-tree-helper.js',
          ]);
          const resp = await this.sendMessageToTab(tab.id, {
            action: 'focusByRef',
            ref: params.ref,
          });
          if (!resp || resp.success !== true) {
            return createErrorResponse(
              resp?.error || 'scroll_to failed: element not found',
            );
          }
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'scroll_to',
                  ref: params.ref,
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          return createErrorResponse(
            `scroll_to failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
      }
      case 'zoom': {
        const region = params.region;
        if (!region) {
          return createErrorResponse('region is required for zoom action');
        }
        const x0 = Number(region.x0);
        const y0 = Number(region.y0);
        const x1 = Number(region.x1);
        const y1 = Number(region.y1);
        if (![x0, y0, x1, y1].every(Number.isFinite)) {
          return createErrorResponse(
            'region must contain finite numbers (x0, y0, x1, y1)',
          );
        }
        if (x0 < 0 || y0 < 0 || x1 <= x0 || y1 <= y0) {
          return createErrorResponse(
            'Invalid region: require x0>=0, y0>=0 and x1>x0, y1>y0',
          );
        }

        // Project coordinates from screenshot space to viewport space
        const p0 = project({ x: x0, y: y0 })!;
        const p1 = project({ x: x1, y: y1 })!;
        const rx0 = Math.min(p0.x, p1.x);
        const ry0 = Math.min(p0.y, p1.y);
        const rx1 = Math.max(p0.x, p1.x);
        const ry1 = Math.max(p0.y, p1.y);
        const w = rx1 - rx0;
        const h = ry1 - ry0;
        if (w <= 0 || h <= 0) {
          return createErrorResponse('Invalid region after projection');
        }

        // Security check: verify domain hasn't changed since last screenshot
        {
          const getHostname = (url: string): string => {
            try {
              return new URL(url).hostname;
            } catch {
              return '';
            }
          };
          const ctx = screenshotContextManager.getContext(tab.id!);
          const contextHostname = (ctx as any)?.hostname as string | undefined;
          const currentHostname = getHostname(tab.url || '');
          if (contextHostname && contextHostname !== currentHostname) {
            return createErrorResponse(
              `Security check failed: Domain changed since last screenshot (from ${contextHostname} to ${currentHostname}) during zoom. Capture a new screenshot first.`,
            );
          }
        }

        try {
          await CDPHelper.attach(tab.id);
          const metrics: any = await CDPHelper.send(
            tab.id,
            'Page.getLayoutMetrics',
            {},
          );
          const viewport = metrics?.layoutViewport ||
            metrics?.visualViewport || {
              clientWidth: 800,
              clientHeight: 600,
              pageX: 0,
              pageY: 0,
            };
          const vw = Math.round(Number(viewport.clientWidth || 800));
          const vh = Math.round(Number(viewport.clientHeight || 600));
          if (rx1 > vw || ry1 > vh) {
            await CDPHelper.detach(tab.id);
            return createErrorResponse(
              `Region exceeds viewport boundaries (${vw}x${vh}). Choose a region within the visible viewport.`,
            );
          }
          const pageX = Number(viewport.pageX || 0);
          const pageY = Number(viewport.pageY || 0);

          const shot: any = await CDPHelper.send(
            tab.id,
            'Page.captureScreenshot',
            {
              format: 'png',
              captureBeyondViewport: false,
              fromSurface: true,
              clip: {
                x: pageX + rx0,
                y: pageY + ry0,
                width: w,
                height: h,
                scale: 1,
              },
            },
          );
          await CDPHelper.detach(tab.id);

          const base64Data = String(shot?.data || '');
          if (!base64Data) {
            return createErrorResponse(
              'Failed to capture zoom screenshot via CDP',
            );
          }
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'zoom',
                  mimeType: 'image/png',
                  base64Data,
                  region: { x0: rx0, y0: ry0, x1: rx1, y1: ry1 },
                }),
              },
            ],
            isError: false,
          };
        } catch (e) {
          await CDPHelper.detach(tab.id);
          return createErrorResponse(
            `zoom failed: ${e instanceof Error ? e.message : String(e)}`,
          );
        }
      }
      case 'screenshot': {
        // Reuse existing screenshot tool; it already supports base64 save option
        const result = await screenshotTool.execute({
          name: 'computer',
          storeBase64: true,
          fullPage: false,
          tabId: tab.id,
          windowId: tab.windowId,
          background: params.background === true,
        });
        return result;
      }
      default:
        return createErrorResponse(`Unsupported action: ${params.action}`);
    }
  }

  /**
   * DOM-based hover fallback when CDP is unavailable
   * Tries ref-based approach first (works with iframes), falls back to coordinates
   */
  private async domHoverFallback(
    tabId: number,
    coord?: Coordinates,
    resolvedBy?: 'ref' | 'selector' | 'coordinates',
    ref?: string,
  ): Promise<ToolResult> {
    // Try ref-based approach first (handles iframes correctly)
    if (ref) {
      try {
        const resp = await this.sendMessageToTab(tabId, {
          action: TOOL_MESSAGE_TYPES.DISPATCH_HOVER_FOR_REF,
          ref,
        });
        if (resp?.success) {
          return {
            content: [
              {
                type: 'text',
                text: JSON.stringify({
                  success: true,
                  action: 'hover',
                  resolvedBy: 'ref',
                  transport: 'dom-ref',
                  target: resp.target,
                }),
              },
            ],
            isError: false,
          };
        }
      } catch (error) {
        console.warn(
          '[ComputerTool] DOM ref hover failed, falling back to coordinates',
          error,
        );
      }
    }

    // Fallback to coordinate-based approach
    if (!coord) {
      return createErrorResponse('Hover fallback requires coordinates or ref');
    }

    try {
      const [injection] = await chrome.scripting.executeScript({
        target: { tabId },
        world: 'MAIN',
        func: (point) => {
          const target = document.elementFromPoint(point.x, point.y);
          if (!target) {
            return { success: false, error: 'No element found at coordinates' };
          }

          // Dispatch hover-related events
          for (const type of ['mousemove', 'mouseover', 'mouseenter']) {
            target.dispatchEvent(
              new MouseEvent(type, {
                bubbles: true,
                cancelable: true,
                clientX: point.x,
                clientY: point.y,
                view: window,
              }),
            );
          }

          return {
            success: true,
            target: {
              tagName: target.tagName,
              id: target.id,
              className: target.className,
              text: target.textContent?.trim()?.slice(0, 100) || '',
            },
          };
        },
        args: [coord],
      });

      const payload = injection?.result;
      if (!payload?.success) {
        return createErrorResponse(
          payload?.error || 'DOM hover fallback failed',
        );
      }

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              success: true,
              action: 'hover',
              coordinates: coord,
              resolvedBy,
              transport: 'dom',
              target: payload.target,
            }),
          },
        ],
        isError: false,
      };
    } catch (error) {
      return createErrorResponse(
        `DOM hover fallback failed: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  /**
   * Trigger GIF auto-capture after a successful action.
   * This is a no-op if auto-capture is not active.
   */
  private async triggerAutoCapture(
    tabId: number,
    actionType: ActionType,
    metadata?: Partial<ActionMetadata>,
  ): Promise<void> {
    if (!isAutoCaptureActive(tabId)) {
      return;
    }

    try {
      await captureFrameOnAction(tabId, {
        type: actionType,
        ...metadata,
      });
    } catch (error) {
      // Log but don't fail the main action
      console.warn('[ComputerTool] Auto-capture failed:', error);
    }
  }
}

export const computerTool = new ComputerTool();
