/**
 * @file Seed and repair a lane's `.env` file(s): copy the source repository's
 * real `.env` into the lane on first boot (or on a `--force` refresh) and then
 * rewrite the per-lane keys (`DATABASE_URL`, `REDIS_URL`, `UPLOAD_DIR`, …) so
 * the file is correct ON ITS OWN — not merely masked by a hook's runtime
 * exports. Ports Shipyard's `lane-env-seed.sh`.
 *
 * Two hard-won behaviours are preserved verbatim. A `--force` refresh keeps
 * `ENV_PRESERVE` keys (e.g. `JWT_SECRET`) from the lane's OWN existing file:
 * swapping in the source's secret would 401 a running lane's tokens until
 * reboot. And a missing source `.env` falls back to `.env.example` with a
 * loud warning rather than a silent, broken seed.
 * @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 { splitList, parseEnvFile } = require("./lane-profile");
const { dataFacts } = require("./lane-slots");

/** Resolve `relative` against `root`, refusing anything that climbs out of
 *  it — same confinement rule `lane-runtime.js:makeLaneDirs` applies to
 *  `LANE_DIRS`, so a `.` file declared by a repository can only ever touch
 *  its own tree. */
function confine(root, relative, code) {
  const target = path.resolve(root, relative);
  const rel = path.relative(root, target);
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
    throw Object.assign(new Error(`path escapes ${root}: ${relative}`), { code });
  }
  return target;
}

/**
 * Rewrite declared keys inside an `.env` file's text, preserving every other
 * line byte-for-byte. A key not already present is appended, mirroring
 * Shipyard's python rewriter.
 *
 * @param {string} text - The file's current contents.
 * @param {Record<string,string>} want - Keys to set, already resolved to their final values.
 * @returns {string}
 */
function rewriteEnvText(text, want) {
  const seen = new Set();
  const lines = text.split("\n").map((line) => {
    const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/.exec(line);
    if (match && Object.hasOwn(want, match[1])) {
      seen.add(match[1]);
      return `${match[1]}=${want[match[1]]}`;
    }
    return line;
  });
  for (const [key, value] of Object.entries(want)) {
    if (!seen.has(key)) lines.push(`${key}=${value}`);
  }
  return lines.join("\n");
}

/**
 * Seed or repair a lane's declared `.env` file(s).
 *
 * A no-op when the profile declares no `ENV_FILES` — the feature is off by
 * default, and an adopted repo that never opts in gets no `.env` writes at
 * all. Refuses on an adopted lane (`assertManaged`): that file is the user's
 * real working config, not a template CCAM may overwrite.
 *
 * @param {object} lane - Lane row; `slot` must already be allocated.
 * @param {object} profile - Resolved profile.
 * @param {Record<string,string>} secrets - From `secrets.js:readSecrets()`.
 * @param {{force?: boolean}} [options] - `force` re-copies from source even when the file exists.
 * @returns {{skipped: boolean, seeded?: Array<{file: string, fromExample: boolean}>}}
 */
function seedEnv(lane, profile, secrets, { force = false } = {}) {
  assertManaged(lane);

  const files = splitList(profile.env.ENV_FILES);
  if (!files.length) return { skipped: true };

  const sources = splitList(profile.env.ENV_SOURCE || profile.env.ENV_FILES);
  const rewriteKeys = new Set(splitList(profile.env.ENV_REWRITE));
  const preserveKeys = splitList(profile.env.ENV_PRESERVE);
  const sourceRoot = lane.source_repo || lane.cwd;
  const facts = dataFacts(lane, profile, secrets);
  const computed = {
    DATABASE_URL: facts.databaseUrl,
    REDIS_URL: facts.redisUrl,
    UPLOAD_DIR: facts.uploadDir,
  };

  const seeded = [];
  for (let i = 0; i < files.length; i += 1) {
    const relFile = files[i];
    const relSource = sources[i] || relFile;
    const targetPath = confine(lane.cwd, relFile, "EBADENVFILE");
    const sourcePath = confine(sourceRoot, relSource, "EBADENVFILE");

    const existed = fs.existsSync(targetPath);
    const preserved = {};
    if (existed && force && preserveKeys.length) {
      try {
        const current = parseEnvFile(fs.readFileSync(targetPath, "utf8"));
        for (const key of preserveKeys) {
          if (current[key] !== undefined) preserved[key] = current[key];
        }
      } catch {
        /* an unreadable existing file has nothing worth preserving */
      }
    }

    if (!existed || force) {
      let content;
      let fromExample = false;
      if (fs.existsSync(sourcePath)) {
        content = fs.readFileSync(sourcePath, "utf8");
      } else if (fs.existsSync(`${sourcePath}.example`)) {
        content = fs.readFileSync(`${sourcePath}.example`, "utf8");
        fromExample = true;
        console.warn(
          `[lane-env] lane ${lane.id}: ${relSource} is missing — seeded ${relFile} from ` +
            `${relSource}.example instead (no real secrets/keys)`
        );
      } else {
        throw Object.assign(
          new Error(`no ${relSource} (or ${relSource}.example) to seed ${relFile} from`),
          { code: "ENOENVSOURCE", relSource }
        );
      }
      fs.mkdirSync(path.dirname(targetPath), { recursive: true });
      fs.writeFileSync(targetPath, content);
      seeded.push({ file: relFile, fromExample });
    }

    const want = {};
    for (const [key, value] of Object.entries(computed)) {
      if (value != null && rewriteKeys.has(key)) want[key] = value;
    }
    Object.assign(want, preserved);
    if (Object.keys(want).length) {
      fs.writeFileSync(targetPath, rewriteEnvText(fs.readFileSync(targetPath, "utf8"), want));
    }
  }

  if (facts.uploadDir) fs.mkdirSync(facts.uploadDir, { recursive: true });

  return { skipped: false, seeded };
}

module.exports = { seedEnv, rewriteEnvText };
