import puppeteer from 'puppeteer-core';
import type { Browser, Page } from 'puppeteer-core';
import { existsSync } from 'fs';

export interface BrowserManagerConfig {
  /** Path to Chrome/Chromium executable. If not set, tries common locations. */
  executablePath?: string;
  /** Connect to a running browser via WebSocket URL instead of launching. */
  wsEndpoint?: string;
  /** Launch headless (default: true) */
  headless?: boolean;
  /** Maximum number of concurrent pages (default: 5) */
  maxPages?: number;
  /** Default navigation timeout in ms (default: 30000) */
  defaultTimeout?: number;
}

/** Find Chrome/Chromium executable path on common locations. */
function findChrome(): string {
  const candidates = [
    // Linux
    '/usr/bin/chromium',
    '/usr/bin/chromium-browser',
    '/usr/bin/google-chrome',
    '/usr/bin/google-chrome-stable',
    // macOS
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '/Applications/Chromium.app/Contents/MacOS/Chromium',
    // Snap/Flatpak
    '/snap/bin/chromium',
  ];

  for (const candidate of candidates) {
    if (existsSync(candidate)) {return candidate;}
  }

  throw new Error('Chrome/Chromium not found. Set browser.executable_path in config or install Chrome.');
}

/**
 * BrowserManager — manages a browser instance and page pool via CDP.
 *
 * Features:
 * - Launch a local browser or connect to a remote one via WebSocket
 * - Page pool with configurable concurrency limit
 * - Graceful shutdown of all pages and the browser
 */
export class BrowserManager {
  private browser: Browser | null = null;
  private pages: Map<string, Page> = new Map();
  private config: BrowserManagerConfig;

  constructor(config: BrowserManagerConfig = {}) {
    this.config = config;
  }

  /** Get or launch the browser instance. */
  async getBrowser(): Promise<Browser> {
    if (this.browser && this.browser.connected) {
      return this.browser;
    }

    if (this.config.wsEndpoint) {
      this.browser = await puppeteer.connect({
        browserWSEndpoint: this.config.wsEndpoint,
      });
    } else {
      const executablePath = this.config.executablePath ?? findChrome();
      this.browser = await puppeteer.launch({
        executablePath,
        headless: this.config.headless ?? true,
        args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
      });
    }

    return this.browser;
  }

  /** Get the default page or create one if none exists. */
  async getPage(pageId?: string): Promise<Page> {
    const id = pageId ?? 'default';
    let page = this.pages.get(id);

    if (page && !page.isClosed()) {
      return page;
    }

    const browser = await this.getBrowser();
    const pages = await browser.pages();
    page = pages[0] ?? await browser.newPage();
    page.setDefaultTimeout(this.config.defaultTimeout ?? 30000);

    this.pages.set(id, page);
    return page;
  }

  /** Create a new page with a given ID. */
  async newPage(pageId: string): Promise<Page> {
    if (this.pages.size >= (this.config.maxPages ?? 5)) {
      throw new Error(`Maximum concurrent pages (${this.config.maxPages ?? 5}) reached`);
    }

    const browser = await this.getBrowser();
    const page = await browser.newPage();
    page.setDefaultTimeout(this.config.defaultTimeout ?? 30000);
    this.pages.set(pageId, page);
    return page;
  }

  /** Close a specific page. */
  async closePage(pageId: string): Promise<void> {
    const page = this.pages.get(pageId);
    if (page && !page.isClosed()) {
      await page.close();
    }
    this.pages.delete(pageId);
  }

  /** Close all pages and the browser. */
  async shutdown(): Promise<void> {
    for (const [, page] of this.pages) {
      try {
        if (!page.isClosed()) {await page.close();}
      } catch { /* ignore */ }
    }
    this.pages.clear();

    if (this.browser) {
      try {
        await this.browser.close();
      } catch { /* ignore */ }
      this.browser = null;
    }
  }

  /** Check if a browser is available (Chrome/Chromium exists). */
  static isAvailable(executablePath?: string): boolean {
    try {
      const path = executablePath ?? findChrome();
      return existsSync(path);
    } catch {
      return false;
    }
  }
}
