import { listActive as listActiveSessions, type SessionEntry } from './session-registry.js';
import type { AgentType } from './types.js';
import { sortSessions, type PromptOption } from '@agentdeck/shared';

export interface EnrichedSession {
  id: string;
  port: number;
  pid?: number;
  projectName: string;
  agentType?: AgentType;
  alive: boolean;
  state?: string;
  modelName?: string;
  effortLevel?: string;
  startedAt?: string;
  controlMode?: 'managed' | 'observed';
  cwd?: string;
  currentTask?: string;
  /** One-line gist of the session's purpose (first user prompt). */
  goal?: string;
  currentTool?: string;
  contextPercent?: number;
  totalTokens?: number;
  question?: string;
  requestId?: string;
  promptType?: 'yes_no' | 'yes_no_always' | 'multi_select' | 'diff_review';
  options?: PromptOption[];
  /** Seconds since startedAt — derived at broadcast time so NTP-less devices (ESP32) render elapsed per session. */
  elapsedSec?: number;
  /** Daemon-synthesized "what is this agent doing right now" one-liner (heuristic,
   *  upgraded to a Foundation Models summary when available). Shared by X3 + TRMNL. */
  activity?: string;
}

/** Cache last-known sibling state to avoid propagating undefined on transient fetch failures.
 * `updatedAt` bounds how long a vanished session may ride its last-known state: once the
 * cache ages past LIVENESS_GRACE_MS without a successful probe/push, we stop treating it as
 * alive so devices (TRMNL e-ink, ESP32, Pixoo) prune ended sessions instead of showing them
 * forever (the registry only prunes on PID death, which lingers when the parent shell lives). */
const siblingStateCache = new Map<string, { state: string; modelName?: string; effortLevel?: string; updatedAt: number }>();
const LIVENESS_GRACE_MS = 20_000;

/** Push-channel state cache — populated by DaemonWsClient session_push_state messages */
const pushStateCache = new Map<string, { state: string; modelName?: string; effortLevel?: string; updatedAt: number }>();

/** Update push-channel cache (called from daemon-server when session_push_state arrives) */
export function updatePushState(sessionId: string, state: string, modelName?: string, effortLevel?: string): void {
  pushStateCache.set(sessionId, { state, modelName, effortLevel, updatedAt: Date.now() });
  // Also update sibling cache so it stays consistent
  siblingStateCache.set(sessionId, { state, modelName, effortLevel, updatedAt: Date.now() });
}

/** Check if push-channel has fresh state (< 30s old) */
export function getPushState(sessionId: string): { state: string; modelName?: string; effortLevel?: string } | undefined {
  const entry = pushStateCache.get(sessionId);
  if (!entry) return undefined;
  if (Date.now() - entry.updatedAt > 30_000) {
    // Drop the stale entry instead of merely skipping it, so the map doesn't
    // accumulate dead sessions over a long-running daemon's lifetime.
    pushStateCache.delete(sessionId);
    return undefined;
  }
  return { state: entry.state, modelName: entry.modelName, effortLevel: entry.effortLevel };
}

/** Clear cache entry when a session is removed (call from session-registry cleanup) */
export function clearSiblingStateCache(sessionId: string): void {
  siblingStateCache.delete(sessionId);
  pushStateCache.delete(sessionId);
}

/**
 * Drop cache entries for sessions that are no longer active. `siblingStateCache`
 * has no TTL (it's the last-known fallback for a still-live session whose
 * /health probe transiently failed), so without this sweep a session that dies
 * without a clean `deregister()` (SIGKILL, daemon crash) would leave its state
 * cached forever. Called from the enricher with the current active id set.
 * Observed sessions never populate these caches, so pruning to the managed set
 * is safe.
 */
function sweepStaleCacheEntries(activeIds: Set<string>): void {
  for (const id of siblingStateCache.keys()) {
    if (!activeIds.has(id)) siblingStateCache.delete(id);
  }
  for (const id of pushStateCache.keys()) {
    if (!activeIds.has(id)) pushStateCache.delete(id);
  }
}

/**
 * Enrich sibling sessions with state from their /health endpoint.
 * For the own session (matched by ownSessionId), uses ownState directly.
 * On fetch failure, falls back to last-known cached state to prevent
 * transient undefined propagation to all clients.
 */
export async function enrichSessionsWithState(
  sessions: SessionEntry[],
  ownSessionId: string,
  ownState: string,
  ownModelName?: string,
  ownEffortLevel?: string,
): Promise<EnrichedSession[]> {
  // Prune cache entries for sessions that have dropped out of the active set
  // (the registry already pruned dead PIDs before handing us this list).
  sweepStaleCacheEntries(new Set(sessions.map((s) => s.id)));
  return Promise.all(sessions.map(async (s) => {
    const base: EnrichedSession = {
      id: s.id,
      port: s.port,
      pid: s.pid,
      projectName: s.projectName,
      agentType: s.agentType as AgentType | undefined,
      alive: true,
      startedAt: s.startedAt,
      controlMode: 'managed',
    };
    if (s.id === ownSessionId) return { ...base, state: ownState, modelName: ownModelName, effortLevel: ownEffortLevel };
    // 1. Use fresh push-channel state if available (< 30s old)
    const pushed = getPushState(s.id);
    if (pushed) return { ...base, state: pushed.state, modelName: pushed.modelName, effortLevel: pushed.effortLevel };
    // 2. Fall back to HTTP polling
    try {
      const res = await fetch(`http://127.0.0.1:${s.port}/health`, { signal: AbortSignal.timeout(2000) });
      const data = await res.json() as { state?: string; modelName?: string; effortLevel?: string };
      if (data.state) {
        siblingStateCache.set(s.id, { state: data.state, modelName: data.modelName, effortLevel: data.effortLevel, updatedAt: Date.now() });
      }
      return { ...base, state: data.state, modelName: data.modelName, effortLevel: data.effortLevel };
    } catch {
      // Probe failed. Within the grace window keep the last-known state alive so a
      // transient blip doesn't flicker a healthy session off the deck; past it,
      // mark the session dead so devices prune the ended session.
      const cached = siblingStateCache.get(s.id);
      if (cached && Date.now() - cached.updatedAt < LIVENESS_GRACE_MS) {
        return { ...base, state: cached.state, modelName: cached.modelName, effortLevel: cached.effortLevel };
      }
      return { ...base, alive: false, state: cached?.state ?? 'disconnected' };
    }
  }));
}

/**
 * Build an enriched sessions list for multi-session display.
 *
 * Includes the own session so single-session mode (`agentdeck claude` without a daemon)
 * still surfaces the active session. The daemon-server enricher overrides this list in
 * multi-session setups, so per-session bridges keeping their own entry is safe.
 */
export async function buildEnrichedSessionsList(
  ownSessionId: string,
  ownState: string,
  ownModelName?: string,
  ownEffortLevel?: string,
): Promise<EnrichedSession[]> {
  const all = listActiveSessions().filter(s => s.agentType !== 'daemon');
  const enriched = await enrichSessionsWithState(all, ownSessionId, ownState, ownModelName, ownEffortLevel);
  return sortSessions(enriched);
}
