import { createReadStream } from "node:fs";
import { lstat, readFile } from "node:fs/promises";
import { createHash, sign, verify } from "node:crypto";
import { dirname, resolve } from "node:path";
import { canonicalJson, sha256Hex } from "./canonical-json.ts";
import { isPathWithinRoot } from "./path-security.ts";
import { parseReleaseManifest, parseReleaseSource, ValidationError } from "./validation.ts";
import {
  CANONICALIZATION_VERSION,
  type ReleaseArtifact,
  type ReleaseManifest,
  type ReleasePayload,
} from "./types.ts";

export async function hashFile(path: string): Promise<{ sha256: string; size: number }> {
  const metadata = await lstat(path);
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size < 1) {
    throw new ValidationError(`Artefact vide ou non régulier: ${path}`);
  }
  const digest = createHash("sha256");
  await new Promise<void>((resolvePromise, reject) => {
    const stream = createReadStream(path);
    stream.on("data", (chunk) => digest.update(chunk));
    stream.on("error", reject);
    stream.on("end", resolvePromise);
  });
  return { sha256: digest.digest("hex"), size: metadata.size };
}

export interface BuildManifestOptions {
  sourcePath: string;
  privateKeyPath?: string;
  keyId?: string;
  schemaRef?: string;
}

export async function buildManifest(options: BuildManifestOptions): Promise<ReleaseManifest> {
  const parsed = JSON.parse(await readFile(options.sourcePath, "utf8")) as unknown;
  const source = parseReleaseSource(parsed);
  const baseDirectory = dirname(resolve(options.sourcePath));
  const artifacts: ReleaseArtifact[] = [];
  for (const artifact of source.artifacts) {
    const localPath = resolve(baseDirectory, artifact.localFile);
    const actualIntegrity = await hashFile(localPath);
    if (
      actualIntegrity.sha256 !== artifact.sourceIntegrity.sha256 ||
      actualIntegrity.size !== artifact.sourceIntegrity.size
    ) {
      throw new ValidationError(`L'artefact source ne correspond pas à l'intégrité épinglée: ${artifact.id}`);
    }
    const { localFile: _localFile, sourceIntegrity: _sourceIntegrity, ...published } = artifact;
    artifacts.push({
      ...published,
      integrity: {
        algorithm: "sha256",
        ...actualIntegrity,
      },
    });
  }
  const { $schema: _schema, schemaVersion: _version, artifacts: _sourceArtifacts, ...sourceBase } = source;
  const payload: ReleasePayload = { ...sourceBase, artifacts };
  const canonicalPayload = canonicalJson(payload);
  let signature: ReleaseManifest["signature"] = null;
  if (options.privateKeyPath || options.keyId) {
    if (!options.privateKeyPath || !options.keyId) {
      throw new ValidationError("--private-key et --key-id doivent être fournis ensemble");
    }
    const privateKey = await readFile(options.privateKeyPath, "utf8");
    signature = {
      algorithm: "Ed25519",
      keyId: options.keyId,
      valueBase64: sign(null, Buffer.from(canonicalPayload), privateKey).toString("base64"),
    };
  }
  const manifest: ReleaseManifest = {
    $schema: options.schemaRef ?? "../schema/release-manifest.schema.json",
    schemaVersion: 1,
    payload,
    integrity: {
      canonicalization: CANONICALIZATION_VERSION,
      algorithm: "sha256",
      payloadSha256: sha256Hex(canonicalPayload),
    },
    signature,
  };
  return parseReleaseManifest(manifest);
}

export interface VerifyManifestOptions {
  publicKeyPath?: string;
  publicKeyPem?: string;
  requireSignature?: boolean;
  artifactRoot?: string;
}

export async function verifyManifest(
  rawManifest: unknown,
  options: VerifyManifestOptions = {},
): Promise<ReleaseManifest> {
  const manifest = parseReleaseManifest(rawManifest);
  const canonicalPayload = canonicalJson(manifest.payload);
  const actualDigest = sha256Hex(canonicalPayload);
  if (actualDigest !== manifest.integrity.payloadSha256) {
    throw new ValidationError("Le digest du payload du manifeste ne correspond pas");
  }
  if (options.requireSignature && manifest.signature === null) {
    throw new ValidationError("Une signature est requise mais le manifeste n'est pas signé");
  }
  if (manifest.signature !== null) {
    if (!options.publicKeyPath && !options.publicKeyPem) {
      throw new ValidationError("Une clé publique est requise pour vérifier la signature présente");
    }
    const publicKey = options.publicKeyPem ?? await readFile(options.publicKeyPath!, "utf8");
    const valid = verify(
      null,
      Buffer.from(canonicalPayload),
      publicKey,
      Buffer.from(manifest.signature.valueBase64, "base64"),
    );
    if (!valid) throw new ValidationError("Signature Ed25519 invalide");
  }
  if (options.artifactRoot) {
    const artifactRoot = resolve(options.artifactRoot);
    for (const artifact of manifest.payload.artifacts) {
      const installedPath = resolve(artifactRoot, artifact.installPath);
      if (!isPathWithinRoot(artifactRoot, installedPath)) {
        throw new ValidationError(`Chemin d'artefact hors racine: ${artifact.installPath}`);
      }
      const actual = await hashFile(installedPath);
      if (actual.size !== artifact.integrity.size || actual.sha256 !== artifact.integrity.sha256) {
        throw new ValidationError(`Artefact invalide: ${artifact.installPath}`);
      }
    }
  }
  return manifest;
}
