// The per-platform runtime artifact table — PG Task 1, implementing "Where the
// binaries come from" / "Where the app fetches it from" in
// docs/todo/2026-08-08-bundled-postgres-decisions.md.
//
// Two different files are involved and conflating them is the easiest mistake
// to make here, so they get two different constants:
//
//   UPSTREAM_WINDOWS_ARCHIVE  EnterpriseDB's official 307.6 MB zip. A MAINTAINER
//                             downloads this ONCE, on their own machine, trims
//                             it to bin/lib/share and republishes. The app never
//                             touches it. Its checksum was verified by hand on
//                             2026-08-08 and is the only fully-verified artifact
//                             the decisions document contains.
//
//   RUNTIME_MANIFEST          The trimmed ~24.7 MB .tar.xz the APP downloads on
//                             first run, from balancedsum.com. Not from EDB:
//                             site/privacy.html publishes that the app talks to
//                             exactly one host, and a first-run fetch from
//                             get.enterprisedb.com would make that false. Fixing
//                             a broken privacy promise with a longer privacy
//                             page is the wrong direction.
//
// macOS rows are typed ABSENT rather than empty strings. An empty url with an
// empty sha256 is a download that starts, succeeds against nothing, and fails a
// checksum comparison with a message about hex digests — when the true answer
// is "we haven't built the Mac one yet, and we don't claim macOS support until
// a human has run it on real hardware" (docs/todo/2026-08-05-install-and-macos.md).
// Making that a distinct state in the type means it CANNOT be papered over by a
// caller that forgot to check.
//
// PURE: a table and a lookup. No fetching, no hashing, no filesystem.

import { type PlatformId } from "./platform.js";

export interface RuntimeArtifact {
  /** Absolute https URL the app downloads on first run. */
  url: string;
  /** Lowercase hex SHA-256 of the bytes at `url`. THE verification gate — the
   *  downloaded file is either exactly this or it is discarded. Nothing else in
   *  this record is allowed to stand in for it. */
  sha256: string;
  /** APPROXIMATE. Only ever for "this will download about N MB" copy and a
   *  progress bar. Never a gate: the decisions document records sizes to one
   *  decimal place of MB, so an exact byte count would be a fabrication, and a
   *  size check that can be off by a few hundred KB is a size check that will
   *  eventually reject a good file. sha256 is the gate. */
  sizeBytes: number;
  /** Full PostgreSQL version this artifact contains, e.g. "16.10". Names the
   *  runtime directory (paths.ts) and is recorded in instance.json. */
  postgresVersion: string;
}

export type ArtifactEntry =
  /** A real, checksummed file exists at a real URL. Safe to download. */
  | { state: "published"; artifact: RuntimeArtifact }
  /** The upstream build is chosen and verified, but our trimmed tarball has not
   *  been produced yet — so its SHA-256 does not exist to record. This is the
   *  state a row occupies between "we picked an upstream build" and "`npm run
   *  pg:stage` has produced a tarball and printed its digest".
   *  Deliberately NOT `published` with a placeholder digest: a wrong checksum
   *  either fails every download with a baffling message, or gets "fixed" by
   *  someone deleting the verification, and the verification is the only thing
   *  standing between a user and an unsigned binary that runs their books. */
  | {
      state: "awaiting-publication";
      postgresVersion: string;
      /** The URL the release step will publish to — recorded so the release
       *  script and the downloader cannot disagree about it. */
      plannedUrl: string;
      approxSizeBytes: number;
      why: string;
    }
  /** No artifact exists and none has been produced. macOS, today. */
  | { state: "not-built"; why: string };

/** Bytes per MB as the decisions document's figures were read (MiB). Only used
 *  to turn "307.6 MB" and "24.7 MB" into the approximate byte counts above. */
const MB = 1024 * 1024;

/** The PostgreSQL version this milestone ships. One place, referenced by the
 *  manifest, the layout, and the cluster-version check. */
export const SHIPPED_POSTGRES_VERSION = "16.10";

/**
 * EnterpriseDB's official Windows binaries zip — the same build postgresql.org
 * links to. Downloaded and inspected on 2026-08-08: `postgres 16.10`,
 * `pg_dump 16.10`, and all of initdb, pg_ctl, pg_dump, pg_dumpall, pg_restore,
 * psql, pg_isready, pg_upgrade present and runnable straight out of the archive
 * with no VC++ redistributable and no registry entries.
 *
 * This is MAINTAINER-side input to the release process, not something the app
 * ever fetches. It lives here rather than in scripts/ so that the version, the
 * checksum and the platform id are stated once and the release tooling imports
 * them instead of restating them.
 */
export const UPSTREAM_WINDOWS_ARCHIVE: RuntimeArtifact = {
  url: "https://get.enterprisedb.com/postgresql/postgresql-16.10-1-windows-x64-binaries.zip",
  sha256: "ebb3b6af4fa69dea9951b66855bc4d42dc04e56ccb9aa7024ce3c58bd89d6b0c",
  sizeBytes: Math.round(307.6 * MB),
  postgresVersion: SHIPPED_POSTGRES_VERSION,
};

/** Where the trimmed tarballs are published. Same host as the update feed and
 *  the download page — the one host site/privacy.html names. */
export const RUNTIME_BASE_URL = "https://balancedsum.com/runtime/";

/** The published filename for a (version, platform) pair. Shared by the release
 *  script and the downloader for the same reason runtimeDirName() is. */
export function runtimeTarballName(postgresVersion: string, platform: PlatformId): string {
  return `postgresql-${postgresVersion}-${platform}.tar.xz`;
}

/**
 * What the app downloads on first run, per platform.
 *
 * Typed as a total Record over PlatformId, so adding a platform to
 * PLATFORM_IDS without adding its row here is a compile error rather than an
 * `undefined` discovered by a user.
 */
export const RUNTIME_MANIFEST: Readonly<Record<PlatformId, ArtifactEntry>> = {
  // Built by `npm run pg:stage` (scripts/stage-pg-runtime.ts) on 2026-08-08
  // from UPSTREAM_WINDOWS_ARCHIVE, and the digest below was recomputed from the
  // staged file independently of that script's own output before being pasted
  // here.
  //
  // TWO THINGS THAT WILL BITE WHOEVER TOUCHES THIS ROW NEXT:
  //
  //  1. xz output is NOT byte-reproducible — it records mtimes, so re-running
  //     `npm run pg:stage` over identical contents produces a DIFFERENT sha256.
  //     The digest here belongs to one specific file. Never mix a hash from one
  //     run with the tarball from another, or every install fails its integrity
  //     check on first launch. This is not hypothetical: the first version of
  //     this row carried a digest read off an earlier run of that script, four
  //     kilobytes and one rebuild away from the file actually staged. Take the
  //     values from `.pg-cache/<tarball>.manifest.json`, written by the same run
  //     that produced the file, and re-hash the staged file before you believe
  //     either of them.
  //  2. `url` has to actually answer before a build carrying this manifest
  //     ships. The tarball is staged into site/public/runtime/ (gitignored —
  //     a 25 MB binary never enters git) and only reaches the URL below when the
  //     site is deployed. RELEASING.md sequences this; the manifest states the
  //     intent, the deploy makes it true.
  "win32-x64": {
    state: "published",
    artifact: {
      url: RUNTIME_BASE_URL + runtimeTarballName(SHIPPED_POSTGRES_VERSION, "win32-x64"),
      sha256: "27f4900cd8289a75466d1bcd09f6ebce051d59f1dbfda33995644a90b2a28db6",
      sizeBytes: 25_915_316,
      postgresVersion: SHIPPED_POSTGRES_VERSION,
    },
  },

  // EnterpriseDB does not publish an arm64 archive at all. The likely source is
  // Postgres.app's universal binaries, trimmed the same way — but that has to be
  // produced and checked on real hardware before it can be claimed.
  "darwin-arm64": {
    state: "not-built",
    why:
      "Balanced Sum doesn't have a Mac download yet. Nothing about the Mac version is finished until someone has " +
      "run it on a real Mac, and that hasn't happened.",
  },
  "darwin-x64": {
    state: "not-built",
    why:
      "Balanced Sum doesn't have a Mac download yet. Nothing about the Mac version is finished until someone has " +
      "run it on a real Mac, and that hasn't happened.",
  },
};

export type ArtifactResult =
  | { ok: true; artifact: RuntimeArtifact }
  | {
      ok: false;
      reason: "awaiting-publication" | "not-built";
      platform: PlatformId;
      /** Plain English, complete sentences (CLAUDE.md §9). */
      message: string;
    };

/**
 * Turns a platform id into either a downloadable artifact or a typed reason
 * there isn't one. The `manifest` parameter exists so tests (and, later, a
 * release-candidate channel) can supply a different table without this module
 * reaching for module-level mutable state.
 *
 * There is no "close enough" fallback on purpose. Serving the Intel Mac
 * tarball to an Apple Silicon Mac would run PostgreSQL under Rosetta, which the
 * install/macOS document explicitly rules out, and would do it silently.
 */
export function resolveArtifact(
  platform: PlatformId,
  manifest: Readonly<Record<PlatformId, ArtifactEntry>> = RUNTIME_MANIFEST,
): ArtifactResult {
  const entry = manifest[platform];
  if (entry.state === "published") {
    return { ok: true, artifact: entry.artifact };
  }
  return { ok: false, reason: entry.state, platform, message: entry.why };
}

/** Case- and whitespace-insensitive SHA-256 comparison. Digests get pasted
 *  between tools that disagree about case (EnterpriseDB publishes uppercase,
 *  `sha256sum` prints lowercase, PowerShell's Get-FileHash prints uppercase),
 *  and a verification that fails on capitalisation alone teaches whoever hits
 *  it to stop trusting the verification. Length is checked too, so a truncated
 *  or empty digest can never compare equal to a truncated or empty actual. */
export function checksumMatches(expected: string, actual: string): boolean {
  const e = expected.trim().toLowerCase();
  const a = actual.trim().toLowerCase();
  return /^[0-9a-f]{64}$/.test(e) && e === a;
}
