// État mémoire de l'extension : module pur (aucun import vscode), testable.
// Seedé via REST au (re)connect, puis patché par les events WS. Émet onDidChange à chaque mutation ;
// les arbres / status bar / notifications s'y abonnent. Principe « groupe léger » : on ne corrèle pas
// les worktrees aux sessions via la liste embarquée (potentiellement périmée) mais par `cwd`, exactement
// comme `sessionsForCwd` côté serveur.
import type {
  GroupSummary,
  RepoSummary,
  SessionSummary,
  WorktreeSummary,
} from '@arboretum/shared';
import type { GroupEvent, SessionEvent, WorktreeEvent } from '../api/ws-client.js';
import type { RestClient } from '../api/rest-client.js';

/** Normalise un chemin absolu pour la corrélation (retire le slash final). */
function normPath(p: string): string {
  return p.replace(/\/+$/, '') || '/';
}

export class Store {
  private readonly repos = new Map<string, RepoSummary>();
  /** worktrees par repo, indexés par chemin (clé stable). */
  private readonly worktrees = new Map<string, Map<string, WorktreeSummary>>();
  private readonly sessions = new Map<string, SessionSummary>();
  private readonly groups = new Map<string, GroupSummary>();
  private readonly listeners = new Set<() => void>();

  /** affiche aussi les sessions externes (découvertes hors Arboretum). */
  showExternalSessions = false;
  /** affiche aussi les sessions managées auto-archivées par ancienneté (P10). */
  showArchivedSessions = false;

  onDidChange(cb: () => void): () => void {
    this.listeners.add(cb);
    return () => this.listeners.delete(cb);
  }

  /** Bascule l'affichage des sessions externes et notifie les abonnés (refiltre les arbres). */
  setShowExternalSessions(value: boolean): void {
    if (this.showExternalSessions === value) return;
    this.showExternalSessions = value;
    this.emit();
  }

  /** Bascule l'affichage des sessions archivées (le re-seed REST est piloté par l'extension). */
  setShowArchivedSessions(value: boolean): void {
    if (this.showArchivedSessions === value) return;
    this.showArchivedSessions = value;
    this.emit();
  }

  private emit(): void {
    for (const cb of this.listeners) cb();
  }

  /** Charge tout l'état depuis le daemon (au premier hello_ok et à chaque reconnexion). */
  async seed(rest: RestClient): Promise<void> {
    const [repos, worktrees, sessions, groups] = await Promise.all([
      rest.listRepos(),
      rest.listWorktrees(),
      rest.listSessions({ includeArchived: this.showArchivedSessions }),
      rest.listGroups(),
    ]);
    this.repos.clear();
    this.worktrees.clear();
    this.sessions.clear();
    this.groups.clear();
    for (const r of repos.repos) this.repos.set(r.id, r);
    for (const w of worktrees.worktrees) this.putWorktree(w);
    for (const s of sessions.sessions) this.sessions.set(s.id, s);
    for (const g of groups.groups) this.groups.set(g.id, g);
    this.emit();
  }

  clear(): void {
    this.repos.clear();
    this.worktrees.clear();
    this.sessions.clear();
    this.groups.clear();
    this.emit();
  }

  private putWorktree(w: WorktreeSummary): void {
    let byPath = this.worktrees.get(w.repoId);
    if (!byPath) {
      byPath = new Map();
      this.worktrees.set(w.repoId, byPath);
    }
    byPath.set(normPath(w.path), w);
    // les sessions embarquées dans le worktree alimentent aussi la map sessions (état initial).
    for (const s of w.sessions) if (!this.sessions.has(s.id)) this.sessions.set(s.id, s);
  }

  // ---- réducteurs d'events WS ----

  applySession(e: SessionEvent): void {
    if (e.type === 'session_update') {
      this.sessions.set(e.session.id, e.session);
    } else {
      // session_exit : marque morte si connue (sinon ignore).
      const prev = this.sessions.get(e.sessionId);
      if (prev) this.sessions.set(e.sessionId, { ...prev, live: false, status: 'exited', exitCode: e.exitCode });
    }
    this.emit();
  }

  /** P10 : une session managée terminée vient d'être auto-archivée (message WS `session_archived`). */
  archiveSession(sessionId: string): void {
    const prev = this.sessions.get(sessionId);
    if (!prev) return;
    this.sessions.set(sessionId, { ...prev, archived: true });
    this.emit();
  }

  applyWorktree(e: WorktreeEvent): void {
    switch (e.type) {
      case 'repo_update':
        this.repos.set(e.repo.id, e.repo);
        break;
      case 'repo_removed':
        this.repos.delete(e.repoId);
        this.worktrees.delete(e.repoId);
        break;
      case 'worktree_update':
        this.putWorktree(e.worktree);
        break;
      case 'worktree_removed':
        this.worktrees.get(e.repoId)?.delete(normPath(e.path));
        break;
    }
    this.emit();
  }

  applyGroup(e: GroupEvent): void {
    if (e.type === 'group_update') this.groups.set(e.group.id, e.group);
    else this.groups.delete(e.groupId);
    this.emit();
  }

  // ---- lecture ----

  listRepos(): RepoSummary[] {
    return [...this.repos.values()]
      .filter((r) => !r.hidden && r.valid)
      .sort((a, b) => a.label.localeCompare(b.label));
  }

  getRepo(id: string): RepoSummary | undefined {
    return this.repos.get(id);
  }

  listWorktrees(repoId: string): WorktreeSummary[] {
    const byPath = this.worktrees.get(repoId);
    if (!byPath) return [];
    // worktree principal en tête, puis tri par branche/chemin.
    return [...byPath.values()].sort((a, b) => {
      if (a.isMain !== b.isMain) return a.isMain ? -1 : 1;
      return (a.branch ?? a.path).localeCompare(b.branch ?? b.path);
    });
  }

  getWorktree(repoId: string, path: string): WorktreeSummary | undefined {
    return this.worktrees.get(repoId)?.get(normPath(path));
  }

  /** sessions corrélées à un worktree par cwd (comme le serveur) ; filtre les externes selon le réglage. */
  sessionsForWorktree(path: string): SessionSummary[] {
    const wp = normPath(path);
    return [...this.sessions.values()]
      .filter((s) => normPath(s.cwd) === wp)
      .filter((s) => this.showExternalSessions || s.source === 'managed')
      .filter((s) => this.showArchivedSessions || !s.archived)
      .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  }

  getSession(id: string): SessionSummary | undefined {
    return this.sessions.get(id);
  }

  listGroups(): GroupSummary[] {
    return [...this.groups.values()].sort((a, b) => a.label.localeCompare(b.label));
  }

  getGroup(id: string): GroupSummary | undefined {
    return this.groups.get(id);
  }

  /** sessions de groupe (couvrant plusieurs repos) attachées à un groupe donné. */
  sessionsForGroup(groupId: string): SessionSummary[] {
    return [...this.sessions.values()]
      .filter((s) => s.groupId === groupId)
      .filter((s) => this.showExternalSessions || s.source === 'managed')
      .filter((s) => this.showArchivedSessions || !s.archived)
      .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
  }

  /** sessions en attente d'input (activity === 'waiting') : pour la status bar et les notifications. */
  waitingSessions(): SessionSummary[] {
    return [...this.sessions.values()].filter((s) => s.live && s.activity === 'waiting');
  }

  allSessions(): SessionSummary[] {
    return [...this.sessions.values()];
  }

  /** trouve le worktree dont le chemin == folderPath (mapping workspace). */
  findWorktreeByPath(folderPath: string): { repoId: string; worktree: WorktreeSummary } | undefined {
    const target = normPath(folderPath);
    for (const [repoId, byPath] of this.worktrees) {
      const wt = byPath.get(target);
      if (wt) return { repoId, worktree: wt };
    }
    return undefined;
  }
}
