import cluster from 'cluster';
import path from 'path';
import sh from 'shelljs';
import { getUserAgent } from 'package-manager-detector/detect';
import { resolveCommand } from 'package-manager-detector/commands';
import { readConfig, defaultBrowser } from './config.js';
import { Config, BrowserConfigObject, isWorkerMessage } from '../types.js';
import { WorkerOptions, OptionsSchema, WorkerOptionsSchema, Options } from '../schema.js';
import { logger } from './logger.js';
import { getStorybookUrl, checkIsStorybookConnected } from './connection.js';
import { SeleniumWebdriver } from './selenium/webdriver.js';
import { LOCALHOST_REGEXP } from './webdriver.js';
import { isInsideDocker, killTree, resolvePlaywrightBrowserType, shutdownWithError } from './utils.js';
import { sendWorkerMessage, subscribeOn } from './messages.js';
import { buildImage } from './docker.js';
import { mkdir, writeFile } from 'fs/promises';
import assert from 'assert';
import * as v from 'valibot';
import { PlaywrightWebdriver } from '../playwright.js';

async function getPlaywrightVersion(): Promise<string> {
  const {
    default: { version },
  } = await import('playwright-core/package.json', { with: { type: 'json' } });
  return version;
}

async function startSelenoid(config: Config, debug = false): Promise<string | undefined> {
  const { startSelenoidContainer, startSelenoidStandalone } = await import('./selenium/selenoid.js');
  const gridUrl = 'http://localhost:4444/wd/hub';
  if (config.useDocker) {
    const host = await startSelenoidContainer(config, debug);
    return isInsideDocker ? gridUrl.replace(LOCALHOST_REGEXP, host) : gridUrl;
  }
  await startSelenoidStandalone(config, debug);
  return gridUrl;
}

async function startPlaywright(config: Config, browser: string, version: string, debug = false): Promise<string> {
  // TODO Re-use dockerImage
  const { startPlaywrightContainer } = await import('./playwright/docker.js');
  const { browserName } = config.browsers[browser] as BrowserConfigObject;

  const imageName = `creevey/${browserName}:v${version}`;
  const host = await startPlaywrightContainer(imageName, browser, config, debug);

  return host;
}

async function buildPlaywright(config: Config, version: string): Promise<void> {
  const { playwrightDockerFile } = await import('./playwright/docker-file.js');
  const {
    default: { version: creeveyVersion },
  } = await import('../../package.json', { with: { type: 'json' } });
  const browsers = [...new Set(Object.values(config.browsers).map((c) => (c as BrowserConfigObject).browserName))];
  await Promise.all(
    browsers.map(async (browserName) => {
      const imageName = `creevey/${browserName}:v${version}`;
      const dockerfile = await playwrightDockerFile(browserName, version, config.experimental?.npmRegistry);

      await buildImage(imageName, creeveyVersion, dockerfile);
    }),
  );

  const { default: getPort } = await import('get-port');

  cluster.on('message', (worker, message: unknown) => {
    if (!isWorkerMessage(message)) return;

    const workerMessage = message;
    if (workerMessage.type != 'port') return;

    void getPort().then((port) => {
      sendWorkerMessage(worker, {
        type: 'port',
        payload: { port },
      });
    });
  });
}

async function startWebdriverServer(config: Config, options: Options): Promise<string | undefined> {
  if (config.webdriver === SeleniumWebdriver) {
    return startSelenoid(config, options.debug);
    // TODO Worker might want to use docker image of browser or start standalone selenium
  } else {
    if (config.gridUrl) return undefined;

    if (config.useDocker) {
      const version = await getPlaywrightVersion();
      await buildPlaywright(config, version);
    }
    // TODO Support gridUrl for playwright
    // NOTE: There is no grid for playwright right now
  }
}

async function waitForStorybook(config: Config, options: Options): Promise<void> {
  const [localUrl, remoteUrl] = getStorybookUrl(config, options);

  if (options.storybookStart) {
    const pm = getUserAgent();
    assert(pm, new Error('Failed to detect current package manager'));
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const { command, args } = resolveCommand(pm, 'run', ['storybook', 'dev'])!;
    const storybookPort = new URL(localUrl).port;
    const storybookCommand = config.storybookAutorunCmd ?? [command, ...args, '--ci', '-p', storybookPort].join(' ');

    logger().info(`Start Storybook via \`${storybookCommand}\`, it should be accessible at:`);
    logger().info(`Local - ${localUrl}`);
    if (remoteUrl && localUrl != remoteUrl) logger().info(`On your network - ${remoteUrl}`);
    logger().info('Waiting Storybook...');

    const storybook = sh.exec(storybookCommand, { async: true });
    subscribeOn('shutdown', () => {
      if (storybook.pid) void killTree(storybook.pid);
    });
  } else {
    logger().info('Storybook should be started and be accessible at:');
    logger().info(`Local - ${localUrl}`);
    if (remoteUrl && localUrl != remoteUrl) logger().info(`On your network - ${remoteUrl}`);
    logger().info(
      'Tip: Creevey can start Storybook automatically by using `-s` option at the command line. (e.g., yarn/npm run creevey -s)',
    );
    logger().info('Waiting Storybook...');
  }

  if (options.storybookStart || process.env.CI !== 'true') {
    const isConnected = await checkIsStorybookConnected(localUrl);
    if (isConnected) {
      logger().info('Storybook connected!\n');
    } else {
      logger().error('Storybook is not responding. Please start Storybook and restart Creevey');
      shutdownWithError();
    }
  }
}

// TODO Why docker containers are not deleting after stop?
export default async function (command: 'report' | 'test' | 'worker', options: Options | WorkerOptions): Promise<void> {
  const config = await readConfig(options);

  await import('./shutdown.js');

  if (v.is(OptionsSchema, options)) {
    const { port, reportDir = config.reportDir } = options;

    // TODO Add package.json with `"type": "commonjs"` as workaround for esm packages to load `data.js`
    await mkdir(reportDir, { recursive: true });
    await writeFile(path.join(reportDir, 'package.json'), '{"type": "commonjs"}');

    if (command == 'report') {
      const { report } = await import('./report.js');
      const { default: getPort } = await import('get-port');
      const freePort = await getPort({ port });

      report(config, reportDir, freePort);
      return;
    }

    if (cluster.isPrimary) {
      let gridUrl: string | undefined = config.gridUrl;

      if (config.hooks.before) {
        await config.hooks.before();
      }

      if (!(gridUrl || (Object.values(config.browsers) as BrowserConfigObject[]).every(({ gridUrl }) => gridUrl))) {
        gridUrl = await startWebdriverServer(config, options);
      }
      await waitForStorybook(config, options);

      if (config.webdriver === SeleniumWebdriver) {
        try {
          await import('selenium-webdriver');
        } catch {
          logger().error('Failed to start Creevey, missing required dependency: "selenium-webdriver"');
          process.exit(-1);
        }
      } else {
        try {
          await import('playwright-core');
        } catch {
          logger().error('Failed to start Creevey, missing required dependency: "playwright-core"');
          process.exit(-1);
        }
      }
      logger().info('Starting Master Process');

      const { default: getPort } = await import('get-port');
      const freePort = await getPort({ port });

      return (await import('./master/start.js')).start(gridUrl, freePort, config, options);
    }
  }

  if (v.is(WorkerOptionsSchema, options) && cluster.isWorker) {
    let gridUrl = options.gridUrl;
    const { browser = defaultBrowser, debug } = options;

    if (!gridUrl) {
      if (config.webdriver === PlaywrightWebdriver) {
        if (config.useDocker) {
          const version = await getPlaywrightVersion();
          gridUrl = await startPlaywright(config, browser, version, debug);
        } else {
          const { browserName } = config.browsers[browser] as BrowserConfigObject;
          gridUrl = `creevey://${resolvePlaywrightBrowserType(browserName)}`;
        }
      } else {
        assert(gridUrl, 'Grid URL is required for Selenium');
      }
    }

    logger().info(`Starting Worker for ${browser}`);

    return (await import('./worker/start.js')).start(browser, gridUrl, config, options);
  }
}
