/**
 * engine/worktree-pool.js — per-project warm pool of git worktrees that can be
 * recycled across branches (W-mp73ya3e000me6c5).
 *
 * Default-off: `ENGINE_DEFAULTS.worktreePoolSize = 0`. Per-project opt-in via
 * `projects[].worktreePoolSize`. When enabled, completed worktrees are parked
 * detached at `origin/<mainBranch>` and reused by the next dispatch for the
 * same project — saving the cold install/build cost on subsequent WIs.
 *
 * Concurrency: all state flips go through `mutateJsonFileLocked` so two
 * spawning ticks cannot double-borrow the same idle entry. Per CLAUDE.md, git
 * commands NEVER run inside the lock callback — callers must do `tryBorrow`,
 * then run git in their own scope, then call `evictEntry` on failure.
 *
 * Idle worktrees are detached at origin/<main>, NOT checked out on the local
 * `main` branch. Local-main is typically already checked out in the project
 * root, and git refuses two checkouts of the same branch. Detached HEAD
 * sidesteps that entirely while keeping the working tree warm with main's
 * package.json + lockfile + node_modules.
 */

const fs = require('fs');
const path = require('path');
const shared = require('./shared');

const { log, ENGINE_DEFAULTS, mutateJsonFileLocked, safeJson } = shared;

const WORKTREE_POOL_FILENAME = 'worktree-pool.json';
const POOL_STATE = Object.freeze({ IDLE: 'idle', BORROWED: 'borrowed' });

function getPoolPath() {
  return path.join(shared.MINIONS_DIR, 'engine', WORKTREE_POOL_FILENAME);
}

function _normalizePath(p) {
  if (!p) return '';
  let resolved;
  try { resolved = path.resolve(String(p)); }
  catch { return ''; }
  // Strip trailing separators
  resolved = resolved.replace(/[\\/]+$/, '');
  // Case-insensitive on Windows
  if (process.platform === 'win32') resolved = resolved.toLowerCase();
  // Use forward-slashes for stable comparison across OS path APIs
  return resolved.replace(/\\/g, '/');
}

function _nowIso(now) {
  return new Date(Number.isFinite(now) ? now : Date.now()).toISOString();
}

function _entriesArray(data) {
  if (!data || typeof data !== 'object' || !Array.isArray(data.entries)) return [];
  return data.entries;
}

function readPool() {
  const data = safeJson(getPoolPath());
  if (!data || !Array.isArray(data.entries)) return { entries: [] };
  return data;
}

function mutateWorktreePool(mutator) {
  // Phase 7: route through shared.mutateWorktreePool (SQL-backed store +
  // JSON mirror). Defensive wrappers around `data.entries` kept for the
  // legacy fallback path inside the helper (when SQLite is unavailable
  // and we fall through to mutateJsonFileLocked).
  return shared.mutateWorktreePool((data) => {
    if (!data || typeof data !== 'object' || Array.isArray(data)) data = { entries: [] };
    if (!Array.isArray(data.entries)) data.entries = [];
    const next = mutator(data);
    return next === undefined ? data : next;
  });
}

/**
 * Resolve effective pool size for a project. Per-project override beats the
 * fleet-wide default. Returns 0 (disabled) when nothing is configured.
 */
function getProjectPoolSize(projectName, config) {
  config = config || {};
  let proj = null;
  if (projectName && Array.isArray(config.projects)) {
    proj = config.projects.find(p => p && p.name === projectName) || null;
  }
  // P-a3f9b206: short-circuit for live-mode projects. The pool is meaningless
  // when dispatches run in-place in the operator's local checkout — there is
  // no "warm" worktree to recycle, and a non-zero pool size could trick the
  // borrow path in spawnAgent into running for a project that no longer
  // wants pooled worktrees. Beats both per-project worktreePoolSize and the
  // engine-wide fleet default.
  if (proj && proj.worktreeMode === shared.WORKTREE_MODES.LIVE) return 0;
  if (proj && Number.isFinite(Number(proj.worktreePoolSize))) {
    return Math.max(0, Math.floor(Number(proj.worktreePoolSize)));
  }
  const fleet = config.engine?.worktreePoolSize ?? ENGINE_DEFAULTS.worktreePoolSize;
  if (Number.isFinite(Number(fleet))) return Math.max(0, Math.floor(Number(fleet)));
  return 0;
}

function getPoolIdleTtlMs(config) {
  config = config || {};
  const v = config.engine?.worktreePoolIdleTtlMs ?? ENGINE_DEFAULTS.worktreePoolIdleTtlMs;
  if (Number.isFinite(Number(v))) return Math.max(60000, Number(v));
  return 6 * 3600 * 1000;
}

function _findEntryByPath(entries, wtPath) {
  const norm = _normalizePath(wtPath);
  if (!norm) return -1;
  return entries.findIndex(e => e && _normalizePath(e.path) === norm);
}

function isPoolMember(wtPath) {
  const entries = _entriesArray(readPool());
  return _findEntryByPath(entries, wtPath) !== -1;
}

function getEntry(wtPath) {
  const entries = _entriesArray(readPool());
  const idx = _findEntryByPath(entries, wtPath);
  return idx === -1 ? null : entries[idx];
}

/**
 * Atomically borrow an idle entry for a project. Returns the borrowed entry
 * (with state flipped to 'borrowed' on disk) or null when none is available.
 *
 * The caller MUST then run the git ops to checkout the new branch, and call
 * `evictEntry` on failure to release the slot.
 */
function tryBorrow(projectName, dispatchId, opts) {
  opts = opts || {};
  if (!projectName || !dispatchId) return null;
  const now = Number.isFinite(opts.now) ? opts.now : Date.now();
  let borrowed = null;
  mutateWorktreePool((data) => {
    const entries = data.entries;
    // Pick the most-recently-used idle entry for this project (warmer caches).
    let bestIdx = -1;
    let bestIdleSince = -Infinity;
    for (let i = 0; i < entries.length; i++) {
      const e = entries[i];
      if (!e || e.project !== projectName || e.state !== POOL_STATE.IDLE) continue;
      // Skip entries whose path no longer exists on disk.
      if (!e.path || !fs.existsSync(e.path)) continue;
      const t = Date.parse(e.idleSince || e.lastUsed || e.createdAt || '') || 0;
      if (t > bestIdleSince) { bestIdleSince = t; bestIdx = i; }
    }
    if (bestIdx === -1) return data;
    const entry = entries[bestIdx];
    entry.state = POOL_STATE.BORROWED;
    entry.borrowedBy = dispatchId;
    entry.borrowedAt = _nowIso(now);
    entry.lastUsed = entry.borrowedAt;
    entry.idleSince = null;
    borrowed = { ...entry };
    return data;
  });
  return borrowed;
}

/**
 * Return a worktree to the pool — flip an existing borrowed entry to idle, or
 * insert a fresh entry as idle when the project is under capacity. Both paths
 * enforce the per-project capacity inside the locked mutation, so two
 * simultaneous returns cannot blow past `poolSize`.
 *
 * Returns one of:
 *   - 'flipped'   — existing borrowed entry flipped to idle
 *   - 'inserted'  — new idle entry added (was a fresh worktree)
 *   - 'rejected'  — pool full or poolSize=0; caller should let normal cleanup
 *                   reap the worktree
 *
 * Caller MUST have already run the git reset/clean/checkout-detach/pull dance
 * before invoking this function. On any git failure earlier, call `evictEntry`
 * and skip the return.
 */
function returnToPool(projectName, wtPath, opts) {
  opts = opts || {};
  if (!projectName || !wtPath) return 'rejected';
  const poolSize = Number.isFinite(opts.poolSize) ? Math.max(0, Math.floor(opts.poolSize)) : 0;
  if (poolSize <= 0) return 'rejected';
  const now = Number.isFinite(opts.now) ? opts.now : Date.now();
  const branch = opts.branch || '';
  let outcome = 'rejected';
  mutateWorktreePool((data) => {
    const entries = data.entries;
    const existingIdx = _findEntryByPath(entries, wtPath);
    if (existingIdx !== -1) {
      const e = entries[existingIdx];
      e.state = POOL_STATE.IDLE;
      e.borrowedBy = null;
      e.borrowedAt = null;
      e.idleSince = _nowIso(now);
      e.lastUsed = _nowIso(now);
      if (branch) e.lastBranch = branch;
      outcome = 'flipped';
      return data;
    }
    // Insert path — gate on capacity inside the lock.
    const projectCount = entries.filter(e => e && e.project === projectName).length;
    if (projectCount >= poolSize) {
      outcome = 'rejected';
      return data;
    }
    entries.push({
      project: projectName,
      path: wtPath,
      state: POOL_STATE.IDLE,
      borrowedBy: null,
      borrowedAt: null,
      idleSince: _nowIso(now),
      lastBranch: branch,
      createdAt: _nowIso(now),
      lastUsed: _nowIso(now),
    });
    outcome = 'inserted';
    return data;
  });
  return outcome;
}

/**
 * Remove an entry from the pool. Used on git failure during borrow/return so
 * the worktree falls back to normal cleanup ownership.
 */
function evictEntry(wtPath, reason) {
  if (!wtPath) return false;
  const norm = _normalizePath(wtPath);
  let removed = false;
  mutateWorktreePool((data) => {
    const before = data.entries.length;
    data.entries = data.entries.filter(e => _normalizePath(e?.path) !== norm);
    if (data.entries.length !== before) {
      removed = true;
      log('info', `worktree-pool: evicted ${wtPath} (${reason || 'unspecified'})`);
    }
    return data;
  });
  return removed;
}

/**
 * Drop entries whose path is gone, whose borrower is no longer active, or
 * whose idle TTL has expired. `activeDispatchIds` is a Set of dispatch ids
 * currently in `dispatch.active`. Returns { dropped: [...], evicted: number }.
 */
function pruneStale(opts) {
  opts = opts || {};
  const now = Number.isFinite(opts.now) ? opts.now : Date.now();
  const ttlMs = Number.isFinite(opts.idleTtlMs) ? opts.idleTtlMs : getPoolIdleTtlMs(opts.config);
  const activeIds = opts.activeDispatchIds instanceof Set
    ? opts.activeDispatchIds
    : new Set(Array.isArray(opts.activeDispatchIds) ? opts.activeDispatchIds : []);
  const dropped = [];
  mutateWorktreePool((data) => {
    const next = [];
    for (const e of data.entries) {
      if (!e || !e.path) { dropped.push({ path: e?.path, reason: 'invalid' }); continue; }
      if (!fs.existsSync(e.path)) { dropped.push({ path: e.path, reason: 'missing-path' }); continue; }
      if (e.state === POOL_STATE.BORROWED) {
        if (!e.borrowedBy || !activeIds.has(e.borrowedBy)) {
          dropped.push({ path: e.path, reason: 'orphan-borrow' });
          continue;
        }
        next.push(e);
        continue;
      }
      if (e.state === POOL_STATE.IDLE) {
        const idleAt = Date.parse(e.idleSince || e.lastUsed || e.createdAt || '') || 0;
        if (idleAt > 0 && (now - idleAt) > ttlMs) {
          dropped.push({ path: e.path, reason: 'ttl-expired' });
          continue;
        }
        next.push(e);
        continue;
      }
      // Unknown state — drop defensively.
      dropped.push({ path: e.path, reason: 'unknown-state' });
    }
    data.entries = next;
    return data;
  });
  return { dropped, evicted: dropped.length };
}

/**
 * Count entries (idle + borrowed) belonging to a project. Used for capacity
 * gating diagnostics and pool-size dashboards.
 */
function countForProject(projectName) {
  if (!projectName) return 0;
  return _entriesArray(readPool()).filter(e => e && e.project === projectName).length;
}

/**
 * Return the set of borrowed pool paths whose borrower is in `activeIds`.
 * Used by cleanup.js to protect them from the age/cap sweep.
 */
function getActiveBorrowedPaths(activeDispatchIds) {
  const activeIds = activeDispatchIds instanceof Set
    ? activeDispatchIds
    : new Set(Array.isArray(activeDispatchIds) ? activeDispatchIds : []);
  const out = new Set();
  for (const e of _entriesArray(readPool())) {
    if (!e || e.state !== POOL_STATE.BORROWED) continue;
    if (e.borrowedBy && activeIds.has(e.borrowedBy)) out.add(_normalizePath(e.path));
  }
  return out;
}

function getIdlePaths() {
  const out = new Set();
  for (const e of _entriesArray(readPool())) {
    if (!e || e.state !== POOL_STATE.IDLE) continue;
    out.add(_normalizePath(e.path));
  }
  return out;
}

module.exports = {
  WORKTREE_POOL_FILENAME,
  POOL_STATE,
  getPoolPath,
  readPool,
  mutateWorktreePool,
  getProjectPoolSize,
  getPoolIdleTtlMs,
  isPoolMember,
  getEntry,
  tryBorrow,
  returnToPool,
  evictEntry,
  pruneStale,
  countForProject,
  getActiveBorrowedPaths,
  getIdlePaths,
  // Exposed for test injection only — not part of public contract.
  _normalizePath,
};
