// Service de découverte : second producteur de SessionSummary (à côté du PtyManager).
// Scanne ~/.claude/projects (JSONL) + ~/.claude/sessions (registre), corrèle par claudeSessionId,
// calcule la vivacité, met en cache et notifie les changements. Lecture seule du disque.
import { EventEmitter } from 'node:events';
import { homedir } from 'node:os';
import { join } from 'node:path';
import type { SessionSummary } from '@arboretum/shared';
import { scanProjects, type DiscoveredJsonl } from './jsonl-discovery.js';
import { readRegistry, type RegistryEntry } from './session-registry.js';
import type { PtyManager } from './pty-manager.js';

const DEFAULT_REFRESH_MS = 10_000;

export interface DiscoveryServiceEvents {
  /** session découverte nouvelle ou modifiée (relayée en session_update par la gateway). */
  discovery_update: [SessionSummary];
}

export interface DiscoveryOptions {
  ptyManager: PtyManager;
  projectsDir?: string;
  sessionsDir?: string;
  refreshMs?: number;
}

export class DiscoveryService extends EventEmitter<DiscoveryServiceEvents> {
  private readonly projectsDir: string;
  private readonly sessionsDir: string;
  private readonly ptyManager: PtyManager;
  private readonly refreshMs: number;
  private timer: NodeJS.Timeout | null = null;
  private cache: SessionSummary[] = [];
  private byId = new Map<string, DiscoveredJsonl>();
  private prevJson = new Map<string, string>();

  constructor(opts: DiscoveryOptions) {
    super();
    this.ptyManager = opts.ptyManager;
    this.projectsDir = opts.projectsDir ?? join(homedir(), '.claude', 'projects');
    this.sessionsDir = opts.sessionsDir ?? join(homedir(), '.claude', 'sessions');
    this.refreshMs = opts.refreshMs ?? DEFAULT_REFRESH_MS;
  }

  start(): void {
    if (this.timer) return;
    void this.refresh(); // premier scan asynchrone : ne bloque pas le boot
    this.timer = setInterval(() => void this.refresh(), this.refreshMs);
    this.timer.unref(); // ne maintient pas le process en vie
  }

  stop(): void {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  }

  /** Cache courant (jamais de scan synchrone dans le chemin chaud). */
  list(): SessionSummary[] {
    return this.cache;
  }

  /** Session découverte (avec son cwd d'origine lu sur disque) — pour resume/fork. null si absente. */
  getDiscovered(claudeSessionId: string): DiscoveredJsonl | null {
    return this.byId.get(claudeSessionId) ?? null;
  }

  /**
   * Vivacité FRAÎCHE d'une session (relit le registre, ne se fie pas au cache) — garde-fou
   * anti-corruption : la route /resume doit refuser une session devenue vivante depuis le dernier scan.
   */
  isClaudeSessionLive(claudeSessionId: string): boolean {
    const entry = readRegistry(this.sessionsDir).find((r) => r.claudeSessionId === claudeSessionId);
    return entry?.live ?? false;
  }

  /** Recalcule le cache depuis le disque. Tolérant : ne lève jamais. */
  async refresh(): Promise<void> {
    const regBySid = new Map<string, RegistryEntry>();
    for (const r of readRegistry(this.sessionsDir)) {
      if (r.claudeSessionId) regBySid.set(r.claudeSessionId, r);
    }
    const known = this.ptyManager.knownClaudeSessionIds();

    // Dédoublonnage des JSONL par claudeSessionId (on retient le plus récent).
    const latest = new Map<string, DiscoveredJsonl>();
    for (const d of await scanProjects(this.projectsDir)) {
      const prev = latest.get(d.claudeSessionId);
      if (!prev || d.mtimeMs > prev.mtimeMs) latest.set(d.claudeSessionId, d);
    }

    const summaries: SessionSummary[] = [];
    const byId = new Map<string, DiscoveredJsonl>();
    for (const d of latest.values()) {
      if (known.has(d.claudeSessionId)) continue; // déjà gérée comme session managée
      byId.set(d.claudeSessionId, d);
      const r = regBySid.get(d.claudeSessionId);
      const live = r?.live ?? false;
      summaries.push({
        id: d.claudeSessionId, // les découvertes s'identifient par leur claudeSessionId
        cwd: d.cwd,
        command: 'claude',
        title: d.title,
        status: live ? 'running' : 'exited',
        live,
        createdAt: new Date(d.mtimeMs).toISOString(),
        endedAt: null,
        exitCode: null,
        clients: 0,
        source: 'discovered',
        claudeSessionId: d.claudeSessionId,
        pid: live ? (r?.pid ?? null) : null,
        resumable: !live, // morte → --resume direct ; vivante → fork/observe (jamais resume : corruption)
        attachable: false, // Arboretum ne tient pas le PTY d'une session externe
        registryStatus: r?.status ?? null,
      });
    }

    this.cache = summaries;
    this.byId = byId;

    // Diff : n'émettre que les sessions nouvelles ou modifiées.
    const nextJson = new Map<string, string>();
    for (const s of summaries) {
      const j = JSON.stringify(s);
      nextJson.set(s.id, j);
      if (this.prevJson.get(s.id) !== j) this.emit('discovery_update', s);
    }
    this.prevJson = nextJson;
  }
}

/** Fusionne sessions managées + découvertes en dédoublonnant par claudeSessionId (managé prioritaire). */
export function mergeSessions(managed: SessionSummary[], discovered: SessionSummary[]): SessionSummary[] {
  const managedSids = new Set(managed.map((s) => s.claudeSessionId).filter((x): x is string => x != null));
  return [...managed, ...discovered.filter((s) => !(s.claudeSessionId && managedSids.has(s.claudeSessionId)))];
}
