import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import {
  copyFileSync,
  existsSync,
  mkdirSync,
  readFileSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { basename, join, resolve } from "node:path";

import { buildAndValidatePortablePackage } from "./portable-package.mjs";

const fixedPort = 43121;
const versionPattern = /^\d+\.\d+\.\d+\.\d+$/;
const sha256Pattern = /^[A-F0-9]{64}$/;

function powershellJson(script) {
  const result = spawnSync(
    "powershell.exe",
    ["-NoProfile", "-NonInteractive", "-Command", script],
    { encoding: "utf8", windowsHide: true },
  );
  if (result.status !== 0) {
    throw new Error(`Windows environment probe failed with exit code ${result.status ?? 1}.`);
  }
  return JSON.parse(result.stdout.trim());
}

export function readCandidateEnvironment() {
  if (process.platform !== "win32" || process.arch !== "x64") {
    throw new Error("Release candidates must be recorded on Windows x64.");
  }
  return powershellJson(String.raw`
$ErrorActionPreference = 'Stop'
function Find-Browser([string] $brand, [string] $fileName, [string[]] $candidates) {
  $path = $candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1
  if (-not $path) { throw "Required browser is not installed: $brand" }
  $item = Get-Item -LiteralPath $path
  if ($item.VersionInfo.ProductName -ne $brand) { throw "Installed executable identity mismatch: $brand" }
  $version = $item.VersionInfo.ProductVersion
  [pscustomobject]@{
    brand = $brand
    executable_sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash
    file_name = $fileName
    full_version = $version
    major = [int]($version.Split('.')[0])
    product_name = $item.VersionInfo.ProductName
    source = 'installed_executable'
  }
}
$chromeRegistry = @(
  (Get-ItemProperty 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' -ErrorAction SilentlyContinue).'(default)',
  (Get-ItemProperty 'Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' -ErrorAction SilentlyContinue).'(default)'
)
$chrome = Find-Browser 'Google Chrome' 'chrome.exe' @(
  (Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe'),
  'C:\Program Files\Google\Chrome\Application\chrome.exe',
  'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
  $chromeRegistry[0],
  $chromeRegistry[1]
)
$edge = Find-Browser 'Microsoft Edge' 'msedge.exe' @(
  'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
  'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
)
$windows = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
[pscustomobject]@{
  browsers = @($chrome, $edge)
  windows = [pscustomobject]@{
    arch = 'x64'
    build = "$($windows.CurrentBuildNumber).$($windows.UBR)"
    display_version = $windows.DisplayVersion
  }
} | ConvertTo-Json -Depth 5 -Compress
`);
}

export function validateReleaseCandidateRecord(record) {
  const errors = [];
  if (record?.schema_version !== "1.0") errors.push("schema_version");
  if (record?.status !== "candidate_unvalidated") errors.push("status");
  if (record?.final_release !== false) errors.push("final_release");
  if (record?.fixed_port !== fixedPort) errors.push("fixed_port");
  if (!/^[a-f0-9]{40}$/.test(record?.build_commit ?? "")) errors.push("build_commit");
  if (!/^\d+\.\d+$/.test(record?.windows?.build ?? "")) errors.push("windows.build");
  if (!Number.isFinite(Date.parse(record?.recorded_at ?? ""))) errors.push("recorded_at");
  if (!sha256Pattern.test(record?.candidate_package?.sha256 ?? "")) errors.push("candidate_package.sha256");
  if (record?.candidate_package?.fixed_port !== fixedPort) errors.push("candidate_package.fixed_port");
  if (record?.candidate_package?.release_status !== "candidate_unvalidated") {
    errors.push("candidate_package.release_status");
  }
  if (!Array.isArray(record?.browsers) || record.browsers.length !== 2) {
    errors.push("browsers");
  } else {
    const brands = record.browsers.map(({ brand }) => brand).sort();
    if (brands.join("|") !== "Google Chrome|Microsoft Edge") errors.push("browsers.brand");
    for (const browser of record.browsers) {
      if (!versionPattern.test(browser.full_version ?? "")) errors.push(`${browser.brand}.full_version`);
      if (browser.major !== Number.parseInt(browser.full_version?.split(".")[0] ?? "", 10)) {
        errors.push(`${browser.brand}.major`);
      }
      if (!sha256Pattern.test(browser.executable_sha256 ?? "")) errors.push(`${browser.brand}.sha256`);
      if (browser.source !== "installed_executable") errors.push(`${browser.brand}.source`);
      if ("path" in browser || "executable_path" in browser) errors.push(`${browser.brand}.path`);
    }
  }
  const serialized = JSON.stringify(record);
  if (/[A-Za-z]:\\Users\\/i.test(serialized)) errors.push("absolute_user_path");
  if (errors.length > 0) throw new Error(`Invalid release candidate record: ${[...new Set(errors)].join(", ")}`);
  return record;
}

function sha256(path) {
  return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase();
}

export async function createReleaseCandidate({ commit, evidenceRoot, outputRoot, recordedAt = new Date().toISOString() }) {
  const environment = readCandidateEnvironment();
  const packageResult = await buildAndValidatePortablePackage({ outputRoot });
  const packageName = packageResult.packageManifest.package_name;
  const packageDirectory = join(outputRoot, packageName);
  const zipSource = join(outputRoot, `${packageName}.zip`);
  const shaSource = `${zipSource}.sha256`;
  const zipSha256 = sha256(zipSource);
  if (zipSha256 !== packageResult.packageManifest.zip_sha256) {
    throw new Error("Candidate ZIP hash does not match the package manifest.");
  }
  if (!readFileSync(shaSource, "utf8").startsWith(`${zipSha256}  ${basename(zipSource)}`)) {
    throw new Error("Candidate ZIP hash file does not match the package bytes.");
  }
  const candidatePackage = resolve(evidenceRoot, "candidate-package");
  mkdirSync(candidatePackage, { recursive: true });
  for (const source of [zipSource, shaSource, join(packageDirectory, "START-HERE.txt")]) {
    if (!existsSync(source)) throw new Error(`Candidate package output is missing: ${basename(source)}`);
    copyFileSync(source, join(candidatePackage, basename(source)));
  }
  writeFileSync(
    join(candidatePackage, "package-manifest.json"),
    `${JSON.stringify(packageResult.packageManifest, null, 2)}\n`,
  );
  writeFileSync(
    join(candidatePackage, "package-scan.json"),
    `${JSON.stringify(packageResult.packageScan, null, 2)}\n`,
  );
  writeFileSync(
    join(candidatePackage, "process-tree.json"),
    `${JSON.stringify(packageResult.processTree, null, 2)}\n`,
  );

  const record = validateReleaseCandidateRecord({
    app_version: packageResult.packageManifest.app_version,
    browsers: environment.browsers.map((browser) => ({
      brand: browser.brand,
      executable_sha256: browser.executable_sha256,
      file_name: browser.file_name,
      full_version: browser.full_version,
      major: browser.major,
      source: browser.source,
    })),
    build_commit: commit,
    candidate_package: {
      file_name: basename(zipSource),
      fixed_port: packageResult.packageManifest.fixed_port,
      release_status: packageResult.packageManifest.release_status,
      sha256: zipSha256,
      size_bytes: statSync(zipSource).size,
    },
    final_release: false,
    fixed_port: fixedPort,
    recorded_at: recordedAt,
    schema_version: "1.0",
    status: "candidate_unvalidated",
    windows: environment.windows,
  });
  writeFileSync(resolve(evidenceRoot, "release-candidate.json"), `${JSON.stringify(record, null, 2)}\n`);
  return { packageResult, record };
}
