// Major-version verdicts for an existing cluster — PG Task 1, implementing the
// "Major-version upgrades" section of
// docs/todo/2026-08-08-bundled-postgres-decisions.md.
//
// The whole point of this module is one sentence from that document: "Design
// this in now; discovering it on the first bump is how people lose books."
// A PostgreSQL data directory is readable only by the major version that
// created it. Ship PG17 over a PG16 cluster and `postgres` refuses to start —
// which is the GOOD outcome. The bad outcomes are (a) the app deciding a
// cluster it cannot read is "missing" and running `initdb` next to it, leaving
// the user staring at an empty chart of accounts with their real books still on
// disk and no explanation, and (b) somebody later "fixing" that by making a
// downgrade path that quietly discards rows a newer build wrote.
//
// So `older-cluster` and `newer-cluster` are DELIBERATELY separate verdicts.
// They look symmetrical and are not:
//
//   older-cluster  Their data predates this build. A dump/restore migration
//                  path is possible and is planned (pg_dumpall with the OLD
//                  runtime, initdb the new major alongside, restore, retire
//                  the old directory). Until that exists and is tested, refuse.
//   newer-cluster  Their data was written by a build NEWER than this one. There
//                  is no migration path and there never will be — you cannot
//                  un-write a schema. The only correct move is to stop and tell
//                  them to run the newer version again. Treating this as
//                  "just a mismatch" is how an accidental double-click on an
//                  old installer becomes a data-loss incident.
//
// Pure: no filesystem, no `postgres --version` shell-out. The caller passes in
// what this build ships and what `instance.json` recorded.

/**
 * `fresh` — no cluster recorded yet; this is a first run and there is nothing
 *   to be compatible with.
 * `match` — recorded major equals the shipped major; start normally.
 * `older-cluster` — the books are in an OLDER major than this build ships.
 * `newer-cluster` — the books are in a NEWER major than this build ships.
 */
export type ClusterVerdict = "fresh" | "match" | "older-cluster" | "newer-cluster";

export interface ClusterVersionCheck {
  verdict: ClusterVerdict;
  /** The major this build of Balanced Sum bundles. */
  shippedMajor: number;
  /** The major `instance.json` recorded, or null when there is no instance. */
  clusterMajor: number | null;
  /** Whether the caller may go ahead and start (or create) a cluster.
   *  False for BOTH mismatch verdicts today — see the file header for why
   *  `older-cluster` is refused even though it is theoretically migratable. */
  canStart: boolean;
  /** Plain English, complete sentences, safe to show a business owner
   *  verbatim (CLAUDE.md §9). Never contains a path or a version number the
   *  reader has no use for. */
  message: string;
}

/** Pulls the major out of "16.10", "16", "17.2.1". Returns null for anything
 *  that doesn't start with a run of digits — a version string we cannot parse
 *  must never be silently coerced to 0, because 0 would compare as "older than
 *  everything" and take the downgrade branch. */
export function parsePostgresMajor(version: string): number | null {
  const m = /^(\d+)(?:\.|$)/.exec(version.trim());
  if (!m) return null;
  const major = Number(m[1]);
  return Number.isSafeInteger(major) && major > 0 ? major : null;
}

/**
 * Compares the major this build ships against the major of the cluster on disk.
 * Pass `null` when there is no cluster; `fresh` comes back, and it answers "are
 * these two versions compatible", never "is it safe to run initdb here".
 *
 * TAKE `clusterMajor` FROM THE DATA DIRECTORY'S OWN `PG_VERSION` FILE, not from
 * `instance.json`. PostgreSQL writes PG_VERSION itself and it cannot disagree
 * with the cluster it sits in; instance.json is ours, and a damaged or
 * hand-edited one would report `null` — which reads as `fresh`, which reads to
 * a careless caller as permission to initdb on top of somebody's books.
 * runtime.ts does it this way and treats a disagreement between the two files
 * as a refusal. instance.json's job is the port, not the version.
 */
export function checkClusterVersion(shippedMajor: number, clusterMajor: number | null): ClusterVersionCheck {
  if (clusterMajor === null) {
    return {
      verdict: "fresh",
      shippedMajor,
      clusterMajor: null,
      canStart: true,
      message: "No database has been set up on this computer yet. Balanced Sum will create one.",
    };
  }

  if (clusterMajor === shippedMajor) {
    return {
      verdict: "match",
      shippedMajor,
      clusterMajor,
      canStart: true,
      message: "The database on this computer matches this version of Balanced Sum.",
    };
  }

  if (clusterMajor < shippedMajor) {
    return {
      verdict: "older-cluster",
      shippedMajor,
      clusterMajor,
      canStart: false,
      message:
        `Your books are stored in a PostgreSQL ${clusterMajor} database, and this version of Balanced Sum ` +
        `comes with PostgreSQL ${shippedMajor}. Moving the data between the two is a one-way step, so ` +
        "Balanced Sum won't do it on its own yet — it would rather stop than risk your books. " +
        "Install the previous version of Balanced Sum again to keep working, and see UPDATING.md.",
    };
  }

  return {
    verdict: "newer-cluster",
    shippedMajor,
    clusterMajor,
    canStart: false,
    message:
      `Your books were created by a NEWER version of Balanced Sum (it uses PostgreSQL ${clusterMajor}; ` +
      `this copy uses PostgreSQL ${shippedMajor}). An older copy can't read them, and trying would risk ` +
      "damaging them. Install the newer version of Balanced Sum again — nothing has been changed.",
  };
}
