import { Browser, BrowserContext, Page } from 'playwright';
import { chromium } from 'playwright-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import * as fs from 'fs';
import * as path from 'path';

import type { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort';

import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
import { PlaywrightAutomationAdapter } from './PlaywrightAutomationAdapter';

chromium.use(StealthPlugin());

export type BrowserModeSource = string;

export class PlaywrightBrowserSession {
  private browser: Browser | null = null;
  private persistentContext: BrowserContext | null = null;
  private context: BrowserContext | null = null;
  private page: Page | null = null;
  private connected = false;
  private isConnecting = false;
  private browserModeLoader: BrowserModeConfigLoader;
  private actualBrowserMode: BrowserMode;
  private browserModeSource: BrowserModeSource;

  constructor(
    private readonly config: Required<PlaywrightConfig>,
    private readonly logger?: LoggerPort,
    browserModeLoader?: BrowserModeConfigLoader,
  ) {
    const automationMode = getAutomationMode();
    this.browserModeLoader = browserModeLoader ?? new BrowserModeConfigLoader();
    const browserModeConfig = this.browserModeLoader.load();
    this.actualBrowserMode = browserModeConfig.mode;
    this.browserModeSource = browserModeConfig.source as BrowserModeSource;

    this.log('info', 'Browser mode configured', {
      mode: this.actualBrowserMode,
      source: this.browserModeSource,
      automationMode,
      configHeadless: this.config.headless,
    });
  }

  private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {
    if (!this.logger) {
      return;
    }
    const logger = this.logger as Record<
      'debug' | 'info' | 'warn' | 'error',
      (msg: string, ctx?: Record<string, unknown>) => void
    >;
    logger[level](message, context);
  }

  private isRealMode(): boolean {
    return this.config.mode === 'real';
  }

  getBrowserMode(): BrowserMode {
    return this.actualBrowserMode;
  }

  getBrowserModeSource(): BrowserModeSource {
    return this.browserModeSource;
  }

  getUserDataDir(): string {
    return this.config.userDataDir;
  }

  getPage(): Page | null {
    return this.page;
  }

  getContext(): BrowserContext | null {
    return this.context;
  }

  getPersistentContext(): BrowserContext | null {
    return this.persistentContext;
  }

  getBrowser(): Browser | null {
    return this.browser;
  }

  isConnected(): boolean {
    return this.connected && this.page !== null;
  }

  async connect(forceHeaded: boolean = false): Promise<{ success: boolean; error?: string }> {
    if (this.connected && this.page) {
      const shouldReuse =
        !forceHeaded ||
        this.actualBrowserMode === 'headed';

      if (shouldReuse) {
        this.log('debug', 'Already connected, reusing existing connection', {
          browserMode: this.actualBrowserMode,
          forcedHeaded: forceHeaded,
        });
        return { success: true };
      }

      this.log('info', 'Existing browser connection is headless, reopening in headed mode for login', {
        browserMode: this.actualBrowserMode,
        forcedHeaded: forceHeaded,
      });
      await this.closeBrowserContext();
    }

    if (this.isConnecting) {
      this.log('debug', 'Connection in progress, waiting...');
      await new Promise(resolve => setTimeout(resolve, 100));
      return this.connect(forceHeaded);
    }

    this.isConnecting = true;
    try {
      const currentConfig = this.browserModeLoader.load();
      this.actualBrowserMode = currentConfig.mode;
      this.browserModeSource = currentConfig.source as BrowserModeSource;
      const effectiveMode = forceHeaded ? 'headed' : currentConfig.mode;

      const adapterWithLauncher = PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
        testLauncher?: typeof chromium;
      };
      const launcher = adapterWithLauncher.testLauncher ?? chromium;

      this.log('debug', 'Effective browser mode at connect', {
        effectiveMode,
        actualBrowserMode: this.actualBrowserMode,
        browserModeSource: this.browserModeSource,
        forced: forceHeaded,
      });

      if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') {
        try {
          const loaderValue = this.browserModeLoader && typeof this.browserModeLoader.load === 'function'
            ? this.browserModeLoader.load()
            : undefined;
          console.debug('[TEST-INSTRUMENT] PlaywrightAutomationAdapter.connect()', {
            effectiveMode,
            forceHeaded,
            loaderValue,
            browserModeSource: this.getBrowserModeSource(),
          });
        } catch {
          // ignore instrumentation errors
        }
      }

      if (this.isRealMode() && this.config.userDataDir) {
        this.log('info', 'Launching persistent browser context', {
          userDataDir: this.config.userDataDir,
          mode: effectiveMode,
          forced: forceHeaded,
        });

        if (!fs.existsSync(this.config.userDataDir)) {
          fs.mkdirSync(this.config.userDataDir, { recursive: true });
        }

        await this.cleanupStaleLockFile(this.config.userDataDir);

        this.persistentContext = await launcher.launchPersistentContext(
          this.config.userDataDir,
          {
            headless: effectiveMode === 'headless',
            args: [
              '--disable-blink-features=AutomationControlled',
              '--disable-features=IsolateOrigins,site-per-process',
            ],
            ignoreDefaultArgs: ['--enable-automation'],
            viewport: { width: 1920, height: 1080 },
            userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
          },
        );
        const persistentContext = this.persistentContext!;
        this.page = persistentContext.pages()[0] || await persistentContext.newPage();
        this.page.setDefaultTimeout(this.config.timeout ?? 10000);
        this.connected = !!this.page;
      } else {
        this.browser = await launcher.launch({
          headless: effectiveMode === 'headless',
          args: [
            '--disable-blink-features=AutomationControlled',
            '--disable-features=IsolateOrigins,site-per-process',
          ],
          ignoreDefaultArgs: ['--enable-automation'],
        });
        const browser = this.browser!;
        this.context = await browser.newContext();
        this.page = await this.context.newPage();
        this.page.setDefaultTimeout(this.config.timeout ?? 10000);
        this.connected = !!this.page;
      }

      if (!this.page) {
        this.log('error', 'Browser session connected without a usable page', {
          hasBrowser: !!this.browser,
          hasContext: !!this.context || !!this.persistentContext,
        });
        await this.closeBrowserContext();
        this.connected = false;
        return { success: false, error: 'Browser not connected' };
      }

      return { success: true };
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.connected = false;
      this.page = null;
      this.context = null;
      this.persistentContext = null;
      this.browser = null;
      return { success: false, error: message };
    } finally {
      this.isConnecting = false;
    }
  }

  async ensureBrowserContext(forceHeaded: boolean = false): Promise<void> {
    const result = await this.connect(forceHeaded);
    if (!result.success) {
      throw new Error(result.error || 'Failed to connect browser');
    }
  }

  private async cleanupStaleLockFile(userDataDir: string): Promise<void> {
    const singletonLockPath = path.join(userDataDir, 'SingletonLock');

    try {
      if (!fs.existsSync(singletonLockPath)) {
        return;
      }

      this.log('info', 'Found existing SingletonLock, attempting cleanup', { path: singletonLockPath });
      fs.unlinkSync(singletonLockPath);
      this.log('info', 'Cleaned up stale SingletonLock file');
    } catch (error) {
      this.log('warn', 'Could not clean up SingletonLock', { error: String(error) });
    }
  }

  async disconnect(): Promise<void> {
    if (this.page) {
      await this.page.close();
      this.page = null;
    }
    if (this.persistentContext) {
      await this.persistentContext.close();
      this.persistentContext = null;
    }
    if (this.context) {
      await this.context.close();
      this.context = null;
    }
    if (this.browser) {
      await this.browser.close();
      this.browser = null;
    }
    this.connected = false;
  }

  async closeBrowserContext(): Promise<void> {
    try {
      if (this.persistentContext) {
        await this.persistentContext.close();
        this.persistentContext = null;
        this.page = null;
        this.connected = false;
        this.log('info', 'Persistent context closed');
        return;
      }

      if (this.context) {
        await this.context.close();
        this.context = null;
        this.page = null;
      }

      if (this.browser) {
        await this.browser.close();
        this.browser = null;
      }

      this.connected = false;
      this.log('info', 'Browser closed successfully');
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.log('warn', 'Error closing browser context', { error: message });
      this.persistentContext = null;
      this.context = null;
      this.browser = null;
      this.page = null;
      this.connected = false;
    }
  }
}