import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { machineFingerprintCachePath } from './paths';

type ExecFile = (
  file: string,
  args: readonly string[],
  options: { encoding: 'utf8'; windowsHide?: boolean; timeout?: number },
) => string;

export type MachineFingerprintDeps = {
  platform?: NodeJS.Platform;
  env?: NodeJS.ProcessEnv;
  /** Electron userData — для дискового кэша hashed fingerprint. */
  userData?: string;
  execFileSync?: ExecFile;
  readFileSync?: (p: string, encoding: 'utf8') => string;
  existsSync?: (p: string) => boolean;
  mkdirSync?: (p: string, opts: { recursive: boolean }) => void;
  writeFileSync?: (p: string, data: string, opts?: { mode?: number }) => void;
};

/** Process-level кэш: повторные вызовы в том же процессе без I/O. */
let memoryFingerprint: string | null = null;

/** Только для тестов. */
export function clearMachineFingerprintMemoryCache(): void {
  memoryFingerprint = null;
}

function isPlausiblyFingerprint(id: string): boolean {
  return id.length >= 8 && id.length <= 128 && !/\s/.test(id);
}

const HASH_PREFIX = 'TTRPGPlayer.machine.v1\0';

/** Стабильный opaque id из сырого машинного идентификатора ОС. */
export function hashMachineRawId(platform: string, rawId: string): string {
  return createHash('sha256')
    .update(HASH_PREFIX, 'utf8')
    .update(platform, 'utf8')
    .update('\0', 'utf8')
    .update(rawId.trim().toLowerCase(), 'utf8')
    .digest('hex');
}

export function parseWindowsMachineGuid(regOutput: string): string | null {
  const m = /MachineGuid\s+REG_SZ\s+([0-9a-fA-F-]{8,})/.exec(regOutput);
  const id = m?.[1]?.trim();
  return id && id.length >= 8 ? id : null;
}

export function parseMacIOPlatformUUID(ioregOutput: string): string | null {
  const m = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(ioregOutput);
  const id = m?.[1]?.trim();
  return id && id.length >= 8 ? id : null;
}

export function parseWmicUuid(wmicOutput: string): string | null {
  const lines = wmicOutput
    .split(/\r?\n/)
    .map((l) => l.trim())
    .filter(Boolean);
  for (const line of lines) {
    if (/^uuid$/i.test(line)) continue;
    if (/^[0-9a-fA-F-]{8,}$/.test(line) && !/^0+-?0+-?0+-?0+-?0+$/.test(line)) {
      return line;
    }
  }
  return null;
}

function tryExec(exec: ExecFile, file: string, args: readonly string[]): string | null {
  try {
    return exec(file, args, { encoding: 'utf8', windowsHide: true, timeout: 8_000 });
  } catch {
    return null;
  }
}

function readWindowsRawId(exec: ExecFile): string | null {
  const regOut = tryExec(exec, 'reg', [
    'query',
    'HKLM\\SOFTWARE\\Microsoft\\Cryptography',
    '/v',
    'MachineGuid',
  ]);
  if (regOut) {
    const guid = parseWindowsMachineGuid(regOut);
    if (guid) return guid;
  }
  const wmicOut = tryExec(exec, 'wmic', ['csproduct', 'get', 'uuid']);
  if (wmicOut) {
    const uuid = parseWmicUuid(wmicOut);
    if (uuid) return uuid;
  }
  return null;
}

function readDarwinRawId(exec: ExecFile): string | null {
  const out = tryExec(exec, 'ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']);
  if (!out) return null;
  return parseMacIOPlatformUUID(out);
}

function readLinuxRawId(readFile: (p: string, encoding: 'utf8') => string): string | null {
  for (const p of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
    try {
      const id = readFile(p, 'utf8').trim();
      if (id.length >= 8 && !/^0+$/.test(id)) return id;
    } catch {
      /* try next */
    }
  }
  try {
    const id = readFile('/sys/class/dmi/id/product_uuid', 'utf8').trim();
    if (id.length >= 8 && !/^0+-?0+-?0+-?0+-?0+$/i.test(id)) return id;
  } catch {
    /* optional, often root-only */
  }
  return null;
}

/** Каталог на уровне машины (не профиль пользователя), для редкого fallback UUID. */
export function machineWideIdPath(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string {
  if (platform === 'win32') {
    const base = env.PROGRAMDATA?.trim() || path.join(env.SystemDrive || 'C:', 'ProgramData');
    return path.join(base, 'TTRPGPlayer', 'machine.id');
  }
  if (platform === 'darwin') {
    return path.join('/Library/Application Support', 'TTRPGPlayer', 'machine.id');
  }
  return path.join('/var/lib', 'ttrpg-player', 'machine.id');
}

function readOrCreateMachineWideFallback(
  platform: NodeJS.Platform,
  env: NodeJS.ProcessEnv,
  deps: Required<
    Pick<MachineFingerprintDeps, 'existsSync' | 'readFileSync' | 'mkdirSync' | 'writeFileSync'>
  >,
): string | null {
  const p = machineWideIdPath(platform, env);
  try {
    if (deps.existsSync(p)) {
      const existing = deps.readFileSync(p, 'utf8').trim();
      if (existing.length >= 8) return existing;
    }
  } catch {
    /* create below */
  }
  const id = createHash('sha256')
    .update(`TTRPGPlayer.machine.fallback\0${platform}\0${os.hostname()}\0${Date.now()}\0${Math.random()}`, 'utf8')
    .digest('hex');
  try {
    deps.mkdirSync(path.dirname(p), { recursive: true });
    deps.writeFileSync(p, `${id}\n`, { mode: 0o644 });
    return id;
  } catch {
    return null;
  }
}

function readDiskFingerprintCache(
  cacheFile: string,
  readFile: (p: string, encoding: 'utf8') => string,
  existsSync: (p: string) => boolean,
): string | null {
  try {
    if (!existsSync(cacheFile)) return null;
    const cached = readFile(cacheFile, 'utf8').trim();
    return isPlausiblyFingerprint(cached) ? cached : null;
  } catch {
    return null;
  }
}

function writeDiskFingerprintCache(
  cacheFile: string,
  fingerprint: string,
  mkdirSync: (p: string, opts: { recursive: boolean }) => void,
  writeFileSync: (p: string, data: string, opts?: { mode?: number }) => void,
): void {
  try {
    mkdirSync(path.dirname(cacheFile), { recursive: true });
    writeFileSync(cacheFile, `${fingerprint}\n`, { mode: 0o644 });
  } catch {
    /* кэш необязателен */
  }
}

/**
 * Стабильный идентификатор физической машины (одинаковый для всех пользователей ОС на одном ПК).
 * Источники: Windows MachineGuid, macOS IOPlatformUUID, Linux /etc/machine-id.
 * Кэш: память процесса → userData/machine.fingerprint → sync probe ОС только при miss.
 */
export function resolveMachineFingerprint(deps: MachineFingerprintDeps = {}): string {
  const platform = deps.platform ?? process.platform;
  const env = deps.env ?? process.env;
  const exec = deps.execFileSync ?? (execFileSync as ExecFile);
  const readFile = deps.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
  const existsSync = deps.existsSync ?? ((p) => fs.existsSync(p));
  const mkdirSync = deps.mkdirSync ?? ((p, opts) => {
    fs.mkdirSync(p, opts);
  });
  const writeFileSync = deps.writeFileSync ?? ((p, data, opts) => {
    fs.writeFileSync(p, data, opts);
  });

  const override = env.DND_LICENSE_DEVICE_ID?.trim();
  if (override && override.length >= 8) {
    memoryFingerprint = override;
    return override;
  }

  if (memoryFingerprint && isPlausiblyFingerprint(memoryFingerprint)) {
    return memoryFingerprint;
  }

  const userData = deps.userData?.trim();
  const cacheFile = userData ? machineFingerprintCachePath(userData) : null;
  if (cacheFile) {
    const fromDisk = readDiskFingerprintCache(cacheFile, readFile, existsSync);
    if (fromDisk) {
      memoryFingerprint = fromDisk;
      return fromDisk;
    }
  }

  let raw: string | null = null;
  if (platform === 'win32') raw = readWindowsRawId(exec);
  else if (platform === 'darwin') raw = readDarwinRawId(exec);
  else if (platform === 'linux') raw = readLinuxRawId(readFile);

  if (!raw) {
    raw = readOrCreateMachineWideFallback(platform, env, {
      existsSync,
      readFileSync: readFile,
      mkdirSync,
      writeFileSync,
    });
  }

  if (!raw) {
    throw new Error(
      'LICENSE_MACHINE_ID_UNAVAILABLE: не удалось получить идентификатор физической машины',
    );
  }

  const fingerprint = hashMachineRawId(platform, raw);
  memoryFingerprint = fingerprint;
  if (cacheFile) {
    writeDiskFingerprintCache(cacheFile, fingerprint, mkdirSync, writeFileSync);
  }
  return fingerprint;
}
