// Where everything lives on the user's machine — PG Task 1, implementing the
// "Layout on the user's machine" section of
// docs/todo/2026-08-08-bundled-postgres-decisions.md verbatim:
//
//   Windows   %LOCALAPPDATA%\BalancedSum\
//   macOS     ~/Library/Application Support/BalancedSum/
//
//     runtime/postgresql-16.10-<platform>/{bin,lib,share}   unpacked tarball
//     data/pg16/                                            the cluster (initdb)
//     logs/postgresql.log
//     instance.json                                         port, versions, created
//     secret                                                generated password, 0600
//
// The single most important property of this layout, and the reason none of it
// lives beside the app: UPDATING.md's entire update story is "unzip over the
// top". A data directory inside the app folder would be destroyed by a routine
// update, which for an accounting system means destroying the books. Per-user
// application data is the only correct home for it.
//
// `data/` is keyed by MAJOR version, not the full version, so a future PG17
// cluster can be created BESIDE the PG16 one during an upgrade instead of on
// top of it — the old directory stays intact and readable by the old runtime
// until the restore has been verified. `runtime/` is keyed by the FULL version
// because a 16.10 -> 16.11 patch bump is a different tarball against the same
// cluster, and both have to be able to sit on disk at once while one replaces
// the other.
//
// PURE. Nothing here touches the filesystem, calls os.homedir(), or reads
// process.env — the environment arrives as an argument. That is what lets the
// macOS layout be asserted from a Windows test runner, which matters because
// nobody has run this on a Mac yet (docs/todo/2026-08-05-install-and-macos.md).
// It is also why path joining goes through path.win32 / path.posix explicitly
// rather than the ambient `path`: the separator must follow the TARGET
// platform, not the machine doing the computing.

import path from "node:path";
import { type PlatformId } from "./platform.js";
import { parsePostgresMajor } from "./version.js";

/** The one folder name that identifies us inside the user's application-data
 *  directory. Changing this string orphans every existing install's books. */
export const APP_DATA_FOLDER = "BalancedSum";

/** Only the variables this module actually reads, and both optional — a caller
 *  can pass `process.env` directly and TypeScript will accept it, but this
 *  module can never reach for a variable it did not declare. */
export interface PathEnv {
  /** Windows: the per-user application data root. */
  LOCALAPPDATA?: string | undefined;
  /** macOS: the user's home directory. */
  HOME?: string | undefined;
}

/** Every PostgreSQL executable the bundled runtime is expected to contain. The
 *  list is closed on purpose: it is exactly the toolset the decisions document
 *  verified as present in EnterpriseDB's archive on 2026-08-08, and it is the
 *  reason zonky.io's three-executable build was rejected (server/src/backup.ts
 *  needs a real `pg_dump`, and RESTORE.md needs a real `pg_restore`). A typo in
 *  a caller becomes a compile error instead of an ENOENT at 2am. */
export type PostgresBinary =
  | "initdb"
  | "postgres"
  | "pg_ctl"
  | "pg_isready"
  | "pg_dump"
  | "pg_dumpall"
  | "pg_restore"
  | "pg_upgrade"
  | "psql";

export interface LocalDbLayout {
  platform: PlatformId;
  /** Full version, e.g. "16.10" — names the runtime directory. */
  postgresVersion: string;
  /** e.g. 16 — names the data directory. */
  postgresMajor: number;

  /** %LOCALAPPDATA%\BalancedSum, or ~/Library/Application Support/BalancedSum. */
  root: string;

  /** root/runtime — holds one directory per (version, platform) pair. */
  runtimeRoot: string;
  /** root/runtime/postgresql-<version>-<platform> — the unpacked tarball. */
  runtimeDir: string;
  /** The three directories the trimmed tarball actually contains. */
  binDir: string;
  libDir: string;
  shareDir: string;

  /** root/data — holds one directory per major version. */
  dataRoot: string;
  /** root/data/pg<major> — the cluster itself. THE precious directory. */
  dataDir: string;
  /** root/data/pg<major>/postmaster.pid. A hard power-off leaves this behind
   *  and it is normal, not exceptional (trap 2 in the decisions doc) — the
   *  start path has to recognise and recover from it rather than telling a
   *  business owner to delete a file they have never heard of. */
  postmasterPidFile: string;

  /** root/logs */
  logsDir: string;
  /** root/logs/postgresql.log — where the detached postmaster's stdio goes.
   *  Trap 1: `postgres` must be spawned with its stdio pointed HERE, because
   *  inheriting the parent's pipe is what hung the spike for ten minutes. */
  logFile: string;

  /** root/instance.json — port, versions, created. See instance.ts. */
  instanceFile: string;
  /** root/secret — the generated superuser password, 0600. See secret.ts. */
  secretFile: string;
}

export type LayoutResult =
  | { ok: true; layout: LocalDbLayout }
  | {
      ok: false;
      /** `missing-localappdata` / `missing-home`: the environment variable that
       *  names the user's application-data directory is not set, so there is no
       *  honest answer to "where do the books go". Guessing (C:\BalancedSum,
       *  /tmp) would put an accounting database somewhere the user will never
       *  look for it and a backup tool will never find it.
       *  `bad-version`: the version string does not start with a major number,
       *  so `data/pg<major>` cannot be named. */
      reason: "missing-localappdata" | "missing-home" | "bad-version";
      message: string;
    };

/** The directory name a runtime tarball unpacks into. Exported because the
 *  maintainer-side release script builds a tarball that must unpack to exactly
 *  this name — the two have to agree, and agreeing via a shared function beats
 *  agreeing via two string literals in two repositories' worth of scripts. */
export function runtimeDirName(postgresVersion: string, platform: PlatformId): string {
  return `postgresql-${postgresVersion}-${platform}`;
}

/** The directory name a cluster of `major` lives in: pg16, pg17. */
export function dataDirName(postgresMajor: number): string {
  return `pg${postgresMajor}`;
}

/** The application-data root for `platform`, or a typed reason it can't be
 *  determined. Split out from resolveLayout so a caller that only wants to say
 *  "your books are in %s" doesn't have to invent a version string to ask. */
export function resolveAppDataRoot(
  platform: PlatformId, env: PathEnv,
): { ok: true; root: string } | { ok: false; reason: "missing-localappdata" | "missing-home"; message: string } {
  if (platform === "win32-x64") {
    const localAppData = env.LOCALAPPDATA?.trim();
    if (!localAppData) {
      return {
        ok: false,
        reason: "missing-localappdata",
        message:
          "Windows didn't tell Balanced Sum where your application data folder is (the LOCALAPPDATA setting is " +
          "empty), so it can't work out where to keep your books. This usually means the app was started in an " +
          "unusual way — try opening it normally from its folder.",
      };
    }
    return { ok: true, root: path.win32.join(localAppData, APP_DATA_FOLDER) };
  }

  const home = env.HOME?.trim();
  if (!home) {
    return {
      ok: false,
      reason: "missing-home",
      message:
        "macOS didn't tell Balanced Sum where your home folder is (the HOME setting is empty), so it can't work " +
        "out where to keep your books. This usually means the app was started in an unusual way — try opening it " +
        "normally from its folder.",
    };
  }
  return { ok: true, root: path.posix.join(home, "Library", "Application Support", APP_DATA_FOLDER) };
}

/** The path module whose separator matches the TARGET platform — not the host.
 *  See the file header for why this is never the ambient `path`. */
function joinerFor(platform: PlatformId): typeof path {
  return platform === "win32-x64" ? path.win32 : path.posix;
}

/**
 * Computes every path in the layout for one (platform, version) pair from a
 * passed-in environment. This is the single place any of these names are
 * spelled; nothing downstream may re-derive `data/pg16` or `instance.json` by
 * joining strings of its own, because two spellings that drift apart is how a
 * cluster gets created in one place and looked for in another.
 */
export function resolveLayout(
  platform: PlatformId, env: PathEnv, postgresVersion: string,
): LayoutResult {
  const major = parsePostgresMajor(postgresVersion);
  if (major === null) {
    return {
      ok: false,
      reason: "bad-version",
      message:
        `Balanced Sum couldn't make sense of the PostgreSQL version it was asked for ("${postgresVersion}"). ` +
        "This is a problem with the download, not with anything you did — reinstalling should fix it.",
    };
  }

  const rootResult = resolveAppDataRoot(platform, env);
  if (!rootResult.ok) return rootResult;

  const p = joinerFor(platform);
  const root = rootResult.root;

  const runtimeRoot = p.join(root, "runtime");
  const runtimeDir = p.join(runtimeRoot, runtimeDirName(postgresVersion, platform));
  const dataRoot = p.join(root, "data");
  const dataDir = p.join(dataRoot, dataDirName(major));
  const logsDir = p.join(root, "logs");

  return {
    ok: true,
    layout: {
      platform,
      postgresVersion,
      postgresMajor: major,
      root,
      runtimeRoot,
      runtimeDir,
      binDir: p.join(runtimeDir, "bin"),
      libDir: p.join(runtimeDir, "lib"),
      shareDir: p.join(runtimeDir, "share"),
      dataRoot,
      dataDir,
      postmasterPidFile: p.join(dataDir, "postmaster.pid"),
      logsDir,
      logFile: p.join(logsDir, "postgresql.log"),
      instanceFile: p.join(root, "instance.json"),
      secretFile: p.join(root, "secret"),
    },
  };
}

/** Absolute path to one of the bundled executables, with `.exe` appended on
 *  Windows. Callers spawn this path directly and never rely on PATH — the whole
 *  reason for bundling is that we know exactly which binary is being run, which
 *  is also what lets server/src/backup.ts stop probing for a `pg_dump`. */
export function binaryPath(layout: LocalDbLayout, name: PostgresBinary): string {
  const p = joinerFor(layout.platform);
  const suffix = layout.platform === "win32-x64" ? ".exe" : "";
  return p.join(layout.binDir, `${name}${suffix}`);
}
