/**
 * Cross-device harness divergence for `agents doctor` (RUSH-2027).
 *
 * `agents fleet status` already fans out `agents doctor --json` per device and
 * builds a {@link FleetHealthReport} of coarse device health (reachable, CLIs
 * installed, agents-cli version skew, local sync drift). It deliberately does
 * NOT compare fine-grained *resource presence* across devices — so a plugin like
 * `swarm` installed on one box but missing on another is silent until a user
 * types `/swarm:run` on the wrong machine and gets `Unknown command`.
 *
 * This module is the missing comparison. Each device self-reports its installed
 * inventory (resource kinds, per-agent version sets, `.agents`/`.system` repo
 * state) in its `doctor --json` payload; {@link compareFleetInventories} takes
 * the collected per-device inventories and, treating the local machine as the
 * baseline, flags:
 *
 *   1. Resource presence gaps  — a resource present on one device, missing on
 *      another (either direction relative to the local baseline).
 *   2. Agent version gaps      — an agent version installed on one device but
 *      not another (e.g. `yosemite-s0 missing claude@2.1.220`).
 *   3. `.agents`/`.system` repo drift — a device whose config-repo HEAD, branch,
 *      or dirty state diverges from the local baseline.
 *
 * Pure and SSH-free: the SSH fan-out lives in the doctor command; this module
 * only consumes the already-collected payloads, so the divergence logic is
 * unit-tested against fixture inventories with no live fleet.
 */
export const FLEET_RESOURCE_KINDS = [
    'commands',
    'skills',
    'hooks',
    'rules',
    'mcp',
    'permissions',
    'subagents',
    'plugins',
    'promptcuts',
    'workflows',
];
/**
 * A deliberately closed summary of one version's generated hook-wrapper
 * runtime. Fleet payloads carry only this state — never the remote wrapper
 * path, embedded source path, or detector text.
 */
export const FLEET_HOOK_RUNTIME_STATES = ['healthy', 'broken', 'not-applicable'];
function sortedUnique(list) {
    return Array.from(new Set(list)).sort();
}
function repoLabel(repo) {
    return repo === 'agents' ? '.agents' : '.system';
}
/** Describe how a remote repo state diverges from the local baseline, or null
 *  when they match. Compares HEAD first (the load-bearing difference), then
 *  branch, then a dirty tree on either side.
 *
 *  HEAD and branch differences are symmetric — by convention the remote is the
 *  one "diverged from the baseline" — but a dirty tree belongs to exactly one
 *  box, and blaming the wrong one sends the user to a clean machine. */
function describeRepoDrift(local, remote) {
    if (local.head && remote.head && local.head !== remote.head) {
        return { detail: `repo diverged: HEAD ${remote.head} != local ${local.head}`, blame: 'remote' };
    }
    if (local.branch !== remote.branch) {
        return {
            detail: `repo diverged: branch ${remote.branch ?? 'detached'} != local ${local.branch ?? 'detached'}`,
            blame: 'remote',
        };
    }
    if (remote.dirty !== local.dirty) {
        return remote.dirty
            ? { detail: 'tree has uncommitted changes', blame: 'remote' }
            : { detail: 'tree has uncommitted changes', blame: 'local' };
    }
    return null;
}
/**
 * Compare a set of per-device inventories against the local baseline and emit
 * every cross-device divergence. The baseline is the device whose name equals
 * {@link baselineName} (the local machine); if that device has no inventory the
 * report is empty (nothing to compare against). Devices with no inventory are
 * recorded under `skippedDevices` and never produce false "missing" findings.
 *
 * Ordering is deterministic (device, then category, then name) so the human and
 * JSON output — and the tests — are stable.
 */
export function compareFleetInventories(devices, baselineName) {
    const baseline = devices.find((d) => d.name === baselineName)?.inventory ?? null;
    const remotes = devices.filter((d) => d.name !== baselineName);
    const comparedDevices = [];
    const skippedDevices = [];
    const divergences = [];
    /** Repos already reported as the BASELINE's problem — one dirty local tree is
     *  one finding, not one per remote compared against. */
    const localBlamed = new Set();
    if (!baseline) {
        // No local baseline to compare against — record every remote as skipped so
        // the caller can say so, but emit no divergences (we can't know the truth).
        for (const d of remotes)
            skippedDevices.push(d.name);
        return {
            baseline: baselineName,
            divergences: [],
            comparedDevices: [],
            skippedDevices: skippedDevices.sort(),
            hasDivergence: false,
        };
    }
    for (const remote of remotes) {
        if (!remote.inventory) {
            skippedDevices.push(remote.name);
            continue;
        }
        comparedDevices.push(remote.name);
        const inv = remote.inventory;
        // 1) Resource presence, both directions.
        for (const kind of FLEET_RESOURCE_KINDS) {
            const localSet = new Set(baseline.resources[kind] ?? []);
            const remoteSet = new Set(inv.resources[kind] ?? []);
            for (const name of sortedUnique(baseline.resources[kind] ?? [])) {
                if (!remoteSet.has(name)) {
                    divergences.push({
                        kind: 'resource-missing-remote',
                        device: remote.name,
                        category: kind,
                        name,
                        message: `${remote.name} is missing ${kind.replace(/s$/, '')} '${name}' (present on ${baselineName})`,
                    });
                }
            }
            for (const name of sortedUnique(inv.resources[kind] ?? [])) {
                if (!localSet.has(name)) {
                    divergences.push({
                        kind: 'resource-missing-local',
                        device: remote.name,
                        category: kind,
                        name,
                        message: `${baselineName} is missing ${kind.replace(/s$/, '')} '${name}' (present on ${remote.name})`,
                    });
                }
            }
        }
        // 2) Agent version parity, both directions, per agent id.
        const agentIds = sortedUnique([
            ...Object.keys(baseline.agentVersions),
            ...Object.keys(inv.agentVersions),
        ]);
        for (const agent of agentIds) {
            const localVers = new Set(baseline.agentVersions[agent] ?? []);
            const remoteVers = new Set(inv.agentVersions[agent] ?? []);
            for (const v of sortedUnique(baseline.agentVersions[agent] ?? [])) {
                if (!remoteVers.has(v)) {
                    divergences.push({
                        kind: 'agent-version-missing-remote',
                        device: remote.name,
                        category: agent,
                        name: v,
                        message: `${remote.name} is missing ${agent}@${v} (installed on ${baselineName})`,
                    });
                }
            }
            for (const v of sortedUnique(inv.agentVersions[agent] ?? [])) {
                if (!localVers.has(v)) {
                    divergences.push({
                        kind: 'agent-version-missing-local',
                        device: remote.name,
                        category: agent,
                        name: v,
                        message: `${baselineName} is missing ${agent}@${v} (installed on ${remote.name})`,
                    });
                }
            }
        }
        // 3) `.agents` / `.system` repo drift vs the local baseline.
        for (const repo of ['agents', 'system']) {
            const localRepo = baseline.repos[repo];
            const remoteRepo = inv.repos[repo];
            if (!localRepo || !remoteRepo)
                continue; // one side isn't a readable repo
            const drift = describeRepoDrift(localRepo, remoteRepo);
            if (!drift)
                continue;
            if (drift.blame === 'local') {
                // The BASELINE owns this one. File it against the baseline, and only
                // once: the local tree being dirty is a single fact about this machine,
                // not one problem per remote we happened to compare against.
                if (localBlamed.has(repo))
                    continue;
                localBlamed.add(repo);
                divergences.push({
                    kind: 'repo-drift',
                    device: baselineName,
                    category: repo,
                    name: repoLabel(repo),
                    message: `${baselineName} ${repoLabel(repo)} ${drift.detail}`,
                });
                continue;
            }
            divergences.push({
                kind: 'repo-drift',
                device: remote.name,
                category: repo,
                name: repoLabel(repo),
                message: `${remote.name} ${repoLabel(repo)} ${drift.detail}`,
            });
        }
    }
    divergences.sort((a, b) => a.device.localeCompare(b.device) ||
        a.kind.localeCompare(b.kind) ||
        a.category.localeCompare(b.category) ||
        a.name.localeCompare(b.name));
    return {
        baseline: baselineName,
        divergences,
        comparedDevices: comparedDevices.sort(),
        skippedDevices: skippedDevices.sort(),
        hasDivergence: divergences.length > 0,
    };
}
