import { spawn } from 'node:child_process';
import { resolve } from 'node:path';

const rootDir = process.cwd();
const viteBin = resolve(rootDir, 'node_modules/vite/bin/vite.js');
const serveScript = resolve(rootDir, 'scripts/serve-app-dist.mjs');
const appDistDir = '/tmp/ai-code-test-app-dist';
const buildEnv = {
  ...process.env,
  VITE_FILTER_PUBLIC_DIR: 'true',
  VITE_DISABLE_MODULE_PRELOAD: 'true',
};
const serveEnv = {
  ...process.env,
  APP_DIST_DIR: appDistDir,
};

function log(message) {
  console.log(`[preview:test-app:watch] ${new Date().toISOString()} ${message}`);
}

function logError(message) {
  console.error(`[preview:test-app:watch] ${new Date().toISOString()} ${message}`);
}

function runNodeScript(args, env, options = {}) {
  return spawn(process.execPath, args, {
    cwd: rootDir,
    env,
    stdio: 'inherit',
    ...options,
  });
}

function pipePrefixedLines(stream, writer, prefix, onLine) {
  if (!stream) {
    return;
  }

  let buffer = '';
  stream.setEncoding('utf8');
  stream.on('data', (chunk) => {
    buffer += chunk;
    const lines = buffer.split(/\r?\n/u);
    buffer = lines.pop() ?? '';

    for (const line of lines) {
      onLine?.(line);
      writer.write(`${prefix}${line}\n`);
    }
  });
  stream.on('end', () => {
    if (!buffer) {
      return;
    }

    onLine?.(buffer);
    writer.write(`${prefix}${buffer}\n`);
    buffer = '';
  });
}

function attachWatchBuildLogging(child) {
  let rebuildStartedAt = null;
  let serviceWorkerBuildPending = false;

  const handleLine = (line) => {
    const normalized = line.replace(/\u001b\[[0-9;]*m/g, '').trim();

    if (!normalized) {
      return;
    }

    if (normalized.startsWith('Building src/sw.js service worker')) {
      serviceWorkerBuildPending = true;
      return;
    }

    if (normalized.includes('building client environment for production')) {
      if (serviceWorkerBuildPending) {
        serviceWorkerBuildPending = false;
        return;
      }

      rebuildStartedAt = Date.now();
      log('watch rebuild started');
      return;
    }

    const builtMatch = normalized.match(/^built in (\d+)ms\.$/u);
    if (!builtMatch) {
      return;
    }

    const reportedMs = Number.parseInt(builtMatch[1], 10);
    const wallMs = rebuildStartedAt === null ? null : Date.now() - rebuildStartedAt;
    const durationText = wallMs === null ? `vite=${reportedMs}ms` : `vite=${reportedMs}ms, wall=${wallMs}ms`;
    log(`watch rebuild completed (${durationText})`);
    rebuildStartedAt = null;
  };

  pipePrefixedLines(child.stdout, process.stdout, '[preview:watch:vite] ', handleLine);
  pipePrefixedLines(child.stderr, process.stderr, '[preview:watch:vite] ', handleLine);
}

function waitForExit(child) {
  return new Promise((resolveExit, rejectExit) => {
    child.once('error', rejectExit);
    child.once('exit', (code, signal) => {
      if (code === 0) {
        resolveExit();
        return;
      }

      rejectExit(new Error(`process exited with code ${code ?? 'null'}${signal ? ` (signal: ${signal})` : ''}`));
    });
  });
}

function terminateChild(child, signal = 'SIGTERM') {
  if (child.killed) {
    return;
  }

  try {
    child.kill(signal);
  } catch {
    // Ignore termination races during shutdown.
  }
}

const backgroundChildren = [];
let shuttingDown = false;

function shutdown(exitCode = 0) {
  if (shuttingDown) {
    return;
  }

  shuttingDown = true;
  backgroundChildren.forEach((child) => terminateChild(child));
  setTimeout(() => {
    backgroundChildren.forEach((child) => terminateChild(child, 'SIGKILL'));
    process.exit(exitCode);
  }, 5_000).unref();
}

process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));

try {
  log('initial build started');
  await waitForExit(runNodeScript([viteBin, 'build', '--outDir', appDistDir], buildEnv));
  log('initial build completed');

  const watchBuild = runNodeScript([viteBin, 'build', '--watch', '--clearScreen', 'false', '--outDir', appDistDir], buildEnv, {
    stdio: ['inherit', 'pipe', 'pipe'],
  });
  const previewServer = runNodeScript([serveScript], serveEnv);
  backgroundChildren.push(watchBuild, previewServer);
  attachWatchBuildLogging(watchBuild);
  log('preview server and build watcher are running');

  watchBuild.once('exit', (code, signal) => {
    if (shuttingDown) {
      return;
    }

    logError(`build watcher stopped: code=${code ?? 'null'} signal=${signal ?? 'none'}`);
    shutdown(code && code > 0 ? code : 1);
  });

  previewServer.once('exit', (code, signal) => {
    if (shuttingDown) {
      return;
    }

    logError(`preview server stopped: code=${code ?? 'null'} signal=${signal ?? 'none'}`);
    shutdown(code && code > 0 ? code : 1);
  });
} catch (error) {
  logError('failed to prepare preview build');
  console.error(error instanceof Error ? error.message : String(error));
  process.exit(1);
}
