import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'webpage-mcp-shared';
import { cdpSessionManager } from '@/utils/cdp-session-manager';
import { consoleBuffer, BufferedConsoleMessage, BufferedConsoleException } from './console-buffer';

const DEFAULT_MAX_MESSAGES = 100;

type ConsoleMode = 'snapshot' | 'buffer';

interface ConsoleToolParams {
  url?: string;
  tabId?: number;
  background?: boolean;
  windowId?: number;
  includeExceptions?: boolean;
  maxMessages?: number;
  // New parameters
  mode?: ConsoleMode;
  buffer?: boolean; // mode="buffer" alias
  clear?: boolean; // Clear before reading
  clearAfterRead?: boolean; // Empty after reading (mcp-tools.js style)
  pattern?: string;
  onlyErrors?: boolean;
  limit?: number;
}

interface ConsoleMessage {
  timestamp: number;
  level: string;
  text: string;
  args?: any[];
  argsSerialized?: any[];
  source?: string;
  url?: string | null;
  urlRedacted?: boolean;
  lineNumber?: number;
  stackTrace?: any;
}

interface ConsoleException {
  timestamp: number;
  text: string;
  url?: string | null;
  urlRedacted?: boolean;
  lineNumber?: number;
  columnNumber?: number;
  stackTrace?: any;
}

interface ConsoleResult {
  success: boolean;
  message: string;
  tabId: number;
  tabUrl: string;
  tabTitle: string;
  captureStartTime: number;
  captureEndTime: number;
  totalDurationMs: number;
  messages: ConsoleMessage[];
  exceptions: ConsoleException[];
  messageCount: number;
  exceptionCount: number;
  messageLimitReached: boolean;
  droppedMessageCount: number;
  droppedExceptionCount: number;
}

// Helper function

function normalizeLimit(value: unknown, fallback: number): number {
  const n = typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : fallback;
  return Math.max(0, n);
}

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 parseRegexPattern(pattern?: string): RegExp | undefined {
  if (typeof pattern !== 'string') return undefined;
  const trimmed = pattern.trim();
  if (!trimmed) return undefined;
  // Support /pattern/flags syntax
  const match = trimmed.match(/^\/(.+)\/([gimsuy]*)$/);
  try {
    return match ? new RegExp(match[1], match[2]) : new RegExp(trimmed);
  } catch (e: unknown) {
    const msg = e instanceof Error ? e.message : String(e);
    throw new Error(`Invalid regex pattern: ${msg}`);
  }
}

function matchesPattern(pattern: RegExp, text: string): boolean {
  pattern.lastIndex = 0;
  return pattern.test(text);
}

function sanitizeConsoleUrl(url: unknown): { url?: string | null; urlRedacted?: true } {
  if (typeof url !== 'string' || !url.trim()) {
    return {};
  }
  if (hasDisallowedPublicPageScheme(url)) {
    return { url: null, urlRedacted: true };
  }
  return { url };
}

function sanitizeNestedUrls(value: unknown, depth = 0): unknown {
  if (depth > 8 || value === null || value === undefined) {
    return value;
  }
  if (Array.isArray(value)) {
    return value.map((item) => sanitizeNestedUrls(item, depth + 1));
  }
  if (typeof value !== 'object') {
    return value;
  }

  const result: Record<string, unknown> = {};
  for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
    if (key === 'url') {
      const sanitized = sanitizeConsoleUrl(nested);
      if ('url' in sanitized) {
        result.url = sanitized.url;
      }
      if (sanitized.urlRedacted) {
        result.urlRedacted = true;
      }
      continue;
    }
    result[key] = sanitizeNestedUrls(nested, depth + 1);
  }
  return result;
}

function sanitizeConsoleMessage(message: ConsoleMessage): ConsoleMessage {
  const sanitizedUrl = sanitizeConsoleUrl(message.url);
  return {
    ...message,
    ...sanitizedUrl,
    ...(message.stackTrace ? { stackTrace: sanitizeNestedUrls(message.stackTrace) } : {}),
  };
}

function sanitizeConsoleException(exception: ConsoleException): ConsoleException {
  const sanitizedUrl = sanitizeConsoleUrl(exception.url);
  return {
    ...exception,
    ...sanitizedUrl,
    ...(exception.stackTrace ? { stackTrace: sanitizeNestedUrls(exception.stackTrace) } : {}),
  };
}

function isErrorLevel(level?: string): boolean {
  const normalized = (level || '').toLowerCase();
  return normalized === 'error' || normalized === 'assert';
}

function applyResultFilters(
  result: ConsoleResult,
  options: { pattern?: RegExp; onlyErrors?: boolean; includeExceptions: boolean },
): ConsoleResult {
  const { pattern, onlyErrors = false, includeExceptions } = options;

  let messages = result.messages;
  if (onlyErrors) {
    messages = messages.filter((m) => isErrorLevel(m.level));
  }
  if (pattern) {
    messages = messages.filter((m) => matchesPattern(pattern, m.text || ''));
  }

  let exceptions = includeExceptions ? result.exceptions : [];
  if (includeExceptions && pattern) {
    exceptions = exceptions.filter((e) => matchesPattern(pattern, e.text || ''));
  }

  return {
    ...result,
    messages,
    exceptions,
    messageCount: messages.length,
    exceptionCount: exceptions.length,
  };
}

function isDebuggerConflictError(error: unknown): boolean {
  const msg = (error instanceof Error ? error.message : String(error)).toLowerCase();
  return msg.includes('debugger is already attached') || msg.includes('another client');
}

function formatDebuggerConflictMessage(tabId: number, originalMessage: string): string {
  return (
    `Failed to attach Chrome Debugger to tab ${tabId}: another debugger client is already attached ` +
    `(likely DevTools or another extension). Close DevTools for this tab or disable the conflicting extension, ` +
    `then retry. Original error: ${originalMessage}`
  );
}

/**
 * Tool for capturing console output from browser tabs
 */
class ConsoleTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.CONSOLE;

  async execute(args: ConsoleToolParams): Promise<ToolResult> {
    const {
      url,
      tabId,
      windowId,
      background = false,
      includeExceptions = true,
      maxMessages = DEFAULT_MAX_MESSAGES,
      mode = 'snapshot',
      buffer,
      clear = false,
      clearAfterRead = false,
      pattern,
      onlyErrors = false,
      limit,
    } = args;

    let targetTab: chrome.tabs.Tab;
    let targetTabId: number | undefined;

    // Parsing regular expressions
    let compiledPattern: RegExp | undefined;
    try {
      compiledPattern = parseRegexPattern(pattern);
    } catch (e: unknown) {
      const msg = e instanceof Error ? e.message : String(e);
      return createErrorResponse(msg);
    }

    try {
      if (url && hasDisallowedPublicPageScheme(url)) {
        return createErrorResponse(
          'Only http:// and https:// pages are supported by chrome_console',
        );
      }

      if (typeof tabId === 'number') {
        // Use explicit tab
        const t = await chrome.tabs.get(tabId);
        if (!t?.id) return createErrorResponse('Failed to identify target tab.');
        targetTab = t;
      } else if (url) {
        // Navigate to the specified URL
        targetTab = await this.navigateToUrl(url, background === true, windowId);
      } else {
        // Use current active tab
        const [activeTab] =
          typeof windowId === 'number'
            ? await chrome.tabs.query({ active: true, windowId })
            : await chrome.tabs.query({ active: true, currentWindow: true });
        if (!activeTab?.id) {
          return createErrorResponse('No active tab found and no URL provided.');
        }
        targetTab = activeTab;
      }

      if (!targetTab?.id) {
        return createErrorResponse('Failed to identify target tab.');
      }
      if (hasDisallowedPublicPageScheme(String(targetTab.url || ''))) {
        return createErrorResponse(
          'Only http:// and https:// pages are supported by chrome_console',
        );
      }

      targetTabId = targetTab.id;

      // Determine the mode: the buffer parameter is an alias for mode="buffer"
      const resolvedMode: ConsoleMode =
        mode === 'buffer' || buffer === true ? 'buffer' : 'snapshot';

      // Calculate effective message limit
      const normalizedMaxMessages = normalizeLimit(maxMessages, DEFAULT_MAX_MESSAGES);
      const effectiveLimit =
        typeof limit === 'number'
          ? normalizeLimit(limit, normalizedMaxMessages)
          : normalizedMaxMessages;

      // Buffer mode
      if (resolvedMode === 'buffer') {
        try {
          await consoleBuffer.ensureStarted(targetTabId);
        } catch (error: unknown) {
          const msg = error instanceof Error ? error.message : String(error);
          if (isDebuggerConflictError(error)) {
            return createErrorResponse(formatDebuggerConflictMessage(targetTabId, msg));
          }
          throw error;
        }

        // Handle flush requests before reading
        let clearedBefore: { clearedMessages: number; clearedExceptions: number } | null = null;
        if (clear === true) {
          clearedBefore = consoleBuffer.clear(targetTabId, 'manual');
        }

        // Read buffer
        const read = consoleBuffer.read(targetTabId, {
          pattern: compiledPattern,
          onlyErrors,
          limit: effectiveLimit,
          includeExceptions,
        });

        if (!read) {
          return createErrorResponse('Console buffer is not available for this tab.');
        }

        // Handle clearing requests after reading (mcp-tools.js style to avoid repeated reading)
        let clearedAfter: { clearedMessages: number; clearedExceptions: number } | null = null;
        if (clearAfterRead === true) {
          clearedAfter = consoleBuffer.clear(targetTabId, 'manual');
        }

        // Build clean summary
        let clearedSummary = '';
        if (clearedBefore) {
          clearedSummary += ` Cleared ${clearedBefore.clearedMessages} messages and ${clearedBefore.clearedExceptions} exceptions before reading.`;
        }
        if (clearedAfter) {
          clearedSummary += ` Cleared ${clearedAfter.clearedMessages} messages and ${clearedAfter.clearedExceptions} exceptions after reading.`;
        }

        const result: ConsoleResult = {
          success: true,
          message:
            `Console buffer read for tab ${targetTabId}.` +
            clearedSummary +
            ` Returned ${read.messageCount} messages and ${read.exceptionCount} exceptions.`,
          tabId: targetTabId,
          tabUrl: read.tabUrl || '',
          tabTitle: read.tabTitle || '',
          captureStartTime: read.captureStartTime,
          captureEndTime: read.captureEndTime,
          totalDurationMs: read.totalDurationMs,
          messages: (read.messages as ConsoleMessage[]).map(sanitizeConsoleMessage),
          exceptions: (read.exceptions as ConsoleException[]).map(sanitizeConsoleException),
          messageCount: read.messageCount,
          exceptionCount: read.exceptionCount,
          messageLimitReached: read.messageLimitReached,
          droppedMessageCount: read.droppedMessageCount,
          droppedExceptionCount: read.droppedExceptionCount,
        };

        return {
          content: [{ type: 'text', text: JSON.stringify(result) }],
          isError: false,
        };
      }

      // Snapshot Mode (one-shot capture)
      const result = await this.captureConsoleMessages(targetTabId, {
        includeExceptions,
        maxMessages: effectiveLimit,
      });

      // Apply filter
      const filtered = applyResultFilters(result, {
        pattern: compiledPattern,
        onlyErrors,
        includeExceptions,
      });

      return {
        content: [{ type: 'text', text: JSON.stringify(filtered) }],
        isError: false,
      };
    } catch (error: unknown) {
      console.error('ConsoleTool: Critical error during execute:', error);
      const msg = error instanceof Error ? error.message : String(error);
      if (typeof targetTabId === 'number' && isDebuggerConflictError(error)) {
        return createErrorResponse(formatDebuggerConflictMessage(targetTabId, msg));
      }
      return createErrorResponse(`Error in ConsoleTool: ${msg}`);
    }
  }

  private async navigateToUrl(
    url: string,
    background = false,
    windowId?: number,
  ): Promise<chrome.tabs.Tab> {
    // Check if URL is already open
    const existingTabs = await chrome.tabs.query({ url });

    if (existingTabs.length > 0 && existingTabs[0]?.id) {
      const tab = existingTabs[0];
      if (!background) {
        // Activate the existing tab
        await chrome.tabs.update(tab.id!, { active: true });
        await chrome.windows.update(tab.windowId, { focused: true });
      }
      return tab;
    } else {
      // Create new tab with the URL
      const createInfo: chrome.tabs.CreateProperties = { url, active: background ? false : true };
      if (typeof windowId === 'number') createInfo.windowId = windowId;
      const newTab = await chrome.tabs.create(createInfo);
      // Wait for tab to be ready
      await this.waitForTabReady(newTab.id!);
      return newTab;
    }
  }

  private async waitForTabReady(tabId: number): Promise<void> {
    return new Promise((resolve) => {
      const checkTab = async () => {
        try {
          const tab = await chrome.tabs.get(tabId);
          if (tab.status === 'complete') {
            resolve();
          } else {
            setTimeout(checkTab, 100);
          }
        } catch (error) {
          // Tab might be closed, resolve anyway
          resolve();
        }
      };
      checkTab();
    });
  }

  private formatConsoleArgs(args: any[]): string {
    if (!args || args.length === 0) return '';

    return args
      .map((arg) => {
        if (arg.type === 'string') {
          return arg.value || '';
        } else if (arg.type === 'number') {
          return String(arg.value || '');
        } else if (arg.type === 'boolean') {
          return String(arg.value || '');
        } else if (arg.type === 'object') {
          return arg.description || '[Object]';
        } else if (arg.type === 'undefined') {
          return 'undefined';
        } else if (arg.type === 'function') {
          return arg.description || '[Function]';
        } else {
          return arg.description || arg.value || String(arg);
        }
      })
      .join(' ');
  }

  private async captureConsoleMessages(
    tabId: number,
    options: {
      includeExceptions: boolean;
      maxMessages: number;
    },
  ): Promise<ConsoleResult> {
    const { includeExceptions, maxMessages } = options;
    const startTime = Date.now();
    const messages: ConsoleMessage[] = [];
    const exceptions: ConsoleException[] = [];
    let limitReached = false;

    try {
      // Get tab information
      const tab = await chrome.tabs.get(tabId);

      // Attach via shared manager
      await cdpSessionManager.attach(tabId, 'console');

      // Set up event listener to collect messages
      const collectedMessages: any[] = [];
      const collectedExceptions: any[] = [];

      const eventListener = (source: chrome.debugger.Debuggee, method: string, params?: any) => {
        if (source.tabId !== tabId) return;

        if (method === 'Log.entryAdded' && params?.entry) {
          collectedMessages.push(params.entry);
        } else if (method === 'Runtime.consoleAPICalled' && params) {
          // Convert Runtime.consoleAPICalled to Log.entryAdded format
          const logEntry = {
            timestamp: params.timestamp,
            level: params.type || 'log',
            text: this.formatConsoleArgs(params.args || []),
            source: 'console-api',
            url: params.stackTrace?.callFrames?.[0]?.url,
            lineNumber: params.stackTrace?.callFrames?.[0]?.lineNumber,
            stackTrace: params.stackTrace,
            args: params.args,
          };
          collectedMessages.push(logEntry);
        } else if (
          method === 'Runtime.exceptionThrown' &&
          includeExceptions &&
          params?.exceptionDetails
        ) {
          collectedExceptions.push(params.exceptionDetails);
        }
      };

      chrome.debugger.onEvent.addListener(eventListener);

      try {
        // Enable Runtime domain first to capture console API calls and exceptions
        await cdpSessionManager.sendCommand(tabId, 'Runtime.enable');

        // Also enable Log domain to capture other log entries
        await cdpSessionManager.sendCommand(tabId, 'Log.enable');

        // Wait for all messages to be flushed
        await new Promise((resolve) => setTimeout(resolve, 2000));

        // Process collected messages
        // Helper to deeply serialize console arguments when possible
        const serializeArg = async (arg: any): Promise<any> => {
          try {
            if (!arg) return arg;
            if (Object.prototype.hasOwnProperty.call(arg, 'unserializableValue')) {
              return arg.unserializableValue;
            }
            if (Object.prototype.hasOwnProperty.call(arg, 'value')) {
              return arg.value;
            }
            if (arg.objectId) {
              const resp = await cdpSessionManager.sendCommand(tabId, 'Runtime.callFunctionOn', {
                objectId: arg.objectId,
                functionDeclaration:
                  'function(maxDepth, maxProps){\n' +
                  '  const seen=new WeakSet();\n' +
                  '  function S(v,d){\n' +
                  '    try{\n' +
                  '      if(d<0) return "[MaxDepth]";\n' +
                  '      if(v===null) return null;\n' +
                  '      const t=typeof v;\n' +
                  '      if(t!=="object"){\n' +
                  '        if(t==="bigint") return v.toString()+"n";\n' +
                  '        return v;\n' +
                  '      }\n' +
                  '      if(seen.has(v)) return "[Circular]";\n' +
                  '      seen.add(v);\n' +
                  '      if(Array.isArray(v)){\n' +
                  '        const out=[];\n' +
                  '        for(let i=0;i<v.length;i++){\n' +
                  '          if(i>=maxProps){ out.push("[...truncated]"); break; }\n' +
                  '          out.push(S(v[i], d-1));\n' +
                  '        }\n' +
                  '        return out;\n' +
                  '      }\n' +
                  '      if(v instanceof Date) return {__type:"Date", value:v.toISOString()};\n' +
                  '      if(v instanceof RegExp) return {__type:"RegExp", value:String(v)};\n' +
                  '      if(v instanceof Map){\n' +
                  '        const out={__type:"Map", entries:[]}; let c=0;\n' +
                  '        for(const [k,val] of v.entries()){\n' +
                  '          if(c++>=maxProps){ out.entries.push(["[...truncated]","[...truncated]"]); break; }\n' +
                  '          out.entries.push([S(k,d-1), S(val,d-1)]);\n' +
                  '        }\n' +
                  '        return out;\n' +
                  '      }\n' +
                  '      if(v instanceof Set){\n' +
                  '        const out={__type:"Set", values:[]}; let c=0;\n' +
                  '        for(const val of v.values()){\n' +
                  '          if(c++>=maxProps){ out.values.push("[...truncated]"); break; }\n' +
                  '          out.values.push(S(val,d-1));\n' +
                  '        }\n' +
                  '        return out;\n' +
                  '      }\n' +
                  '      const out={}; let c=0;\n' +
                  '      for(const key in v){\n' +
                  '        if(c++>=maxProps){ out.__truncated__=true; break; }\n' +
                  '        try{ out[key]=S(v[key], d-1); }catch(e){ out[key]="[Thrown]"; }\n' +
                  '      }\n' +
                  '      return out;\n' +
                  '    }catch(e){ return "[Unserializable]" }\n' +
                  '  }\n' +
                  '  return S(this, maxDepth);\n' +
                  '}',
                arguments: [{ value: 3 }, { value: 100 }],
                silent: true,
                returnByValue: true,
              });
              return resp?.result?.value ?? '[Unavailable]';
            }
            return '[Unknown]';
          } catch (e) {
            return '[SerializeError]';
          }
        };

        for (const entry of collectedMessages) {
          if (messages.length >= maxMessages) {
            limitReached = true;
            break;
          }

          const message: ConsoleMessage = {
            timestamp: entry.timestamp,
            level: entry.level || 'log',
            text: entry.text || '',
            source: entry.source,
            ...sanitizeConsoleUrl(entry.url),
            lineNumber: entry.lineNumber,
          };

          if (entry.stackTrace) {
            message.stackTrace = sanitizeNestedUrls(entry.stackTrace);
          }

          if (entry.args && Array.isArray(entry.args)) {
            message.args = entry.args;
            // Attempt deep serialization for better fidelity
            const serialized: any[] = [];
            for (const a of entry.args) {
              serialized.push(await serializeArg(a));
            }
            message.argsSerialized = serialized;
          }

          messages.push(message);
        }

        // Process collected exceptions
        for (const exceptionDetails of collectedExceptions) {
          const exception: ConsoleException = {
            timestamp: Date.now(),
            text:
              exceptionDetails.text ||
              exceptionDetails.exception?.description ||
              'Unknown exception',
            ...sanitizeConsoleUrl(exceptionDetails.url),
            lineNumber: exceptionDetails.lineNumber,
            columnNumber: exceptionDetails.columnNumber,
          };

          if (exceptionDetails.stackTrace) {
            exception.stackTrace = sanitizeNestedUrls(exceptionDetails.stackTrace);
          }

          exceptions.push(exception);
        }
      } finally {
        // Clean up
        chrome.debugger.onEvent.removeListener(eventListener);

        // If buffer mode is using this tab, do not close the Runtime/Log field
        const keepDomainsEnabled = consoleBuffer.isCapturing(tabId);
        if (!keepDomainsEnabled) {
          try {
            await cdpSessionManager.sendCommand(tabId, 'Runtime.disable');
          } catch (e) {
            console.warn(`ConsoleTool: Error disabling Runtime for tab ${tabId}:`, e);
          }

          try {
            await cdpSessionManager.sendCommand(tabId, 'Log.disable');
          } catch (e) {
            console.warn(`ConsoleTool: Error disabling Log for tab ${tabId}:`, e);
          }
        }

        try {
          await cdpSessionManager.detach(tabId, 'console');
        } catch (e) {
          console.warn(`ConsoleTool: Error detaching debugger for tab ${tabId}:`, e);
        }
      }

      const endTime = Date.now();

      // Sort messages by timestamp
      messages.sort((a, b) => a.timestamp - b.timestamp);
      exceptions.sort((a, b) => a.timestamp - b.timestamp);

      return {
        success: true,
        message: `Console capture completed for tab ${tabId}. ${messages.length} messages, ${exceptions.length} exceptions captured.`,
        tabId,
        tabUrl: tab.url || '',
        tabTitle: tab.title || '',
        captureStartTime: startTime,
        captureEndTime: endTime,
        totalDurationMs: endTime - startTime,
        messages,
        exceptions,
        messageCount: messages.length,
        exceptionCount: exceptions.length,
        messageLimitReached: limitReached,
        droppedMessageCount: 0,
        droppedExceptionCount: 0,
      };
    } catch (error: any) {
      console.error(`ConsoleTool: Error capturing console messages for tab ${tabId}:`, error);
      throw error;
    }
  }
}

export const consoleTool = new ConsoleTool();
