import { createHash } from "node:crypto";
import { constants } from "node:fs";
import { chmod, copyFile, lstat, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { assertKnownArgs, optionalArg, parseArgs, reportCliError, requiredArg } from "./cli.ts";
import { ValidationError } from "./validation.ts";

const SHA256 = /^[a-f0-9]{64}$/;
const MAX_PINNED_ARTIFACT_BYTES = 64 * 1024 * 1024;

export interface PinnedArtifactOptions {
  destination: string;
  sha256: string;
  size: number;
  url: string;
  seedPath?: string;
  download?: (url: URL, expectedSize: number) => Promise<Buffer>;
}

function digest(payload: Buffer): string {
  return createHash("sha256").update(payload).digest("hex");
}

async function validFile(path: string, sha256: string, size: number): Promise<boolean> {
  const metadata = await lstat(path).catch(() => null);
  if (metadata === null || !metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== size) return false;
  return digest(await readFile(path)) === sha256;
}

async function downloadPinned(url: URL, expectedSize: number): Promise<Buffer> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 30_000);
  try {
    const response = await fetch(url, { redirect: "follow", signal: controller.signal });
    if (!response.ok || response.body === null) {
      throw new ValidationError(`Téléchargement impossible (${response.status})`);
    }
    const finalUrl = new URL(response.url);
    if (finalUrl.protocol !== "https:" || finalUrl.username || finalUrl.password) {
      throw new ValidationError("La redirection finale de l'artefact doit rester en HTTPS sans identifiants");
    }
    const announcedLength = response.headers.get("content-length");
    if (announcedLength !== null && Number(announcedLength) !== expectedSize) {
      throw new ValidationError(`Taille HTTP inattendue: ${announcedLength}`);
    }
    const chunks: Buffer[] = [];
    let total = 0;
    const reader = response.body.getReader();
    while (true) {
      const next = await reader.read();
      if (next.done) break;
      total += next.value.byteLength;
      if (total > expectedSize) {
        await reader.cancel();
        throw new ValidationError("L'artefact téléchargé dépasse la taille épinglée");
      }
      chunks.push(Buffer.from(next.value));
    }
    if (total !== expectedSize) throw new ValidationError(`Taille téléchargée inattendue: ${total}`);
    return Buffer.concat(chunks, total);
  } finally {
    clearTimeout(timeout);
  }
}

export async function ensurePinnedArtifact(options: PinnedArtifactOptions): Promise<"cached" | "seeded" | "downloaded"> {
  if (!SHA256.test(options.sha256)) throw new ValidationError("SHA-256 épinglé invalide");
  if (!Number.isSafeInteger(options.size) || options.size < 1 || options.size > MAX_PINNED_ARTIFACT_BYTES) {
    throw new ValidationError(`Taille épinglée invalide: ${options.size}`);
  }
  let url: URL;
  try {
    url = new URL(options.url);
  } catch {
    throw new ValidationError("URL d'artefact invalide");
  }
  if (url.protocol !== "https:" || url.username || url.password) {
    throw new ValidationError("L'URL d'artefact doit être HTTPS sans identifiants");
  }
  const destination = resolve(options.destination);
  if (await validFile(destination, options.sha256, options.size)) return "cached";

  await mkdir(dirname(destination), { recursive: true });
  const temporary = `${destination}.tmp-${process.pid}-${Date.now()}`;
  try {
    if (options.seedPath !== undefined && await validFile(resolve(options.seedPath), options.sha256, options.size)) {
      await copyFile(resolve(options.seedPath), temporary, constants.COPYFILE_EXCL);
      await chmod(temporary, 0o644);
      await rename(temporary, destination);
      return "seeded";
    }
    const payload = await (options.download ?? downloadPinned)(url, options.size);
    if (payload.byteLength !== options.size || digest(payload) !== options.sha256) {
      throw new ValidationError("L'artefact téléchargé ne correspond pas au SHA-256/taille épinglés");
    }
    await writeFile(temporary, payload, { flag: "wx", mode: 0o644 });
    await rename(temporary, destination);
    return "downloaded";
  } finally {
    await rm(temporary, { force: true });
  }
}

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2));
  assertKnownArgs(args, ["destination", "sha256", "size", "url", "seed"]);
  const rawSize = requiredArg(args, "size");
  if (!/^\d+$/.test(rawSize)) throw new ValidationError("--size doit être un entier positif");
  const seedPath = optionalArg(args, "seed");
  const status = await ensurePinnedArtifact({
    destination: requiredArg(args, "destination"),
    sha256: requiredArg(args, "sha256"),
    size: Number(rawSize),
    url: requiredArg(args, "url"),
    ...(seedPath === undefined ? {} : { seedPath }),
  });
  process.stdout.write(`Dépendance épinglée prête (${status}).\n`);
}

if (import.meta.url === `file://${process.argv[1]}`) main().catch(reportCliError);
