import {
  listRuntimeProcessTableForCurrentPlatform,
  type RuntimeProcessTableRow,
} from '@features/tmux-installer/main';
import { killProcessByPid } from '@main/utils/processKill';
import { listWindowsProcessTable } from '@main/utils/windowsProcessTable';
import { execFile, type ExecFileException } from 'child_process';

export type OpenCodeManagedHostCleanupMode = 'orphaned' | 'force';

export interface OpenCodeManagedHostCleanupCandidate {
  pid: number;
  ppid: number;
  action: 'killed' | 'kept_excluded' | 'kept_recent' | 'kept_unmanaged' | 'failed';
  reason: string;
}

export interface OpenCodeManagedHostCleanupResult {
  scanned: number;
  killed: number;
  candidates: OpenCodeManagedHostCleanupCandidate[];
  diagnostics: string[];
}

export interface OpenCodeManagedHostProcessCleanupOptions {
  mode: OpenCodeManagedHostCleanupMode;
  excludePids?: ReadonlySet<number>;
  requiredDetailsMarkers?: readonly string[];
  startedBeforeMs?: number | null;
  platform?: NodeJS.Platform;
  listProcessRows?: () => Promise<RuntimeProcessTableRow[]>;
  readProcessDetails?: (pid: number) => Promise<string | null>;
  readProcessStartTimeMs?: (pid: number) => Promise<number | null>;
  disposeServeHost?: (baseUrl: string) => Promise<void>;
  killProcess?: (pid: number) => void;
  forceKillProcess?: (pid: number) => void;
  isProcessAlive?: (pid: number) => boolean;
  sleepMs?: (ms: number) => Promise<void>;
}

const OPENCODE_SERVE_COMMAND_RE =
  /(^|[/\\\s"])opencode(?:\.exe)?(?:"?)(?=\s|$).*?(?:^|\s)serve(?=\s|$)/i;
const WINDOWS_APP_MANAGED_OPENCODE_SERVE_RE =
  /[\\/]runtimes[\\/]opencode[\\/]versions[\\/][^"'\s]+[\\/]opencode-windows-[^"'\s]+[\\/]opencode\.exe(?:"|\s|$)/i;
const MANAGED_ENV_MARKERS = ['CLAUDE_MULTIMODEL_DATA_HOME=', 'OPENCODE_CONFIG_CONTENT='] as const;
const MANAGED_ENV_IDENTITY_MARKERS = [
  'AGENT_TEAMS_MCP_CLAUDE_DIR=',
  'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY=',
  'CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_URL=',
] as const;

export async function cleanupManagedOpenCodeServeProcesses(
  options: OpenCodeManagedHostProcessCleanupOptions
): Promise<OpenCodeManagedHostCleanupResult> {
  const platform = options.platform ?? process.platform;
  const result: OpenCodeManagedHostCleanupResult = {
    scanned: 0,
    killed: 0,
    candidates: [],
    diagnostics: [],
  };

  const rows = await (
    options.listProcessRows ??
    (platform === 'win32' ? listWindowsProcessTable : listRuntimeProcessTableForCurrentPlatform)
  )();
  const excludePids = options.excludePids ?? new Set<number>();
  const requiredDetailsMarkers = options.requiredDetailsMarkers ?? [];
  const readDetails =
    options.readProcessDetails ??
    (platform === 'win32' ? async () => null : readNativeProcessCommandWithEnv);
  const readStartTimeMs =
    options.readProcessStartTimeMs ??
    (platform === 'win32' ? readWindowsProcessStartTimeMs : readNativeProcessStartTimeMs);
  const disposeServeHost = options.disposeServeHost ?? disposeOpenCodeServeHost;
  const killProcess = options.killProcess ?? killProcessByPid;
  const forceKillProcess =
    options.forceKillProcess ?? ((pid: number) => process.kill(pid, 'SIGKILL'));
  const isProcessAlive = options.isProcessAlive ?? isNativeProcessAlive;
  const sleepMs = options.sleepMs ?? sleep;

  for (const row of rows) {
    if (!isOpenCodeServeCommand(row.command)) {
      continue;
    }
    result.scanned += 1;

    if (excludePids.has(row.pid)) {
      result.candidates.push({
        pid: row.pid,
        ppid: row.ppid,
        action: 'kept_excluded',
        reason: 'pid is known to the bridge host registry cleanup result',
      });
      continue;
    }

    const details = await readDetails(row.pid);
    const isManagedByWindowsCommand =
      platform === 'win32' && isAppManagedWindowsOpenCodeServeCommand(row.command);
    const isManaged =
      isManagedByWindowsCommand ||
      Boolean(details && isManagedOpenCodeServeProcessDetails(details));
    const hasRequiredDetailsMarkers =
      requiredDetailsMarkers.length === 0 ||
      Boolean(details && processDetailsIncludeMarkers(details, requiredDetailsMarkers));
    if (!isManaged || !hasRequiredDetailsMarkers) {
      result.candidates.push({
        pid: row.pid,
        ppid: row.ppid,
        action: 'kept_unmanaged',
        reason:
          platform === 'win32'
            ? 'process is not an app-managed Windows OpenCode serve command'
            : 'process does not carry Agent Teams managed OpenCode environment markers',
      });
      continue;
    }

    if (options.mode === 'orphaned') {
      const startedAtMs =
        typeof options.startedBeforeMs === 'number' ? await readStartTimeMs(row.pid) : null;
      if (
        typeof options.startedBeforeMs === 'number' &&
        (!Number.isFinite(startedAtMs) ||
          startedAtMs === null ||
          startedAtMs >= options.startedBeforeMs)
      ) {
        result.candidates.push({
          pid: row.pid,
          ppid: row.ppid,
          action: 'kept_recent',
          reason: 'process started after this app instance began',
        });
        continue;
      }
      const parentMayStillOwnProcess =
        platform === 'win32' ? row.ppid > 0 && isProcessAlive(row.ppid) : row.ppid !== 1;
      if (parentMayStillOwnProcess) {
        result.candidates.push({
          pid: row.pid,
          ppid: row.ppid,
          action: 'kept_recent',
          reason: 'process is still parented and may belong to an active bridge command',
        });
        continue;
      }
    }

    try {
      const baseUrl = getOpenCodeServeLoopbackBaseUrl(row.command);
      if (baseUrl) {
        await disposeServeHost(baseUrl).catch(() => undefined);
      }
      killProcess(row.pid);
      if (options.mode === 'force' && isProcessAlive(row.pid)) {
        await sleepMs(250);
        if (isProcessAlive(row.pid)) {
          try {
            forceKillProcess(row.pid);
          } catch (error) {
            if (isProcessAlive(row.pid)) {
              throw error;
            }
          }
        }
      }
      result.killed += 1;
      result.candidates.push({
        pid: row.pid,
        ppid: row.ppid,
        action: 'killed',
        reason: `managed OpenCode serve ${options.mode === 'force' ? 'cleanup' : 'orphan cleanup'}`,
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      result.diagnostics.push(`Failed to kill managed OpenCode serve pid=${row.pid}: ${message}`);
      result.candidates.push({
        pid: row.pid,
        ppid: row.ppid,
        action: 'failed',
        reason: message,
      });
    }
  }

  return result;
}

export function isOpenCodeServeCommand(command: string): boolean {
  return OPENCODE_SERVE_COMMAND_RE.test(command.trim());
}

export function isAppManagedWindowsOpenCodeServeCommand(command: string): boolean {
  const normalizedCommand = command.trim().replace(/\//g, '\\');
  return (
    isOpenCodeServeCommand(normalizedCommand) &&
    WINDOWS_APP_MANAGED_OPENCODE_SERVE_RE.test(normalizedCommand)
  );
}

export function isManagedOpenCodeServeProcessDetails(details: string): boolean {
  return (
    processDetailsIncludeMarkers(details, MANAGED_ENV_MARKERS) &&
    MANAGED_ENV_IDENTITY_MARKERS.some((marker) => processDetailsIncludeMarker(details, marker))
  );
}

export function getOpenCodeServeLoopbackBaseUrl(command: string): string | null {
  const portMatch = /(?:^|\s)--port(?:=|\s+)(\d{1,5})(?=\s|$)/.exec(command);
  if (!portMatch) {
    return null;
  }
  const port = Number.parseInt(portMatch[1], 10);
  if (!Number.isFinite(port) || port < 1 || port > 65535) {
    return null;
  }

  const hostnameMatch = /(?:^|\s)--hostname(?:=|\s+)(\S+)(?=\s|$)/.exec(command);
  const hostname = hostnameMatch?.[1] ?? '127.0.0.1';
  if (!isLoopbackHostname(hostname)) {
    return null;
  }
  const normalizedHostname = hostname === '::1' ? '[::1]' : hostname;
  return `http://${normalizedHostname}:${port}`;
}

function processDetailsIncludeMarkers(details: string, markers: readonly string[]): boolean {
  return markers.every((marker) => processDetailsIncludeMarker(details, marker));
}

function processDetailsIncludeMarker(details: string, marker: string): boolean {
  const valueBoundary = marker.endsWith('=') ? '' : '(?=\\s|$)';
  return new RegExp(`(^|\\s)${escapeRegExp(marker)}${valueBoundary}`).test(details);
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function isLoopbackHostname(hostname: string): boolean {
  return (
    hostname === '127.0.0.1' ||
    hostname === 'localhost' ||
    hostname === '::1' ||
    hostname === '[::1]'
  );
}

async function disposeOpenCodeServeHost(baseUrl: string): Promise<void> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 1_000);
  try {
    await fetch(`${baseUrl}/global/dispose`, {
      method: 'POST',
      signal: controller.signal,
    });
  } finally {
    clearTimeout(timeout);
  }
}

async function readNativeProcessCommandWithEnv(pid: number): Promise<string | null> {
  return execFileText('ps', ['eww', '-p', String(pid), '-o', 'command='], 2_000, 2 * 1024 * 1024);
}

async function readNativeProcessStartTimeMs(pid: number): Promise<number | null> {
  const output = await execFileText('ps', ['-p', String(pid), '-o', 'lstart='], 2_000, 64 * 1024);
  if (!output) {
    return null;
  }
  const parsed = Date.parse(output.trim());
  return Number.isFinite(parsed) ? parsed : null;
}

async function readWindowsProcessStartTimeMs(pid: number): Promise<number | null> {
  const normalizedPid = Math.trunc(pid);
  if (!Number.isFinite(normalizedPid) || normalizedPid <= 0) {
    return null;
  }

  const script = [
    '$ErrorActionPreference = "Stop"',
    `$process = Get-Process -Id ${normalizedPid} -ErrorAction Stop`,
    '$process.StartTime.ToUniversalTime().ToString("o")',
  ].join('; ');
  const output = await execFileText(
    'powershell.exe',
    ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script],
    2_000,
    64 * 1024
  );
  if (!output) {
    return null;
  }
  const parsed = Date.parse(output.trim());
  return Number.isFinite(parsed) ? parsed : null;
}

function isNativeProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch (error) {
    return (error as NodeJS.ErrnoException).code === 'EPERM';
  }
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function execFileText(
  command: string,
  args: string[],
  timeout: number,
  maxBuffer: number
): Promise<string | null> {
  return new Promise((resolve) => {
    execFile(
      command,
      args,
      {
        encoding: 'utf8',
        timeout,
        maxBuffer,
        windowsHide: true,
      },
      (error: ExecFileException | null, stdout: string | Buffer) => {
        if (error) {
          resolve(null);
          return;
        }
        resolve(String(stdout));
      }
    );
  });
}
