'use strict';

// Branch + worktree inventory: classify every local branch and worktree against
// the set of refs that any CTM session ever touched (live sessions, startup_tasks,
// agent_sessions). A ref is "orphaned" when NO session — live or in the DB — ever
// referenced it. Cleanup safety is conservative: only merged orphans are auto-safe.
//
// `classifyInventory` is PURE (no git, no DB) so it is fully unit-testable; the
// caller supplies git facts (branches/worktrees/mergedSet) and the linked-ref set.
// `gatherGitFacts` is the thin async git layer.

function normalizeBranch(value) {
  return String(value || '').trim().replace(/^refs\/heads\//, '');
}

function isMainBranch(ref) {
  const b = normalizeBranch(ref);
  return b === 'main' || b === 'master';
}

// CTM-internal refs that should never be swept by the bulk "Clean orphaned (safe)"
// action even when merged + session-less: auto-snapshot/sync branches CTM creates
// (`ctm/pre-sync/*`) and underscore-prefixed bookkeeping refs (`_refs/...`,
// `_remote_main`). They still classify as orphaned (visible) — just not auto-safe.
function isInternalRef(ref) {
  const b = normalizeBranch(ref);
  if (!b) return false;
  return b.startsWith('_') || b.startsWith('ctm/pre-sync/');
}

// Normalize a path for set membership compares. Callers should pass realpath-
// resolved paths when possible; this only strips trailing separators.
function normalizePathKey(p) {
  const s = String(p || '').trim();
  if (!s) return '';
  return s.replace(/[\\/]+$/, '');
}

function ageDaysFrom(iso, now) {
  if (!iso) return null;
  const t = new Date(iso).getTime();
  if (!Number.isFinite(t)) return null;
  return Math.max(0, Math.floor((now - t) / 86400000));
}

/**
 * @param {object} input
 * @param {Array<{ref,head,committerDate}>} input.branches  all local branches
 * @param {Array<object>} input.worktrees                   rich worktrees (live)
 * @param {Set<string>} input.mergedSet                     branch names merged into main
 * @param {object} input.linked  { branches:[{ref,source,sessionId,label,status}],
 *                                  worktreePaths:[{path,source,sessionId,label,status}] }
 * @param {Set<string>} input.liveBranches        branch names used by live sessions now
 * @param {Set<string>} input.liveWorktreePaths   worktree paths used by live sessions now
 * @param {number} input.now                       ms epoch (caller-supplied; deterministic)
 */
function classifyInventory(input = {}) {
  const {
    branches = [],
    worktrees = [],
    mergedSet = new Set(),
    linked = {},
    liveBranches = new Set(),
    liveWorktreePaths = new Set(),
    now = 0,
  } = input;

  // Index the linked-ref set.
  const linkedBranch = new Map(); // ref -> {source, sessionId, label, status}
  for (const b of (linked.branches || [])) {
    const ref = normalizeBranch(b.ref);
    if (ref && !linkedBranch.has(ref)) linkedBranch.set(ref, b);
  }
  const linkedWtByPath = new Map();
  for (const w of (linked.worktreePaths || [])) {
    const key = normalizePathKey(w.path);
    if (key && !linkedWtByPath.has(key)) linkedWtByPath.set(key, w);
  }
  const liveBranchSet = new Set([...liveBranches].map(normalizeBranch).filter(Boolean));
  const liveWtSet = new Set([...liveWorktreePaths].map(normalizePathKey).filter(Boolean));

  // Live worktree truth: branch -> worktree.
  const wtByBranch = new Map();
  for (const wt of worktrees) {
    const ref = normalizeBranch(wt && wt.branch);
    if (ref) wtByBranch.set(ref, wt);
  }

  function linkInfoForBranch(ref, wtPathKey) {
    const hasLive = liveBranchSet.has(ref) || (!!wtPathKey && liveWtSet.has(wtPathKey));
    const rec = linkedBranch.get(ref)
      || (wtPathKey ? linkedWtByPath.get(wtPathKey) : null)
      || null;
    const isLinked = hasLive || !!rec;
    let source = '';
    if (hasLive) source = 'live_session';
    else if (rec) source = rec.source || 'session';
    return {
      linked: isLinked,
      hasLiveSession: hasLive,
      linkedSource: source,
      sessionId: rec ? (rec.sessionId || '') : '',
      sessionLabel: rec ? (rec.label || '') : '',
    };
  }

  // ---- Classify worktrees (decorate, don't replace existing rich fields) ----
  const classifiedWorktrees = worktrees.map(wt => {
    const ref = normalizeBranch(wt && wt.branch);
    const wtPathKey = normalizePathKey(wt && wt.path);
    const info = linkInfoForBranch(ref, wtPathKey);
    const isMain = !!(wt && (wt.isMain === true || isMainBranch(ref)));
    const merged = ref ? mergedSet.has(ref) : false;
    const isGhost = !!(wt && wt.isGhost);
    const isOrphan = !isMain && !info.linked && !isGhost;
    const dirty = Number((wt && wt.dirtyFiles) || 0);
    const internal = isInternalRef(ref);
    return {
      ...wt,
      orphan: isOrphan,
      orphanMerged: merged,
      internal,
      linked: info.linked,
      hasLiveSession: info.hasLiveSession,
      linkedSource: info.linkedSource,
      orphanSessionId: info.sessionId,
      orphanSessionLabel: info.sessionLabel,
      safeToClean: isOrphan && merged && dirty === 0 && !internal,
    };
  });

  // ---- Classify branches (all local branches, incl. those with worktrees) ----
  const classifiedBranches = branches.map(b => {
    const ref = normalizeBranch(b.ref);
    const wt = wtByBranch.get(ref) || null;
    const wtPathKey = wt ? normalizePathKey(wt.path) : '';
    const info = linkInfoForBranch(ref, wtPathKey);
    const isMain = isMainBranch(ref);
    const merged = mergedSet.has(ref);
    const hasWorktree = !!wt;
    const isOrphan = !isMain && !info.linked;
    const internal = isInternalRef(ref);
    return {
      ref,
      head: b.head || '',
      ageDays: ageDaysFrom(b.committerDate, now),
      committerDate: b.committerDate || '',
      hasWorktree,
      worktreePath: wt ? wt.path : '',
      merged,
      isMain,
      internal,
      linked: info.linked,
      hasLiveSession: info.hasLiveSession,
      linkedSource: info.linkedSource,
      sessionId: info.sessionId,
      sessionLabel: info.sessionLabel,
      isOrphan,
      // Bare-branch deletion is auto-safe only when merged + orphaned + no
      // worktree, and not a CTM-internal snapshot/bookkeeping ref.
      safeToClean: isOrphan && merged && !hasWorktree && !internal,
    };
  });

  const orphanWorktreeRefs = new Set(
    classifiedWorktrees.filter(w => w.orphan && w.branch).map(w => normalizeBranch(w.branch))
  );
  const counts = {
    totalBranches: classifiedBranches.length,
    bareBranches: classifiedBranches.filter(b => !b.hasWorktree && !b.isMain).length,
    orphanBranches: classifiedBranches.filter(b => b.isOrphan).length,
    safeBranches: classifiedBranches.filter(b => b.safeToClean).length,
    orphanWorktrees: classifiedWorktrees.filter(w => w.orphan).length,
    safeWorktrees: classifiedWorktrees.filter(w => w.safeToClean).length,
  };
  // orphanTotal counts distinct refs (a branch + its worktree are one thing).
  counts.orphanTotal = counts.orphanWorktrees
    + classifiedBranches.filter(b => b.isOrphan && !orphanWorktreeRefs.has(b.ref)).length;
  counts.safeTotal = counts.safeBranches + counts.safeWorktrees;

  return { branches: classifiedBranches, worktrees: classifiedWorktrees, counts };
}

/**
 * Gather raw git facts needed for classification. `git` is git-utils' `git(cwd,args)`.
 * Returns { branches:[{ref,head,committerDate}], mergedSet:Set }.
 */
async function gatherGitFacts(cwd, git, mainBranch = 'main') {
  const SEP = String.fromCharCode(31); // unit separator (iso8601 dates contain spaces)
  const fmt = `%(refname:short)${SEP}%(objectname:short)${SEP}%(committerdate:iso8601)`;
  const [refsOut, mergedOut] = await Promise.all([
    git(cwd, ['for-each-ref', `--format=${fmt}`, 'refs/heads']).catch(() => ''),
    git(cwd, ['branch', '--merged', mainBranch, '--format=%(refname:short)']).catch(() => ''),
  ]);
  const branches = String(refsOut || '').split('\n').filter(Boolean).map(line => {
    const [ref, head, committerDate] = line.split(SEP);
    return { ref: normalizeBranch(ref), head: head || '', committerDate: committerDate || '' };
  });
  const mergedSet = new Set(
    String(mergedOut || '').split('\n').map(s => normalizeBranch(s)).filter(Boolean)
  );
  return { branches, mergedSet };
}

module.exports = {
  classifyInventory,
  gatherGitFacts,
  normalizeBranch,
  isMainBranch,
  isInternalRef,
  normalizePathKey,
};
