import { createErrorResponse, ToolResult } from '@/common/tool-handler';
import { BaseBrowserToolExecutor } from '../base-browser';
import { TOOL_NAMES } from 'chrome-mcp-shared';

interface NetworkDebuggerStartToolParams {
  url?: string; // URL to navigate to or focus. If not provided, uses active tab.
  maxCaptureTime?: number;
  inactivityTimeout?: number; // Inactivity timeout (milliseconds)
  includeStatic?: boolean; // if include static resources
}

// Network request object interface
interface NetworkRequestInfo {
  requestId: string;
  url: string;
  method: string;
  requestHeaders?: Record<string, string>; // Will be removed after common headers extraction
  responseHeaders?: Record<string, string>; // Will be removed after common headers extraction
  requestTime?: number; // Timestamp of the request
  responseTime?: number; // Timestamp of the response
  type: string; // Resource type (e.g., Document, XHR, Fetch, Script, Stylesheet)
  status: string; // 'pending', 'complete', 'error'
  statusCode?: number;
  statusText?: string;
  requestBody?: string;
  responseBody?: string;
  base64Encoded?: boolean; // For responseBody
  encodedDataLength?: number; // Actual bytes received
  errorText?: string; // If loading failed
  canceled?: boolean; // If loading was canceled
  mimeType?: string;
  specificRequestHeaders?: Record<string, string>; // Headers unique to this request
  specificResponseHeaders?: Record<string, string>; // Headers unique to this response
  [key: string]: any; // Allow other properties from debugger events
}

// Static resource file extensions list
const STATIC_RESOURCE_EXTENSIONS = [
  '.png',
  '.jpg',
  '.jpeg',
  '.gif',
  '.bmp',
  '.webp',
  '.svg',
  '.ico',
  '.cur',
  '.css',
  '.woff',
  '.woff2',
  '.ttf',
  '.eot',
  '.otf',
  '.mp3',
  '.mp4',
  '.avi',
  '.mov',
  '.webm',
  '.ogg',
  '.wav',
  '.pdf',
  '.zip',
  '.rar',
  '.7z',
  '.iso',
  '.dmg',
  '.js',
  '.jsx',
  '.ts',
  '.tsx',
  '.map', // Source maps
];

// Ad and analytics domains list
const AD_ANALYTICS_DOMAINS = [
  'google-analytics.com',
  'googletagmanager.com',
  'analytics.google.com',
  'doubleclick.net',
  'googlesyndication.com',
  'googleads.g.doubleclick.net',
  'facebook.com/tr',
  'connect.facebook.net',
  'bat.bing.com',
  'linkedin.com', // Often for tracking pixels/insights
  'analytics.twitter.com',
  'static.hotjar.com',
  'script.hotjar.com',
  'stats.g.doubleclick.net',
  'amazon-adsystem.com',
  'adservice.google.com',
  'pagead2.googlesyndication.com',
  'ads-twitter.com',
  'ads.yahoo.com',
  'adroll.com',
  'adnxs.com',
  'criteo.com',
  'quantserve.com',
  'scorecardresearch.com',
  'segment.io',
  'amplitude.com',
  'mixpanel.com',
  'optimizely.com',
  'crazyegg.com',
  'clicktale.net',
  'mouseflow.com',
  'fullstory.com',
  'clarity.ms',
];

const DEBUGGER_PROTOCOL_VERSION = '1.3';
const MAX_RESPONSE_BODY_SIZE_BYTES = 1 * 1024 * 1024; // 1MB
const DEFAULT_MAX_CAPTURE_TIME_MS = 3 * 60 * 1000; // 3 minutes
const DEFAULT_INACTIVITY_TIMEOUT_MS = 60 * 1000; // 1 minute

/**
 * Network capture start tool - uses Chrome Debugger API to start capturing network requests
 */
class NetworkDebuggerStartTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.NETWORK_DEBUGGER_START;
  private captureData: Map<number, any> = new Map(); // tabId -> capture data
  private captureTimers: Map<number, NodeJS.Timeout> = new Map(); // tabId -> max capture timer
  private inactivityTimers: Map<number, NodeJS.Timeout> = new Map(); // tabId -> inactivity timer
  private lastActivityTime: Map<number, number> = new Map(); // tabId -> timestamp of last network activity
  private pendingResponseBodies: Map<string, Promise<any>> = new Map(); // requestId -> promise for getResponseBody
  private requestCounters: Map<number, number> = new Map(); // tabId -> count of captured requests (after filtering)
  private static MAX_REQUESTS_PER_CAPTURE = 100; // Max requests to store to prevent memory issues
  public static instance: NetworkDebuggerStartTool | null = null;

  constructor() {
    super();
    if (NetworkDebuggerStartTool.instance) {
      return NetworkDebuggerStartTool.instance;
    }
    NetworkDebuggerStartTool.instance = this;

    // Only add listeners if chrome APIs are available (not during build)
    if (typeof chrome !== 'undefined' && chrome.debugger?.onEvent) {
      chrome.debugger.onEvent.addListener(this.handleDebuggerEvent.bind(this));
      chrome.debugger.onDetach.addListener(this.handleDebuggerDetach.bind(this));
      chrome.tabs.onRemoved.addListener(this.handleTabRemoved.bind(this));
      chrome.tabs.onCreated.addListener(this.handleTabCreated.bind(this));
    }
  }

  private handleTabRemoved(tabId: number) {
    if (this.captureData.has(tabId)) {
      console.log(`NetworkDebuggerStartTool: Tab ${tabId} was closed, cleaning up resources.`);
      this.cleanupCapture(tabId);
    }
  }

  /**
   * Handle tab creation events
   * If a new tab is opened from a tab that is currently capturing, automatically start capturing the new tab's requests
   */
  private async handleTabCreated(tab: chrome.tabs.Tab) {
    try {
      // Check if there are any tabs currently capturing
      if (this.captureData.size === 0) return;

      // Get the openerTabId of the new tab (ID of the tab that opened this tab)
      const openerTabId = tab.openerTabId;
      if (!openerTabId) return;

      // Check if the opener tab is currently capturing
      if (!this.captureData.has(openerTabId)) return;

      // Get the new tab's ID
      const newTabId = tab.id;
      if (!newTabId) return;

      console.log(
        `NetworkDebuggerStartTool: New tab ${newTabId} created from capturing tab ${openerTabId}, will extend capture to it.`,
      );

      // Get the opener tab's capture settings
      const openerCaptureInfo = this.captureData.get(openerTabId);
      if (!openerCaptureInfo) return;

      // Wait a short time to ensure the tab is ready
      await new Promise((resolve) => setTimeout(resolve, 500));

      // Start capturing requests for the new tab
      await this.startCaptureForTab(newTabId, {
        maxCaptureTime: openerCaptureInfo.maxCaptureTime,
        inactivityTimeout: openerCaptureInfo.inactivityTimeout,
        includeStatic: openerCaptureInfo.includeStatic,
      });

      console.log(`NetworkDebuggerStartTool: Successfully extended capture to new tab ${newTabId}`);
    } catch (error) {
      console.error(`NetworkDebuggerStartTool: Error extending capture to new tab:`, error);
    }
  }

  /**
   * Start network request capture for specified tab
   * @param tabId Tab ID
   * @param options Capture options
   */
  private async startCaptureForTab(
    tabId: number,
    options: {
      maxCaptureTime: number;
      inactivityTimeout: number;
      includeStatic: boolean;
    },
  ): Promise<void> {
    const { maxCaptureTime, inactivityTimeout, includeStatic } = options;

    // If already capturing, stop first
    if (this.captureData.has(tabId)) {
      console.log(
        `NetworkDebuggerStartTool: Already capturing on tab ${tabId}. Stopping previous session.`,
      );
      await this.stopCapture(tabId);
    }

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

      // Check if debugger is already attached
      const targets = await chrome.debugger.getTargets();
      const existingTarget = targets.find(
        (t) => t.tabId === tabId && t.attached && t.type === 'page',
      );
      if (existingTarget && !existingTarget.extensionId) {
        throw new Error(
          `Debugger is already attached to tab ${tabId} by another tool (e.g., DevTools).`,
        );
      }

      // Attach debugger
      try {
        await chrome.debugger.attach({ tabId }, DEBUGGER_PROTOCOL_VERSION);
      } catch (error: any) {
        if (error.message?.includes('Cannot attach to the target with an attached client')) {
          throw new Error(
            `Debugger is already attached to tab ${tabId}. This might be DevTools or another extension.`,
          );
        }
        throw error;
      }

      // Enable network tracking
      try {
        await chrome.debugger.sendCommand({ tabId }, 'Network.enable');
      } catch (error: any) {
        await chrome.debugger
          .detach({ tabId })
          .catch((e) => console.warn('Error detaching after failed enable:', e));
        throw error;
      }

      // Initialize capture data
      this.captureData.set(tabId, {
        startTime: Date.now(),
        tabUrl: tab.url,
        tabTitle: tab.title,
        maxCaptureTime,
        inactivityTimeout,
        includeStatic,
        requests: {},
        limitReached: false,
      });

      // Initialize request counter
      this.requestCounters.set(tabId, 0);

      // Update last activity time
      this.updateLastActivityTime(tabId);

      console.log(
        `NetworkDebuggerStartTool: Started capture for tab ${tabId} (${tab.url}). Max requests: ${NetworkDebuggerStartTool.MAX_REQUESTS_PER_CAPTURE}, Max time: ${maxCaptureTime}ms, Inactivity: ${inactivityTimeout}ms.`,
      );

      // Set maximum capture time
      if (maxCaptureTime > 0) {
        this.captureTimers.set(
          tabId,
          setTimeout(async () => {
            console.log(
              `NetworkDebuggerStartTool: Max capture time (${maxCaptureTime}ms) reached for tab ${tabId}.`,
            );
            await this.stopCapture(tabId, true); // Auto-stop due to max time
          }, maxCaptureTime),
        );
      }
    } catch (error: any) {
      console.error(`NetworkDebuggerStartTool: Error starting capture for tab ${tabId}:`, error);

      // Clean up resources
      if (this.captureData.has(tabId)) {
        await chrome.debugger
          .detach({ tabId })
          .catch((e) => console.warn('Cleanup detach error:', e));
        this.cleanupCapture(tabId);
      }

      throw error;
    }
  }

  private handleDebuggerEvent(source: chrome.debugger.Debuggee, method: string, params?: any) {
    if (!source.tabId) return;

    const tabId = source.tabId;
    const captureInfo = this.captureData.get(tabId);

    if (!captureInfo) return; // Not capturing for this tab

    // Update last activity time for any relevant network event
    this.updateLastActivityTime(tabId);

    switch (method) {
      case 'Network.requestWillBeSent':
        this.handleRequestWillBeSent(tabId, params);
        break;
      case 'Network.responseReceived':
        this.handleResponseReceived(tabId, params);
        break;
      case 'Network.loadingFinished':
        this.handleLoadingFinished(tabId, params);
        break;
      case 'Network.loadingFailed':
        this.handleLoadingFailed(tabId, params);
        break;
    }
  }

  private handleDebuggerDetach(source: chrome.debugger.Debuggee, reason: string) {
    if (source.tabId && this.captureData.has(source.tabId)) {
      console.log(
        `NetworkDebuggerStartTool: Debugger detached from tab ${source.tabId}, reason: ${reason}. Cleaning up.`,
      );
      // Potentially inform the user or log the result if the detachment was unexpected
      this.cleanupCapture(source.tabId); // Ensure cleanup happens
    }
  }

  private updateLastActivityTime(tabId: number) {
    this.lastActivityTime.set(tabId, Date.now());
    const captureInfo = this.captureData.get(tabId);

    if (captureInfo && captureInfo.inactivityTimeout > 0) {
      if (this.inactivityTimers.has(tabId)) {
        clearTimeout(this.inactivityTimers.get(tabId)!);
      }
      this.inactivityTimers.set(
        tabId,
        setTimeout(() => this.checkInactivity(tabId), captureInfo.inactivityTimeout),
      );
    }
  }

  private checkInactivity(tabId: number) {
    const captureInfo = this.captureData.get(tabId);
    if (!captureInfo) return;

    const lastActivity = this.lastActivityTime.get(tabId) || captureInfo.startTime; // Use startTime if no activity yet
    const now = Date.now();
    const inactiveTime = now - lastActivity;

    if (inactiveTime >= captureInfo.inactivityTimeout) {
      console.log(
        `NetworkDebuggerStartTool: No activity for ${inactiveTime}ms (threshold: ${captureInfo.inactivityTimeout}ms), stopping capture for tab ${tabId}`,
      );
      this.stopCaptureByInactivity(tabId);
    } else {
      // Reschedule check for the remaining time, this handles system sleep or other interruptions
      const remainingTime = Math.max(0, captureInfo.inactivityTimeout - inactiveTime);
      this.inactivityTimers.set(
        tabId,
        setTimeout(() => this.checkInactivity(tabId), remainingTime),
      );
    }
  }

  private async stopCaptureByInactivity(tabId: number) {
    const captureInfo = this.captureData.get(tabId);
    if (!captureInfo) return;

    console.log(`NetworkDebuggerStartTool: Stopping capture due to inactivity for tab ${tabId}.`);
    // Potentially, we might want to notify the client/user that this happened.
    // For now, just stop and make the results available if StopTool is called.
    await this.stopCapture(tabId, true); // Pass a flag indicating it's an auto-stop
  }

  // Static resource MIME types list (used when includeStatic is false)
  private static STATIC_MIME_TYPES_TO_FILTER = [
    'image/', // all image types (image/png, image/jpeg, etc.)
    'font/', // all font types (font/woff, font/ttf, etc.)
    'audio/', // all audio types
    'video/', // all video types
    'text/css',
    // Note: text/javascript, application/javascript etc. are often filtered by extension.
    // If script files need to be filtered by MIME type as well, add them here.
    // 'application/javascript',
    // 'application/x-javascript',
    'application/pdf',
    'application/zip',
    'application/octet-stream', // Often used for downloads or generic binary data
  ];

  // API-like response MIME types (these are generally NOT filtered, and we might want their bodies)
  private static API_MIME_TYPES = [
    'application/json',
    'application/xml',
    'text/xml',
    // 'text/json' is not standard, but sometimes seen. 'application/json' is preferred.
    'text/plain', // Can be API response, handle with care. Often captured.
    'application/x-www-form-urlencoded', // Form submissions, can be API calls
    'application/graphql',
    // Add other common API types if needed
  ];

  private shouldFilterRequestByUrl(url: string): boolean {
    try {
      const urlObj = new URL(url);
      // Filter ad/analytics domains
      if (AD_ANALYTICS_DOMAINS.some((domain) => urlObj.hostname.includes(domain))) {
        // console.log(`NetworkDebuggerStartTool: Filtering ad/analytics domain: ${urlObj.hostname}`);
        return true;
      }
      return false;
    } catch (e) {
      // Invalid URL? Log and don't filter.
      console.error(`NetworkDebuggerStartTool: Error parsing URL for filtering: ${url}`, e);
      return false;
    }
  }

  private shouldFilterRequestByExtension(url: string, includeStatic: boolean): boolean {
    if (includeStatic) return false; // If including static, don't filter by extension

    try {
      const urlObj = new URL(url);
      const path = urlObj.pathname.toLowerCase();
      if (STATIC_RESOURCE_EXTENSIONS.some((ext) => path.endsWith(ext))) {
        // console.log(`NetworkDebuggerStartTool: Filtering static resource by extension: ${path}`);
        return true;
      }
      return false;
    } catch (e) {
      console.error(
        `NetworkDebuggerStartTool: Error parsing URL for extension filtering: ${url}`,
        e,
      );
      return false;
    }
  }

  // MIME type-based filtering, called after response is received
  private shouldFilterByMimeType(mimeType: string, includeStatic: boolean): boolean {
    if (!mimeType) return false; // No MIME type, don't make a decision based on it here

    // If API_MIME_TYPES contains this mimeType, we explicitly DON'T want to filter it by MIME.
    if (NetworkDebuggerStartTool.API_MIME_TYPES.some((apiMime) => mimeType.startsWith(apiMime))) {
      return false;
    }

    // If we are NOT including static files, then check against the list of static MIME types.
    if (!includeStatic) {
      if (
        NetworkDebuggerStartTool.STATIC_MIME_TYPES_TO_FILTER.some((staticMime) =>
          mimeType.startsWith(staticMime),
        )
      ) {
        // console.log(`NetworkDebuggerStartTool: Filtering static resource by MIME type: ${mimeType}`);
        return true;
      }
    }

    // Default: don't filter by MIME type if no other rule matched
    return false;
  }

  private handleRequestWillBeSent(tabId: number, params: any) {
    const captureInfo = this.captureData.get(tabId);
    if (!captureInfo) return;

    const { requestId, request, timestamp, type, loaderId, frameId } = params;

    // Initial filtering by URL (ads, analytics) and extension (if !includeStatic)
    if (
      this.shouldFilterRequestByUrl(request.url) ||
      this.shouldFilterRequestByExtension(request.url, captureInfo.includeStatic)
    ) {
      return;
    }

    const currentCount = this.requestCounters.get(tabId) || 0;
    if (currentCount >= NetworkDebuggerStartTool.MAX_REQUESTS_PER_CAPTURE) {
      // console.log(`NetworkDebuggerStartTool: Request limit (${NetworkDebuggerStartTool.MAX_REQUESTS_PER_CAPTURE}) reached for tab ${tabId}. Ignoring: ${request.url}`);
      captureInfo.limitReached = true; // Mark that limit was hit
      return;
    }

    // Store initial request info
    // Ensure we don't overwrite if a redirect (same requestId) occurred, though usually loaderId changes
    if (!captureInfo.requests[requestId]) {
      // Or check based on loaderId as well if needed
      captureInfo.requests[requestId] = {
        requestId,
        url: request.url,
        method: request.method,
        requestHeaders: request.headers, // Temporary, will be processed
        requestTime: timestamp * 1000, // Convert seconds to milliseconds
        type: type || 'Other',
        status: 'pending', // Initial status
        loaderId, // Useful for tracking redirects
        frameId, // Useful for context
      };

      if (request.postData) {
        captureInfo.requests[requestId].requestBody = request.postData;
      }
      // console.log(`NetworkDebuggerStartTool: Captured request for tab ${tabId}: ${request.method} ${request.url}`);
    } else {
      // This could be a redirect. Update URL and other relevant fields.
      // Chrome often issues a new `requestWillBeSent` for redirects with the same `requestId` but a new `loaderId`.
      // console.log(`NetworkDebuggerStartTool: Request ${requestId} updated (likely redirect) for tab ${tabId} to URL: ${request.url}`);
      const existingRequest = captureInfo.requests[requestId];
      existingRequest.url = request.url; // Update URL due to redirect
      existingRequest.requestTime = timestamp * 1000; // Update time for the redirected request
      if (request.headers) existingRequest.requestHeaders = request.headers;
      if (request.postData) existingRequest.requestBody = request.postData;
      else delete existingRequest.requestBody;
    }
  }

  private handleResponseReceived(tabId: number, params: any) {
    const captureInfo = this.captureData.get(tabId);
    if (!captureInfo) return;

    const { requestId, response, timestamp, type } = params; // type here is resource type
    const requestInfo: NetworkRequestInfo = captureInfo.requests[requestId];

    if (!requestInfo) {
      // console.warn(`NetworkDebuggerStartTool: Received response for unknown requestId ${requestId} on tab ${tabId}`);
      return;
    }

    // Secondary filtering based on MIME type, now that we have it
    if (this.shouldFilterByMimeType(response.mimeType, captureInfo.includeStatic)) {
      // console.log(`NetworkDebuggerStartTool: Filtering request by MIME type (${response.mimeType}): ${requestInfo.url}`);
      delete captureInfo.requests[requestId]; // Remove from captured data
      // Note: We don't decrement requestCounter here as it's meant to track how many *potential* requests were processed up to MAX_REQUESTS.
      // Or, if MAX_REQUESTS is strictly for *stored* requests, then decrement. For now, let's assume it's for stored.
      // const currentCount = this.requestCounters.get(tabId) || 0;
      // if (currentCount > 0) this.requestCounters.set(tabId, currentCount -1);
      return;
    }

    // If not filtered by MIME, then increment actual stored request counter
    const currentStoredCount = Object.keys(captureInfo.requests).length; // A bit inefficient but accurate
    this.requestCounters.set(tabId, currentStoredCount);

    requestInfo.status = response.status === 0 ? 'pending' : 'complete'; // status 0 can mean pending or blocked
    requestInfo.statusCode = response.status;
    requestInfo.statusText = response.statusText;
    requestInfo.responseHeaders = response.headers; // Temporary
    requestInfo.mimeType = response.mimeType;
    requestInfo.responseTime = timestamp * 1000; // Convert seconds to milliseconds
    if (type) requestInfo.type = type; // Update resource type if provided by this event

    // console.log(`NetworkDebuggerStartTool: Received response for ${requestId} on tab ${tabId}: ${response.status}`);
  }

  private async handleLoadingFinished(tabId: number, params: any) {
    const captureInfo = this.captureData.get(tabId);
    if (!captureInfo) return;

    const { requestId, encodedDataLength } = params;
    const requestInfo: NetworkRequestInfo = captureInfo.requests[requestId];

    if (!requestInfo) {
      // console.warn(`NetworkDebuggerStartTool: LoadingFinished for unknown requestId ${requestId} on tab ${tabId}`);
      return;
    }

    requestInfo.encodedDataLength = encodedDataLength;
    if (requestInfo.status === 'pending') requestInfo.status = 'complete'; // Mark as complete if not already
    // requestInfo.responseTime is usually set by responseReceived, but this timestamp is later.
    // timestamp here is when the resource finished loading. Could be useful for duration calculation.

    if (this.shouldCaptureResponseBody(requestInfo)) {
      try {
        // console.log(`NetworkDebuggerStartTool: Attempting to get response body for ${requestId} (${requestInfo.url})`);
        const responseBodyData = await this.getResponseBody(tabId, requestId);
        if (responseBodyData) {
          if (
            responseBodyData.body &&
            responseBodyData.body.length > MAX_RESPONSE_BODY_SIZE_BYTES
          ) {
            requestInfo.responseBody =
              responseBodyData.body.substring(0, MAX_RESPONSE_BODY_SIZE_BYTES) +
              `\n\n... [Response truncated, total size: ${responseBodyData.body.length} bytes] ...`;
          } else {
            requestInfo.responseBody = responseBodyData.body;
          }
          requestInfo.base64Encoded = responseBodyData.base64Encoded;
          // console.log(`NetworkDebuggerStartTool: Successfully got response body for ${requestId}, size: ${requestInfo.responseBody?.length || 0} bytes`);
        }
      } catch (error) {
        // console.warn(`NetworkDebuggerStartTool: Failed to get response body for ${requestId}:`, error);
        requestInfo.errorText =
          (requestInfo.errorText || '') +
          ` Failed to get body: ${error instanceof Error ? error.message : String(error)}`;
      }
    }
  }

  private shouldCaptureResponseBody(requestInfo: NetworkRequestInfo): boolean {
    const mimeType = requestInfo.mimeType || '';

    // Prioritize API MIME types for body capture
    if (NetworkDebuggerStartTool.API_MIME_TYPES.some((type) => mimeType.startsWith(type))) {
      return true;
    }

    // Heuristics for other potential API calls not perfectly matching MIME types
    const url = requestInfo.url.toLowerCase();
    if (
      /\/(api|service|rest|graphql|query|data|rpc|v[0-9]+)\//i.test(url) ||
      url.includes('.json') ||
      url.includes('json=') ||
      url.includes('format=json')
    ) {
      // If it looks like an API call by URL structure, try to get body,
      // unless it's a known non-API MIME type that slipped through (e.g. a script from a /api/ path)
      if (
        mimeType &&
        NetworkDebuggerStartTool.STATIC_MIME_TYPES_TO_FILTER.some((staticMime) =>
          mimeType.startsWith(staticMime),
        )
      ) {
        return false; // e.g. a CSS file served from an /api/ path
      }
      return true;
    }

    return false;
  }

  private handleLoadingFailed(tabId: number, params: any) {
    const captureInfo = this.captureData.get(tabId);
    if (!captureInfo) return;

    const { requestId, errorText, canceled, type } = params;
    const requestInfo: NetworkRequestInfo = captureInfo.requests[requestId];

    if (!requestInfo) {
      // console.warn(`NetworkDebuggerStartTool: LoadingFailed for unknown requestId ${requestId} on tab ${tabId}`);
      return;
    }

    requestInfo.status = 'error';
    requestInfo.errorText = errorText;
    requestInfo.canceled = canceled;
    if (type) requestInfo.type = type;
    // timestamp here is when loading failed.
    // console.log(`NetworkDebuggerStartTool: Loading failed for ${requestId} on tab ${tabId}: ${errorText}`);
  }

  private async getResponseBody(
    tabId: number,
    requestId: string,
  ): Promise<{ body: string; base64Encoded: boolean } | null> {
    const pendingKey = `${tabId}_${requestId}`;
    if (this.pendingResponseBodies.has(pendingKey)) {
      return this.pendingResponseBodies.get(pendingKey)!; // Return existing promise
    }

    const responseBodyPromise = (async () => {
      try {
        // Check if debugger is still attached to this tabId
        const attachedTabs = await chrome.debugger.getTargets();
        if (!attachedTabs.some((target) => target.tabId === tabId && target.attached)) {
          // console.warn(`NetworkDebuggerStartTool: Debugger not attached to tab ${tabId} when trying to get response body for ${requestId}.`);
          throw new Error(`Debugger not attached to tab ${tabId}`);
        }

        const result = (await chrome.debugger.sendCommand({ tabId }, 'Network.getResponseBody', {
          requestId,
        })) as { body: string; base64Encoded: boolean };
        return result;
      } finally {
        this.pendingResponseBodies.delete(pendingKey); // Clean up after promise resolves or rejects
      }
    })();

    this.pendingResponseBodies.set(pendingKey, responseBodyPromise);
    return responseBodyPromise;
  }

  private cleanupCapture(tabId: number) {
    if (this.captureTimers.has(tabId)) {
      clearTimeout(this.captureTimers.get(tabId)!);
      this.captureTimers.delete(tabId);
    }
    if (this.inactivityTimers.has(tabId)) {
      clearTimeout(this.inactivityTimers.get(tabId)!);
      this.inactivityTimers.delete(tabId);
    }

    this.lastActivityTime.delete(tabId);
    this.captureData.delete(tabId);
    this.requestCounters.delete(tabId);

    // Abort pending getResponseBody calls for this tab
    // Note: Promises themselves cannot be "aborted" externally in a standard way once created.
    // We can delete them from the map, so new calls won't use them,
    // and the original promise will eventually resolve or reject.
    const keysToDelete: string[] = [];
    this.pendingResponseBodies.forEach((_, key) => {
      if (key.startsWith(`${tabId}_`)) {
        keysToDelete.push(key);
      }
    });
    keysToDelete.forEach((key) => this.pendingResponseBodies.delete(key));

    console.log(`NetworkDebuggerStartTool: Cleaned up resources for tab ${tabId}.`);
  }

  // isAutoStop is true if stop was triggered by timeout, false if by user/explicit call
  async stopCapture(tabId: number, isAutoStop: boolean = false): Promise<any> {
    const captureInfo = this.captureData.get(tabId);
    if (!captureInfo) {
      return { success: false, message: 'No capture in progress for this tab.' };
    }

    console.log(
      `NetworkDebuggerStartTool: Stopping capture for tab ${tabId}. Auto-stop: ${isAutoStop}`,
    );

    try {
      // Detach debugger first to prevent further events.
      // Check if debugger is attached before trying to send commands or detach
      const attachedTargets = await chrome.debugger.getTargets();
      const isAttached = attachedTargets.some(
        (target) => target.tabId === tabId && target.attached,
      );

      if (isAttached) {
        try {
          await chrome.debugger.sendCommand({ tabId }, 'Network.disable');
        } catch (e) {
          console.warn(
            `NetworkDebuggerStartTool: Error disabling network for tab ${tabId} (possibly already detached):`,
            e,
          );
        }
        try {
          await chrome.debugger.detach({ tabId });
        } catch (e) {
          console.warn(
            `NetworkDebuggerStartTool: Error detaching debugger for tab ${tabId} (possibly already detached):`,
            e,
          );
        }
      } else {
        console.log(
          `NetworkDebuggerStartTool: Debugger was not attached to tab ${tabId} at stopCapture.`,
        );
      }
    } catch (error: any) {
      // Catch errors from getTargets or general logic
      console.error(
        'NetworkDebuggerStartTool: Error during debugger interaction in stopCapture:',
        error,
      );
      // Proceed to cleanup and data formatting
    }

    // Process data even if detach/disable failed, as some data might have been captured.
    const allRequests = Object.values(captureInfo.requests) as NetworkRequestInfo[];
    const commonRequestHeaders = this.analyzeCommonHeaders(allRequests, 'requestHeaders');
    const commonResponseHeaders = this.analyzeCommonHeaders(allRequests, 'responseHeaders');

    const processedRequests = allRequests.map((req) => {
      const finalReq: Partial<NetworkRequestInfo> &
        Pick<NetworkRequestInfo, 'requestId' | 'url' | 'method' | 'type' | 'status'> = { ...req };

      if (finalReq.requestHeaders) {
        finalReq.specificRequestHeaders = this.filterOutCommonHeaders(
          finalReq.requestHeaders,
          commonRequestHeaders,
        );
        delete finalReq.requestHeaders; // Remove original full headers
      } else {
        finalReq.specificRequestHeaders = {};
      }

      if (finalReq.responseHeaders) {
        finalReq.specificResponseHeaders = this.filterOutCommonHeaders(
          finalReq.responseHeaders,
          commonResponseHeaders,
        );
        delete finalReq.responseHeaders; // Remove original full headers
      } else {
        finalReq.specificResponseHeaders = {};
      }
      return finalReq as NetworkRequestInfo; // Cast back to full type
    });

    // Sort requests by requestTime
    processedRequests.sort((a, b) => (a.requestTime || 0) - (b.requestTime || 0));

    const resultData = {
      captureStartTime: captureInfo.startTime,
      captureEndTime: Date.now(),
      totalDurationMs: Date.now() - captureInfo.startTime,
      commonRequestHeaders,
      commonResponseHeaders,
      requests: processedRequests,
      requestCount: processedRequests.length, // Actual stored requests
      totalRequestsReceivedBeforeLimit: captureInfo.limitReached
        ? NetworkDebuggerStartTool.MAX_REQUESTS_PER_CAPTURE
        : processedRequests.length,
      requestLimitReached: !!captureInfo.limitReached,
      stoppedBy: isAutoStop
        ? this.lastActivityTime.get(tabId)
          ? 'inactivity_timeout'
          : 'max_capture_time'
        : 'user_request',
      tabUrl: captureInfo.tabUrl,
      tabTitle: captureInfo.tabTitle,
    };

    console.log(
      `NetworkDebuggerStartTool: Capture stopped for tab ${tabId}. ${resultData.requestCount} requests processed. Limit reached: ${resultData.requestLimitReached}. Stopped by: ${resultData.stoppedBy}`,
    );

    this.cleanupCapture(tabId); // Final cleanup of all internal states for this tab

    return {
      success: true,
      message: `Capture stopped. ${resultData.requestCount} requests.`,
      data: resultData,
    };
  }

  private analyzeCommonHeaders(
    requests: NetworkRequestInfo[],
    headerTypeKey: 'requestHeaders' | 'responseHeaders',
  ): Record<string, string> {
    if (!requests || requests.length === 0) return {};

    const headerValueCounts = new Map<string, Map<string, number>>(); // headerName -> (headerValue -> count)
    let requestsWithHeadersCount = 0;

    for (const req of requests) {
      const headers = req[headerTypeKey] as Record<string, string> | undefined;
      if (headers && Object.keys(headers).length > 0) {
        requestsWithHeadersCount++;
        for (const name in headers) {
          // Normalize header name to lowercase for consistent counting
          const lowerName = name.toLowerCase();
          const value = headers[name];
          if (!headerValueCounts.has(lowerName)) {
            headerValueCounts.set(lowerName, new Map());
          }
          const values = headerValueCounts.get(lowerName)!;
          values.set(value, (values.get(value) || 0) + 1);
        }
      }
    }

    if (requestsWithHeadersCount === 0) return {};

    const commonHeaders: Record<string, string> = {};
    headerValueCounts.forEach((values, name) => {
      values.forEach((count, value) => {
        if (count === requestsWithHeadersCount) {
          // This (name, value) pair is present in all requests that have this type of headers.
          // We need to find the original casing for the header name.
          // This is tricky as HTTP headers are case-insensitive. Let's pick the first encountered one.
          // A more robust way would be to store original names, but lowercase comparison is standard.
          // For simplicity, we'll use the lowercase name for commonHeaders keys.
          // Or, find one original casing:
          let originalName = name;
          for (const req of requests) {
            const hdrs = req[headerTypeKey] as Record<string, string> | undefined;
            if (hdrs) {
              const foundName = Object.keys(hdrs).find((k) => k.toLowerCase() === name);
              if (foundName) {
                originalName = foundName;
                break;
              }
            }
          }
          commonHeaders[originalName] = value;
        }
      });
    });
    return commonHeaders;
  }

  private filterOutCommonHeaders(
    headers: Record<string, string>,
    commonHeaders: Record<string, string>,
  ): Record<string, string> {
    if (!headers || typeof headers !== 'object') return {};

    const specificHeaders: Record<string, string> = {};
    const commonHeadersLower: Record<string, string> = {};

    // Use Object.keys to avoid ESLint no-prototype-builtins warning
    Object.keys(commonHeaders).forEach((commonName) => {
      commonHeadersLower[commonName.toLowerCase()] = commonHeaders[commonName];
    });

    // Use Object.keys to avoid ESLint no-prototype-builtins warning
    Object.keys(headers).forEach((name) => {
      const lowerName = name.toLowerCase();
      // If the header (by name, case-insensitively) is not in commonHeaders OR
      // if its value is different from the common one, then it's specific.
      if (!(lowerName in commonHeadersLower) || headers[name] !== commonHeadersLower[lowerName]) {
        specificHeaders[name] = headers[name];
      }
    });

    return specificHeaders;
  }

  async execute(args: NetworkDebuggerStartToolParams): Promise<ToolResult> {
    const {
      url: targetUrl,
      maxCaptureTime = DEFAULT_MAX_CAPTURE_TIME_MS,
      inactivityTimeout = DEFAULT_INACTIVITY_TIMEOUT_MS,
      includeStatic = false,
    } = args;

    console.log(
      `NetworkDebuggerStartTool: Executing with args: url=${targetUrl}, maxTime=${maxCaptureTime}, inactivityTime=${inactivityTimeout}, includeStatic=${includeStatic}`,
    );

    let tabToOperateOn: chrome.tabs.Tab | undefined;

    try {
      if (targetUrl) {
        const existingTabs = await chrome.tabs.query({
          url: targetUrl.startsWith('http') ? targetUrl : `*://*/*${targetUrl}*`,
        }); // More specific query
        if (existingTabs.length > 0 && existingTabs[0]?.id) {
          tabToOperateOn = existingTabs[0];
          // Ensure window gets focus and tab is truly activated
          await chrome.windows.update(tabToOperateOn.windowId, { focused: true });
          await chrome.tabs.update(tabToOperateOn.id!, { active: true });
        } else {
          tabToOperateOn = await chrome.tabs.create({ url: targetUrl, active: true });
          // Wait for tab to be somewhat ready. A better way is to listen to tabs.onUpdated status='complete'
          // but for debugger attachment, it just needs the tabId.
          await new Promise((resolve) => setTimeout(resolve, 500)); // Short delay
        }
      } else {
        const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true });
        if (activeTabs.length > 0 && activeTabs[0]?.id) {
          tabToOperateOn = activeTabs[0];
        } else {
          return createErrorResponse('No active tab found and no URL provided.');
        }
      }

      if (!tabToOperateOn?.id) {
        return createErrorResponse('Failed to identify or create a target tab.');
      }
      const tabId = tabToOperateOn.id;

      // Use startCaptureForTab method to start capture
      try {
        await this.startCaptureForTab(tabId, {
          maxCaptureTime,
          inactivityTimeout,
          includeStatic,
        });
      } catch (error: any) {
        return createErrorResponse(
          `Failed to start capture for tab ${tabId}: ${error.message || String(error)}`,
        );
      }

      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              success: true,
              message: `Network capture started on tab ${tabId}. Waiting for stop command or timeout.`,
              tabId,
              url: tabToOperateOn.url,
              maxCaptureTime,
              inactivityTimeout,
              includeStatic,
              maxRequests: NetworkDebuggerStartTool.MAX_REQUESTS_PER_CAPTURE,
            }),
          },
        ],
        isError: false,
      };
    } catch (error: any) {
      console.error('NetworkDebuggerStartTool: Critical error during execute:', error);
      // If a tabId was involved and debugger might be attached, try to clean up.
      const tabIdToClean = tabToOperateOn?.id;
      if (tabIdToClean && this.captureData.has(tabIdToClean)) {
        await chrome.debugger
          .detach({ tabId: tabIdToClean })
          .catch((e) => console.warn('Cleanup detach error:', e));
        this.cleanupCapture(tabIdToClean);
      }
      return createErrorResponse(
        `Error in NetworkDebuggerStartTool: ${error.message || String(error)}`,
      );
    }
  }
}

/**
 * Network capture stop tool - stops capture and returns results for the active tab
 */
class NetworkDebuggerStopTool extends BaseBrowserToolExecutor {
  name = TOOL_NAMES.BROWSER.NETWORK_DEBUGGER_STOP;
  public static instance: NetworkDebuggerStopTool | null = null;

  constructor() {
    super();
    if (NetworkDebuggerStopTool.instance) {
      return NetworkDebuggerStopTool.instance;
    }
    NetworkDebuggerStopTool.instance = this;
  }

  async execute(): Promise<ToolResult> {
    console.log(`NetworkDebuggerStopTool: Executing command.`);

    const startTool = NetworkDebuggerStartTool.instance;
    if (!startTool) {
      return createErrorResponse(
        'NetworkDebuggerStartTool instance not available. Cannot stop capture.',
      );
    }

    // Get all tabs currently capturing
    const ongoingCaptures = Array.from(startTool['captureData'].keys());
    console.log(
      `NetworkDebuggerStopTool: Found ${ongoingCaptures.length} ongoing captures: ${ongoingCaptures.join(', ')}`,
    );

    if (ongoingCaptures.length === 0) {
      return createErrorResponse('No active network captures found in any tab.');
    }

    // Get current active tab
    const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true });
    const activeTabId = activeTabs[0]?.id;

    // Determine the primary tab to stop
    let primaryTabId: number;

    if (activeTabId && startTool['captureData'].has(activeTabId)) {
      // If current active tab is capturing, prioritize stopping it
      primaryTabId = activeTabId;
      console.log(
        `NetworkDebuggerStopTool: Active tab ${activeTabId} is capturing, will stop it first.`,
      );
    } else if (ongoingCaptures.length === 1) {
      // If only one tab is capturing, stop it
      primaryTabId = ongoingCaptures[0];
      console.log(
        `NetworkDebuggerStopTool: Only one tab ${primaryTabId} is capturing, stopping it.`,
      );
    } else {
      // If multiple tabs are capturing but current active tab is not among them, stop the first one
      primaryTabId = ongoingCaptures[0];
      console.log(
        `NetworkDebuggerStopTool: Multiple tabs capturing, active tab not among them. Stopping tab ${primaryTabId} first.`,
      );
    }

    // Stop capture for the primary tab
    const result = await this.performStop(startTool, primaryTabId);

    // If multiple tabs are capturing, stop other tabs
    if (ongoingCaptures.length > 1) {
      const otherTabIds = ongoingCaptures.filter((id) => id !== primaryTabId);
      console.log(
        `NetworkDebuggerStopTool: Stopping ${otherTabIds.length} additional captures: ${otherTabIds.join(', ')}`,
      );

      for (const tabId of otherTabIds) {
        try {
          await startTool.stopCapture(tabId);
        } catch (error) {
          console.error(`NetworkDebuggerStopTool: Error stopping capture on tab ${tabId}:`, error);
        }
      }
    }

    return result;
  }

  private async performStop(
    startTool: NetworkDebuggerStartTool,
    tabId: number,
  ): Promise<ToolResult> {
    console.log(`NetworkDebuggerStopTool: Attempting to stop capture for tab ${tabId}.`);
    const stopResult = await startTool.stopCapture(tabId);

    if (!stopResult?.success) {
      return createErrorResponse(
        stopResult?.message ||
          `Failed to stop network capture for tab ${tabId}. It might not have been capturing.`,
      );
    }

    const resultData = stopResult.data || {};

    // Get all tabs still capturing (there might be other tabs still capturing after stopping)
    const remainingCaptures = Array.from(startTool['captureData'].keys());

    // Sort requests by time
    if (resultData.requests && Array.isArray(resultData.requests)) {
      resultData.requests.sort(
        (a: NetworkRequestInfo, b: NetworkRequestInfo) =>
          (a.requestTime || 0) - (b.requestTime || 0),
      );
    }

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify({
            success: true,
            message: `Capture for tab ${tabId} (${resultData.tabUrl || 'N/A'}) stopped. ${resultData.requestCount || 0} requests captured.`,
            tabId: tabId,
            tabUrl: resultData.tabUrl || 'N/A',
            tabTitle: resultData.tabTitle || 'Unknown Tab',
            requestCount: resultData.requestCount || 0,
            commonRequestHeaders: resultData.commonRequestHeaders || {},
            commonResponseHeaders: resultData.commonResponseHeaders || {},
            requests: resultData.requests || [],
            captureStartTime: resultData.captureStartTime,
            captureEndTime: resultData.captureEndTime,
            totalDurationMs: resultData.totalDurationMs,
            settingsUsed: resultData.settingsUsed || {},
            remainingCaptures: remainingCaptures,
            totalRequestsReceived: resultData.totalRequestsReceived || resultData.requestCount || 0,
            requestLimitReached: resultData.requestLimitReached || false,
          }),
        },
      ],
      isError: false,
    };
  }
}

export const networkDebuggerStartTool = new NetworkDebuggerStartTool();
export const networkDebuggerStopTool = new NetworkDebuggerStopTool();
