import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { execFileSync } from 'child_process';
import { nexusEventBus } from './event-bus.js';
import { readBootstrapManifest } from './client-bootstrap.js';
const HOME = os.homedir();
const KNOWN_CLIENTS = [
    {
        clientId: 'codex',
        displayName: 'Codex',
        aliases: ['codex'],
        configPaths: [path.join(HOME, '.codex')],
        recentPaths: [path.join(HOME, '.codex', 'sessions')],
    },
    {
        clientId: 'cursor',
        displayName: 'Cursor',
        aliases: ['cursor'],
        configPaths: [path.join(HOME, '.cursor')],
        recentPaths: [path.join(HOME, '.cursor')],
        adapterNames: ['cursor'],
    },
    {
        clientId: 'claude-code',
        displayName: 'Claude Code',
        aliases: ['claude', 'claude-code'],
        configPaths: [path.join(HOME, '.claude'), path.join(HOME, '.config', 'claude-code')],
        recentPaths: [path.join(HOME, '.claude', 'projects')],
        adapterNames: ['claude-code'],
    },
    {
        clientId: 'antigravity',
        displayName: 'Antigravity',
        aliases: ['antigravity', 'openclaw'],
        configPaths: [path.join(HOME, '.antigravity'), path.join(HOME, '.openclaw')],
        recentPaths: [path.join(HOME, '.antigravity', 'sessions'), path.join(HOME, '.openclaw', 'sessions')],
        adapterNames: ['openclaw'],
    },
    {
        clientId: 'hermes',
        displayName: 'Hermes',
        aliases: ['hermes'],
        configPaths: [path.join(HOME, '.hermes')],
        recentPaths: [path.join(HOME, '.hermes', 'sessions')],
        adapterNames: ['hermes'],
    },
    {
        clientId: 'nanoclaw',
        displayName: 'NanoClaw',
        aliases: ['nanoclaw', 'nano-claw'],
        configPaths: [path.join(HOME, '.nanoclaw')],
        recentPaths: [path.join(HOME, '.nanoclaw', 'sessions')],
        adapterNames: ['nanoclaw'],
    },
    {
        clientId: 'picoclaw',
        displayName: 'PicoClaw',
        aliases: ['picoclaw', 'pico-claw'],
        configPaths: [path.join(HOME, '.picoclaw')],
        recentPaths: [path.join(HOME, '.picoclaw', 'sessions')],
        adapterNames: ['picoclaw'],
    },
    {
        clientId: 'opencode',
        displayName: 'Opencode',
        aliases: ['opencode'],
        configPaths: [path.join(HOME, '.opencode')],
        recentPaths: [path.join(HOME, '.opencode', 'sessions')],
    },
    {
        clientId: 'windsurf',
        displayName: 'Windsurf',
        aliases: ['windsurf'],
        configPaths: [path.join(HOME, '.windsurf'), path.join(HOME, '.codeium', 'windsurf')],
        recentPaths: [path.join(HOME, '.windsurf'), path.join(HOME, '.codeium', 'windsurf')],
        adapterNames: ['windsurf'],
    },
    {
        clientId: 'mcp',
        displayName: 'MCP',
        aliases: ['mcp'],
        configPaths: [],
        recentPaths: [],
        adapterNames: ['mcp'],
    },
];
export class ClientRegistry {
    heartbeats = new Map();
    cachedPsOutput = '';
    lastPsScan = 0;
    psTtlMs = 5000;
    processScanUnavailable = false;
    recordHeartbeat(clientKey, input = {}) {
        const descriptor = this.resolveDescriptor(clientKey);
        const clientId = descriptor.clientId;
        const previous = this.resolveExplicitState(this.heartbeats.get(clientId));
        const current = this.heartbeats.get(clientId);
        const nextState = {
            displayName: input.displayName ?? descriptor.displayName,
            source: input.source ?? 'adapter-heartbeat',
            metadata: { ...(current?.metadata ?? {}), ...(input.metadata ?? {}) },
            lastHeartbeat: Date.now(),
            lastDisconnect: current?.lastDisconnect,
        };
        this.heartbeats.set(clientId, nextState);
        const record = this.getClient(clientId);
        nexusEventBus.emit('client.heartbeat', {
            clientId,
            displayName: record.displayName,
            source: nextState.source,
            state: record.state,
        });
        if (previous !== record.state) {
            nexusEventBus.emit('client.status', {
                clientId,
                displayName: record.displayName,
                previous,
                next: record.state,
                source: nextState.source,
            });
        }
        return record;
    }
    recordDisconnect(clientKey, input = {}) {
        const descriptor = this.resolveDescriptor(clientKey);
        const clientId = descriptor.clientId;
        const current = this.heartbeats.get(clientId);
        const previous = this.resolveExplicitState(current);
        this.heartbeats.set(clientId, {
            displayName: input.displayName ?? current?.displayName ?? descriptor.displayName,
            source: input.source ?? current?.source ?? 'adapter-heartbeat',
            metadata: { ...(current?.metadata ?? {}), ...(input.metadata ?? {}) },
            lastHeartbeat: current?.lastHeartbeat,
            lastDisconnect: Date.now(),
        });
        const record = this.getClient(clientId);
        if (previous !== record.state) {
            nexusEventBus.emit('client.status', {
                clientId,
                displayName: record.displayName,
                previous,
                next: record.state,
                source: this.heartbeats.get(clientId)?.source ?? 'adapter-heartbeat',
            });
        }
        return record;
    }
    clear(clientKey) {
        const descriptor = this.resolveDescriptor(clientKey);
        this.heartbeats.delete(descriptor.clientId);
    }
    reconnect(clientKey) {
        return this.recordHeartbeat(clientKey, { source: 'manual' });
    }
    syncAdapters(adapters) {
        for (const adapter of adapters) {
            if (adapter.connected) {
                this.recordHeartbeat(adapter.name, {
                    source: 'adapter-heartbeat',
                    metadata: {
                        adapterType: adapter.type,
                        agents: adapter.agents.length,
                    },
                });
            }
            else {
                this.recordDisconnect(adapter.name, {
                    source: 'adapter-heartbeat',
                    metadata: {
                        adapterType: adapter.type,
                        agents: adapter.agents.length,
                    },
                });
            }
        }
    }
    listClients(adapters = []) {
        if (adapters.length) {
            this.syncAdapters(adapters);
        }
        const records = KNOWN_CLIENTS.map((descriptor) => this.buildRecord(descriptor));
        for (const adapter of adapters) {
            const descriptor = this.resolveDescriptor(adapter.name, adapter.type);
            if (!records.find((record) => record.clientId === descriptor.clientId)) {
                records.push(this.buildRecord(descriptor));
            }
        }
        const primary = this.getPrimaryCandidate(records);
        const normalized = records.map((record) => (primary && record.clientId === primary.clientId && record.state !== 'offline'
            ? { ...record, state: 'primaryActive' }
            : record));
        return normalized.sort((a, b) => this.rankState(a.state) - this.rankState(b.state)
            || this.rankSource(a.source) - this.rankSource(b.source)
            || (b.lastSeen ?? 0) - (a.lastSeen ?? 0)
            || a.displayName.localeCompare(b.displayName));
    }
    getClient(clientKey, adapters = []) {
        const requested = this.resolveDescriptor(clientKey).clientId;
        return this.listClients(adapters).find((record) => record.clientId === requested) ?? this.buildRecord(this.resolveDescriptor(clientKey));
    }
    getPrimaryClient(adapters = []) {
        const records = this.listClients(adapters);
        return records.find((record) => record.state === 'primaryActive')
            ?? records.find((record) => record.state !== 'offline');
    }
    listDistributionClients(adapters = []) {
        return this.listClients(adapters).filter((record) => this.isDistributionEligible(record));
    }
    buildSkillDistributionManifest(skills, adapters = []) {
        const clients = this.listDistributionClients(adapters);
        const distributableSkills = skills.filter((skill) => this.isDistributableSkill(skill));
        const distributableClientIds = clients.map((client) => client.clientId);
        return {
            generatedAt: Date.now(),
            serverGoverned: true,
            policy: {
                allowedScopes: ['base', 'global'],
                excludedClientIds: ['mcp'],
                eligibleStates: ['primaryActive', 'active', 'idle', 'installed'],
            },
            clients: clients.map((client) => ({
                ...client,
                eligibleForDistribution: true,
                distributableSkillIds: distributableSkills.map((skill) => skill.skillId),
                distributableSkillNames: distributableSkills.map((skill) => skill.name),
            })),
            skills: distributableSkills.map((skill) => ({
                ...skill,
                distributableClientIds,
            })),
            summary: {
                totalClients: this.listClients(adapters).length,
                eligibleClients: clients.length,
                totalSkills: skills.length,
                distributableSkills: distributableSkills.length,
            },
        };
    }
    buildRecord(descriptor) {
        const heartbeat = this.heartbeats.get(descriptor.clientId);
        const explicitState = this.resolveExplicitState(heartbeat);
        const heuristic = this.detectHeuristic(descriptor);
        if (explicitState === 'active' || explicitState === 'idle') {
            return {
                clientId: descriptor.clientId,
                displayName: heartbeat?.displayName ?? descriptor.displayName,
                state: explicitState,
                source: heartbeat?.source ?? 'adapter-heartbeat',
                inferred: false,
                lastHeartbeat: heartbeat?.lastHeartbeat,
                lastSeen: heartbeat?.lastHeartbeat ?? heartbeat?.lastDisconnect,
                lastExplicitState: heartbeat?.lastDisconnect && (!heartbeat.lastHeartbeat || heartbeat.lastDisconnect > heartbeat.lastHeartbeat)
                    ? 'disconnected'
                    : 'connected',
                confidence: explicitState === 'active' ? 1 : 0.78,
                evidence: heuristic.evidence,
                metadata: { ...(heartbeat?.metadata ?? {}), heuristic: heuristic.evidence },
            };
        }
        return {
            clientId: descriptor.clientId,
            displayName: descriptor.displayName,
            state: heuristic.state,
            source: heuristic.source,
            inferred: heuristic.state !== 'offline',
            lastHeartbeat: heartbeat?.lastHeartbeat,
            lastSeen: heuristic.lastSeen,
            lastExplicitState: heartbeat ? 'disconnected' : undefined,
            confidence: heuristic.confidence,
            evidence: heuristic.evidence,
            metadata: {
                ...(heartbeat?.metadata ?? {}),
                heuristic: heuristic.evidence,
            },
        };
    }
    isDistributionEligible(record) {
        return record.clientId !== 'mcp' && record.state !== 'offline';
    }
    isDistributableSkill(skill) {
        const scope = String(skill.scope ?? '').toLowerCase();
        const rolloutStatus = String(skill.rolloutStatus ?? '').toLowerCase();
        return rolloutStatus === 'promoted' && (scope === 'global' || scope === 'base');
    }
    resolveExplicitState(heartbeat) {
        if (!heartbeat)
            return 'offline';
        const now = Date.now();
        if (heartbeat.lastDisconnect && (!heartbeat.lastHeartbeat || heartbeat.lastDisconnect > heartbeat.lastHeartbeat)) {
            const sinceDisconnect = now - heartbeat.lastDisconnect;
            return sinceDisconnect < 6 * 60 * 60 * 1000 ? 'idle' : 'offline';
        }
        if (!heartbeat.lastHeartbeat)
            return 'offline';
        const age = now - heartbeat.lastHeartbeat;
        if (age < 3 * 60 * 1000)
            return 'active';
        if (age < 6 * 60 * 60 * 1000)
            return 'idle';
        return 'offline';
    }
    detectHeuristic(descriptor) {
        const evidence = [];
        let lastSeen = 0;
        const envSignal = this.detectEnvSignal(descriptor.clientId);
        if (envSignal) {
            evidence.push(envSignal);
        }
        const bootstrapSignal = this.detectBootstrapSignal(descriptor.clientId);
        if (bootstrapSignal.evidence) {
            evidence.push(bootstrapSignal.evidence);
            lastSeen = Math.max(lastSeen, bootstrapSignal.updatedAt ?? 0);
        }
        const processOutput = this.readProcessSnapshot();
        if (descriptor.aliases.some((alias) => processOutput.includes(alias))) {
            evidence.push('process marker detected');
        }
        let configPresent = false;
        for (const configPath of descriptor.configPaths) {
            if (fs.existsSync(configPath)) {
                configPresent = true;
                evidence.push(`config:${path.basename(configPath)}`);
                lastSeen = Math.max(lastSeen, this.safeMtime(configPath));
            }
        }
        let recentSession = false;
        for (const recentPath of descriptor.recentPaths) {
            if (fs.existsSync(recentPath)) {
                const recentMtime = this.walkRecentMtime(recentPath);
                if (recentMtime > 0) {
                    recentSession = true;
                    evidence.push(`recent:${path.basename(recentPath)}`);
                    lastSeen = Math.max(lastSeen, recentMtime);
                }
            }
        }
        if (envSignal) {
            return {
                state: 'active',
                source: 'env',
                confidence: 0.96,
                evidence,
                lastSeen: Date.now(),
            };
        }
        if (recentSession) {
            return {
                state: 'idle',
                source: 'recent-session',
                confidence: 0.63,
                evidence,
                lastSeen: lastSeen || undefined,
            };
        }
        if (bootstrapSignal.state === 'installed') {
            return {
                state: 'installed',
                source: 'heuristic',
                confidence: 0.68,
                evidence,
                lastSeen: lastSeen || undefined,
            };
        }
        if (configPresent) {
            return {
                state: 'installed',
                source: 'heuristic',
                confidence: 0.42,
                evidence,
                lastSeen: lastSeen || undefined,
            };
        }
        if (evidence.includes('process marker detected')) {
            return {
                state: 'idle',
                source: 'process',
                confidence: 0.74,
                evidence,
                lastSeen: Date.now(),
            };
        }
        return { state: 'offline', source: 'none', confidence: 0, evidence: [] };
    }
    detectBootstrapSignal(clientId) {
        const manifest = readBootstrapManifest();
        if (!manifest?.clients?.length) {
            return { state: 'offline' };
        }
        const targetId = this.toBootstrapClientId(clientId);
        const match = manifest.clients.find((client) => client.clientId === targetId);
        if (!match) {
            return { state: 'offline' };
        }
        if (match.state === 'installed' || match.state === 'drifted') {
            return {
                state: 'installed',
                evidence: `bootstrap:${match.state}`,
                updatedAt: match.updatedAt,
            };
        }
        return { state: 'offline' };
    }
    toBootstrapClientId(clientId) {
        if (clientId === 'claude-code')
            return 'claude';
        if (clientId === 'openclaw')
            return 'antigravity';
        return clientId;
    }
    getPrimaryCandidate(records) {
        const preferredClientId = this.detectCurrentClientId();
        const visible = records.filter((record) => record.state !== 'offline');
        if (preferredClientId) {
            const preferred = visible.find((record) => record.clientId === preferredClientId);
            if (preferred)
                return preferred;
        }
        return [...visible].sort((a, b) => this.rankState(a.state) - this.rankState(b.state)
            || this.rankSource(a.source) - this.rankSource(b.source)
            || (b.lastSeen ?? 0) - (a.lastSeen ?? 0)
            || a.displayName.localeCompare(b.displayName))[0];
    }
    detectCurrentClientId() {
        if (process.env.NEXUS_CLIENT_PROCESS_SCAN_DISABLED === '1') {
            return process.env.MCP_CLIENT_NAME ? this.resolveDescriptor(process.env.MCP_CLIENT_NAME).clientId : undefined;
        }
        if (process.env.CODEX_HOME || process.env.CODEX_SESSION)
            return 'codex';
        if (process.env.CURSOR_HOME || process.env.CURSOR_SESSION)
            return 'cursor';
        if (process.env.CLAUDE_CODE || process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_PROJECT_DIR ||
            process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_ENTRYPOINT)
            return 'claude-code';
        if (process.env.OPENCODE_HOME)
            return 'opencode';
        if (process.env.OPENCLAW_HOME || process.env.ANTIGRAVITY_HOME)
            return 'antigravity';
        if (process.env.HERMES_HOME || process.env.HERMES_SESSION)
            return 'hermes';
        if (process.env.NANOCLAW_HOME || process.env.NANOCLAW_SESSION)
            return 'nanoclaw';
        if (process.env.PICOCLAW_HOME || process.env.PICOCLAW_SESSION)
            return 'picoclaw';
        if (process.env.WINDSURF_HOME || process.env.WINDSURF_SESSION)
            return 'windsurf';
        if (process.env.MCP_CLIENT_NAME) {
            return this.resolveDescriptor(process.env.MCP_CLIENT_NAME).clientId;
        }
        if (this.processScanUnavailable) {
            return undefined;
        }
        try {
            const ps = execFileSync('ps', ['-p', String(process.ppid), '-o', 'comm='], {
                encoding: 'utf8',
                stdio: ['ignore', 'pipe', 'ignore'],
                timeout: 1500,
            }).trim().toLowerCase();
            if (ps.includes('codex'))
                return 'codex';
            if (ps.includes('cursor'))
                return 'cursor';
            if (ps.includes('claude'))
                return 'claude-code';
            if (ps.includes('opencode'))
                return 'opencode';
            if (ps.includes('windsurf'))
                return 'windsurf';
            if (ps.includes('antigravity') || ps.includes('openclaw'))
                return 'antigravity';
            if (ps.includes('hermes'))
                return 'hermes';
            if (ps.includes('nanoclaw') || ps.includes('nano-claw'))
                return 'nanoclaw';
            if (ps.includes('picoclaw') || ps.includes('pico-claw'))
                return 'picoclaw';
        }
        catch (error) {
            this.disableProcessScanIfUnavailable(error);
            // ignore
        }
        return undefined;
    }
    detectEnvSignal(clientId) {
        if (clientId === 'codex' && (process.env.CODEX_HOME || process.env.CODEX_SESSION)) {
            return 'env:CODEX detected';
        }
        if (clientId === 'cursor' && (process.env.CURSOR_HOME || process.env.CURSOR_SESSION)) {
            return 'env:CURSOR detected';
        }
        if (clientId === 'claude-code' && (process.env.CLAUDE_CODE || process.env.CLAUDE_SESSION_ID || process.env.CLAUDE_PROJECT_DIR)) {
            return 'env:CLAUDE detected';
        }
        if (clientId === 'opencode' && process.env.OPENCODE_HOME) {
            return 'env:OPENCODE detected';
        }
        if (clientId === 'antigravity' && (process.env.OPENCLAW_HOME || process.env.ANTIGRAVITY_HOME)) {
            return 'env:ANTIGRAVITY detected';
        }
        if (clientId === 'hermes' && (process.env.HERMES_HOME || process.env.HERMES_SESSION)) {
            return 'env:HERMES detected';
        }
        if (clientId === 'nanoclaw' && (process.env.NANOCLAW_HOME || process.env.NANOCLAW_SESSION)) {
            return 'env:NANOCLAW detected';
        }
        if (clientId === 'picoclaw' && (process.env.PICOCLAW_HOME || process.env.PICOCLAW_SESSION)) {
            return 'env:PICOCLAW detected';
        }
        if (clientId === 'windsurf' && (process.env.WINDSURF_HOME || process.env.WINDSURF_SESSION)) {
            return 'env:WINDSURF detected';
        }
        return undefined;
    }
    readProcessSnapshot() {
        if (process.env.NEXUS_CLIENT_PROCESS_SCAN_DISABLED === '1') {
            return '';
        }
        if (this.processScanUnavailable) {
            return '';
        }
        const now = Date.now();
        if (now - this.lastPsScan < this.psTtlMs) {
            return this.cachedPsOutput;
        }
        this.lastPsScan = now;
        try {
            this.cachedPsOutput = execFileSync('ps', ['-ax', '-o', 'command='], {
                encoding: 'utf8',
                stdio: ['ignore', 'pipe', 'ignore'],
                timeout: 400,
            }).toLowerCase();
        }
        catch (error) {
            this.disableProcessScanIfUnavailable(error);
            this.cachedPsOutput = '';
        }
        return this.cachedPsOutput;
    }
    disableProcessScanIfUnavailable(error) {
        const code = typeof error === 'object' && error && 'code' in error ? String(error.code) : '';
        const message = error instanceof Error ? error.message : String(error);
        if (code === 'EPERM' || code === 'EACCES' || code === 'ENOENT' || /operation not permitted|permission denied|not found/i.test(message)) {
            this.processScanUnavailable = true;
        }
    }
    resolveDescriptor(clientKey, adapterType) {
        const normalized = clientKey.toLowerCase();
        const known = KNOWN_CLIENTS.find((descriptor) => descriptor.clientId === normalized ||
            descriptor.aliases.includes(normalized) ||
            descriptor.adapterNames?.includes(normalized));
        if (known)
            return known;
        return {
            clientId: normalized,
            displayName: adapterType ? normalizeDisplayName(adapterType) : normalizeDisplayName(clientKey),
            aliases: [normalized],
            configPaths: [],
            recentPaths: [],
            adapterNames: [normalized],
        };
    }
    safeMtime(targetPath) {
        try {
            return fs.statSync(targetPath).mtimeMs;
        }
        catch {
            return 0;
        }
    }
    walkRecentMtime(targetPath) {
        try {
            const stat = fs.statSync(targetPath);
            if (!stat.isDirectory())
                return stat.mtimeMs;
            const entries = fs.readdirSync(targetPath).slice(0, 20);
            let latest = stat.mtimeMs;
            for (const entry of entries) {
                latest = Math.max(latest, this.safeMtime(path.join(targetPath, entry)));
            }
            return latest;
        }
        catch {
            return 0;
        }
    }
    rankState(state) {
        return {
            primaryActive: 0,
            active: 1,
            idle: 2,
            installed: 3,
            offline: 4,
        }[state];
    }
    rankSource(source) {
        return {
            manual: 0,
            'adapter-heartbeat': 1,
            env: 2,
            process: 3,
            'recent-session': 4,
            heuristic: 5,
            none: 6,
        }[source];
    }
}
function normalizeDisplayName(value) {
    if (value === 'openclaw')
        return 'Antigravity';
    if (value === 'hermes')
        return 'Hermes';
    if (value === 'nanoclaw' || value === 'nano-claw')
        return 'NanoClaw';
    if (value === 'picoclaw' || value === 'pico-claw')
        return 'PicoClaw';
    if (value === 'claude-code')
        return 'Claude Code';
    if (value === 'cursor')
        return 'Cursor';
    if (value === 'windsurf')
        return 'Windsurf';
    if (value === 'opencode')
        return 'Opencode';
    if (value === 'mcp')
        return 'MCP';
    return value
        .split(/[-_\s]+/)
        .filter(Boolean)
        .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
        .join(' ');
}
