import os from "node:os";
import { execFile as execFileCb } from "node:child_process";
import si from "systeminformation";

export type HardwareTier = "gpu" | "apple_silicon" | "cpu_only";

export interface VoiceCapability {
  whisperAvailable: boolean;
  piperAvailable: boolean;
  /** true if hardware tier >= apple_silicon OR (cpu_only with >= 4GB free RAM) */
  voiceTierSufficient: boolean;
}

export interface HardwareInfo {
  totalGb: number;
  freeGb: number;
  usableGb: number;
  platform: NodeJS.Platform;
  gpuName: string | null;
  gpuVramGb: number | null;
  unifiedMemory: boolean;
  hardwareTier: HardwareTier;
  cpuModel: string | null;
  voiceCapability: VoiceCapability;
}

const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes

let cache: HardwareInfo | null = null;
let cacheExpiry = 0;

/** Reset the hardware detection cache. Exposed for testing only. */
export function _resetHardwareCache(): void {
  cache = null;
  cacheExpiry = 0;
}

/** Promisified execFile wrapper for consistent mocking in tests. */
function execFileAsync(
  cmd: string,
  args: string[],
  opts: { timeout?: number }
): Promise<{ stdout: string; stderr: string }> {
  return new Promise((resolve, reject) => {
    execFileCb(cmd, args, opts as never, (err, stdout, stderr) => {
      if (err) {
        reject(err);
      } else {
        resolve({
          stdout: Buffer.isBuffer(stdout) ? stdout.toString() : String(stdout ?? ""),
          stderr: Buffer.isBuffer(stderr) ? stderr.toString() : String(stderr ?? ""),
        });
      }
    });
  });
}

/** Probe for Whisper and Piper binary availability. */
async function detectVoiceCapability(
  hardwareTier: HardwareTier,
  freeGb: number
): Promise<VoiceCapability> {
  // Probe whisper-cpp first, fall back to whisper (openai-whisper)
  let whisperAvailable = false;
  try {
    await execFileAsync("whisper-cpp", ["--help"], { timeout: 2000 });
    whisperAvailable = true;
  } catch {
    try {
      await execFileAsync("whisper", ["--help"], { timeout: 2000 });
      whisperAvailable = true;
    } catch {
      whisperAvailable = false;
    }
  }

  // Probe piper
  let piperAvailable = false;
  try {
    await execFileAsync("piper", ["--help"], { timeout: 2000 });
    piperAvailable = true;
  } catch {
    piperAvailable = false;
  }

  // voiceTierSufficient: true for apple_silicon/gpu, or cpu_only with >= 4GB free
  const voiceTierSufficient =
    hardwareTier === "apple_silicon" ||
    hardwareTier === "gpu" ||
    (hardwareTier === "cpu_only" && freeGb >= 4);

  return { whisperAvailable, piperAvailable, voiceTierSufficient };
}

export function hardwareService() {
  async function detect(): Promise<HardwareInfo> {
    const now = Date.now();
    if (cache && now < cacheExpiry) {
      return cache;
    }

    const totalBytes = os.totalmem();
    const freeBytes = os.freemem();
    const totalGb = Math.round((totalBytes / (1024 * 1024 * 1024)) * 10) / 10;
    const freeGb = Math.round((freeBytes / (1024 * 1024 * 1024)) * 10) / 10;
    const usableGb = Math.round((freeBytes * 0.75) / (1024 * 1024 * 1024) * 10) / 10;
    const cpuModel = os.cpus()[0]?.model ?? null;
    const platform = process.platform;

    // Apple Silicon detection: darwin platform + CPU brand starting with "Apple"
    if (platform === "darwin" && cpuModel?.startsWith("Apple")) {
      const voiceCapability = await withVoiceTimeout(
        detectVoiceCapability("apple_silicon", freeGb)
      );
      const info: HardwareInfo = {
        totalGb,
        freeGb,
        usableGb,
        platform,
        gpuName: null,
        gpuVramGb: null,
        unifiedMemory: true,
        hardwareTier: "apple_silicon",
        cpuModel,
        voiceCapability,
      };
      cache = info;
      cacheExpiry = now + CACHE_TTL_MS;
      return info;
    }

    // Non-Apple Silicon: probe GPU with 3-second timeout
    let gpuName: string | null = null;
    let gpuVramGb: number | null = null;
    let hardwareTier: HardwareTier = "cpu_only";

    try {
      const timeoutPromise = new Promise<never>((_resolve, reject) => {
        setTimeout(() => reject(new Error("GPU detection timeout")), 3000);
      });
      const graphicsResult = await Promise.race([si.graphics(), timeoutPromise]);
      const controllers = graphicsResult.controllers ?? [];
      if (controllers.length > 0 && controllers[0]) {
        const vramMb = controllers[0].vram ?? 0;
        const vramGb = vramMb / 1024;
        if (vramGb >= 4) {
          gpuName = controllers[0].model ?? null;
          gpuVramGb = Math.round(vramGb * 10) / 10;
          hardwareTier = "gpu";
        }
      }
    } catch {
      // Timeout or error — degrade to cpu_only
      hardwareTier = "cpu_only";
    }

    const voiceCapability = await withVoiceTimeout(
      detectVoiceCapability(hardwareTier, freeGb)
    );

    const info: HardwareInfo = {
      totalGb,
      freeGb,
      usableGb,
      platform,
      gpuName,
      gpuVramGb,
      unifiedMemory: false,
      hardwareTier,
      cpuModel,
      voiceCapability,
    };

    cache = info;
    cacheExpiry = now + CACHE_TTL_MS;
    return info;
  }

  return { detect };
}

/** Wrap voice capability probe with 3s timeout; return safe defaults on failure. */
async function withVoiceTimeout(
  probe: Promise<VoiceCapability>
): Promise<VoiceCapability> {
  const timeout = new Promise<VoiceCapability>((resolve) =>
    setTimeout(
      () => resolve({ whisperAvailable: false, piperAvailable: false, voiceTierSufficient: false }),
      3000
    )
  );
  return Promise.race([probe, timeout]);
}
