import printMessage from 'print-message';
import { pathToFileURL } from 'url';
import crawlSitemap from './crawlers/crawlSitemap.js';
import crawlDomain from './crawlers/crawlDomain.js';
import crawlLocalFile from './crawlers/crawlLocalFile.js';
import crawlIntelligentSitemap from './crawlers/crawlIntelligentSitemap.js';
import generateArtifacts from './mergeAxeResults.js';
import { getHost, createAndUpdateResultsFolders, cleanUpAndExit, getStoragePath, getEntryPageTitle } from './utils.js';
import constants, { ScannerTypes, UrlsCrawled } from './constants/constants.js';
import { getBlackListedPatterns, submitForm } from './constants/common.js';
import { consoleLogger, silentLogger } from './logs.js';
import runCustom from './crawlers/runCustom.js';
import { alertMessageOptions } from './constants/cliFunctions.js';
import { Data } from './index.js';
import {
  isS3UploadEnabled,
  getS3MetadataFromEnv,
  getS3UploadPrefix,
  uploadFolderToS3,
} from './services/s3Uploader.js';
import { writeManifest, resetCaptureEntries, isPageCaptureEnabled } from './crawlers/pageCapture.js';

// Class exports
export class ViewportSettingsClass {
  deviceChosen: string;
  customDevice: string;
  viewportWidth: number;
  playwrightDeviceDetailsObject: any; // You can replace 'any' with a more specific type if possible

  constructor(
    deviceChosen: string,
    customDevice: string,
    viewportWidth: number,
    playwrightDeviceDetailsObject: any,
  ) {
    this.deviceChosen = deviceChosen;
    this.customDevice = customDevice;
    this.viewportWidth = viewportWidth;
    this.playwrightDeviceDetailsObject = playwrightDeviceDetailsObject;
  }
}

const combineRun = async (details: Data, deviceToScan: string) => {
  const envDetails = { ...details };

  const {
    type,
    url,
    entryUrl,
    nameEmail,
    randomToken,
    deviceChosen,
    customDevice,
    viewportWidth,
    playwrightDeviceDetailsObject,
    maxRequestsPerCrawl,
    browser,
    userDataDirectory,
    strategy, // Allow subdomains: if checked, = 'same-domain'
    specifiedMaxConcurrency, // Slow scan mode: if checked, = '1'
    fileTypes,
    blacklistedPatternsFilename,
    includeScreenshots, // Include screenshots: if checked, = 'true'
    followRobots, // Adhere to robots.txt: if checked, = 'true'
    metadata,
    customFlowLabel = 'None',
    extraHTTPHeaders,
    safeMode,
    zip,
    ruleset, // Enable custom checks, Enable WCAG AAA: if checked, = 'enable-wcag-aaa')
    generateJsonFiles,
    scanDuration,
  } = envDetails;

  process.env.CRAWLEE_LOG_LEVEL = 'ERROR';
  // Absolute path — @crawlee/memory-storage@3.18 rejects absolute paths passed as
  // storage names, so CRAWLEE_STORAGE_DIR must carry the full location and callers
  // pass relative names ('crawlee', 'crawlee_rq') to Dataset/RequestQueue.open().
  process.env.CRAWLEE_STORAGE_DIR = getStoragePath(randomToken);
  constants.sitemapFetchedLinks = null;

  if (isPageCaptureEnabled() && !process.env.OOBEE_SCAN_PRODUCT) {
    process.env.OOBEE_SCAN_PRODUCT = 'U&A';
  }

  if (process.env.CRAWLEE_SYSTEM_INFO_V2 === undefined) {
    // Set the environment variable to enable system info v2
    // Resolves issue with when wmic is not installed on Windows
    process.env.CRAWLEE_SYSTEM_INFO_V2 = '1';
  }

  // Suppress non-fatal Crawlee ps-tree errors on Windows with non-English locales.
  // The system info module tries to parse process listing headers and crashes when
  // headers are in a different language (e.g. "Wo" instead of "PID").
  const psTreeHandler = (err: Error) => {
    if (err.message?.includes('Unknown process listing header')) {
      consoleLogger.info(`Suppressed Crawlee ps-tree locale error: ${err.message}`);
      return;
    }
    // Suppress stale Playwright in-process connection errors that fire asynchronously
    // after the browser is closed in writeSummaryPdf. The deferred IPC message arrives
    // via setImmediate after the browser instance has already been disposed.
    if (err.message?.includes('was not bound in the connection')) {
      consoleLogger.info(`Suppressed Playwright post-close connection error: ${err.message}`);
      return;
    }
    // Suppress EPERM errors from Crawlee's async lock-file operations that fire
    // after a crawl phase finishes. On Windows, the sitemap phase's async lock
    // cleanup can race with the domain phase starting in the same directory.
    if (err.message?.includes('EPERM') && err.message?.includes('.json.lock')) {
      consoleLogger.info(`Suppressed Crawlee lock-file EPERM (stale async cleanup): ${err.message}`);
      return;
    }
    throw err;
  };
  process.on('uncaughtException', psTreeHandler);

  const host = type === ScannerTypes.SITEMAP || type === ScannerTypes.LOCALFILE ? '' : getHost(url);

  let blacklistedPatterns: string[] | null = null;
  try {
    blacklistedPatterns = getBlackListedPatterns(blacklistedPatternsFilename);
  } catch (error) {
    consoleLogger.error(error);
    cleanUpAndExit(1);
  }

  // remove basic-auth credentials from URL
  const finalUrl = !(type === ScannerTypes.SITEMAP || type === ScannerTypes.LOCALFILE)
    ? new URL(entryUrl)
    : new URL(pathToFileURL(entryUrl));

  // Use the string version of finalUrl to reduce logic at submitForm
  const finalUrlString = finalUrl.toString();

  const scanDetails = {
    startTime: new Date(),
    endTime: new Date(),
    crawlType: type,
    requestUrl: finalUrl,
    urlsCrawled: new UrlsCrawled(),
    isIncludeScreenshots: envDetails.includeScreenshots,
    isAllowSubdomains: envDetails.strategy,
    isEnableCustomChecks: envDetails.ruleset,
    isEnableWcagAaa: envDetails.ruleset,
    isSlowScanMode: envDetails.specifiedMaxConcurrency,
    isAdhereRobots: envDetails.followRobots,
    deviceChosen: deviceToScan,
    nameEmail: undefined as { name: string; email: string } | undefined,
  };

  // Parse nameEmail and add it to scanDetails for use in generateArtifacts
  if (nameEmail) {
    const [name, email] = nameEmail.split(':');
    scanDetails.nameEmail = { name, email };
  }

  const viewportSettings: ViewportSettingsClass = new ViewportSettingsClass(
    deviceChosen,
    customDevice,
    viewportWidth,
    playwrightDeviceDetailsObject,
  );

  let urlsCrawledObj: UrlsCrawled;
  let uiCustomFlowLabel: string | undefined;
  let durationExceeded = false;

  switch (type) {
    case ScannerTypes.CUSTOM:
      const res = await runCustom(
        url,
        randomToken,
        browser,
        userDataDirectory,
        viewportSettings,
        blacklistedPatterns,
        includeScreenshots,
        customFlowLabel && customFlowLabel !== 'None' ? customFlowLabel : '',
        extraHTTPHeaders,
      );

      urlsCrawledObj = res.urlsCrawled;
      uiCustomFlowLabel = res.customFlowLabel;
      break;

    case ScannerTypes.SITEMAP:
      const sitemapResult = await crawlSitemap({
        sitemapUrl: url,
        randomToken,
        host,
        viewportSettings,
        maxRequestsPerCrawl,
        browser,
        userDataDirectory,
        specifiedMaxConcurrency,
        fileTypes,
        blacklistedPatterns,
        includeScreenshots,
        extraHTTPHeaders,
        strategy,
        userUrl: url,
        scanDuration,
        ruleset,
      });
      urlsCrawledObj = sitemapResult.urlsCrawled;
      durationExceeded = sitemapResult.durationExceeded;
      break;

    case ScannerTypes.LOCALFILE:
      const localFileResult = await crawlLocalFile({
        url,
        randomToken,
        host,
        viewportSettings,
        maxRequestsPerCrawl,
        browser,
        userDataDirectory,
        specifiedMaxConcurrency,
        fileTypes,
        blacklistedPatterns,
        includeScreenshots,
        extraHTTPHeaders,
        scanDuration,
        ruleset,
      });
      if (localFileResult) {
        if ('urlsCrawled' in localFileResult) {
          urlsCrawledObj = localFileResult.urlsCrawled;
          durationExceeded = localFileResult.durationExceeded;
        } else {
          urlsCrawledObj = localFileResult;
        }
      }
      break;

    case ScannerTypes.INTELLIGENT:
      const intelligentResult = await crawlIntelligentSitemap(
        url,
        randomToken,
        host,
        viewportSettings,
        maxRequestsPerCrawl,
        browser,
        userDataDirectory,
        strategy,
        specifiedMaxConcurrency,
        fileTypes,
        blacklistedPatterns,
        includeScreenshots,
        followRobots,
        extraHTTPHeaders,
        safeMode,
        scanDuration,
        ruleset,
      );
      urlsCrawledObj = intelligentResult.urlsCrawled;
      durationExceeded = intelligentResult.durationExceeded;
      break;

    case ScannerTypes.WEBSITE:
      const websiteResult = await crawlDomain({
        url,
        randomToken,
        host,
        viewportSettings,
        maxRequestsPerCrawl,
        browser,
        userDataDirectory,
        strategy,
        specifiedMaxConcurrency,
        fileTypes,
        blacklistedPatterns,
        includeScreenshots,
        followRobots,
        extraHTTPHeaders,
        scanDuration,
        safeMode,
        ruleset,
      });
      urlsCrawledObj = websiteResult.urlsCrawled;
      durationExceeded = websiteResult.durationExceeded;
      break;

    default:
      consoleLogger.error(`type: ${type} not defined`);
      cleanUpAndExit(1);
  }

  scanDetails.endTime = new Date();
  scanDetails.urlsCrawled = urlsCrawledObj;

  if (scanDetails.urlsCrawled) {
    if (scanDetails.urlsCrawled.scanned.length > 0) {
      await createAndUpdateResultsFolders(randomToken);
      try {
        await writeManifest(randomToken);
      } finally {
        resetCaptureEntries();
      }
      const pagesNotScanned = [
        ...urlsCrawledObj.error,
        ...urlsCrawledObj.invalid,
        ...urlsCrawledObj.forbidden,
        ...urlsCrawledObj.userExcluded,
      ];
      const basicFormHTMLSnippet = await generateArtifacts(
        randomToken,
        url,
        type,
        deviceToScan,
        urlsCrawledObj.scanned,
        pagesNotScanned,
        uiCustomFlowLabel && uiCustomFlowLabel.length > 0
          ? uiCustomFlowLabel
          : customFlowLabel || 'None',
        undefined,
        scanDetails,
        zip,
        generateJsonFiles,
        browser,
      );
      const [name, email] = nameEmail.split(':');

      // Upload results to S3 if environment variables are set
      if (isS3UploadEnabled()) {
        const siteName = getEntryPageTitle(urlsCrawledObj.scanned, url)
          .replace(/^\d+\s*:\s*/, '')
          .trim();
        const scanMetadata = getS3MetadataFromEnv(siteName, durationExceeded);
        const s3Prefix = getS3UploadPrefix();

        if (scanMetadata && s3Prefix) {
          try {
            const storagePath = getStoragePath(randomToken);
            consoleLogger.info('Starting S3 upload...');
            consoleLogger.info(`Upload path: ${s3Prefix}`);

            const uploadedFiles = await uploadFolderToS3(storagePath, s3Prefix, scanMetadata);

            consoleLogger.info(`Successfully uploaded ${uploadedFiles.length} files to S3`);
          } catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            consoleLogger.error(`Failed to upload results to S3: ${errorMessage}`);
            // Don't fail the scan if S3 upload fails
            consoleLogger.warn('Continuing without S3 upload...');
          }
        } else {
          consoleLogger.warn('S3 upload enabled but metadata/prefix not available');
        }
      } else {
        consoleLogger.info('S3 upload not enabled (missing environment variables)');
      }

      await submitForm(
        browser,
        userDataDirectory,
        url, // scannedUrl
        new URL(finalUrlString).href, // entryUrl
        type,
        email,
        name,
        JSON.stringify(basicFormHTMLSnippet),
        urlsCrawledObj.scanned.length,
        urlsCrawledObj.scannedRedirects.length,
        pagesNotScanned.length,
        metadata,
      );
    } else {
      // No page were scanned because the URL loaded does not meet the crawler requirements
      printMessage([`No pages were scanned.`], alertMessageOptions);
      cleanUpAndExit(1, randomToken, true);
    }
  } else {
    // No page were scanned because the URL loaded does not meet the crawler requirements
    printMessage([`No pages were scanned.`], alertMessageOptions);
    cleanUpAndExit(1, randomToken, true);
  }
};

export default combineRun;
