import { execFile } from 'child_process';
import { promisify } from 'util';
import { readFile, stat } from 'fs/promises';
import { existsSync } from 'fs';
import { appendDashboardWarning } from '../../lib/dashboard-alerts.js';
import type {
  HermesOpsCronJob,
  HermesOpsInstance,
  HermesOpsRepo,
  HermesOpsSnapshot,
  HermesOpsTimer,
  ProbeStatus,
} from './types.js';

const execFileAsync = promisify(execFile);

// Serve the snapshot from a short-TTL cache so the panel poll (~60s) and any
// concurrent refreshes don't fan out ~20 systemctl/git/ps/du subprocesses each
// time. Mirrors the health module's caching approach.
const CACHE_TTL = 30000; // 30 seconds
let cache: { snapshot: HermesOpsSnapshot; at: number } | null = null;
let inflight: Promise<HermesOpsSnapshot> | null = null;

const instances = [
  {
    id: 'vijay' as const,
    label: 'Vijay / root',
    hermesHome: '/root/.hermes',
    gatewayKind: 'system',
    gatewayService: 'hermes-gateway.service',
    dashboardService: 'hermes-root-dashboard.service',
    dashboardPort: 9119,
    backupTimer: 'hermes-root-backup.timer',
    repoPath: '/root/repos/bytelyst_hostinger_hermes_vm',
    opsExportPath: '/root/.hermes/ops-export.json',
    driveFolder: 'Vijay Drive',
  },
  {
    id: 'bheem' as const,
    label: 'Bheem / Uma',
    hermesHome: '/home/uma/.hermes',
    gatewayKind: 'uma-user',
    gatewayService: 'uma-hermes-gateway.service',
    dashboardService: 'uma-hermes-dashboard.service',
    dashboardPort: 9120,
    backupTimer: 'uma-hermes-backup.timer',
    repoPath: '/home/uma/repos/uma_hostinger_hermes_vm',
    opsExportPath: '/home/uma/.hermes/ops-export.json',
    driveFolder: 'Bheem Drive',
  },
];

interface OpsExport {
  gateway?: { active?: boolean; enabled?: boolean; status?: ProbeStatus };
  dashboard?: { active?: boolean; status?: ProbeStatus };
  backupTimer?: HermesOpsTimer;
  repo?: HermesOpsRepo;
  restoredFileCount?: number | null;
  restoredCronJobs?: number | null;
  googleWorkspaceToken?: boolean;
}

interface ExecResult {
  // Trimmed stdout. Present even when the command exited non-zero (e.g.
  // `systemctl is-active` prints "inactive" and exits 3).
  stdout: string;
  // Whether the command actually executed. False only when it could not run at
  // all — binary missing (ENOENT) or killed by the timeout. A non-zero exit with
  // output still counts as `ran: true` so callers can read the output.
  ran: boolean;
}

async function exec(command: string, args: string[], cwd?: string): Promise<ExecResult> {
  try {
    const { stdout } = await execFileAsync(command, args, {
      cwd,
      timeout: 5000,
      maxBuffer: 1024 * 1024,
    });
    return { stdout: stdout.trim(), ran: true };
  } catch (error) {
    const err = error as NodeJS.ErrnoException & { stdout?: string; killed?: boolean };
    // Command could not be spawned, or was killed by the timeout → unknown.
    if (err?.code === 'ENOENT' || err?.killed) {
      return { stdout: '', ran: false };
    }
    // Ran but exited non-zero; the output is still meaningful.
    if (typeof err?.stdout === 'string') {
      return { stdout: err.stdout.trim(), ran: true };
    }
    return { stdout: '', ran: false };
  }
}

function activeFromResult(result: ExecResult): { active: boolean; status: ProbeStatus } {
  if (!result.ran) return { active: false, status: 'unknown' };
  const active = result.stdout === 'active';
  return { active, status: active ? 'up' : 'down' };
}

async function probeSystemActive(unit: string): Promise<{ active: boolean; status: ProbeStatus }> {
  return activeFromResult(await exec('systemctl', ['is-active', unit]));
}

async function probeSystemEnabled(unit: string): Promise<boolean> {
  const result = await exec('systemctl', ['is-enabled', unit]);
  return result.ran && result.stdout === 'enabled';
}

// Bheem's gateway runs under Uma's *user* systemd. Use the authoritative
// `systemctl --user` check via `runuser`; if that probe can't run (no root,
// no user runtime dir, etc.) fall back to the legacy process-table scan so we
// degrade to the previous behaviour rather than reporting a false "down".
async function probeUmaGatewayActive(): Promise<{ active: boolean; status: ProbeStatus }> {
  const userCheck = await exec('runuser', [
    '-u',
    'uma',
    '--',
    'systemctl',
    '--user',
    'is-active',
    'uma-hermes-gateway.service',
  ]);
  if (userCheck.ran) {
    const active = userCheck.stdout === 'active';
    return { active, status: active ? 'up' : 'down' };
  }

  const ps = await exec('ps', ['-eo', 'user=,args=']);
  if (ps.ran) {
    const active = ps.stdout.split('\n').some((line) => {
      const trimmed = line.trimStart();
      return trimmed.startsWith('uma ') && trimmed.includes('hermes_cli.main gateway');
    });
    return { active, status: active ? 'up' : 'down' };
  }

  return { active: false, status: 'unknown' };
}

async function probeUmaGatewayEnabled(): Promise<boolean> {
  const userCheck = await exec('runuser', [
    '-u',
    'uma',
    '--',
    'systemctl',
    '--user',
    'is-enabled',
    'uma-hermes-gateway.service',
  ]);
  if (userCheck.ran) return userCheck.stdout === 'enabled';
  return existsSync('/home/uma/.config/systemd/user/default.target.wants/uma-hermes-gateway.service');
}

async function getTimer(name: string): Promise<HermesOpsTimer> {
  const { active, status } = await probeSystemActive(name);
  const show = await exec('systemctl', [
    'show',
    name,
    '-p',
    'NextElapseUSecRealtime',
    '-p',
    'LastTriggerUSec',
    '--no-pager',
  ]);
  const properties = Object.fromEntries(
    (show.ran ? show.stdout : '')
      .split('\n')
      .map((line) => {
        const [key, ...value] = line.split('=');
        return [key, value.join('=') || null] as const;
      })
      .filter(([key]) => key),
  );

  return {
    name,
    active,
    status,
    nextRun: properties.NextElapseUSecRealtime ?? null,
    lastRun: properties.LastTriggerUSec ?? null,
  };
}

async function getRepo(path: string): Promise<HermesOpsRepo> {
  const [branch, statusOut, head, lastCommitAt, gitSize, backupSize] = await Promise.all([
    exec('git', ['branch', '--show-current'], path),
    exec('git', ['status', '--porcelain'], path),
    exec('git', ['rev-parse', '--short', 'HEAD'], path),
    exec('git', ['log', '-1', '--format=%cI'], path),
    exec('du', ['-sh', '.git'], path),
    exec('du', ['-sh', 'hermes_persistent_backup'], path),
  ]);
  const size = [gitSize, backupSize]
    .filter((r) => r.ran && r.stdout)
    .map((r) => r.stdout)
    .join(' / ');

  // HEAD readable ⇒ the repo could be inspected; otherwise we can't tell.
  const status: ProbeStatus = head.ran ? 'up' : 'unknown';

  return {
    path,
    branch: branch.ran ? branch.stdout || null : null,
    // `clean` only means something when we could actually read status.
    clean: statusOut.ran ? statusOut.stdout === '' : false,
    head: head.ran ? head.stdout || null : null,
    lastCommitAt: lastCommitAt.ran ? lastCommitAt.stdout || null : null,
    size: size ? size.replace(/\n/g, ' / ') : null,
    status,
  };
}

async function manifestStats(hermesHome: string): Promise<{ files: number | null; cronJobs: number | null }> {
  try {
    const manifestPath = `${hermesHome}/MANIFEST.json`;
    const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as { files?: unknown[] };
    const jobsPath = `${hermesHome}/cron/jobs.json`;
    const jobs = JSON.parse(await readFile(jobsPath, 'utf8'));
    const cronJobs = Array.isArray(jobs) ? jobs.length : Array.isArray(jobs?.jobs) ? jobs.jobs.length : null;
    return {
      files: Array.isArray(manifest.files) ? manifest.files.length : null,
      cronJobs,
    };
  } catch {
    return { files: null, cronJobs: null };
  }
}

async function tokenExists(path: string): Promise<boolean> {
  try {
    const info = await stat(path);
    return info.isFile() && info.size > 100;
  } catch {
    return false;
  }
}

async function readOpsExport(path: string): Promise<OpsExport | null> {
  try {
    const parsed = JSON.parse(await readFile(path, 'utf8')) as OpsExport;
    return parsed && typeof parsed === 'object' ? parsed : null;
  } catch {
    return null;
  }
}

async function getTailscaleIp(): Promise<string | null> {
  const result = await exec('tailscale', ['ip', '-4']);
  if (!result.ran) return null;
  return result.stdout.split('\n')[0] || null;
}

async function getActiveHermesSessionCount(): Promise<number> {
  const result = await exec('ps', ['-ef']);
  if (!result.ran) return 0;
  return result.stdout
    .split('\n')
    .filter((line) => line.includes('hermes_cli.main') && !line.includes('gateway') && !line.includes('grep'))
    .length;
}

async function buildSnapshot(): Promise<HermesOpsSnapshot> {
  const tailscaleIp = await getTailscaleIp();
  const warnings: string[] = [];
  const emergencyDriveUpload = await getTimer('hermes-emergency-drive-upload.timer');
  const activeSessions = await getActiveHermesSessionCount();

  const results: HermesOpsInstance[] = [];
  for (const item of instances) {
    const opsExport = await readOpsExport(item.opsExportPath);
    const gatewayActiveCheck =
      item.gatewayKind === 'uma-user' ? probeUmaGatewayActive() : probeSystemActive(item.gatewayService);
    const gatewayEnabledCheck =
      item.gatewayKind === 'uma-user' ? probeUmaGatewayEnabled() : probeSystemEnabled(item.gatewayService);
    const [probedGateway, probedGatewayEnabled, probedDashboard, probedBackupTimer, probedRepo, probedStats, probedGoogleToken] = await Promise.all([
      gatewayActiveCheck,
      gatewayEnabledCheck,
      probeSystemActive(item.dashboardService),
      getTimer(item.backupTimer),
      getRepo(item.repoPath),
      manifestStats(`${item.repoPath}/hermes_persistent_backup`),
      tokenExists(`${item.hermesHome}/google_token.json`),
    ]);
    const gateway = opsExport?.gateway?.status ? {
      active: Boolean(opsExport.gateway.active),
      status: opsExport.gateway.status,
    } : probedGateway;
    const gatewayEnabled = typeof opsExport?.gateway?.enabled === 'boolean' ? opsExport.gateway.enabled : probedGatewayEnabled;
    const dashboard = opsExport?.dashboard?.status ? {
      active: Boolean(opsExport.dashboard.active),
      status: opsExport.dashboard.status,
    } : probedDashboard;
    const backupTimer = opsExport?.backupTimer ?? probedBackupTimer;
    const repo = opsExport?.repo ?? probedRepo;
    const stats = {
      files: typeof opsExport?.restoredFileCount === 'number' || opsExport?.restoredFileCount === null ? opsExport.restoredFileCount : probedStats.files,
      cronJobs: typeof opsExport?.restoredCronJobs === 'number' || opsExport?.restoredCronJobs === null ? opsExport.restoredCronJobs : probedStats.cronJobs,
    };
    const googleToken = typeof opsExport?.googleWorkspaceToken === 'boolean' ? opsExport.googleWorkspaceToken : probedGoogleToken;

    const dashboardUrl = tailscaleIp ? `http://${tailscaleIp}:${item.dashboardPort}/` : `:${item.dashboardPort}`;

    if (gateway.status === 'down') warnings.push(`${item.label} gateway is not active`);
    else if (gateway.status === 'unknown') warnings.push(`${item.label} gateway status could not be determined`);
    if (gateway.status !== 'unknown' && !gatewayEnabled) {
      warnings.push(`${item.label} gateway auto-start is not enabled`);
    }
    if (dashboard.status === 'down') warnings.push(`${item.label} private dashboard is not active`);
    else if (dashboard.status === 'unknown') {
      warnings.push(`${item.label} private dashboard status could not be determined`);
    }
    if (backupTimer.status === 'down') warnings.push(`${item.label} backup timer is not active`);
    else if (backupTimer.status === 'unknown') {
      warnings.push(`${item.label} backup timer status could not be determined`);
    }
    if (repo.status === 'unknown') warnings.push(`${item.label} backup repo HEAD could not be read`);
    else if (!repo.clean) warnings.push(`${item.label} backup repo has uncommitted changes`);
    if (!googleToken) warnings.push(`${item.label} Google Workspace token is missing`);

    results.push({
      id: item.id,
      label: item.label,
      hermesHome: item.hermesHome,
      gateway: {
        service: item.gatewayService,
        active: gateway.active,
        enabled: gatewayEnabled,
        status: gateway.status,
      },
      dashboard: {
        service: item.dashboardService,
        active: dashboard.active,
        url: dashboardUrl,
        status: dashboard.status,
      },
      backup: {
        timer: backupTimer,
        repo,
        restoredFileCount: stats.files,
        restoredCronJobs: stats.cronJobs,
      },
      google: {
        workspaceToken: googleToken,
        driveFolder: item.driveFolder,
      },
    });
  }

  if (emergencyDriveUpload.status === 'down') warnings.push('Emergency Google Drive upload timer is not active');
  else if (emergencyDriveUpload.status === 'unknown') {
    warnings.push('Emergency Google Drive upload timer status could not be determined');
  }
  if (!existsSync('/root/.config/hermes-google-drive/user-token.json')) {
    warnings.push('Emergency Drive OAuth token is missing');
  }

  await Promise.all(warnings.map((message) => {
    const lower = message.toLowerCase();
    const instance = lower.includes('bheem') || lower.includes('uma')
      ? 'bheem'
      : lower.includes('vijay') || lower.includes('root')
        ? 'vijay'
        : 'all';
    return appendDashboardWarning({ severity: 'warn', instance, message });
  }));

  const cronJobs: HermesOpsCronJob[] = [
    {
      name: emergencyDriveUpload.name,
      label: 'Emergency Drive upload',
      active: emergencyDriveUpload.active,
      status: emergencyDriveUpload.status,
      nextRun: emergencyDriveUpload.nextRun,
      lastRun: emergencyDriveUpload.lastRun,
    },
    ...results.map((instance) => ({
      name: instance.backup.timer.name,
      label: `${instance.label} backup`,
      active: instance.backup.timer.active,
      status: instance.backup.timer.status,
      nextRun: instance.backup.timer.nextRun,
      lastRun: instance.backup.timer.lastRun,
    })),
  ];

  const now = new Date().toISOString();
  return {
    generatedAt: now,
    cached: false,
    tailscaleIp,
    emergencyDriveUpload,
    activeSessions: {
      active: activeSessions,
      updatedAt: now,
    },
    cronJobs,
    recentAlerts: warnings.slice(0, 6),
    quickLinks: [
      {
        label: 'Hermes operations',
        href: 'https://github.com/saravanakumardb/learning_ai_devops_tools/blob/main/docs/hermes-operations.md',
        description: 'Runbook for gateways, backups, fallbacks, and recovery.',
      },
      {
        label: 'Disaster recovery',
        href: 'https://github.com/saravanakumardb/learning_ai_devops_tools/blob/main/docs/hermes-disaster-recovery.md',
        description: 'Restore and rebuild steps for a fresh VM.',
      },
      {
        label: 'Setup roadmap',
        href: 'https://github.com/saravanakumardb/learning_ai_devops_tools/blob/main/docs/hermes-setup-upgrade-roadmap.md',
        description: 'Tracked rollout, security, and workflow checklist.',
      },
    ],
    instances: results,
    warnings,
  };
}

export async function getHermesOpsSnapshot(options?: { force?: boolean }): Promise<HermesOpsSnapshot> {
  const force = options?.force ?? false;

  if (!force && cache && Date.now() - cache.at < CACHE_TTL) {
    return { ...cache.snapshot, cached: true };
  }

  // Coalesce concurrent requests onto a single in-flight computation.
  if (!force && inflight) return inflight;

  const promise = buildSnapshot()
    .then((snapshot) => {
      cache = { snapshot, at: Date.now() };
      return snapshot;
    })
    .finally(() => {
      if (inflight === promise) inflight = null;
    });

  if (!force) inflight = promise;
  return promise;
}

// Test hook: reset the module-level cache between cases.
export function clearHermesOpsCache(): void {
  cache = null;
  inflight = null;
}
