// Slot-port + dev-server process plumbing shared by the per-platform surfaces.
// Re-homed here (out of commands/launch.ts) so the surface implementations own
// port/device resolution and the extension watcher-stop without launch.ts and
// the surface registry forming an import cycle.

import { execFileSync, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

import { readRuntimeContextField, resolveRuntimeContextPath } from '../harness.ts';
import { recipeRuntimeDir, runnerDir } from '../paths.ts';

// Apply KEY=VALUE lines from slot resolution to process.env.
// overwrite=true → pool/context match, always overrides existing env.
// overwrite=false → formula match, only fills vars that are unset.
export function applyKVLines(output: string, overwrite: boolean): void {
  for (const line of output.split('\n')) {
    const m = /^([A-Z_]+)=(.+)$/u.exec(line.trim());
    if (!m) continue;
    const [, key, val] = m;
    switch (key) {
      case 'WATCHER_PORT':
        if (overwrite || !process.env['WATCHER_PORT']) {
          process.env['WATCHER_PORT'] = val;
          process.env['METRO_PORT'] = val;
          process.env['RECIPE_WATCHER_PORT'] = val;
        }
        break;
      case 'IOS_SIMULATOR':
        if (overwrite || !process.env['IOS_SIMULATOR']) process.env['IOS_SIMULATOR'] = val;
        break;
      case 'SLOT_ID':
        if (overwrite || !process.env['RECIPE_SLOT_ID']) process.env['RECIPE_SLOT_ID'] = val;
        break;
      case 'CDP_PORT':
        if (overwrite || !process.env['CDP_PORT']) {
          process.env['CDP_PORT'] = val;
          process.env['RECIPE_CDP_PORT'] = val;
        }
        break;
    }
  }
}

function sourceResolve(fn: string, target: string): string {
  const resolveScript = path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh');
  try {
    return execFileSync('bash', ['-c', `source "${resolveScript}" && ${fn} "${target}"`], {
      encoding: 'utf8',
      timeout: 5000,
      stdio: ['ignore', 'pipe', 'ignore'],
    });
  } catch {
    return '';
  }
}

// Resolve mobile slot port/simulator: the slot context the orchestrator wrote
// into the checkout wins first, then the farmslot pool (both overwrite env),
// then the slot-suffix formula (only fills unset vars). Called before explicit
// CLI flag overrides so flags always win at the top.
export function resolveMobileSlotPorts(target: string): void {
  // The checkout's own runtime context is authoritative — it names the exact
  // simulator/port this slot was prepared with, surviving pool renames.
  const ctxOut = sourceResolve('resolve_mobile_runtime_context', target);
  if (ctxOut.trim()) {
    applyKVLines(ctxOut, true);
    return;
  }
  // Pool match always wins — overwrite whatever is in the environment.
  const poolOut = sourceResolve('resolve_farmslot_ports_by_repo', target);
  if (poolOut.trim()) {
    applyKVLines(poolOut, true);
    return;
  }
  // Formula match only fills unset vars (never overrides explicit env/pool).
  const defOut = sourceResolve('resolve_mobile_slot_defaults', target);
  if (defOut.trim()) applyKVLines(defOut, false);
}

// Resolve extension slot ports the same way as mobile: the checkout's runtime
// context first (cdpPort/devServerPort written by the orchestrator's prepare),
// then the farmslot pool, then the directory-suffix formula (fills unset only).
export function resolveExtensionSlotPorts(target: string): void {
  // The prepared checkout's context OVERWRITES inherited env (same authority as
  // mobile's context/pool resolution): a stale CDP_PORT from the shell must not
  // hijack the slot's browser. Explicit CLI flags are applied after and win.
  const contextPath = resolveRuntimeContextPath(target);
  const cdp = readRuntimeContextField(contextPath, 'cdpPort');
  if (cdp) {
    process.env['CDP_PORT'] = cdp;
    process.env['RECIPE_CDP_PORT'] = cdp;
  }
  const dev = readRuntimeContextField(contextPath, 'devServerPort');
  if (dev) {
    process.env['WATCHER_PORT'] = dev;
    process.env['RECIPE_WATCHER_PORT'] = dev;
  }
  if (process.env['CDP_PORT']) return;
  const poolOut = sourceResolve('resolve_farmslot_ports_by_repo', target);
  if (poolOut.trim()) {
    applyKVLines(poolOut, true);
    return;
  }
  const defOut = sourceResolve('resolve_default_extension_ports', target);
  if (defOut.trim()) applyKVLines(defOut, false);
}

// Kill the harness-owned webpack watcher for this checkout: pid file first,
// then a ps-scan for orphans (argv or lsof-cwd match), TERM then KILL. Scoped
// to the target checkout — watchers of other slots are never touched. Returns
// how many processes were signalled so callers can state the outcome.
export function stopExtensionWatcher(target: string): number {
  const runtimeAbs = path.join(target, recipeRuntimeDir());
  const webpackPidFile = path.join(runtimeAbs, 'recipe-harness-webpack.pid');
  let signalled = 0;
  try {
    const pid = fs.readFileSync(webpackPidFile, 'utf8').trim();
    if (/^\d+$/u.test(pid)) {
      try { process.kill(Number(pid), 'SIGTERM'); signalled += 1; } catch { /* already dead */ }
    }
    fs.rmSync(webpackPidFile, { force: true });
  } catch { /* no pid file */ }
  // Scan for any remaining orphan webpack/yarn-start processes in this checkout.
  try {
    const psOut = execFileSync('ps', ['-axo', 'pid=,command='], { encoding: 'utf8' });
    const orphanPids: number[] = [];
    for (const line of psOut.split('\n')) {
      const match = /^\s*(\d+)\s+(.*)$/u.exec(line);
      if (!match) continue;
      const [, pidStr, cmd] = match;
      const isWatcher =
        cmd.includes('yarn start') ||
        cmd.includes('webpack --watch') ||
        cmd.includes('development/webpack/launch.ts --watch');
      if (!isWatcher) continue;
      if (cmd.includes(target)) {
        orphanPids.push(Number(pidStr));
        continue;
      }
      // lsof cwd fallback for processes that don't embed the path in argv.
      try {
        const cwd = execFileSync('lsof', ['-a', `-p${pidStr}`, '-dcwd', '-Fn'], {
          encoding: 'utf8',
          timeout: 2000,
        });
        if (cwd.split('\n').some((l) => l.startsWith('n') && l.slice(1) === target)) {
          orphanPids.push(Number(pidStr));
        }
      } catch { /* lsof unavailable or permission denied */ }
    }
    if (orphanPids.length > 0) {
      for (const pid of orphanPids) {
        try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
      }
      // Brief pause then force-kill survivors.
      Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
      for (const pid of orphanPids) {
        try { process.kill(pid, 'SIGKILL'); } catch { /* already dead */ }
      }
      signalled += orphanPids.length;
    }
  } catch { /* ps not available */ }
  return signalled;
}
