import type { Tool, ToolExecutionContext, ToolResult } from '../../types.js';
import type { BrowserManager } from './manager.js';

function abortError(): string {
  return 'Operation aborted';
}

function isAbortError(error: unknown): boolean {
  if (!(error instanceof Error)) {
    return false;
  }
  return error.name === 'AbortError' || error.message.toLowerCase().includes('aborted');
}

function throwIfAborted(signal?: AbortSignal): void {
  if (signal?.aborted) {
    throw new Error(abortError());
  }
}

async function withAbort<T>(
  operation: Promise<T>,
  signal?: AbortSignal,
  onAbort?: () => void,
): Promise<T> {
  if (!signal) {
    return operation;
  }
  if (signal.aborted) {
    onAbort?.();
    throw new Error(abortError());
  }
  return new Promise<T>((resolve, reject) => {
    const cleanup = () => {
      signal.removeEventListener('abort', handleAbort);
    };

    const handleAbort = () => {
      onAbort?.();
      cleanup();
      reject(new Error(abortError()));
    };

    signal.addEventListener('abort', handleAbort, { once: true });

    operation
      .then((value) => {
        cleanup();
        resolve(value);
      })
      .catch((error) => {
        cleanup();
        reject(error);
      });
  });
}

/** Create all browser tools bound to a BrowserManager instance. */
export function createBrowserTools(manager: BrowserManager): Tool[] {
  return [
    createBrowserNavigateTool(manager),
    createBrowserScreenshotTool(manager),
    createBrowserClickTool(manager),
    createBrowserTypeTool(manager),
    createBrowserContentTool(manager),
    createBrowserEvalTool(manager),
    createBrowserEvaluateTool(manager),
  ];
}

function createBrowserNavigateTool(manager: BrowserManager): Tool {
  return {
    name: 'browser.navigate',
    description: 'Navigate to a URL in the browser. Returns the page title and URL after navigation.',
    inputSchema: {
      type: 'object',
      properties: {
        url: { type: 'string', description: 'The URL to navigate to' },
        waitUntil: {
          type: 'string',
          description: 'When to consider navigation complete: load, domcontentloaded, networkidle0, networkidle2 (default: domcontentloaded)',
        },
      },
      required: ['url'],
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as { url: string; waitUntil?: string };
      try {
        throwIfAborted(context?.signal);
        const page = await manager.getPage();
        throwIfAborted(context?.signal);
        const waitUntil = (args.waitUntil ?? 'domcontentloaded') as 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
        await withAbort(
          page.goto(args.url, { waitUntil }),
          context?.signal,
          () => {
            void page.evaluate(() => {
              (globalThis as { stop?: () => void }).stop?.();
            }).catch(() => undefined);
          },
        );
        throwIfAborted(context?.signal);
        const title = await page.title();
        const currentUrl = page.url();
        return {
          success: true,
          output: `Navigated to: ${currentUrl}\nTitle: ${title}`,
        };
      } catch (error) {
        if (isAbortError(error)) {
          return { success: false, output: '', error: abortError() };
        }
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };
}

function createBrowserScreenshotTool(manager: BrowserManager): Tool {
  return {
    name: 'browser.screenshot',
    description: 'Take a screenshot of the current page. Returns the screenshot as a base64-encoded PNG.',
    inputSchema: {
      type: 'object',
      properties: {
        fullPage: { type: 'boolean', description: 'Capture full scrollable page (default: false)' },
        selector: { type: 'string', description: 'CSS selector to screenshot a specific element' },
      },
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as { fullPage?: boolean; selector?: string };
      try {
        throwIfAborted(context?.signal);
        const page = await manager.getPage();
        throwIfAborted(context?.signal);

        let screenshotData: string;
        if (args.selector) {
          const element = await page.$(args.selector);
          if (!element) {
            return { success: false, output: '', error: `Element not found: ${args.selector}` };
          }
          screenshotData = (await element.screenshot({ encoding: 'base64' })) as string;
        } else {
          screenshotData = (await page.screenshot({
            encoding: 'base64',
            fullPage: args.fullPage ?? false,
          })) as string;
        }

        return {
          success: true,
          output: `Screenshot captured (base64 PNG, ${screenshotData.length} chars):\n${screenshotData.slice(0, 200)}...`,
        };
      } catch (error) {
        if (isAbortError(error)) {
          return { success: false, output: '', error: abortError() };
        }
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };
}

function createBrowserClickTool(manager: BrowserManager): Tool {
  return {
    name: 'browser.click',
    description: 'Click an element on the page identified by CSS selector.',
    inputSchema: {
      type: 'object',
      properties: {
        selector: { type: 'string', description: 'CSS selector of the element to click' },
      },
      required: ['selector'],
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as { selector: string };
      try {
        throwIfAborted(context?.signal);
        const page = await manager.getPage();
        throwIfAborted(context?.signal);
        await withAbort(page.click(args.selector), context?.signal);
        return { success: true, output: `Clicked element: ${args.selector}` };
      } catch (error) {
        if (isAbortError(error)) {
          return { success: false, output: '', error: abortError() };
        }
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };
}

function createBrowserTypeTool(manager: BrowserManager): Tool {
  return {
    name: 'browser.type',
    description: 'Type text into an input element on the page.',
    inputSchema: {
      type: 'object',
      properties: {
        selector: { type: 'string', description: 'CSS selector of the input element' },
        text: { type: 'string', description: 'Text to type' },
        clear: { type: 'boolean', description: 'Clear the field before typing (default: false)' },
      },
      required: ['selector', 'text'],
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as { selector: string; text: string; clear?: boolean };
      try {
        throwIfAborted(context?.signal);
        const page = await manager.getPage();
        throwIfAborted(context?.signal);
        if (args.clear) {
          await withAbort(page.click(args.selector, { count: 3 }), context?.signal); // Select all
          await withAbort(page.keyboard.press('Backspace'), context?.signal);
        }
        await withAbort(page.type(args.selector, args.text), context?.signal);
        return { success: true, output: `Typed "${args.text}" into ${args.selector}` };
      } catch (error) {
        if (isAbortError(error)) {
          return { success: false, output: '', error: abortError() };
        }
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };
}

function createBrowserContentTool(manager: BrowserManager): Tool {
  return {
    name: 'browser.content',
    description: 'Get the text content of the page or a specific element. Returns extracted text (not raw HTML).',
    inputSchema: {
      type: 'object',
      properties: {
        selector: { type: 'string', description: 'CSS selector to get content of a specific element (default: body)' },
        maxLength: { type: 'number', description: 'Maximum characters to return (default: 10000)' },
      },
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as { selector?: string; maxLength?: number };
      try {
        throwIfAborted(context?.signal);
        const page = await manager.getPage();
        throwIfAborted(context?.signal);
        const selector = args.selector ?? 'body';
        const maxLength = args.maxLength ?? 10000;

        // The $eval callback executes in the browser context where DOM types exist,
        // but TS checks against Node.js lib (no DOM). Use `any` to bridge the gap.
        const text = await withAbort(page.$eval(selector, (el) => {
          const htmlEl = el as unknown as { innerText?: string; textContent?: string | null };
          return htmlEl.innerText || htmlEl.textContent || '';
        }), context?.signal);

        const truncated = text.length > maxLength
          ? text.slice(0, maxLength) + `\n... (truncated, ${text.length} total chars)`
          : text;

        const title = await page.title();
        const url = page.url();

        return {
          success: true,
          output: `URL: ${url}\nTitle: ${title}\n\n${truncated}`,
        };
      } catch (error) {
        if (isAbortError(error)) {
          return { success: false, output: '', error: abortError() };
        }
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };
}

function createBrowserEvalTool(manager: BrowserManager): Tool {
  return createBrowserEvalLikeTool(
    manager,
    'browser.eval',
    'Evaluate JavaScript in the browser page context. Returns the result as a string.',
  );
}

function createBrowserEvaluateTool(manager: BrowserManager): Tool {
  return createBrowserEvalLikeTool(
    manager,
    'browser.evaluate',
    'Alias of browser.eval for compatibility. Evaluates JavaScript in the browser page context.',
  );
}

function createBrowserEvalLikeTool(manager: BrowserManager, name: 'browser.eval' | 'browser.evaluate', description: string): Tool {
  return {
    name,
    description,
    inputSchema: {
      type: 'object',
      properties: {
        expression: { type: 'string', description: 'JavaScript expression to evaluate in the page context' },
      },
      required: ['expression'],
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as { expression: string };
      try {
        throwIfAborted(context?.signal);
        const page = await manager.getPage();
        throwIfAborted(context?.signal);
        // Use evaluate with a function that evaluates the expression string
        const result = await withAbort(page.evaluate((expr: string) => {
          // eslint-disable-next-line no-eval
          return eval(expr);
        }, args.expression), context?.signal);
        const output = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
        return { success: true, output: output ?? 'undefined' };
      } catch (error) {
        if (isAbortError(error)) {
          return { success: false, output: '', error: abortError() };
        }
        return {
          success: false,
          output: '',
          error: error instanceof Error ? error.message : String(error),
        };
      }
    },
  };
}
