/**
 * Consumer-side state tracking — `.baldart/state.json`.
 *
 * Centralised view of "what version is installed, when was it installed,
 * when did we last update, when did we last push". Read by `baldart version`
 * and the `/baldart-push` skill so the user always knows which framework
 * version they are working against.
 *
 * The file is committed to the consumer's repo. It is never overwritten by
 * `baldart update` — only mutated by the CLI commands that need to record
 * an event.
 *
 * Schema (v1):
 *   {
 *     "state_version": 1,
 *     "installed_version": "3.1.0",
 *     "install_date": "2026-05-21T15:30:00.000Z",
 *     "last_update_date": "2026-05-21T16:00:00.000Z",
 *     "last_pushed_version": "3.0.0",
 *     "last_push_date": "2026-05-21T17:00:00.000Z",
 *     "framework_repo": "antbald/BALDART",
 *     "history": [
 *       { "ts": "...", "event": "install"|"update"|"push", "from": "x", "to": "y", "note": "..." }
 *     ]
 *   }
 *
 * History keeps the last 20 entries (rolling). Older entries are dropped.
 */

const fs = require('fs');
const path = require('path');

const STATE_DIR = '.baldart';
const STATE_FILE = path.join(STATE_DIR, 'state.json');
const STATE_VERSION = 1;
const HISTORY_LIMIT = 20;

// Upstream framework repository. SSOT for the "where do we install from"
// default — duplicated literals in version.js / doctor.js / recordInstall
// used to drift independently. Always resolve through `resolveRepo()` so a
// consumer's fork (recorded in state.framework_repo at install time) is
// preserved instead of being silently overridden by this constant.
const DEFAULT_REPO = 'antbald/BALDART';

function defaultState() {
  return {
    state_version: STATE_VERSION,
    installed_version: null,
    install_date: null,
    last_update_date: null,
    last_pushed_version: null,
    last_push_date: null,
    framework_repo: null,
    history: [],
  };
}

function fullPath(cwd = process.cwd()) {
  return path.join(cwd, STATE_FILE);
}

function load(cwd = process.cwd()) {
  const full = fullPath(cwd);
  if (!fs.existsSync(full)) return defaultState();
  try {
    const raw = JSON.parse(fs.readFileSync(full, 'utf8'));
    // Merge with defaults so missing keys get sane values without crashing.
    return { ...defaultState(), ...raw };
  } catch (err) {
    // Corrupt state file — log and fall back to defaults. Do not throw;
    // versioning is auxiliary, never block the user's CLI flow.
    return defaultState();
  }
}

function save(state, cwd = process.cwd()) {
  const dir = path.join(cwd, STATE_DIR);
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  const full = fullPath(cwd);
  fs.writeFileSync(full, JSON.stringify(state, null, 2) + '\n', 'utf8');
}

function appendHistory(state, entry) {
  const next = [...(state.history || []), { ts: new Date().toISOString(), ...entry }];
  return next.slice(-HISTORY_LIMIT);
}

/**
 * Record an install event. Idempotent: calling twice for the same version is a no-op
 * other than the history entry.
 */
function recordInstall({ version, repo }, cwd = process.cwd()) {
  const state = load(cwd);
  const now = new Date().toISOString();
  const wasFirstInstall = !state.installed_version;
  state.installed_version = version;
  state.framework_repo = repo || state.framework_repo || DEFAULT_REPO;
  if (wasFirstInstall) state.install_date = now;
  state.history = appendHistory(state, {
    event: wasFirstInstall ? 'install' : 'reinstall',
    to: version,
  });
  save(state, cwd);
  return state;
}

function recordUpdate({ from, to }, cwd = process.cwd()) {
  const state = load(cwd);
  state.installed_version = to;
  state.last_update_date = new Date().toISOString();
  state.history = appendHistory(state, { event: 'update', from, to });
  save(state, cwd);
  return state;
}

/**
 * Reconcile the ledger's installed_version with the actual framework version
 * on disk (read from .framework/VERSION). Used by `baldart doctor` when it
 * detects a mismatch between the payload and the ledger — typically caused by
 * an interrupted update flow, a manual commit that excluded state.json, or a
 * filesystem write failure that the silent catch in update.js used to hide.
 *
 * Idempotent: calling with the already-current version is a no-op (no history
 * entry produced). History entry uses event "ledger-reconcile" so the source
 * of the bump is auditable separately from real updates.
 */
function reconcileInstalledVersion({ to, reason }, cwd = process.cwd()) {
  const state = load(cwd);
  const from = state.installed_version;
  if (from === to) return { state, changed: false };
  state.installed_version = to;
  state.history = appendHistory(state, {
    event: 'ledger-reconcile',
    from,
    to,
    note: reason || 'reconcile ledger with .framework/VERSION'
  });
  save(state, cwd);
  return { state, changed: true, from, to };
}

function recordPush({ from, to, description }, cwd = process.cwd()) {
  const state = load(cwd);
  state.last_pushed_version = to;
  state.last_push_date = new Date().toISOString();
  state.installed_version = to; // a push bumps the local version too
  state.history = appendHistory(state, { event: 'push', from, to, note: description });
  save(state, cwd);
  return state;
}

/**
 * Resolve the upstream framework repository to install/update from. Cascade:
 * `state.framework_repo` (recorded at install — preserves a consumer's fork)
 * → DEFAULT_REPO. Never returns undefined, so callers that thread the result
 * into git.addSubtree() / normalizeRepoUrl() can't trip the "undefined repo"
 * crash that broke `update --reset` (it called add() with no repo).
 *
 * `baldart.config.yml` and the subtree's git remote are intentionally NOT
 * consulted — neither stores the repo, so they'd be dead lookups.
 */
function resolveRepo(cwd = process.cwd()) {
  return load(cwd).framework_repo || DEFAULT_REPO;
}

/**
 * Read the framework's VERSION file from .framework/VERSION. Used by version /
 * status commands to cross-check what the symlinked framework reports vs what
 * the state file remembers.
 */
function readFrameworkVersion(cwd = process.cwd()) {
  const versionPath = path.join(cwd, '.framework', 'VERSION');
  if (!fs.existsSync(versionPath)) return null;
  try {
    return fs.readFileSync(versionPath, 'utf8').trim();
  } catch (_) {
    return null;
  }
}

module.exports = {
  load,
  save,
  recordInstall,
  recordUpdate,
  recordPush,
  reconcileInstalledVersion,
  resolveRepo,
  readFrameworkVersion,
  DEFAULT_REPO,
  STATE_FILE,
  STATE_VERSION,
};
