import { cpus, freemem, platform, release } from "node:os";
import { existsSync, readFileSync, statfsSync } from "node:fs";
import { execSync } from "node:child_process";
import { getSystemTotalMb } from "@hyperframes/engine";
import {
  detectAgentRuntime,
  detectAgentHints,
  detectSandboxRuntime,
  type AgentRuntime,
  type SandboxRuntime,
} from "./agent_runtime.js";
import { detectWSL } from "./platform.js";

// ---------------------------------------------------------------------------
// System metadata collected once per CLI session and attached to all events.
// Follows the same patterns as Next.js, Turborepo, and Gatsby telemetry.
// No PII — only hardware/environment characteristics useful for debugging.
// ---------------------------------------------------------------------------

/** Convert bytes to whole megabytes. */
export function bytesToMb(bytes: number): number {
  return Math.trunc(bytes / (1024 * 1024));
}

export interface SystemMeta {
  os_release: string;
  cpu_count: number;
  cpu_model: string | null;
  cpu_speed: number | null;
  memory_total_mb: number;
  is_docker: boolean;
  is_ci: boolean;
  ci_name: string | null;
  is_wsl: boolean;
  is_tty: boolean;
  /**
   * Managed sandbox runtime hosting this invocation, when one is detectable
   * (gvisor / firecracker / docker / kvm / wsl). null on a normal dev
   * machine. Lets us distinguish "real laptop" from "ephemeral cloud
   * sandbox driving the CLI" without geo guesswork.
   */
  sandbox_runtime: SandboxRuntime;
  /**
   * Coding-agent vendor that spawned this process, if any (see the
   * `AgentRuntime` union in agent_runtime.ts for the full, current set).
   * Most rules check env-var existence only — values are never read; a few
   * use filesystem/kernel markers (e.g. the Gemini managed-agent mount).
   * Every rule keys on a marker with a source citation in agent_runtime.ts;
   * unverified guesses are deliberately omitted (false-negative > guess).
   * null when no agent is detected.
   */
  agent_runtime: AgentRuntime;
  /**
   * New-agent discovery signals for the agent_runtime=null bucket, so an agent
   * we have no rule for surfaces on its own instead of vanishing into null.
   * All three are null on a classified event (agent_runtime != null) and on a
   * plain shell with no markers. See `detectAgentHints` in agent_runtime.ts for
   * the fields and the privacy contract.
   */
  agent_hint: string | null;
  term_program: string | null;
  agent_env_hints: string | null;
}

let cached: SystemMeta | null = null;

/**
 * Collect system metadata. Cached after first call.
 * Only includes static values — use `freemem()` directly for volatile readings.
 */
export function getSystemMeta(): SystemMeta {
  if (cached) return cached;

  const cpuInfo = cpus();
  const firstCpu = cpuInfo[0] ?? null;

  // Only compute discovery hints for the unclassified bucket — a known agent
  // needs no hint, and gating keeps them off the ~80%+ of classified events.
  const agent_runtime = detectAgentRuntime();
  const hints =
    agent_runtime === null
      ? detectAgentHints()
      : { agent_hint: null, term_program: null, agent_env_hints: null };

  cached = {
    os_release: release(),
    cpu_count: cpuInfo.length,
    cpu_model: firstCpu?.model?.trim() ?? null,
    cpu_speed: firstCpu?.speed ?? null,
    memory_total_mb: getSystemTotalMb(),
    is_docker: detectDocker(),
    is_ci: detectCI(),
    ci_name: getCIName(),
    is_wsl: detectWSL(),
    is_tty: Boolean(process.stdout?.isTTY),
    sandbox_runtime: detectSandboxRuntime(),
    agent_runtime,
    agent_hint: hints.agent_hint,
    term_program: hints.term_program,
    agent_env_hints: hints.agent_env_hints,
  };
  return cached;
}

// ---------------------------------------------------------------------------
// Environment detectors
// ---------------------------------------------------------------------------

function detectDocker(): boolean {
  // Standard detection: /.dockerenv file or "docker" in /proc/1/cgroup
  try {
    if (existsSync("/.dockerenv")) return true;
    if (platform() === "linux") {
      const cgroup = readFileSync("/proc/1/cgroup", "utf-8");
      if (cgroup.includes("docker") || cgroup.includes("containerd")) return true;
    }
  } catch {
    // Ignore — not in Docker
  }
  return false;
}

// Named providers come first so getCIName() picks the most specific match.
// `truthy` accepts 'true' or '1'; `presence` matches any non-null value.
type CIProvider =
  | { name: string | null; envVar: string; mode: "truthy" }
  | { name: string | null; envVar: string; mode: "presence" };

const CI_PROVIDERS: CIProvider[] = [
  { name: "github_actions", envVar: "GITHUB_ACTIONS", mode: "truthy" },
  { name: "gitlab_ci", envVar: "GITLAB_CI", mode: "truthy" },
  { name: "circleci", envVar: "CIRCLECI", mode: "truthy" },
  { name: "jenkins", envVar: "JENKINS_URL", mode: "presence" },
  { name: "buildkite", envVar: "BUILDKITE", mode: "truthy" },
  { name: "travis", envVar: "TRAVIS", mode: "truthy" },
  { name: null, envVar: "CONTINUOUS_INTEGRATION", mode: "truthy" },
  { name: null, envVar: "CI", mode: "truthy" },
];

function matchesProvider(p: CIProvider): boolean {
  const v = process.env[p.envVar];
  if (p.mode === "presence") return v != null;
  return v === "true" || v === "1";
}

function detectCI(): boolean {
  return CI_PROVIDERS.some(matchesProvider);
}

function getCIName(): string | null {
  for (const provider of CI_PROVIDERS) {
    if (provider.name && matchesProvider(provider)) return provider.name;
  }
  return detectCI() ? "unknown" : null;
}

// ---------------------------------------------------------------------------
// Extended hardware checks (for doctor command and detailed render events)
// ---------------------------------------------------------------------------

/**
 * Get /dev/shm size in MB (Linux only). Chrome uses shared memory heavily;
 * Docker's default 64MB limit causes crashes.
 */
export function getShmSizeMb(): number | null {
  if (platform() !== "linux") return null;
  try {
    const stats = statfsSync("/dev/shm");
    return bytesToMb(stats.bsize * stats.blocks);
  } catch {
    return null;
  }
}

/**
 * Get available disk space in MB at a given path.
 */
export function getFreeDiskMb(path: string = "."): number | null {
  try {
    const stats = statfsSync(path);
    return bytesToMb(stats.bsize * stats.bavail);
  } catch {
    return null;
  }
}

/**
 * Get available memory in MB, accounting for OS-level page caching.
 *
 * `os.freemem()` on macOS returns only truly free pages — ignoring
 * inactive/purgeable/speculative pages that the kernel reclaims on demand.
 * On a 24 GB Mac this reports ~0.1 GB "free" when ~5 GB is actually
 * available. Linux has a similar (milder) issue; its kernel exposes the
 * correct value via `MemAvailable` in /proc/meminfo.
 */
export function getAvailableMemoryMb(): number {
  const fallback = bytesToMb(freemem());

  if (platform() === "darwin") {
    try {
      const raw = execSync("vm_stat", { encoding: "utf-8", timeout: 5000 });
      const pageSize = parseInt(raw.match(/page size of (\d+)/)?.[1] ?? "0", 10);
      if (!pageSize) return fallback;

      const pages = (key: string) =>
        parseInt(raw.match(new RegExp(`${key}:\\s+(\\d+)`))?.[1] ?? "0", 10);

      const available =
        (pages("Pages free") +
          pages("Pages inactive") +
          pages("Pages purgeable") +
          pages("Pages speculative")) *
        pageSize;

      return available > 0 ? bytesToMb(available) : fallback;
    } catch {
      return fallback;
    }
  }

  if (platform() === "linux") {
    try {
      const meminfo = readFileSync("/proc/meminfo", "utf-8");
      const match = meminfo.match(/MemAvailable:\s+(\d+)\s+kB/);
      if (match) {
        return Math.trunc(parseInt(match[1]!, 10) / 1024);
      }
      return fallback;
    } catch {
      return fallback;
    }
  }

  return fallback;
}
