/**
 * Non-secret daemon state exchanged through the already fleet-synced user repo.
 *
 * Each device owns exactly one file under `~/.agents/devices/<device>/`, so
 * normal `agents repo push/pull user` merges the fleet without devices dialing
 * one another on every daemon tick. OAuth/setup-token values never belong here:
 * auth publishes only a readiness verdict; the existing encrypted SSH transport
 * remains the one path that may carry secret material.
 */
import * as fs from 'node:fs';
import * as fsp from 'node:fs/promises';
import * as path from 'node:path';
import { assertValidDeviceName } from './devices/registry.js';
import { atomicWriteFileSync, ensureLockTarget, withFileLock, withFileLockAsync } from './fs-atomic.js';
import { getUserAgentsDir } from './state.js';
export const FLEET_SHARED_STATE_VERSION = 1;
export const FLEET_SHARED_STATE_FILE = 'daemon-state.json';
/** Path owned by one device in the conflict-free tracked device-doc tree. */
export function fleetSharedStatePath(device, userAgentsDir = getUserAgentsDir()) {
    assertValidDeviceName(device);
    return path.join(userAgentsDir, 'devices', device, FLEET_SHARED_STATE_FILE);
}
function isRecord(value) {
    return !!value && typeof value === 'object' && !Array.isArray(value);
}
function parseFleetSharedDeviceState(raw, owner) {
    const parsed = JSON.parse(raw);
    if (!isRecord(parsed) || parsed.version !== FLEET_SHARED_STATE_VERSION || parsed.device !== owner) {
        throw new Error('unrecognized shared-state envelope');
    }
    const usage = parsed.usage;
    if (usage !== undefined && (!isRecord(usage) || !isRecord(usage.rows))) {
        throw new Error('unrecognized usage snapshot');
    }
    const auth = parsed.auth;
    if (auth !== undefined &&
        (!isRecord(auth) || !['ready', 'missing', 'invalid'].includes(String(auth.status)))) {
        throw new Error('unrecognized auth verdict');
    }
    const sessions = parsed.sessions;
    if (sessions !== undefined && (!isRecord(sessions) || !Array.isArray(sessions.rows))) {
        throw new Error('unrecognized session mirror');
    }
    return parsed;
}
/**
 * Merge one daemon-owned field into this device's shared file under a real
 * inter-process lock. Stable serialization avoids dirtying the user repo when
 * neither usage nor auth state changed.
 */
/** Merge `patch` onto the current on-disk state; returns the serialized next state, or null when unchanged. Shared by the sync and async writers. */
function mergeFleetState(currentRaw, device, patch) {
    let current = { version: FLEET_SHARED_STATE_VERSION, device };
    const trimmed = currentRaw.trim();
    if (trimmed) {
        try {
            current = parseFleetSharedDeviceState(trimmed, device);
        }
        catch { /* owning device repairs its own malformed file; peers are never repaired here */ }
    }
    const next = {
        ...current,
        ...(patch.usage !== undefined ? { usage: patch.usage } : {}),
        ...(patch.auth !== undefined ? { auth: patch.auth } : {}),
        ...(patch.sessions !== undefined ? { sessions: patch.sessions } : {}),
        version: FLEET_SHARED_STATE_VERSION,
        device,
    };
    const serialized = `${JSON.stringify(next, null, 2)}\n`;
    return { serialized, changed: currentRaw !== serialized };
}
export function updateFleetSharedDeviceState(device, patch, userAgentsDir = getUserAgentsDir()) {
    const file = fleetSharedStatePath(device, userAgentsDir);
    ensureLockTarget(file, '');
    return withFileLock(file, () => {
        let raw = '';
        try {
            raw = fs.readFileSync(file, 'utf-8');
        }
        catch { /* missing → treat as empty */ }
        const { serialized, changed } = mergeFleetState(raw, device, patch);
        if (!changed)
            return { changed: false, path: file };
        atomicWriteFileSync(file, serialized, 'utf-8');
        return { changed: true, path: file };
    });
}
/**
 * Async, non-blocking twin of {@link updateFleetSharedDeviceState} for the
 * daemon's usage-sync / auth-sync ticks (PHNX-3695). The sync version acquires
 * the file lock with `sleepSync` (`Atomics.wait`), freezing the shared event
 * loop for up to 30s under contention on EVERY tick; this uses
 * `withFileLockAsync`. The under-lock read is async; the atomic write is a tiny
 * bounded write held inside the lock.
 */
export async function updateFleetSharedDeviceStateAsync(device, patch, userAgentsDir = getUserAgentsDir()) {
    const file = fleetSharedStatePath(device, userAgentsDir);
    ensureLockTarget(file, '');
    return withFileLockAsync(file, async () => {
        let raw = '';
        try {
            raw = await fsp.readFile(file, 'utf-8');
        }
        catch { /* missing → treat as empty */ }
        const { serialized, changed } = mergeFleetState(raw, device, patch);
        if (!changed)
            return { changed: false, path: file };
        atomicWriteFileSync(file, serialized, 'utf-8');
        return { changed: true, path: file };
    });
}
/** Read every valid peer-owned state file; malformed peers fail separately. */
export function readFleetSharedDeviceStates(userAgentsDir = getUserAgentsDir()) {
    const devicesDir = path.join(userAgentsDir, 'devices');
    let entries;
    try {
        entries = fs.readdirSync(devicesDir, { withFileTypes: true });
    }
    catch (err) {
        if (err.code === 'ENOENT')
            return { states: [], errors: [] };
        return { states: [], errors: [{ device: '*', message: err.message }] };
    }
    const states = [];
    const errors = [];
    for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
        if (!entry.isDirectory())
            continue;
        const file = path.join(devicesDir, entry.name, FLEET_SHARED_STATE_FILE);
        if (!fs.existsSync(file))
            continue;
        try {
            states.push(parseFleetSharedDeviceState(fs.readFileSync(file, 'utf-8'), entry.name));
        }
        catch (err) {
            errors.push({ device: entry.name, message: err.message });
        }
    }
    return { states, errors };
}
