import path from 'path';
import { execFile, spawn } from 'child_process';
import { promisify } from 'util';

import {
  NerdctlAppDeployPlan,
  getTenantAppRuntimeDir,
  getTenantAppUnitName,
} from './nerdctl-hosting.js';
import { logger } from './logger.js';

const execFileAsync = promisify(execFile);
const RUNTIME_SHUTDOWN_POLL_MS = 100;

// Global registry of active tenant runtime child processes for build operations.
// Populated when a build/reset command is spawned; cleared on completion or timeout.
// The nanoclaw SIGTERM handler reads this to kill orphaned processes on shutdown.
export const activeTenantRuntimeProcs: Map<number, import("child_process").ChildProcess> = new Map();
const TENANT_RUNTIME_HELPER =
  '/run/current-system/sw/bin/nanoclaw-tenant-runtime';

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

function signalTenantRuntimeProcessGroup(
  pid: number,
  signal: NodeJS.Signals,
): boolean {
  try {
    process.kill(-pid, signal);
    return true;
  } catch {
    activeTenantRuntimeProcs.delete(pid);
    return false;
  }
}

async function waitForActiveTenantRuntimeProcsToDrain(
  timeoutMs: number,
  trackedPids: number[],
): Promise<number[]> {
  let remaining = Math.max(0, timeoutMs);
  while (remaining > 0) {
    const activePids = trackedPids.filter((pid) => activeTenantRuntimeProcs.has(pid));
    if (activePids.length === 0) return [];
    const sleepMs = Math.min(RUNTIME_SHUTDOWN_POLL_MS, remaining);
    await sleep(sleepMs);
    remaining -= sleepMs;
  }
  return trackedPids.filter((pid) => activeTenantRuntimeProcs.has(pid));
}

export async function shutdownActiveTenantRuntimeProcs(
  gracePeriodMs: number,
): Promise<void> {
  const trackedPids = [...activeTenantRuntimeProcs.keys()];
  if (trackedPids.length === 0) return;

  const killWaitMs = Math.min(1000, Math.max(0, Math.floor(gracePeriodMs / 5)));
  const termWaitMs = Math.max(0, gracePeriodMs - killWaitMs);

  for (const pid of trackedPids) {
    signalTenantRuntimeProcessGroup(pid, 'SIGTERM');
  }

  let remainingPids = await waitForActiveTenantRuntimeProcsToDrain(
    termWaitMs,
    trackedPids,
  );

  if (remainingPids.length > 0) {
    for (const pid of remainingPids) {
      signalTenantRuntimeProcessGroup(pid, 'SIGKILL');
    }
    remainingPids = await waitForActiveTenantRuntimeProcsToDrain(
      killWaitMs,
      remainingPids,
    );
  }

  logger.info(
    { trackedPids, remainingPids },
    remainingPids.length === 0
      ? 'Tenant runtime shutdown complete'
      : 'Tenant runtime shutdown timed out with detached helpers remaining',
  );
}

/**
 * Detect containerd snapshot store corruption errors.
 * These occur when containerd restarts and the overlayfs snapshotter's
 * metadata references layers from a previous (now-dead) rootlesskit mount
 * namespace.
 */
function isSnapshotCorruptionError(err: any): boolean {
  const msg = String(err?.stderr || err?.message || '');
  return (
    msg.includes('failed to prepare extraction snapshot') &&
    msg.includes('parent snapshot') &&
    msg.includes('does not exist')
  );
}

function isSnapshotCorruptionJournal(output: string): boolean {
  return (
    output.includes('failed to prepare extraction snapshot') &&
    output.includes('does not exist')
  );
}

/**
 * Recover from app-scoped snapshot corruption by resetting only the affected
 * app runtime, then retrying the supplied operation.
 */
async function recoverAppRuntimeAndRetry<T>(
  tenantId: string,
  appName: string,
  operation: () => Promise<T>,
): Promise<T> {
  await resetTenantRuntime(tenantId, appName);
  return operation();
}

async function writeTenantOwnedFile(options: {
  tenantId: string;
  targetPath: string;
  content: string;
  mode?: string;
}): Promise<void> {
  const { tenantId, targetPath, content, mode = '0644' } = options;
  await runTenantRuntimeWithInput(
    tenantId,
    'write-file',
    [targetPath, mode],
    content,
  );
}

async function ensureTenantOwnedDir(
  tenantId: string,
  targetPath: string,
  mode = '0755',
): Promise<void> {
  await runTenantRuntime(tenantId, 'mkdir', [targetPath, mode]);
}

async function runTenantRuntime(
  tenantId: string,
  kind:
    | 'nerdctl'
    | 'systemctl-user'
    | 'journalctl-user'
    | 'runtime-ensure'
    | 'mkdir'
    | 'mkdir-workspace'
    | 'write-file'
    | 'rm-app-runtime'
    | 'clean-project-containers'
    | 'runtime-prune'
    | 'runtime-prune-images-all'
    | 'runtime-disk-usage'
    | 'nerdctl-exec'
    | 'nerdctl-logs'
    | 'buildkit-reset'
    | 'runtime-reset',
  args: string[],
): Promise<{ stdout: string; stderr: string }> {
  const LONG_KINDS = new Set([
    'nerdctl', 'systemctl-user', 'runtime-ensure', 'buildkit-reset', 'runtime-reset',
    'runtime-prune', 'runtime-prune-images-all', 'clean-project-containers',
  ]);
  // Build kinds spawn detached process groups so the full nerdctl/buildkitd tree
  // can be killed on timeout or nanoclaw shutdown — prevents orphaned processes
  // from blocking future IPC operations.
  const BUILD_KINDS = new Set([
    'nerdctl', 'buildkit-reset', 'runtime-reset', 'runtime-prune-images-all',
  ]);
  const timeoutMs = LONG_KINDS.has(kind) ? 30 * 60 * 1000 : 30 * 1000; // 30min for builds (fuse-overlayfs extraction is slow)

  if (!BUILD_KINDS.has(kind)) {
    // Fast path: short-lived commands
    const { stdout, stderr } = await execFileAsync('sudo', [
      TENANT_RUNTIME_HELPER,
      tenantId,
      kind,
      ...args,
    ], { timeout: timeoutMs, maxBuffer: 50 * 1024 * 1024 });
    return {
      stdout: String(stdout || '').trim(),
      stderr: String(stderr || '').trim(),
    };
  }

  // Build path: spawn with detached process group so SIGKILL on timeout kills
  // the full subprocess tree (nerdctl → buildkitd → runc), not just the sudo wrapper.
  return new Promise((resolve, reject) => {
    const child = spawn('sudo', [TENANT_RUNTIME_HELPER, tenantId, kind, ...args], {
      detached: true,
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    const pid = child.pid;
    if (pid === undefined) {
      reject(new Error('Failed to spawn tenant runtime process'));
      return;
    }
    activeTenantRuntimeProcs.set(pid, child);

    const killGroup = () => {
      try { process.kill(-pid, 'SIGKILL'); } catch (_) {}
      activeTenantRuntimeProcs.delete(pid);
    };

    const timer = setTimeout(() => {
      killGroup();
      reject(Object.assign(
        new Error(`Tenant runtime "${kind}" timed out after ${timeoutMs}ms for tenant "${tenantId}"`),
        { stderr: `timeout after ${timeoutMs}ms` },
      ));
    }, timeoutMs);

    const stdoutChunks: Buffer[] = [];
    const stderrChunks: Buffer[] = [];
    child.stdout?.on('data', (d: Buffer) => stdoutChunks.push(d));
    child.stderr?.on('data', (d: Buffer) => stderrChunks.push(d));

    child.on('close', (code) => {
      clearTimeout(timer);
      activeTenantRuntimeProcs.delete(pid);
      const stdout = Buffer.concat(stdoutChunks).toString('utf8').trim();
      const stderr = Buffer.concat(stderrChunks).toString('utf8').trim();
      if (code === 0) {
        resolve({ stdout, stderr });
      } else {
        reject(Object.assign(
          new Error(`Tenant runtime "${kind}" exited with code ${code}`),
          { stderr, stdout },
        ));
      }
    });

    child.on('error', (err) => {
      clearTimeout(timer);
      activeTenantRuntimeProcs.delete(pid);
      reject(err);
    });
  });
}

async function runTenantRuntimeWithInput(
  tenantId: string,
  kind: 'write-file',
  args: string[],
  input: string,
): Promise<void> {
  await new Promise<void>((resolve, reject) => {
    const child = spawn(
      'sudo',
      [TENANT_RUNTIME_HELPER, tenantId, kind, ...args],
      { stdio: ['pipe', 'pipe', 'pipe'] },
    );

    let stderr = '';
    child.stderr.on('data', (chunk) => {
      stderr += String(chunk);
    });
    child.on('error', reject);
    child.on('close', (code) => {
      if (code === 0) {
        resolve();
        return;
      }
      reject(new Error(stderr.trim() || `sudo ${kind} exited with code ${code}`));
    });
    child.stdin.end(input);
  });
}

export async function ensureTenantOwnedWorkspaceDir(
  tenantId: string,
  targetPath: string,
  mode?: string,
): Promise<void> {
  const args = mode ? [targetPath, mode] : [targetPath];
  await runTenantRuntime(tenantId, 'mkdir-workspace', args);
}

export async function restartTenantNerdctlApp(
  tenantId: string,
  appName: string,
  services?: string[],
): Promise<{ stdout: string; stderr: string }> {
  const projectName = `nanoclaw-${tenantId}-${appName}`;
  const composeFilePath = getTenantAppRuntimeDir(tenantId, appName) + '/compose.yml';
  // Stop and remove containers before recreating so the restart policy cannot
  // race to reclaim the port before the new container binds it.
  const rmArgs = [
    'compose',
    '--project-name', projectName,
    '-f', composeFilePath,
    'rm', '-sf',
    ...(services ?? []),
  ];
  await runTenantRuntime(tenantId, 'nerdctl', rmArgs).catch(() => {
    // best-effort — proceed even if rm fails (e.g. no containers exist yet)
  });
  const args = [
    'compose',
    '--project-name', projectName,
    '-f', composeFilePath,
    'up', '-d', '--no-build',
    ...(services ?? []),
  ];
  return runTenantRuntime(tenantId, 'nerdctl', args);
}

export async function removeTenantNerdctlVolume(
  tenantId: string,
  appName: string,
  volumeName: string,
): Promise<{ stdout: string; stderr: string }> {
  // Full volume name matches what buildComposeContent generates
  const projectName = `nanoclaw-${tenantId}-${appName}`;
  const fullVolumeName = `${projectName}-${volumeName}`;
  // Security: must start with tenant prefix
  if (!fullVolumeName.startsWith(`nanoclaw-${tenantId}-`)) {
    throw new Error(`Volume name "${fullVolumeName}" is outside tenant scope`);
  }
  return runTenantRuntime(tenantId, 'nerdctl', ['volume', 'rm', fullVolumeName]);
}

export async function listTenantNerdctlVolumes(
  tenantId: string,
  appName?: string,
): Promise<string> {
  const prefix = appName
    ? `nanoclaw-${tenantId}-${appName}-`
    : `nanoclaw-${tenantId}-`;
  const { stdout } = await runTenantRuntime(tenantId, 'nerdctl', ['volume', 'ls', '--format', '{{.Name}}']);
  const volumes = stdout
    .split('\n')
    .map((l) => l.trim())
    .filter((l) => l.startsWith(prefix));
  return volumes.join('\n');
}

export async function execTenantNerdctlContainer(
  tenantId: string,
  appName: string,
  service: string,
  command: string[],
): Promise<{ stdout: string; stderr: string }> {
  const projectName = `nanoclaw-${tenantId}-${appName}`;
  const containerName = `${projectName}-${service}-1`;
  return runTenantRuntime(tenantId, 'nerdctl-exec', [containerName, ...command]);
}

export async function getTenantNerdctlContainerLogs(
  tenantId: string,
  appName: string,
  service: string,
  tail: number,
): Promise<{ stdout: string }> {
  const projectName = `nanoclaw-${tenantId}-${appName}`;
  const containerName = `${projectName}-${service}-1`;
  const { stdout } = await runTenantRuntime(tenantId, 'nerdctl-logs', [containerName, String(tail)]);
  return { stdout };
}

export async function deployTenantNerdctlApp(
  plan: NerdctlAppDeployPlan,
  options?: { forceRebuild?: boolean; services?: string[] },
): Promise<void> {
  const runtimeDir = getTenantAppRuntimeDir(plan.tenantId, plan.appName);
  await ensureTenantOwnedDir(plan.tenantId, runtimeDir);
  await ensureTenantOwnedDir(plan.tenantId, path.dirname(plan.unitFilePath));

  await runTenantRuntime(plan.tenantId, 'runtime-ensure', []);

  await writeTenantOwnedFile({
    tenantId: plan.tenantId,
    targetPath: plan.composeFilePath,
    content: plan.composeContent,
  });
  await writeTenantOwnedFile({
    tenantId: plan.tenantId,
    targetPath: plan.unitFilePath,
    content: plan.unitContent,
  });

  await runTenantRuntime(plan.tenantId, 'systemctl-user', ['daemon-reload']);
  await runTenantRuntime(plan.tenantId, 'systemctl-user', [
    'enable',
    plan.unitName,
  ]);
  // Pre-deploy: clean stale containers to prevent name-store conflicts from
  // previous failed deploys (ghost containers in Created/Unknown state).
  try {
    await cleanTenantProjectContainers(plan.tenantId, plan.projectName);
  } catch {
    // best-effort — don't block deploy if cleanup fails
  }

  const buildArgs = [
    'compose',
    '--project-name', plan.projectName,
    '-f', plan.composeFilePath,
    'build',
  ];
  if (options?.forceRebuild) {
    buildArgs.push('--no-cache');
  }
  if (options?.services?.length) {
    buildArgs.push(...options.services);
  }

  const buildApp = async (): Promise<void> => {
    if (!plan.hasBuildServices) return;
    await runTenantRuntime(plan.tenantId, 'nerdctl', buildArgs);
  };

  try {
    await buildApp();
  } catch (buildErr: any) {
    if (isSnapshotCorruptionError(buildErr)) {
      await recoverAppRuntimeAndRetry(plan.tenantId, plan.appName, buildApp);
    } else {
      throw buildErr;
    }
  }

  // reload-or-restart: runs ExecReload if already active, or ExecStart if inactive.
  // Because the unit no longer builds images, every deploy explicitly builds first
  // and the unit is now responsible only for container lifecycle.
  const reloadRuntime = async (): Promise<void> => {
    await runTenantRuntime(plan.tenantId, 'systemctl-user', [
      'reload-or-restart',
      plan.unitName,
    ]);
  };

  const recoverRuntimeAndReload = async (): Promise<void> => {
    await recoverAppRuntimeAndRetry(plan.tenantId, plan.appName, async () => {
      await buildApp();
      await reloadRuntime();
    });
  };

  try {
    await reloadRuntime();
  } catch (reloadErr: any) {
    const errMsg = String(reloadErr?.stderr || reloadErr?.message || '');
    if (isSnapshotCorruptionError(reloadErr)) {
      await recoverRuntimeAndReload();
    } else if (errMsg.includes('failed') || errMsg.includes('exit code')) {
      try {
        const { stdout: journal } = await runTenantRuntime(
          plan.tenantId,
          'journalctl-user',
          [plan.unitName, '20'],
        );
        if (isSnapshotCorruptionJournal(journal)) {
          await recoverRuntimeAndReload();
        } else {
          throw reloadErr;
        }
      } catch (journalErr) {
        if (journalErr === reloadErr) throw reloadErr;
        throw reloadErr;
      }
    } else {
      throw reloadErr;
    }
  }
}

export async function cleanTenantProjectContainers(
  tenantId: string,
  projectName: string,
): Promise<void> {
  await runTenantRuntime(tenantId, 'clean-project-containers', [projectName]);
}

export async function removeTenantNerdctlApp(
  tenantId: string,
  appName: string,
): Promise<void> {
  const unitName = getTenantAppUnitName(appName);
  const projectName = `nanoclaw-${tenantId}-${appName}`;

  try {
    await runTenantRuntime(tenantId, 'systemctl-user', [
      'disable',
      '--now',
      unitName,
    ]);
  } catch {
    try {
      await runTenantRuntime(tenantId, 'systemctl-user', ['stop', unitName]);
    } catch {
      // ignore
    }
  }

  // Force-remove any containers still in the name store (e.g. if compose down
  // failed or left root-owned oplock dirs behind). Fixes the root-owned oplock
  // issue that blocks subsequent deploys.
  try {
    await cleanTenantProjectContainers(tenantId, projectName);
  } catch {
    // best-effort — don't let cleanup failure block the rest of removal
  }

  await runTenantRuntime(tenantId, 'rm-app-runtime', [appName]);

  await runTenantRuntime(tenantId, 'systemctl-user', ['daemon-reload']);
}

export interface TenantNerdctlAppStatus {
  appName: string;
  serviceName: string;
  active: boolean;
  subState: string;
  activeState: string;
  result: string;
}

export interface TenantNerdctlAppDebugInfo extends TenantNerdctlAppStatus {
  recentLogs: string;
}

export async function getTenantNerdctlAppStatus(
  tenantId: string,
  appName: string,
): Promise<TenantNerdctlAppStatus> {
  const serviceName = getTenantAppUnitName(appName);

  try {
    const { stdout } = await runTenantRuntime(tenantId, 'systemctl-user', [
      'show',
      serviceName,
      '--property=ActiveState,SubState,Result',
    ]);
    const activeState = stdout
      .split('\n')
      .find((line) => line.startsWith('ActiveState='))
      ?.split('=')[1]
      ?.trim();
    const subState = stdout
      .split('\n')
      .find((line) => line.startsWith('SubState='))
      ?.split('=')[1]
      ?.trim();
    const result = stdout
      .split('\n')
      .find((line) => line.startsWith('Result='))
      ?.split('=')[1]
      ?.trim();
    return {
      appName,
      serviceName,
      active: activeState === 'active',
      subState: subState || 'unknown',
      activeState: activeState || 'unknown',
      result: result || 'unknown',
    };
  } catch {
    return {
      appName,
      serviceName,
      active: false,
      subState: 'not-found',
      activeState: 'not-found',
      result: 'unknown',
    };
  }
}

export async function getTenantNerdctlAppDebugInfo(
  tenantId: string,
  appName: string,
  lines = 40,
): Promise<TenantNerdctlAppDebugInfo> {
  const serviceName = getTenantAppUnitName(appName);
  const status = await getTenantNerdctlAppStatus(tenantId, appName);

  let result = 'unknown';
  let recentLogs = '';
  try {
    const { stdout } = await runTenantRuntime(tenantId, 'systemctl-user', [
      'show',
      serviceName,
      '--property=Result',
    ]);
    result =
      stdout
        .split('\n')
        .find((line) => line.startsWith('Result='))
        ?.split('=')[1]
        ?.trim() || result;
  } catch {
    // ignore
  }

  try {
    const { stdout } = await runTenantRuntime(tenantId, 'journalctl-user', [
      serviceName,
      String(lines),
    ]);
    recentLogs = stdout.trim();
  } catch {
    // ignore
  }

  return {
    ...status,
    result,
    recentLogs,
  };
}

export async function pruneTenantRuntime(
  tenantId: string,
  scope: 'builder' | 'images' | 'volumes' | 'all',
): Promise<{ stdout: string; stderr: string }> {
  return runTenantRuntime(tenantId, 'runtime-prune', [scope]);
}


export async function resetTenantBuildkitStore(tenantId: string): Promise<{ stdout: string; stderr: string }> {
  return runTenantRuntime(tenantId, 'buildkit-reset', []);
}
export async function resetTenantRuntime(tenantId: string, appName: string): Promise<{ stdout: string; stderr: string }> {
  return runTenantRuntime(tenantId, 'runtime-reset', [appName]);
}

export async function getTenantDiskUsage(tenantId: string): Promise<{
  containerd_mb: number;
  buildkit_mb: number;
  nerdctl_mb: number;
  workspace_mb: number;
  volumes: Record<string, number>;
}> {
  const { stdout } = await runTenantRuntime(tenantId, 'runtime-disk-usage', []);
  return JSON.parse(stdout);
}
