/**
 * Unified sync-status engine — the SINGLE source of truth for "is this resource
 * synced to this agent version?" consumed by `agents doctor`, `agents view`, the
 * menu-bar app, and any Agency surface.
 *
 * Why this exists: before this module there were four different notions of
 * "synced" living in four files:
 *   - doctor:  isStale() vs .sync-manifest.json  — catches SOURCE drift only.
 *   - view:    git working-tree state of ~/.agents/ — a resource can show green
 *              while its installed copy is stale/deleted/corrupted (false positive).
 *   - lists:   file-exists-in-home — never reports content drift at all.
 *   - menubar: read doctor --json (so it inherited doctor's source-only blind spot).
 *
 * The reliable signal is diffVersionResources() (src/lib/doctor-diff.ts): it reads
 * the ACTUAL version home and compares it to the resolved sources, so it catches
 * every drift class — source-side changes AND home-side rot (deleted / corrupted /
 * hand-edited installed copies) AND orphans. This module wraps it once, maps its
 * per-resource DiffStatus onto one stable enum, folds in `.system` repo freshness,
 * and lets every surface render the same warnings instead of re-deriving them.
 */
import simpleGit from 'simple-git';
import { ALL_AGENT_IDS } from './agents.js';
import { diffVersionResources, } from './doctor-diff.js';
import { listInstalledVersions, getGlobalDefault } from './installations/versions.js';
import { loadManifest } from './staleness/index.js';
import { getSystemAgentsDir, getUserAgentsDir } from './state.js';
import * as fs from 'fs';
import { isGitRepo, readOriginUrl } from './git.js';
import { detectConfigDrift } from './config-drift.js';
/**
 * Re-check that a version's home now matches its resolved sources after a
 * reconcile. This is the post-write verification the `agents sync` success line
 * depends on (PHNX-3186): the exit line must not read "reconciled" while drift
 * it was asked to fix stays put. Resolves against non-project layers only
 * (`excludeProject: true`), mirroring what the sync writer targets. Returns null
 * when the version converged (no drifted/missing), else the residual rows.
 */
export function verifyVersionConverged(agent, version, cwd = process.cwd()) {
    const report = diffVersionResources(agent, version, { cwd, excludeProject: true });
    const rows = rowsFromReport(agent, version, report)
        .filter((r) => r.status === 'drifted' || r.status === 'missing');
    if (rows.length === 0)
        return null;
    return { agent, version, rows };
}
/** One-line-per-resource description of residual drift, for the sync exit line. */
export function formatResidualDrift(residual) {
    const lines = [];
    for (const r of residual) {
        for (const row of r.rows) {
            const detail = row.detail ? ` (${row.detail})` : '';
            lines.push(`${r.agent}@${r.version}: ${row.status} ${row.kind} '${row.name}'${detail}`);
        }
    }
    return lines;
}
const STATUS_MAP = {
    ok: 'synced',
    diff: 'drifted',
    missing: 'missing',
    extra: 'orphan',
};
function rowsFromReport(agent, version, report) {
    const out = [];
    for (const list of Object.values(report.kinds)) {
        for (const r of list) {
            out.push({
                agent,
                version,
                kind: r.kind,
                name: r.name,
                status: STATUS_MAP[r.status],
                ...(r.detail ? { detail: r.detail } : {}),
            });
        }
    }
    return out;
}
/**
 * Read `.system` repo freshness WITHOUT touching the network. `git status`
 * reports ahead/behind against the remote-tracking ref, which the detached
 * auto-pull worker keeps warm via periodic `git fetch`. This is the same number
 * the menu-bar surfaces; we read it once, here, so every surface agrees.
 */
export async function getSystemRepoStatus() {
    const dir = getSystemAgentsDir();
    const base = { dir, behind: 0, ahead: 0, branch: null, unknown: true };
    if (!isGitRepo(dir))
        return base;
    try {
        const status = await simpleGit(dir).status();
        return {
            dir,
            behind: status.behind ?? 0,
            ahead: status.ahead ?? 0,
            branch: status.tracking ?? status.current ?? null,
            // Without a tracking branch there's no upstream to compare against.
            unknown: !status.tracking,
        };
    }
    catch {
        return base;
    }
}
/**
 * Detect whether `~/.agents` (the user config layer) is git-backed. A partial
 * install — runtime state present but no `.git` (or no `origin`) — is a distinct
 * drift state that `agents repo sync user` heals by adopting in place (PHNX-3301),
 * surfaced separately from per-version resource gaps. Purely local; no network.
 */
export async function getUserRepoStatus() {
    const dir = getUserAgentsDir();
    if (!fs.existsSync(dir))
        return { dir, notGitRepo: false };
    if (!isGitRepo(dir))
        return { dir, notGitRepo: true };
    // A repo with no `origin` is just as partial for adopt's purposes — reuse the
    // single origin-URL reader rather than a second remote check.
    return { dir, notGitRepo: readOriginUrl(dir) === null };
}
/**
 * Compute unified sync status across the fleet. Resolves against non-project
 * layers only (`excludeProject: true`) — the GLOBAL version home is never
 * reconciled against per-cwd `<cwd>/.agents/` resources, so counting them as
 * "missing" there would be a false gap (matches doctor's overview semantics).
 */
export async function computeSyncStatus(options = {}) {
    const cwd = options.cwd ?? process.cwd();
    const agentIds = options.agents ?? ALL_AGENT_IDS;
    const agents = [];
    for (const agent of agentIds) {
        const def = getGlobalDefault(agent);
        for (const version of listInstalledVersions(agent)) {
            const report = diffVersionResources(agent, version, {
                cwd,
                excludeProject: true,
                ...(options.kinds ? { kinds: options.kinds } : {}),
            });
            const resources = rowsFromReport(agent, version, report);
            const counts = { synced: 0, drifted: 0, missing: 0, orphan: 0 };
            for (const r of resources)
                counts[r.status]++;
            agents.push({
                agent,
                version,
                isDefault: version === def,
                everSynced: loadManifest(agent, version) !== null,
                counts,
                needsSync: counts.drifted + counts.missing > 0,
                resources,
            });
        }
    }
    const system = await getSystemRepoStatus();
    const user = await getUserRepoStatus();
    const config = detectConfigDrift();
    const agentsNeedingSync = new Set();
    let drifted = 0, missing = 0, orphan = 0, versionsNeedingSync = 0, versionsNeverSynced = 0;
    for (const v of agents) {
        drifted += v.counts.drifted;
        missing += v.counts.missing;
        orphan += v.counts.orphan;
        if (!v.everSynced)
            versionsNeverSynced++;
        if (v.needsSync) {
            versionsNeedingSync++;
            agentsNeedingSync.add(v.agent);
        }
    }
    return {
        system,
        user,
        config,
        agents,
        totals: {
            drifted,
            missing,
            orphan,
            versionsNeedingSync,
            versionsNeverSynced,
            agentsNeedingSync: agentsNeedingSync.size,
        },
    };
}
