import { execFile } from 'child_process';
import { promisify } from 'util';
import { readdir, readFile, stat } from 'fs/promises';
import { basename, join } from 'path';
import { appendDashboardWarning } from '../../lib/dashboard-alerts.js';
import { childLogger } from '../../lib/logger.js';
import type {
  HermesBackupHistory,
  HermesBackupHistoryEntry,
  HermesCronEntry,
  HermesCronList,
  HermesInstanceId,
  HermesSessionEntry,
  HermesSessionEvent,
  HermesSessionEventList,
  HermesSessionList,
  HermesMemoryList,
  HermesSessionStats,
  HermesSkillList,
  HermesTelemetrySnapshot,
  HermesTokenUsageSnapshot,
  HermesWatchdogAlert,
  HermesWatchdogFeed,
  HermesWatchdogSeverity,
} from './types.js';

const execFileAsync = promisify(execFile);
const log = childLogger('hermes-telemetry/repository');

// Per-instance config. Mirrors `instances` in `hermes-ops/repository.ts`.
// Anything we shell out to as the live instance owner ("uma" for Bheem, root
// for Vijay) is wrapped in `runuser -u <user>` so the command runs in the
// owner's environment, not the backend's.
interface InstanceConfig {
  id: HermesInstanceId;
  user: string | null; // null → run as the backend's own user (root in prod)
  repoPath: string;
  watchdogLog: string;
  sessionsIndex: string;
  sessionsDir: string;
  stateDb: string;
}

const INSTANCES: Record<HermesInstanceId, InstanceConfig> = {
  vijay: {
    id: 'vijay',
    user: null,
    repoPath: '/root/repos/bytelyst_hostinger_hermes_vm',
    watchdogLog: '/root/.hermes/logs/hermes-health-watchdog.log',
    sessionsIndex: '/root/.hermes/sessions/sessions.json',
    sessionsDir: '/root/.hermes/sessions',
    stateDb: '/root/.hermes/state.db',
  },
  bheem: {
    id: 'bheem',
    user: 'uma',
    repoPath: '/home/uma/repos/uma_hostinger_hermes_vm',
    watchdogLog: '/home/uma/.hermes/logs/hermes-health-watchdog.log',
    sessionsIndex: '/home/uma/.hermes/sessions/sessions.json',
    sessionsDir: '/home/uma/.hermes/sessions',
    stateDb: '/home/uma/.hermes/state.db',
  },
};

interface ExecResult {
  stdout: string;
  ran: boolean;
}

async function exec(
  command: string,
  args: string[],
  cwd?: string,
  timeoutMs = 5000,
): Promise<ExecResult> {
  try {
    const { stdout } = await execFileAsync(command, args, { cwd, timeout: timeoutMs });
    return { stdout: stdout.trim(), ran: true };
  } catch (err) {
    const e = err as NodeJS.ErrnoException & { code?: string | number; stdout?: string };
    if (e?.code === 'ENOENT' || e?.code === 'ETIMEDOUT') {
      return { stdout: '', ran: false };
    }
    // A non-zero exit is still useful — `git log` on an empty repo, etc.
    return { stdout: (e?.stdout ?? '').toString().trim(), ran: true };
  }
}

// Wrap a command in `runuser -u <user> --` when needed so it runs in the
// instance owner's environment (PATH, ~/.hermes config). For the local
// instance (user=null) we just call the binary directly.
async function execAs(
  inst: InstanceConfig,
  command: string,
  args: string[],
  timeoutMs = 5000,
): Promise<ExecResult> {
  if (!inst.user) return exec(command, args, undefined, timeoutMs);
  return exec('runuser', ['-u', inst.user, '--', command, ...args], undefined, timeoutMs);
}

// --- Sessions ---------------------------------------------------------------
//
// The Hermes CLI exposes `hermes sessions stats --json` (when present). When
// it's not available we report status:'unknown' rather than fabricating zeros.
async function readSessionStats(inst: InstanceConfig): Promise<HermesSessionStats> {
  const result = await execAs(inst, 'hermes', ['sessions', 'stats', '--json']);
  if (!result.ran) return { totalSessions: 0, totalMessages: 0, status: 'unknown' };

  try {
    const parsed = JSON.parse(result.stdout) as { sessions?: number; messages?: number };
    return {
      totalSessions: Number(parsed.sessions ?? 0),
      totalMessages: Number(parsed.messages ?? 0),
      status: 'up',
    };
  } catch {
    return { totalSessions: 0, totalMessages: 0, status: 'unknown' };
  }
}

async function readSessionList(inst: InstanceConfig): Promise<HermesSessionList> {
  try {
    const parsed = JSON.parse(await readFile(inst.sessionsIndex, 'utf8')) as Record<string, Record<string, unknown>>;
    const entries: HermesSessionEntry[] = Object.values(parsed)
      .map((row) => ({
        id: String(row.session_id ?? row.id ?? row.session_key ?? ''),
        sessionKey: String(row.session_key ?? ''),
        platform: row.platform ? String(row.platform) : null,
        chatType: row.chat_type ? String(row.chat_type) : null,
        displayName: row.display_name ? String(row.display_name) : null,
        createdAt: row.created_at ? String(row.created_at) : null,
        updatedAt: row.updated_at ? String(row.updated_at) : null,
        suspended: Boolean(row.suspended ?? false),
        resumePending: Boolean(row.resume_pending ?? false),
        totalTokens: typeof row.total_tokens === 'number' ? row.total_tokens : null,
        estimatedCostUsd: typeof row.estimated_cost_usd === 'number' ? row.estimated_cost_usd : null,
      }))
      .filter((entry) => entry.id || entry.sessionKey)
      .sort((a, b) => new Date(b.updatedAt ?? b.createdAt ?? 0).getTime() - new Date(a.updatedAt ?? a.createdAt ?? 0).getTime())
      .slice(0, 50);
    return { entries, status: 'up' };
  } catch {
    return { entries: [], status: 'unknown' };
  }
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function extractToolNames(row: Record<string, unknown>): string[] {
  const names = new Set<string>();
  const addName = (value: unknown) => {
    if (typeof value === 'string' && value.trim()) names.add(value.trim());
  };

  const collectFromItem = (item: unknown) => {
    if (!isRecord(item)) return;
    addName(item.name);
    if (isRecord(item.function)) addName(item.function.name);
  };

  if (Array.isArray(row.tool_calls)) row.tool_calls.forEach(collectFromItem);
  if (Array.isArray(row.codex_message_items)) row.codex_message_items.forEach(collectFromItem);
  return Array.from(names).slice(0, 8);
}

function extractItemTypes(row: Record<string, unknown>): string[] {
  const itemTypes = new Set<string>();
  if (Array.isArray(row.codex_message_items)) {
    for (const item of row.codex_message_items) {
      if (isRecord(item) && typeof item.type === 'string') itemTypes.add(item.type);
    }
  }
  return Array.from(itemTypes).slice(0, 8);
}

function classifySessionEvent(row: Record<string, unknown>, toolNames: string[], itemTypes: string[]): HermesSessionEvent['eventType'] {
  const role = typeof row.role === 'string' ? row.role : '';
  if (role === 'session_meta') return 'system';
  if (toolNames.length > 0 || row.finish_reason === 'tool_calls') return 'tool-call';
  if (itemTypes.some((type) => type.includes('tool'))) return 'tool-result';
  if (itemTypes.includes('reasoning') || row.reasoning) return 'reasoning';
  if (role === 'user' || role === 'assistant' || typeof row.content === 'string') return 'message';
  return 'unknown';
}

function summarizeSessionEvent(row: Record<string, unknown>, eventType: HermesSessionEvent['eventType'], toolNames: string[]): string {
  const role = typeof row.role === 'string' ? row.role : 'unknown';
  if (eventType === 'system') return 'session metadata recorded';
  if (eventType === 'tool-call') {
    const toolText = toolNames.length > 0 ? `: ${toolNames.join(', ')}` : '';
    return `${role} tool call${toolNames.length === 1 ? '' : 's'}${toolText}`;
  }
  if (eventType === 'tool-result') return `${role} tool result recorded`;
  if (eventType === 'reasoning') return `${role} reasoning item recorded`;
  if (eventType === 'message') return `${role} message (content redacted)`;
  return `${role} event recorded`;
}

function parseSessionJsonlLine(line: string, sessionFile: string, lineIndex: number): HermesSessionEvent | null {
  if (!line.trim()) return null;
  try {
    const row = JSON.parse(line) as unknown;
    if (!isRecord(row)) return null;
    const toolNames = extractToolNames(row);
    const itemTypes = extractItemTypes(row);
    const eventType = classifySessionEvent(row, toolNames, itemTypes);
    const timestamp = typeof row.timestamp === 'string' ? row.timestamp : null;
    const status = typeof row.status === 'string'
      ? row.status
      : (typeof row.finish_reason === 'string' ? row.finish_reason : null);
    return {
      id: `${sessionFile}:${lineIndex}`,
      sessionFile,
      timestamp,
      role: typeof row.role === 'string' ? row.role : null,
      eventType,
      summary: summarizeSessionEvent(row, eventType, toolNames),
      toolNames,
      itemTypes,
      status,
    };
  } catch {
    return null;
  }
}

async function readSessionEvents(inst: InstanceConfig): Promise<HermesSessionEventList> {
  try {
    const files = (await readdir(inst.sessionsDir))
      .filter((name) => name.endsWith('.jsonl'))
      .sort()
      .slice(-10);
    if (files.length === 0) return { entries: [], status: 'up', sourceCount: 0 };

    const entries: HermesSessionEvent[] = [];
    for (const file of files) {
      const sessionFile = basename(file);
      const content = await readFile(join(inst.sessionsDir, file), 'utf8');
      const lines = content.split('\n');
      const start = Math.max(0, lines.length - 200);
      for (let index = start; index < lines.length; index += 1) {
        const event = parseSessionJsonlLine(lines[index], sessionFile, index + 1);
        if (event) entries.push(event);
      }
    }

    entries.sort((a, b) => new Date(b.timestamp ?? 0).getTime() - new Date(a.timestamp ?? 0).getTime());
    return { entries: entries.slice(0, 100), status: 'up', sourceCount: files.length };
  } catch (err) {
    log.warn({ err, instance: inst.id, source: inst.sessionsDir }, 'failed to read Hermes session events');
    return { entries: [], status: 'unknown', sourceCount: 0 };
  }
}

// --- Cron -------------------------------------------------------------------
//
// `hermes cron list --json` is the canonical source. It's distinct from
// systemd timers (which `hermes-ops` already covers) — this is for Hermes'
// own scheduled tasks (memory rotations, telegram digests, etc).
async function readCron(inst: InstanceConfig): Promise<HermesCronList> {
  const result = await execAs(inst, 'hermes', ['cron', 'list', '--json']);
  if (!result.ran) return { entries: [], status: 'unknown' };

  try {
    const raw = JSON.parse(result.stdout) as Array<Record<string, unknown>>;
    const entries: HermesCronEntry[] = raw.map((row) => ({
      id: String(row.id ?? row.name ?? ''),
      name: String(row.name ?? row.id ?? ''),
      schedule: row.schedule ? String(row.schedule) : null,
      lastRun: row.last_run ? String(row.last_run) : (row.lastRun ? String(row.lastRun) : null),
      nextRun: row.next_run ? String(row.next_run) : (row.nextRun ? String(row.nextRun) : null),
      lastStatus: row.last_status ? String(row.last_status) : (row.lastStatus ? String(row.lastStatus) : null),
      active: Boolean(row.active ?? row.enabled ?? true),
    }));
    return { entries, status: 'up' };
  } catch {
    return { entries: [], status: 'unknown' };
  }
}

// --- Memory + skills --------------------------------------------------------
async function readMemory(inst: InstanceConfig): Promise<HermesMemoryList> {
  const result = await execAs(inst, 'hermes', ['memory', 'list', '--json']);
  if (!result.ran) return { items: [], status: 'unknown' };

  try {
    const raw = JSON.parse(result.stdout) as Array<Record<string, unknown>>;
    return {
      items: raw.map((row) => ({
        id: String(row.id ?? ''),
        type: String(row.type ?? 'note'),
        key: String(row.key ?? row.name ?? ''),
        summary: String(row.summary ?? row.value ?? ''),
        updatedAt: row.updated_at ? String(row.updated_at) : (row.updatedAt ? String(row.updatedAt) : null),
      })),
      status: 'up',
    };
  } catch {
    return { items: [], status: 'unknown' };
  }
}

async function readSkills(inst: InstanceConfig): Promise<HermesSkillList> {
  const result = await execAs(inst, 'hermes', ['skills', 'list', '--json']);
  if (!result.ran) return { items: [], status: 'unknown' };

  try {
    const raw = JSON.parse(result.stdout) as Array<Record<string, unknown>>;
    return {
      items: raw.map((row) => ({
        id: String(row.id ?? row.name ?? ''),
        name: String(row.name ?? row.id ?? ''),
        description: String(row.description ?? ''),
        enabled: Boolean(row.enabled ?? true),
      })),
      status: 'up',
    };
  } catch {
    return { items: [], status: 'unknown' };
  }
}

// --- Watchdog ---------------------------------------------------------------
//
// Tail the last N lines of the watchdog log and bucket them by severity.
// The log format used by `scripts/hermes-health-watchdog.py` today is roughly:
//   2026-01-01T12:34:56 WARNING gateway is degraded: ...
//   2026-01-01T12:35:01 CRITICAL backup repo HEAD missing
// We accept any RFC3339-ish leading timestamp and a severity word.
const WATCHDOG_LINE = /^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.\d]+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(\w+)\s+(.*)$/;

function normalizeSeverity(token: string): HermesWatchdogSeverity {
  const upper = token.toUpperCase();
  if (upper === 'CRITICAL' || upper === 'ERROR' || upper === 'FATAL') return 'critical';
  if (upper === 'WARNING' || upper === 'WARN') return 'warn';
  return 'info';
}

async function readWatchdog(inst: InstanceConfig): Promise<HermesWatchdogFeed> {
  try {
    await stat(inst.watchdogLog);
  } catch {
    return { alerts: [], source: inst.watchdogLog, status: 'unknown' };
  }

  try {
    const content = await readFile(inst.watchdogLog, 'utf8');
    const lines = content.split('\n').filter(Boolean);
    // Cap to the last 50 entries; anything older isn't useful for the panel.
    const tail = lines.slice(-50);
    const alerts: HermesWatchdogAlert[] = [];
    for (const line of tail) {
      const match = WATCHDOG_LINE.exec(line);
      if (!match) continue;
      const [, ts, severityToken, message] = match;
      alerts.push({
        timestamp: ts,
        severity: normalizeSeverity(severityToken),
        message: message.trim(),
      });
    }
    return { alerts, source: inst.watchdogLog, status: 'up' };
  } catch (err) {
    log.warn({ err, instance: inst.id, source: inst.watchdogLog }, 'failed to read watchdog log');
    return { alerts: [], source: inst.watchdogLog, status: 'unknown' };
  }
}

// --- Backup history ---------------------------------------------------------
//
// Cheap proxy for "is the backup pipeline alive": last N commits on the
// backup repo. `git -C <repo> log --pretty=...` runs as the backend user;
// repos are world-readable on the live host.
async function readBackupHistory(inst: InstanceConfig): Promise<HermesBackupHistory> {
  const result = await exec('git', [
    '-C', inst.repoPath,
    'log',
    '--pretty=format:%H\x1f%cI\x1f%s',
    '-n', '20',
  ]);
  if (!result.ran) return { entries: [], repoPath: inst.repoPath, status: 'unknown' };
  if (!result.stdout) return { entries: [], repoPath: inst.repoPath, status: 'up' };

  const entries: HermesBackupHistoryEntry[] = [];
  for (const line of result.stdout.split('\n')) {
    const [sha, committedAt, ...rest] = line.split('\x1f');
    if (!sha || !committedAt) continue;
    entries.push({ sha, committedAt, subject: rest.join('\x1f') });
  }
  return { entries, repoPath: inst.repoPath, status: 'up' };
}

// --- Token usage analytics --------------------------------------------------
// Reads aggregate-only usage from Hermes' SQLite state database. This never
// selects message content; the dashboard gets session totals, model/provider
// buckets, day buckets, and recent-session metadata only.
const TOKEN_USAGE_SCRIPT = String.raw`
import json, sqlite3, sys
from datetime import datetime, timezone

def iso(value):
    if value is None:
        return None
    try:
        ts = float(value)
        return datetime.fromtimestamp(ts, timezone.utc).isoformat().replace('+00:00', 'Z')
    except Exception:
        return str(value)

def day(value):
    text = iso(value)
    return text[:10] if text else 'unknown'

db = sys.argv[1]
con = sqlite3.connect(db)
con.row_factory = sqlite3.Row

def total_expr():
    return "coalesce(input_tokens,0)+coalesce(output_tokens,0)+coalesce(cache_read_tokens,0)+coalesce(cache_write_tokens,0)+coalesce(reasoning_tokens,0)"

summary_row = con.execute(f'''
select count(*) session_count,
       coalesce(sum(message_count),0) message_count,
       coalesce(sum(tool_call_count),0) tool_call_count,
       coalesce(sum(input_tokens),0) input_tokens,
       coalesce(sum(output_tokens),0) output_tokens,
       coalesce(sum(cache_read_tokens),0) cache_read_tokens,
       coalesce(sum(cache_write_tokens),0) cache_write_tokens,
       coalesce(sum(reasoning_tokens),0) reasoning_tokens,
       coalesce(sum({total_expr()}),0) total_tokens,
       coalesce(sum(estimated_cost_usd),0) estimated_cost_usd,
       coalesce(sum(actual_cost_usd),0) actual_cost_usd,
       min(started_at) first_started_at,
       max(coalesce(ended_at, started_at)) last_activity_at
from sessions
''').fetchone()

by_model = [
    {
        'model': row['model'] or '(unknown)',
        'provider': row['provider'] or '(unknown)',
        'sessionCount': row['session_count'],
        'totalTokens': row['total_tokens'],
        'estimatedCostUsd': row['estimated_cost_usd'],
    }
    for row in con.execute(f'''
        select coalesce(model, '(unknown)') model,
               coalesce(billing_provider, '(unknown)') provider,
               count(*) session_count,
               coalesce(sum({total_expr()}),0) total_tokens,
               coalesce(sum(estimated_cost_usd),0) estimated_cost_usd
        from sessions
        group by 1,2
        order by total_tokens desc
        limit 25
    ''')
]

raw_days = {}
for row in con.execute(f'''
    select coalesce(ended_at, started_at) ts,
           count(*) session_count,
           coalesce(sum({total_expr()}),0) total_tokens,
           coalesce(sum(estimated_cost_usd),0) estimated_cost_usd
    from sessions
    group by ts
'''):
    key = day(row['ts'])
    item = raw_days.setdefault(key, {'day': key, 'sessionCount': 0, 'totalTokens': 0, 'estimatedCostUsd': 0})
    item['sessionCount'] += row['session_count']
    item['totalTokens'] += row['total_tokens']
    item['estimatedCostUsd'] += row['estimated_cost_usd']
by_day = sorted(raw_days.values(), key=lambda x: x['day'], reverse=True)[:90]

recent_sessions = [
    {
        'id': row['id'],
        'title': row['title'],
        'source': row['source'],
        'model': row['model'] or '(unknown)',
        'provider': row['provider'] or '(unknown)',
        'startedAt': iso(row['started_at']),
        'endedAt': iso(row['ended_at']),
        'totalTokens': row['total_tokens'],
        'messageCount': row['message_count'],
        'toolCallCount': row['tool_call_count'],
        'estimatedCostUsd': row['estimated_cost_usd'],
    }
    for row in con.execute(f'''
        select id, title, source, model, billing_provider provider, started_at, ended_at,
               coalesce({total_expr()},0) total_tokens,
               coalesce(message_count,0) message_count,
               coalesce(tool_call_count,0) tool_call_count,
               coalesce(estimated_cost_usd,0) estimated_cost_usd
        from sessions
        order by coalesce(ended_at, started_at) desc
        limit 100
    ''')
]

payload = {
    'summary': {
        'sessionCount': summary_row['session_count'],
        'messageCount': summary_row['message_count'],
        'toolCallCount': summary_row['tool_call_count'],
        'inputTokens': summary_row['input_tokens'],
        'outputTokens': summary_row['output_tokens'],
        'cacheReadTokens': summary_row['cache_read_tokens'],
        'cacheWriteTokens': summary_row['cache_write_tokens'],
        'reasoningTokens': summary_row['reasoning_tokens'],
        'totalTokens': summary_row['total_tokens'],
        'estimatedCostUsd': summary_row['estimated_cost_usd'],
        'actualCostUsd': summary_row['actual_cost_usd'],
        'firstStartedAt': iso(summary_row['first_started_at']),
        'lastActivityAt': iso(summary_row['last_activity_at']),
    },
    'byModel': by_model,
    'byDay': by_day,
    'recentSessions': recent_sessions,
}
print(json.dumps(payload))
`;

const emptyTokenUsage = (instanceId: HermesInstanceId, source: string, warning: string): HermesTokenUsageSnapshot => ({
  generatedAt: new Date().toISOString(),
  cached: false,
  instanceId,
  status: 'unknown',
  source,
  summary: {
    sessionCount: 0,
    messageCount: 0,
    toolCallCount: 0,
    inputTokens: 0,
    outputTokens: 0,
    cacheReadTokens: 0,
    cacheWriteTokens: 0,
    reasoningTokens: 0,
    totalTokens: 0,
    estimatedCostUsd: 0,
    actualCostUsd: 0,
    firstStartedAt: null,
    lastActivityAt: null,
  },
  byModel: [],
  byDay: [],
  recentSessions: [],
  warnings: [warning],
});

async function buildTokenUsageSnapshot(instanceId: HermesInstanceId): Promise<HermesTokenUsageSnapshot> {
  const inst = INSTANCES[instanceId];
  const result = await execAs(inst, 'python3', ['-c', TOKEN_USAGE_SCRIPT, inst.stateDb], 8000);
  if (!result.ran || !result.stdout) {
    const warning = `${instanceId}: Hermes state database not readable at ${inst.stateDb}`;
    await appendDashboardWarning({ severity: 'warn', instance: instanceId, message: warning });
    return emptyTokenUsage(instanceId, inst.stateDb, warning);
  }

  try {
    const parsed = JSON.parse(result.stdout) as Omit<HermesTokenUsageSnapshot, 'generatedAt' | 'cached' | 'instanceId' | 'status' | 'source' | 'warnings'>;
    return {
      generatedAt: new Date().toISOString(),
      cached: false,
      instanceId,
      status: 'up',
      source: inst.stateDb,
      summary: parsed.summary,
      byModel: parsed.byModel,
      byDay: parsed.byDay,
      recentSessions: parsed.recentSessions,
      warnings: [],
    };
  } catch (err) {
    const warning = `${instanceId}: Hermes token analytics parse failed`;
    log.warn({ err, instance: instanceId, source: inst.stateDb }, 'failed to parse Hermes token usage');
    await appendDashboardWarning({ severity: 'warn', instance: instanceId, message: warning });
    return emptyTokenUsage(instanceId, inst.stateDb, warning);
  }
}

const CACHE_TTL = 30000;
const cache = new Map<HermesInstanceId, { snapshot: HermesTelemetrySnapshot; at: number }>();
const inflight = new Map<HermesInstanceId, Promise<HermesTelemetrySnapshot>>();
const tokenCache = new Map<HermesInstanceId, { snapshot: HermesTokenUsageSnapshot; at: number }>();
const tokenInflight = new Map<HermesInstanceId, Promise<HermesTokenUsageSnapshot>>();

async function buildSnapshot(instanceId: HermesInstanceId): Promise<HermesTelemetrySnapshot> {
  const inst = INSTANCES[instanceId];
  const [sessions, sessionList, sessionEvents, cron, memory, skills, watchdog, backupHistory] = await Promise.all([
    readSessionStats(inst),
    readSessionList(inst),
    readSessionEvents(inst),
    readCron(inst),
    readMemory(inst),
    readSkills(inst),
    readWatchdog(inst),
    readBackupHistory(inst),
  ]);

  const warnings: string[] = [];
  if (sessions.status === 'unknown') warnings.push(`${instanceId}: hermes sessions stats unavailable (CLI missing or non-zero exit)`);
  if (sessionList.status === 'unknown') warnings.push(`${instanceId}: Hermes session index not readable`);
  if (sessionEvents.status === 'unknown') warnings.push(`${instanceId}: Hermes session event JSONL not readable at ${inst.sessionsDir}`);
  if (cron.status === 'unknown') warnings.push(`${instanceId}: hermes cron list unavailable`);
  if (memory.status === 'unknown') warnings.push(`${instanceId}: hermes memory list unavailable`);
  if (skills.status === 'unknown') warnings.push(`${instanceId}: hermes skills list unavailable`);
  if (watchdog.status === 'unknown') warnings.push(`${instanceId}: watchdog log not readable at ${watchdog.source ?? 'unknown path'}`);
  if (backupHistory.status === 'unknown') warnings.push(`${instanceId}: backup repo not readable at ${backupHistory.repoPath ?? 'unknown path'}`);

  await Promise.all([
    ...warnings.map((message) => appendDashboardWarning({ severity: 'warn', instance: instanceId, message })),
    ...watchdog.alerts
      .filter((alert) => alert.severity === 'critical')
      .map((alert) => appendDashboardWarning({ severity: 'critical', instance: instanceId, message: alert.message })),
  ]);

  return {
    generatedAt: new Date().toISOString(),
    cached: false,
    instanceId,
    sessions,
    sessionList,
    sessionEvents,
    cron,
    memory,
    skills,
    watchdog,
    backupHistory,
    warnings,
  };
}

export async function getHermesTelemetrySnapshot(
  instanceId: HermesInstanceId,
  options?: { force?: boolean },
): Promise<HermesTelemetrySnapshot> {
  const force = options?.force ?? false;

  if (!force) {
    const cached = cache.get(instanceId);
    if (cached && Date.now() - cached.at < CACHE_TTL) {
      return { ...cached.snapshot, cached: true };
    }

    const pending = inflight.get(instanceId);
    if (pending) return pending;
  }

  const promise = buildSnapshot(instanceId)
    .then((snapshot) => {
      cache.set(instanceId, { snapshot, at: Date.now() });
      return snapshot;
    })
    .finally(() => {
      if (inflight.get(instanceId) === promise) inflight.delete(instanceId);
    });

  if (!force) inflight.set(instanceId, promise);
  return promise;
}

export async function getHermesTokenUsageSnapshot(
  instanceId: HermesInstanceId,
  options?: { force?: boolean },
): Promise<HermesTokenUsageSnapshot> {
  const force = options?.force ?? false;

  if (!force) {
    const cached = tokenCache.get(instanceId);
    if (cached && Date.now() - cached.at < CACHE_TTL) {
      return { ...cached.snapshot, cached: true };
    }

    const pending = tokenInflight.get(instanceId);
    if (pending) return pending;
  }

  const promise = buildTokenUsageSnapshot(instanceId)
    .then((snapshot) => {
      tokenCache.set(instanceId, { snapshot, at: Date.now() });
      return snapshot;
    })
    .finally(() => {
      if (tokenInflight.get(instanceId) === promise) tokenInflight.delete(instanceId);
    });

  if (!force) tokenInflight.set(instanceId, promise);
  return promise;
}

// Test hook so `vitest` cases don't bleed cached state across runs.
export function clearHermesTelemetryCache(): void {
  cache.clear();
  inflight.clear();
  tokenCache.clear();
  tokenInflight.clear();
}
