/**
 * Computer Tool - Mouse, keyboard, and screenshot actions
 */

import { KeyInput } from 'puppeteer-core';
import { MCPServer } from '../mcp-server';
import { MCPToolDefinition, MCPResult, MCPContent, ToolHandler, ToolContext, hasBudget } from '../types/mcp';
import { TOOL_ANNOTATIONS } from '../types/tool-annotations';
import { getSessionManager } from '../session-manager';
import { getScreenshotScheduler } from '../cdp/screenshot-scheduler';
import { getRefIdManager, formatStaleRefError, makeStaleRefError } from '../utils/ref-id-manager';
import { DEFAULT_SCREENSHOT_RACE_TIMEOUT_MS } from '../config/defaults';
import { withDomDelta } from '../utils/dom-delta';
import { generateVisualSummary } from '../utils/visual-summary';
import { AdaptiveScreenshot } from '../utils/adaptive-screenshot';
import { makeImageContent } from '../utils/image-mime';
import { withTimeout } from '../utils/with-timeout';
import { retryWithFallback } from '../utils/retry-with-fallback';
import { detectPagination, type PaginationInfo } from '../utils/pagination-detector';
import {
  getBase64EncodedByteLength,
  resolveViewportDimensions,
  validateCaptureArea,
  validateInlineImagePayload,
} from '../utils/screenshot-guards';
import {
  appendReturnAfterState,
  parseReturnAfterState,
  RETURN_AFTER_STATE_SCHEMA,
} from './_shared/return-after-state';

function formatPaginationGuidance(pagination: PaginationInfo): string {
  const detail = pagination.type === 'none'
    ? ''
    : ` detected (${pagination.type}${pagination.currentPage !== undefined ? ` page ${pagination.currentPage}` : ''}${pagination.totalPages !== undefined ? ` of ${pagination.totalPages}` : ''})`;
  return `[Pagination Detected] Pagination${detail}. Use batch_paginate to collect pages/items; avoid manual next/scroll loops.`;
}

async function getPaginationGuidance(page: import('puppeteer-core').Page, tabId: string): Promise<string | null> {
  try {
    const pagination = await detectPagination(page, tabId);
    if (pagination.type === 'none' || !pagination.hasNext) return null;
    return formatPaginationGuidance(pagination);
  } catch {
    return null;
  }
}

const definition: MCPToolDefinition = {
  name: 'computer',
  description: 'Mouse, keyboard, and screenshot actions on a tab. Supports click, type, scroll, key, hover, and screenshot by pixel coordinate or element ref.\n\nWhen to use: Precise coordinate-based input, screenshots, or keyboard shortcuts.\nWhen NOT to use: Use interact for natural-language element actions, or act for multi-step sequences.',
  inputSchema: {
    type: 'object',
    properties: {
      tabId: {
        type: 'string',
        description: 'Tab ID',
      },
      action: {
        type: 'string',
        enum: [
          'left_click',
          'right_click',
          'double_click',
          'triple_click',
          'type',
          'screenshot',
          'wait',
          'scroll',
          'key',
          'scroll_to',
          'hover',
        ],
        description: 'Action to perform',
      },
      coordinate: {
        type: 'array',
        items: { type: 'number' },
        description: '[x, y] for click/scroll actions',
      },
      text: {
        type: 'string',
        description: 'Text to type or key to press',
      },
      duration: {
        type: 'number',
        description: 'Wait duration in seconds',
      },
      scroll_direction: {
        type: 'string',
        enum: ['up', 'down', 'left', 'right'],
        description: 'Scroll direction',
      },
      scroll_amount: {
        type: 'number',
        description: 'Scroll wheel ticks. Default: 3',
      },
      ref: {
        type: 'string',
        description: 'Element ref or backendNodeId',
      },
      screenshotQuality: {
        type: 'string',
        enum: ['high', 'normal', 'low'],
        description: 'Screenshot quality. low: reduced resolution and quality for smaller payload.',
      },
      screenshotFormat: {
        type: 'string',
        enum: ['webp', 'png', 'jpeg'],
        description: 'Only for action "screenshot". Image format for the returned base64. Default: "webp" (smallest payload). Use "png" for clients that cannot decode webp inline (e.g. some MCP UIs), at the cost of larger payloads. "screenshotQuality" is ignored for png (PNG is lossless).',
      },
      includeUserAgentShadowDOM: {
        type: 'boolean',
        description: 'Include user-agent shadow DOM in hit detection. Default: false',
      },
      force: {
        type: 'boolean',
        description: 'Only for action "screenshot". Force full screenshot, bypassing adaptive degradation. Default: false.',
      },
      returnAfterState: RETURN_AFTER_STATE_SCHEMA,
    },
    required: ['action', 'tabId'],
  },
  annotations: TOOL_ANNOTATIONS.computer,
};

const innerHandler: ToolHandler = async (
  sessionId: string,
  args: Record<string, unknown>,
  context?: ToolContext
): Promise<MCPResult> => {
  const tabId = args.tabId as string;
  const action = args.action as string;
  const coordinate = args.coordinate as [number, number] | undefined;
  const text = args.text as string | undefined;
  const duration = args.duration as number | undefined;
  const scrollDirection = args.scroll_direction as string | undefined;
  const scrollAmount = (args.scroll_amount as number) || 3;
  const ref = args.ref as string | undefined;
  const includeUAShadow = (args.includeUserAgentShadowDOM as boolean) ?? false;

  const sessionManager = getSessionManager();

  if (!tabId) {
    return {
      content: [{ type: 'text', text: 'Error: tabId is required' }],
      isError: true,
    };
  }

  try {
    const page = await sessionManager.getPage(sessionId, tabId);
    if (!page) {
      const available = await sessionManager.getAvailableTargets(sessionId);
      const availableInfo = available.length > 0
        ? `\nAvailable tabs:\n${available.map(t => `  - tabId: ${t.tabId} | ${t.url} | ${t.title}`).join('\n')}`
        : '\nNo tabs available. Call navigate without tabId to create a new tab.';
      return {
        content: [{ type: 'text', text: `Error: Tab ${tabId} not found or no longer available.${availableInfo}` }],
        isError: true,
      };
    }

    switch (action) {
      case 'screenshot': {
        // Phase 1: Page readiness guard
        try {
          const readyState = await withTimeout(page.evaluate(() => document.readyState), 10000, 'computer', context);
          if (readyState !== 'complete') {
            await page.waitForFunction(() => document.readyState === 'complete', { timeout: 3000 }).catch(() => {});
          }
        } catch {
          // Page may be navigating — proceed anyway
        }

        // Quality presets for screenshot compression
        const QUALITY_PRESETS = {
          high:   { quality: 85 },
          normal: { quality: 60 },
          low:    { quality: 40 },
        } as const;

        // Phase 1.5: Adaptive screenshot — decide response mode based on repetition
        const force = (args.force as boolean) ?? false;
        const adaptive = AdaptiveScreenshot.getInstance();

        // Force mode: reset adaptive history to prevent degradation spiral
        if (force) {
          adaptive.reset(tabId);
        }

        const screenshotMode = await adaptive.evaluate(page, tabId);
        const effectiveMode = force ? 'full' : screenshotMode;

        // Determine effective quality: explicit arg overrides adaptive suggestion
        const qualityArg = args.screenshotQuality as string | undefined;
        const effectiveQuality: 'high' | 'normal' | 'low' =
          (qualityArg === 'high' || qualityArg === 'normal' || qualityArg === 'low')
            ? qualityArg
            : adaptive.getQualityForMode(effectiveMode);
        const preset = QUALITY_PRESETS[effectiveQuality];

        // Determine output format. Default stays webp for backward compatibility
        // and smallest payload; agents can request png for clients that cannot
        // decode webp inline (e.g. some MCP UIs surface webp as
        // "Mime type image/webp does not support decoding").
        const formatArg = args.screenshotFormat as string | undefined;
        const SUPPORTED_FORMATS = ['webp', 'png', 'jpeg'] as const;
        type ScreenshotFormat = (typeof SUPPORTED_FORMATS)[number];
        const effectiveFormat: ScreenshotFormat =
          formatArg && (SUPPORTED_FORMATS as readonly string[]).includes(formatArg)
            ? (formatArg as ScreenshotFormat)
            : 'webp';
        const mimeTypeForFormat: Record<ScreenshotFormat, string> = {
          webp: 'image/webp',
          png: 'image/png',
          jpeg: 'image/jpeg',
        };

        // text_only mode: skip expensive screenshot, return visual summary
        if (effectiveMode === 'text_only') {
          const summary = (context && !hasBudget(context, 5_000)) ? null : await generateVisualSummary(page);
          return {
            content: [{
              type: 'text',
              text: summary
                ? `[Adaptive] Screenshot replaced with text summary (repeated capture at same scroll position).\n${summary}\nTip: Use read_page(mode="dom") for structured page state, or inspect(query="...") for targeted extraction.`
                : '[Adaptive] Repeated screenshot at same scroll position. No visual change detected. Use read_page(mode="dom") or inspect(query="...") for page state.',
            }],
          };
        }

        // page.viewport() can return null when Chrome is launched with
        // defaultViewport: null. Read live window dimensions in that case so
        // the area guard reflects the real capture size, not a hardcoded
        // fallback that could underestimate when --window-size is large.
        const captureDimensions = await resolveViewportDimensions(page);
        const areaError = validateCaptureArea(captureDimensions, 'Screenshot');
        if (areaError) {
          return {
            content: [{ type: 'text', text: `Error: ${areaError}` }],
            isError: true,
          };
        }

        // Phase 2: Screenshot with retry
        type ScreenshotAttempt =
          | { status: 'success'; data: string; mimeType: string }
          | { status: 'timeout' }
          | { status: 'error' };

        const attemptScreenshot = async (): Promise<ScreenshotAttempt> => {
          try {
            const screenshotData = await Promise.race([
              (async () => {
                // PNG is lossless: omit the quality field (CDP rejects
                // quality for png) and skip optimizeForSpeed (it only
                // applies to lossy encoders).
                const captureParams: Record<string, unknown> =
                  effectiveFormat === 'png'
                    ? { format: 'png' }
                    : {
                        format: effectiveFormat,
                        quality: preset.quality,
                        optimizeForSpeed: true,
                      };
                const { data } = await getScreenshotScheduler().capture(
                  page,
                  sessionManager.getCDPClient(),
                  captureParams
                );
                return data;
              })(),
              new Promise<null>((resolve) => setTimeout(() => resolve(null), DEFAULT_SCREENSHOT_RACE_TIMEOUT_MS)),
            ]);
            if (screenshotData === null) return { status: 'timeout' };
            return { status: 'success', data: screenshotData, mimeType: mimeTypeForFormat[effectiveFormat] };
          } catch {
            return { status: 'error' };
          }
        };

        // First attempt
        let screenshot = await attemptScreenshot();

        // Retry once after 500ms on synchronous CDP failure. Do not start a
        // duplicate expensive capture after timeout because Chrome may still be
        // working on the first Page.captureScreenshot request.
        if (screenshot.status === 'error') {
          await new Promise(r => setTimeout(r, 500));
          screenshot = await attemptScreenshot();
        }

        if (screenshot.status === 'success') {
          const payloadError = validateInlineImagePayload(
            getBase64EncodedByteLength(screenshot.data),
            'Screenshot'
          );
          if (payloadError) {
            return {
              content: [{ type: 'text', text: `Error: ${payloadError}` }],
              isError: true,
            };
          }

          const content: MCPContent[] = [
            makeImageContent(screenshot.data, screenshot.mimeType as 'image/webp' | 'image/png' | 'image/jpeg'),
          ];

          const paginationGuidance = await getPaginationGuidance(page, tabId);
          if (paginationGuidance) {
            content.push({ type: 'text', text: paginationGuidance });
          }

          // annotated mode: append note about repeated screenshot
          if (effectiveMode === 'annotated') {
            content.push({ type: 'text', text: adaptive.getAnnotation() });
          }

          return { content };
        }

        // Phase 3: DOM fallback — always give the LLM page state
        try {
          const pageInfo = await withTimeout(page.evaluate(() => {
            const body = document.body;
            const text = body ? body.innerText.substring(0, 2000) : '';
            return {
              url: window.location.href,
              title: document.title,
              readyState: document.readyState,
              textPreview: text,
            };
          }), 10000, 'computer', context);

          return {
            content: [{
              type: 'text',
              text: [
                `Screenshot failed (timeout). DOM fallback provided:`,
                `[page_info] url: ${pageInfo.url} | title: ${pageInfo.title} | readyState: ${pageInfo.readyState}`,
                `[text_preview]`,
                pageInfo.textPreview || '(empty page)',
                ``,
                `Tip: Use read_page mode="dom" for structured page state, or wait_for to wait for page load.`,
              ].join('\n'),
            }],
          };
        } catch {
          // Even DOM fallback failed — page is completely unresponsive
          return {
            content: [{
              type: 'text',
              text: 'Screenshot failed and page is unresponsive. The page may still be loading. Use wait_for with type "selector" to wait for specific content, then retry.',
            }],
            isError: true,
          };
        }
      }

      case 'left_click': {
        let clickCoord: [number, number] | undefined = coordinate;
        let refInfo = '';

        if (ref && !coordinate) {
          const resolved = await resolveRefToCoordinates(sessionId, tabId, ref, page, sessionManager);
          if (resolved.error) return resolved.error;
          clickCoord = resolved.coord;
          refInfo = ` [${ref}]`;
        }

        if (!clickCoord) {
          return {
            content: [{ type: 'text', text: 'Error: coordinate is required for left_click' }],
            isError: true,
          };
        }

        // Reset adaptive screenshot on click (page state changes)
        AdaptiveScreenshot.getInstance().reset(tabId);

        if (refInfo) {
          const { delta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1]));
          const summary = (context && !hasBudget(context, 5_000)) ? null : await generateVisualSummary(page);
          const summaryText = summary ? `\n${summary}` : '';
          return {
            content: [{ type: 'text', text: `Clicked element ${ref} at (${clickCoord[0]}, ${clickCoord[1]})${delta}${summaryText}` }],
          };
        }

        const leftClickValidation = await validateCoordinates(page, clickCoord[0], clickCoord[1]);
        if (!leftClickValidation.valid) {
          return {
            content: [{ type: 'text', text: `Error: ${leftClickValidation.warning}` }],
            isError: true,
          };
        }

        const leftClickHitInfo = await getHitElementInfo(page, sessionManager.getCDPClient(), clickCoord[0], clickCoord[1], includeUAShadow, context);
        const { delta: leftDelta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1]));

        // Internal fallback: if hit non-interactive element, suggest but don't auto-retry
        // (auto-retry could cause unintended side effects on elements the LLM didn't intend)
        const summary = (context && !hasBudget(context, 5_000)) ? null : await generateVisualSummary(page);
        const summaryText = summary ? `\n${summary}` : '';

        const resultText = leftClickValidation.warning
          ? `Clicked at (${clickCoord[0]}, ${clickCoord[1]}). Warning: ${leftClickValidation.warning}${leftDelta}`
          : `Clicked at (${clickCoord[0]}, ${clickCoord[1]})${leftDelta}`;
        return {
          content: [{ type: 'text', text: resultText + leftClickHitInfo + summaryText }],
        };
      }

      case 'right_click': {
        let clickCoord: [number, number] | undefined = coordinate;
        let refInfo = '';

        if (ref && !coordinate) {
          const resolved = await resolveRefToCoordinates(sessionId, tabId, ref, page, sessionManager);
          if (resolved.error) return resolved.error;
          clickCoord = resolved.coord;
          refInfo = ` [${ref}]`;
        }

        if (!clickCoord) {
          return {
            content: [{ type: 'text', text: 'Error: coordinate is required for right_click' }],
            isError: true,
          };
        }

        if (refInfo) {
          const { delta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1], { button: 'right' }));
          return {
            content: [{ type: 'text', text: `Right-clicked element ${ref} at (${clickCoord[0]}, ${clickCoord[1]})${delta}` }],
          };
        }

        const rightClickValidation = await validateCoordinates(page, clickCoord[0], clickCoord[1]);
        if (!rightClickValidation.valid) {
          return {
            content: [{ type: 'text', text: `Error: ${rightClickValidation.warning}` }],
            isError: true,
          };
        }

        const rightClickHitInfo = await getHitElementInfo(page, sessionManager.getCDPClient(), clickCoord[0], clickCoord[1], includeUAShadow, context);
        const { delta: rightDelta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1], { button: 'right' }));

        const rightClickText = rightClickValidation.warning
          ? `Right-clicked at (${clickCoord[0]}, ${clickCoord[1]}). Warning: ${rightClickValidation.warning}${rightDelta}`
          : `Right-clicked at (${clickCoord[0]}, ${clickCoord[1]})${rightDelta}`;
        return {
          content: [{ type: 'text', text: rightClickText + rightClickHitInfo }],
        };
      }

      case 'double_click': {
        let clickCoord: [number, number] | undefined = coordinate;
        let refInfo = '';

        if (ref && !coordinate) {
          const resolved = await resolveRefToCoordinates(sessionId, tabId, ref, page, sessionManager);
          if (resolved.error) return resolved.error;
          clickCoord = resolved.coord;
          refInfo = ` [${ref}]`;
        }

        if (!clickCoord) {
          return {
            content: [
              { type: 'text', text: 'Error: coordinate is required for double_click' },
            ],
            isError: true,
          };
        }

        if (refInfo) {
          const { delta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1], { clickCount: 2 }));
          return {
            content: [{ type: 'text', text: `Double-clicked element ${ref} at (${clickCoord[0]}, ${clickCoord[1]})${delta}` }],
          };
        }

        const doubleClickValidation = await validateCoordinates(page, clickCoord[0], clickCoord[1]);
        if (!doubleClickValidation.valid) {
          return {
            content: [{ type: 'text', text: `Error: ${doubleClickValidation.warning}` }],
            isError: true,
          };
        }

        const doubleClickHitInfo = await getHitElementInfo(page, sessionManager.getCDPClient(), clickCoord[0], clickCoord[1], includeUAShadow, context);
        const { delta: doubleDelta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1], { clickCount: 2 }));

        const doubleClickText = doubleClickValidation.warning
          ? `Double-clicked at (${clickCoord[0]}, ${clickCoord[1]}). Warning: ${doubleClickValidation.warning}${doubleDelta}`
          : `Double-clicked at (${clickCoord[0]}, ${clickCoord[1]})${doubleDelta}`;
        return {
          content: [{ type: 'text', text: doubleClickText + doubleClickHitInfo }],
        };
      }

      case 'triple_click': {
        let clickCoord: [number, number] | undefined = coordinate;
        let refInfo = '';

        if (ref && !coordinate) {
          const resolved = await resolveRefToCoordinates(sessionId, tabId, ref, page, sessionManager);
          if (resolved.error) return resolved.error;
          clickCoord = resolved.coord;
          refInfo = ` [${ref}]`;
        }

        if (!clickCoord) {
          return {
            content: [
              { type: 'text', text: 'Error: coordinate is required for triple_click' },
            ],
            isError: true,
          };
        }

        if (refInfo) {
          const { delta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1], { clickCount: 3 }));
          return {
            content: [{ type: 'text', text: `Triple-clicked element ${ref} at (${clickCoord[0]}, ${clickCoord[1]})${delta}` }],
          };
        }

        const tripleClickValidation = await validateCoordinates(page, clickCoord[0], clickCoord[1]);
        if (!tripleClickValidation.valid) {
          return {
            content: [{ type: 'text', text: `Error: ${tripleClickValidation.warning}` }],
            isError: true,
          };
        }

        const tripleClickHitInfo = await getHitElementInfo(page, sessionManager.getCDPClient(), clickCoord[0], clickCoord[1], includeUAShadow, context);
        const { delta: tripleDelta } = await withDomDelta(page, () => page.mouse.click(clickCoord[0], clickCoord[1], { clickCount: 3 }));

        const tripleClickText = tripleClickValidation.warning
          ? `Triple-clicked at (${clickCoord[0]}, ${clickCoord[1]}). Warning: ${tripleClickValidation.warning}${tripleDelta}`
          : `Triple-clicked at (${clickCoord[0]}, ${clickCoord[1]})${tripleDelta}`;
        return {
          content: [{ type: 'text', text: tripleClickText + tripleClickHitInfo }],
        };
      }

      case 'hover': {
        let hoverCoord: [number, number] | undefined = coordinate;
        let refInfo = '';

        if (ref && !coordinate) {
          const resolved = await resolveRefToCoordinates(sessionId, tabId, ref, page, sessionManager);
          if (resolved.error) return resolved.error;
          hoverCoord = resolved.coord;
          refInfo = ` [${ref}]`;
        }

        if (!hoverCoord) {
          return {
            content: [{ type: 'text', text: 'Error: coordinate is required for hover' }],
            isError: true,
          };
        }

        if (refInfo) {
          const { delta } = await withDomDelta(page, () => page.mouse.move(hoverCoord[0], hoverCoord[1]));
          return {
            content: [{ type: 'text', text: `Hovered element ${ref} at (${hoverCoord[0]}, ${hoverCoord[1]})${delta}` }],
          };
        }

        const hoverValidation = await validateCoordinates(page, hoverCoord[0], hoverCoord[1]);
        if (!hoverValidation.valid) {
          return {
            content: [{ type: 'text', text: `Error: ${hoverValidation.warning}` }],
            isError: true,
          };
        }

        const { delta: hoverDelta } = await withDomDelta(page, () => page.mouse.move(hoverCoord[0], hoverCoord[1]));

        const hoverText = hoverValidation.warning
          ? `Hovered at (${hoverCoord[0]}, ${hoverCoord[1]}). Warning: ${hoverValidation.warning}${hoverDelta}`
          : `Hovered at (${hoverCoord[0]}, ${hoverCoord[1]})${hoverDelta}`;

        return {
          content: [{ type: 'text', text: hoverText }],
        };
      }

      case 'type': {
        if (!text) {
          return {
            content: [{ type: 'text', text: 'Error: text is required for type action' }],
            isError: true,
          };
        }
        const { delta: typeDelta } = await withDomDelta(page, () => page.keyboard.type(text));
        return {
          content: [{ type: 'text', text: `Typed: ${text}${typeDelta}` }],
        };
      }

      case 'key': {
        if (!text) {
          return {
            content: [{ type: 'text', text: 'Error: text is required for key action' }],
            isError: true,
          };
        }
        const { delta: keyDelta } = await withDomDelta(page, async () => {
          // Handle multiple keys separated by space
          const keys = text.split(' ');
          for (const key of keys) {
            if (key.includes('+')) {
              // Handle modifier keys like ctrl+a
              const parts = key.split('+');
              const modifiers = parts.slice(0, -1);
              const mainKey = parts[parts.length - 1];

              for (const mod of modifiers) {
                await page.keyboard.down(normalizeKey(mod));
              }
              await page.keyboard.press(normalizeKey(mainKey));
              for (const mod of modifiers.reverse()) {
                await page.keyboard.up(normalizeKey(mod));
              }
            } else {
              await page.keyboard.press(normalizeKey(key));
            }
          }
        });
        return {
          content: [{ type: 'text', text: `Pressed: ${text}${keyDelta}` }],
        };
      }

      case 'wait': {
        // Validate duration
        if (duration !== undefined && duration < 0) {
          return {
            content: [{ type: 'text', text: 'Error: duration cannot be negative' }],
            isError: true,
          };
        }

        const waitTime = Math.min(Math.max((duration || 1) * 1000, 0), 30000);
        await new Promise((resolve) => setTimeout(resolve, waitTime));
        return {
          content: [{ type: 'text', text: `Waited ${waitTime / 1000} seconds` }],
        };
      }

      case 'scroll': {
        let scrollCoord: [number, number] | undefined = coordinate;

        if (ref && !coordinate) {
          const resolved = await resolveRefToCoordinates(sessionId, tabId, ref, page, sessionManager);
          if (resolved.error) return resolved.error;
          scrollCoord = resolved.coord;
        }

        let usedViewportCenter = false;
        if (!scrollCoord) {
          // Fall back to viewport center
          const viewport = page.viewport();
          scrollCoord = [
            Math.floor((viewport?.width ?? 1280) / 2),
            Math.floor((viewport?.height ?? 800) / 2),
          ];
          usedViewportCenter = true;
        }

        const direction = scrollDirection || 'down';

        // Skip validation for viewport center fallback — it's always within bounds
        if (!usedViewportCenter) {
          const scrollValidation = await validateCoordinates(page, scrollCoord[0], scrollCoord[1]);
          if (!scrollValidation.valid) {
            return {
              content: [{ type: 'text', text: `Error: ${scrollValidation.warning}` }],
              isError: true,
            };
          }
        }

        await page.mouse.move(scrollCoord[0], scrollCoord[1]);

        const deltaMultiplier = 100;
        let deltaX = 0;
        let deltaY = 0;

        switch (direction) {
          case 'up':
            deltaY = -scrollAmount * deltaMultiplier;
            break;
          case 'down':
            deltaY = scrollAmount * deltaMultiplier;
            break;
          case 'left':
            deltaX = -scrollAmount * deltaMultiplier;
            break;
          case 'right':
            deltaX = scrollAmount * deltaMultiplier;
            break;
        }

        // Primary: CDP mouse wheel scroll
        const primaryWheel = () => page.mouse.wheel({ deltaX, deltaY });

        // Fallback: JS window.scrollBy (works even when CDP input is busy)
        const fallbackWheel = () => withTimeout(
          page.evaluate((dx: number, dy: number) => window.scrollBy(dx, dy), deltaX, deltaY),
          5000,
          'scroll-fallback'
        , context);

        const { recovered: scrollRecovered, method: scrollMethod } = await retryWithFallback(
          primaryWheel,
          [fallbackWheel],
          { label: 'computer_scroll', retryDelayMs: 500 }
        );

        const centerNote = usedViewportCenter ? ' [viewport center]' : '';
        const recoveryNote = scrollRecovered ? ` [recovered:${scrollMethod}]` : '';
        return {
          content: [
            {
              type: 'text',
              text: `Scrolled ${direction} at (${scrollCoord[0]}, ${scrollCoord[1]})${centerNote}${recoveryNote}`,
            },
          ],
        };
      }

      case 'scroll_to': {
        // Note: scroll_to does not validate ref identity (less destructive than click)
        if (!ref) {
          return {
            content: [{ type: 'text', text: 'Error: ref is required for scroll_to' }],
            isError: true,
          };
        }

        const refIdManager = getRefIdManager();
        const backendNodeId = refIdManager.resolveToBackendNodeId(sessionId, tabId, ref);

        if (backendNodeId === undefined) {
          return {
            content: [{ type: 'text', text: `Error: Element ref or node ID '${ref}' not found` }],
            isError: true,
          };
        }

        // Use CDP to scroll element into view
        const cdpClient = sessionManager.getCDPClient();
        await cdpClient.send(page, 'DOM.scrollIntoViewIfNeeded', {
          backendNodeId,
        });

        return {
          content: [{ type: 'text', text: `Scrolled to ${ref}` }],
        };
      }

      default:
        return {
          content: [{ type: 'text', text: `Error: Unknown action: ${action}` }],
          isError: true,
        };
    }
  } catch (error) {
    return {
      content: [
        {
          type: 'text',
          text: `Computer action error: ${error instanceof Error ? error.message : String(error)}${
            (error instanceof Error && error.message.includes('timed out'))
              ? '. Hint: Page may still be loading. Use wait_for with type "selector" to wait for specific content, or increase timeout.'
              : ''
          }`,
        },
      ],
      isError: true,
    };
  }
};

/**
 * Outer handler — runs the inner action, then optionally appends a fresh
 * page snapshot per the `returnAfterState` chaining option. The snapshot is
 * captured by reusing `read_page` so this stays a thin wrapper.
 */
const handler: ToolHandler = async (
  sessionId: string,
  args: Record<string, unknown>,
  context?: ToolContext,
): Promise<MCPResult> => {
  const result = await innerHandler(sessionId, args, context);
  const returnAfterState = parseReturnAfterState(args.returnAfterState);
  if (returnAfterState === 'none' || result.isError) return result;

  const tabId = args.tabId as string | undefined;
  if (!tabId) return result;
  const sessionManager = getSessionManager();
  const page = await sessionManager.getPage(sessionId, tabId).catch(() => null);
  if (!page) return result;

  await appendReturnAfterState(result, page, sessionId, tabId, returnAfterState, context);
  return result;
};

/**
 * Resolve a ref to screen coordinates, scrolling the element into view first.
 * Returns either { coord } on success or { error } on failure.
 */
async function resolveRefToCoordinates(
  sessionId: string,
  tabId: string,
  ref: string,
  page: import('puppeteer-core').Page,
  sessionManager: ReturnType<typeof getSessionManager>
): Promise<{ coord: [number, number]; error?: never } | { coord?: never; error: MCPResult }> {
  const refIdManager = getRefIdManager();
  let backendNodeId = refIdManager.resolveToBackendNodeId(sessionId, tabId, ref);
  const isRefIdFormat = /^ref_\d+$/.test(ref);

  if (isRefIdFormat) {
    const entry = refIdManager.getRef(sessionId, tabId, ref);
    if (!entry || refIdManager.isRefStale(sessionId, tabId, ref)) {
      return {
        error: {
          content: [{ type: 'text', text: formatStaleRefError(ref) }],
          isError: true,
          error: makeStaleRefError(ref),
        },
      };
    }
  }

  if (backendNodeId === undefined) {
    return {
      error: {
        content: [{ type: 'text', text: `Error: Element ref or node ID '${ref}' not found` }],
        isError: true,
      },
    };
  }

  const cdpClient = sessionManager.getCDPClient();
  try {
    // Validate ref identity before clicking (only for ref_N refs with stored fingerprint)
    const refEntry = refIdManager.getRef(sessionId, tabId, ref);
    if (refEntry && refEntry.tagName) {
      let nodeLocalName: string | undefined;
      let nodeTextContent: string | undefined;
      let nodeName: string | undefined;
      try {
        const { node } = await cdpClient.send<{
          node: {
            localName: string;
            nodeValue?: string;
            attributes?: string[];
            children?: Array<{ nodeType?: number; nodeValue?: string }>;
          };
        }>(page, 'DOM.describeNode', { backendNodeId, depth: 1 });
        nodeLocalName = node?.localName;
        nodeTextContent = getDescribeNodeTextContent(node);
        nodeName = getDescribeNodeName(node);
      } catch {
        // If validation CDP calls fail, proceed with the click
      }

      if (nodeLocalName !== undefined) {
        const validation = refIdManager.validateRef(
          sessionId, tabId, ref,
          nodeLocalName,
          nodeTextContent,
          nodeName,
        );

        if (!validation.valid && validation.stale) {
          if (isRefIdFormat) {
            return {
              error: {
                content: [{ type: 'text', text: formatStaleRefError(ref) }],
                isError: true,
                error: makeStaleRefError(ref),
              },
            };
          }

          // Attempt transparent recovery: re-find the element using stored metadata
          const relocated = await refIdManager.tryRelocateRef(
            sessionId, tabId, ref, page, cdpClient
          );

          if (relocated) {
            console.error(`[ref-recovery] ${ref} was stale, re-located as ${relocated.newRef}`);
            backendNodeId = relocated.backendNodeId;
          } else {
            return {
              error: {
                content: [{
                  type: 'text',
                  text: `Error: ${ref} is stale — ${validation.reason}. Element could not be re-located. Run find or read_page again to get fresh refs.`,
                }],
                isError: true,
              },
            };
          }
        }
      }
    }

    // Log staleness warning (non-blocking)
    if (refEntry && refIdManager.isRefStale(sessionId, tabId, ref)) {
      const age = Math.round((Date.now() - refEntry.createdAt) / 1000);
      console.error(`[ref-validation] ${ref} is ${age}s old — may be stale`);
    }

    await cdpClient.send(page, 'DOM.scrollIntoViewIfNeeded', {
      backendNodeId,
    });

    const { model } = await cdpClient.send<{
      model: { content: number[] };
    }>(page, 'DOM.getBoxModel', {
      backendNodeId,
    });

    const x = (model.content[0] + model.content[2]) / 2;
    const y = (model.content[1] + model.content[5]) / 2;
    return { coord: [Math.round(x), Math.round(y)] };
  } catch (e) {
    return {
      error: {
        content: [{ type: 'text', text: `Error: Could not get position for ${ref}: ${e instanceof Error ? e.message : String(e)}` }],
        isError: true,
      },
    };
  }
}

function getDescribeNodeAttributes(node?: { attributes?: string[] }): Record<string, string> {
  const attrs: Record<string, string> = {};
  const raw = node?.attributes || [];
  for (let i = 0; i + 1 < raw.length; i += 2) {
    attrs[raw[i]] = raw[i + 1];
  }
  return attrs;
}

function getDescribeNodeName(node?: { attributes?: string[] }): string | undefined {
  const attrs = getDescribeNodeAttributes(node);
  return attrs['aria-label'] || attrs.title || attrs.placeholder || attrs.alt || attrs.name || undefined;
}

function getDescribeNodeTextContent(node?: {
  nodeValue?: string;
  children?: Array<{ nodeType?: number; nodeValue?: string }>;
}): string | undefined {
  const ownValue = node?.nodeValue?.trim();
  if (ownValue) return ownValue;
  const childText = (node?.children || [])
    .filter((child) => child.nodeType === 3 && child.nodeValue)
    .map((child) => child.nodeValue)
    .join('')
    .trim();
  return childText || undefined;
}

/**
 * Get hit element info via CDP after a coordinate-based click.
 * Returns a string like '\nHit: <button id="submit"> "Submit" [interactive]'
 * or empty string if CDP fails.
 */
async function getHitElementInfo(
  page: import('puppeteer-core').Page,
  cdpClient: ReturnType<ReturnType<typeof getSessionManager>['getCDPClient']>,
  x: number,
  y: number,
  includeUserAgentShadowDOM = false,
  context?: ToolContext
): Promise<string> {
  try {
    const locationResult = await cdpClient.send<{ backendNodeId: number; nodeId: number }>(
      page,
      'DOM.getNodeForLocation',
      { x, y, includeUserAgentShadowDOM }
    );

    const backendNodeId = locationResult?.backendNodeId;
    if (!backendNodeId) return '';

    const { node: hitNode } = await cdpClient.send<{
      node: { localName: string; attributes: string[]; nodeType: number };
    }>(page, 'DOM.describeNode', { backendNodeId });

    const localName = hitNode.localName || '';
    const attrs = hitNode.attributes || [];

    // attrs is a flat array: [name0, val0, name1, val1, ...]
    const attrMap: Record<string, string> = {};
    for (let i = 0; i + 1 < attrs.length; i += 2) {
      attrMap[attrs[i]] = attrs[i + 1];
    }

    const interactiveTags = new Set(['input', 'button', 'select', 'textarea', 'a']);
    const interactiveRoles = new Set([
      'button', 'link', 'textbox', 'checkbox', 'radio',
      'combobox', 'listbox', 'menu', 'menuitem', 'tab', 'switch', 'slider',
    ]);

    const isHitInteractive =
      interactiveTags.has(localName) ||
      interactiveRoles.has((attrMap['role'] || '').toLowerCase()) ||
      attrMap['contenteditable'] === 'true';

    // Build attribute string with key attrs only
    const keyAttrs = ['id', 'class', 'role', 'aria-label', 'data-testid', 'type', 'href', 'contenteditable'];
    const attrStr = keyAttrs
      .filter((k) => attrMap[k] !== undefined)
      .map((k) => `${k}="${attrMap[k]}"`)
      .join(' ');

    // Get textContent from page by querying the element at the click coordinates
    let textContent = '';
    try {
      textContent = await withTimeout(page.evaluate(
        (px: number, py: number) => {
          const el = document.elementFromPoint(px, py);
          return el ? (el.textContent || '').trim().substring(0, 50) : '';
        },
        x,
        y
      ), 3000, 'computer', context);
    } catch { /* skip */ }

    // Build hit tag representation
    const openTag = attrStr ? `<${localName} ${attrStr}>` : `<${localName}>`;
    const textPart = textContent ? ` "${textContent.substring(0, 50)}"` : '';
    const interactiveFlag = isHitInteractive ? '[interactive]' : '[not interactive]';

    let hitInfo = `\nHit: ${openTag}${textPart} ${interactiveFlag}`;

    // If hit element is not interactive, find nearest interactive element
    if (!isHitInteractive) {
      try {
        const nearestInfo = await withTimeout(page.evaluate(
          (px: number, py: number) => {
            const offsets: [number, number][] = [
              [0, -20], [0, 20], [-20, 0], [20, 0],
              [0, -40], [0, 40], [-40, 0], [40, 0],
            ];
            for (const [dx, dy] of offsets) {
              const el = document.elementFromPoint(px + dx, py + dy);
              if (
                el &&
                el.matches(
                  'a,button,input,select,textarea,[contenteditable="true"],[role="button"],[role="link"],[role="tab"],[role="menuitem"]'
                )
              ) {
                const rect = el.getBoundingClientRect();
                const cx = Math.round(rect.x + rect.width / 2);
                const cy = Math.round(rect.y + rect.height / 2);
                return {
                  tag: el.tagName.toLowerCase(),
                  text: el.textContent?.substring(0, 40) || '',
                  x: cx,
                  y: cy,
                  dx: Math.round(cx - px),
                  dy: Math.round(cy - py),
                };
              }
            }
            return null;
          },
          x,
          y
        ), 3000, 'computer', context);

        if (nearestInfo) {
          const absDx = Math.abs(nearestInfo.dx);
          const absDy = Math.abs(nearestInfo.dy);
          let direction: string;
          let distance: number;
          if (absDy >= absDx) {
            direction = nearestInfo.dy > 0 ? 'below' : 'above';
            distance = absDy;
          } else {
            direction = nearestInfo.dx > 0 ? 'right' : 'left';
            distance = absDx;
          }
          hitInfo += `\nNearest interactive: <${nearestInfo.tag}> "${nearestInfo.text}" at (${nearestInfo.x}, ${nearestInfo.y}), ${distance}px ${direction}`;
        }
      } catch { /* silently skip */ }
    }

    return hitInfo;
  } catch {
    // CDP failed — fall back to no hit info
    return '';
  }
}

/**
 * Validate and check coordinates against viewport bounds
 */
async function validateCoordinates(
  page: import('puppeteer-core').Page,
  x: number,
  y: number
): Promise<{ valid: boolean; warning?: string }> {
  // Check for negative coordinates
  if (x < 0 || y < 0) {
    return {
      valid: false,
      warning: `Negative coordinates (${x}, ${y}) are not allowed`,
    };
  }

  try {
    // Get viewport dimensions
    const viewport = page.viewport();
    if (viewport) {
      const { width, height } = viewport;

      if (x > width || y > height) {
        return {
          valid: true,
          warning: `Coordinates (${x}, ${y}) are outside visible viewport (${width}x${height}). The click may not hit the intended target.`,
        };
      }
    }
  } catch {
    // If we can't get viewport, just allow the operation
  }

  return { valid: true };
}

function normalizeKey(key: string): KeyInput {
  const keyMap: Record<string, KeyInput> = {
    // Modifiers
    ctrl: 'Control',
    cmd: 'Meta',
    meta: 'Meta',
    alt: 'Alt',
    shift: 'Shift',
    // Common keys
    enter: 'Enter',
    tab: 'Tab',
    escape: 'Escape',
    esc: 'Escape',
    backspace: 'Backspace',
    delete: 'Delete',
    // Arrow keys
    up: 'ArrowUp',
    down: 'ArrowDown',
    left: 'ArrowLeft',
    right: 'ArrowRight',
    // Navigation keys
    home: 'Home',
    end: 'End',
    pageup: 'PageUp',
    pagedown: 'PageDown',
    // macOS conventions
    return: 'Enter',
    option: 'Alt',
    command: 'Meta',
    // Windows/Linux conventions
    super: 'Meta',
    win: 'Meta',
    windows: 'Meta',
    // Common key names
    space: 'Space',
    spacebar: 'Space',
    del: 'Delete',
    ins: 'Insert',
    insert: 'Insert',
    pgup: 'PageUp',
    pgdn: 'PageDown',
    prtsc: 'PrintScreen',
    printscreen: 'PrintScreen',
    apps: 'ContextMenu',
    contextmenu: 'ContextMenu',
    // Lock keys (case-sensitive names destroyed by toLowerCase)
    capslock: 'CapsLock',
    numlock: 'NumLock',
    scrolllock: 'ScrollLock',
    numpadenter: 'NumpadEnter',
  };

  const mapped = keyMap[key.toLowerCase()];
  if (mapped) return mapped;

  // Single characters are always valid (a-z, 0-9, punctuation)
  if (key.length === 1) return key as KeyInput;

  // Pass through unknown multi-character keys (e.g., F13-F24, Numpad keys,
  // lateralized modifiers like ShiftLeft/ControlRight, media keys).
  // These are valid CDP/DOM key values not covered by the alias map.
  return key as KeyInput;
}

export function registerComputerTool(server: MCPServer): void {
  server.registerTool('computer', handler, definition);
}
