/**
 * @file A lane's own database: create it once at provisioning and boot, drop
 * it on remove. CCAM stays stack-agnostic here — `createdb` vs `mysqladmin
 * create` vs `touch foo.db` genuinely differ, so the actual command lives in
 * the profile's `db-create.sh`/`db-drop.sh` hooks (already in the A1
 * allowlist); this module only decides WHEN to call them and guards WHICH
 * name a drop may ever target.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const fs = require("node:fs");
const path = require("node:path");

const { assertManaged } = require("./worktree");
const { dbName, slotDirs } = require("./lane-slots");
const { runHook } = require("./lane-profile");

/**
 * Where CCAM records that a slot's database has already been created.
 *
 * A hook can't be trusted to know this on its own without being stack-aware
 * (a plain `createdb` errors on a database that already exists; `touch`
 * wouldn't), so CCAM tracks it itself with one flag file per database name,
 * beside the rest of a slot's runtime bookkeeping. This is also how `upLane`
 * tells "freshly created" from "already existed" to decide whether to seed.
 */
function markerPath(slot, name) {
  return path.join(slotDirs(slot).stateDir, `db-created-${name}`);
}

/**
 * Ensure a lane's database exists, creating it via the profile's `db-create`
 * hook the first time only.
 *
 * A no-op when the profile declares no `DB_PREFIX` — no name is derived, so
 * no hook is ever called and nothing is created.
 *
 * @param {object} lane - Lane row; `slot` must already be allocated.
 * @param {object} profile - Resolved profile.
 * @param {{onLine?: Function, timeoutMs?: number}} [options]
 * @returns {Promise<{name: string|null, created: boolean}>}
 */
async function ensureDatabase(lane, profile, options = {}) {
  const name = dbName(profile, lane.slot);
  if (!name) return { name: null, created: false };

  const marker = markerPath(lane.slot, name);
  if (fs.existsSync(marker)) return { name, created: false };

  const result = await runHook(lane, profile, "db-create", [name], options);
  if (result.code !== 0) {
    throw Object.assign(new Error(`db-create hook exited ${result.code}`), {
      code: "EDBCREATE",
      logPath: result.logPath,
    });
  }
  fs.mkdirSync(path.dirname(marker), { recursive: true });
  fs.writeFileSync(marker, new Date().toISOString());
  return { name, created: true };
}

/**
 * Drop a database via the profile's `db-drop` hook.
 *
 * Two checks run before anything is spawned, and neither trusts the caller:
 * `assertManaged` (an adopted lane's data is the user's own, never CCAM's to
 * destroy) and a name check against what THIS lane's slot actually derives —
 * the lane's own database or its `_test` sibling, and nothing else. Only a
 * name CCAM itself computed can ever reach the hook.
 *
 * @param {object} lane - Lane row; `slot` must already be allocated.
 * @param {object} profile - Resolved profile.
 * @param {string} name - The database to drop; must be `dbName` or `${dbName}_test`.
 * @param {{onLine?: Function, timeoutMs?: number}} [options]
 * @returns {Promise<{name: string}>}
 */
async function dropDatabase(lane, profile, name, options = {}) {
  assertManaged(lane);
  const derived = dbName(profile, lane.slot);
  const allowed = derived && (name === derived || name === `${derived}_test`);
  if (!allowed) {
    throw Object.assign(new Error(`refusing to drop undeclared database: ${name}`), {
      code: "EBADDBNAME",
    });
  }

  const result = await runHook(lane, profile, "db-drop", [name], options);
  if (result.code !== 0) {
    throw Object.assign(new Error(`db-drop hook exited ${result.code}`), {
      code: "EDBDROP",
      logPath: result.logPath,
    });
  }
  fs.rmSync(markerPath(lane.slot, name), { force: true });
  return { name };
}

module.exports = { ensureDatabase, dropDatabase, markerPath };
