import type { FastifyInstance } from 'fastify';
import type { CreateSessionRequest, HideDiscoveredResponse, SessionResponse, SessionsListResponse } from '@arboretum/shared';
import type { PtyManager } from '../core/pty-manager.js';
import { mergeSessions, type DiscoveryService } from '../core/discovery-service.js';
import type { SessionArchiveService } from '../core/session-archive.js';
import { archiveSession, hideSession, sessionExists, unarchiveSession, unhideSession, type Db } from '../db/index.js';
import { recordAudit } from '../core/audit-log.js';

export function registerSessionRoutes(
  app: FastifyInstance,
  manager: PtyManager,
  discovery: DiscoveryService,
  sessionArchive: SessionArchiveService,
  db: Db,
): void {
  app.get('/api/v1/sessions', async (req): Promise<SessionsListResponse> => {
    const q = req.query as { includeHidden?: string; includeArchived?: string };
    const includeHidden = q.includeHidden === 'true';
    const includeArchived = q.includeArchived === 'true';
    // `includeArchived` ne concerne QUE les sessions managées (manager.list) ; les découvertes
    // n'ont pas de notion d'archivage. `hidden` est filtré ensuite, indépendamment.
    const all = mergeSessions(manager.list({ includeArchived }), discovery.list());
    return { sessions: includeHidden ? all : all.filter((s) => !s.hidden) };
  });

  // Résout le claudeSessionId d'un :id de session (managée par UUID, ou découverte dont l'id EST le sid).
  const resolveClaudeSid = (id: string): string | null => {
    const managed = manager.resumeTargetById(id);
    if (managed?.claudeSessionId) return managed.claudeSessionId;
    return discovery.getDiscovered(id) ? id : null;
  };

  app.post('/api/v1/sessions', async (req, reply) => {
    const body = req.body as Partial<CreateSessionRequest> | null;
    if (!body || typeof body.cwd !== 'string' || !body.cwd.startsWith('/')) {
      return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'cwd (absolute path) is required' } });
    }
    if (body.command !== undefined && body.command !== 'claude' && body.command !== 'bash') {
      return reply.status(400).send({ error: { code: 'BAD_REQUEST', message: 'command must be claude or bash' } });
    }
    try {
      const session = manager.spawn({ cwd: body.cwd, ...(body.command ? { command: body.command } : {}) });
      const res: SessionResponse = { session };
      return reply.status(201).send(res);
    } catch (err) {
      const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
      return reply.status(statusCode).send({ error: { code: 'SPAWN_FAILED', message: (err as Error).message } });
    }
  });

  // Reprise d'une session morte : nouveau PTY managé `--resume <claudeSessionId>` DANS SON CWD D'ORIGINE
  // (spike S1). Le cwd n'est JAMAIS fourni par le client. Deux origines d'id possibles :
  //  - session managée morte (id = UUID Arboretum) → cwd + claudeSessionId + groupe lus en DB ;
  //  - session claude EXTERNE découverte (id = claudeSessionId) → cwd lu sur disque.
  app.post('/api/v1/sessions/:id/resume', async (req, reply) => {
    const { id } = req.params as { id: string };
    const managed = manager.resumeTargetById(id);
    const discovered = managed ? null : discovery.getDiscovered(id);
    if (!managed && !discovered) {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No resumable session with this id' } });
    }
    const cwd = managed ? managed.cwd : discovered!.cwd;
    const claudeSessionId = managed ? managed.claudeSessionId : id;
    // Garde-fou anti-corruption : jamais de resume direct d'une session vivante (vérif FRAÎCHE).
    if (discovery.isClaudeSessionLive(claudeSessionId) || manager.findLiveByClaudeSessionId(claudeSessionId)) {
      return reply.status(409).send({ error: { code: 'SESSION_LIVE', message: 'Session is live, fork it instead' } });
    }
    try {
      // Session de groupe (P6) : re-relie les mêmes répertoires (--add-dir) et son groupe au resume.
      const ctx = managed ? { addedDirs: managed.addedDirs, groupId: managed.groupId } : manager.groupSessionContext(claudeSessionId);
      const session = manager.spawn({
        cwd,
        resume: { claudeSessionId },
        ...(ctx?.addedDirs.length ? { addDirs: ctx.addedDirs } : {}),
        ...(ctx?.groupId ? { groupId: ctx.groupId } : {}),
      });
      const res: SessionResponse = { session };
      return reply.status(201).send(res);
    } catch (err) {
      const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
      return reply.status(statusCode).send({ error: { code: 'SPAWN_FAILED', message: (err as Error).message } });
    }
  });

  // Fork : duplique une session (vivante ou morte) sans la corrompre (`--resume <id> --fork-session`).
  // Même résolution d'id que /resume (managée morte par UUID, sinon externe découverte par claudeSessionId).
  app.post('/api/v1/sessions/:id/fork', async (req, reply) => {
    const { id } = req.params as { id: string };
    const managed = manager.resumeTargetById(id);
    const discovered = managed ? null : discovery.getDiscovered(id);
    if (!managed && !discovered) {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No session with this id to fork' } });
    }
    const cwd = managed ? managed.cwd : discovered!.cwd;
    const claudeSessionId = managed ? managed.claudeSessionId : id;
    try {
      const session = manager.spawn({ cwd, resume: { claudeSessionId, fork: true } });
      const res: SessionResponse = { session };
      return reply.status(201).send(res);
    } catch (err) {
      const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
      return reply.status(statusCode).send({ error: { code: 'SPAWN_FAILED', message: (err as Error).message } });
    }
  });

  // Masque une session découverte (ou managée morte) : exclue de la liste sauf ?includeHidden=true.
  // Reste reprenable/forkable : c'est un filtre d'affichage, pas une suppression.
  app.post('/api/v1/sessions/:id/hide', async (req, reply) => {
    const { id } = req.params as { id: string };
    const sid = resolveClaudeSid(id);
    if (!sid) {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No session with this id to hide' } });
    }
    hideSession(db, sid);
    await discovery.refresh(); // rediffuse l'état (hidden) aux clients
    recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.hide', resourceId: sid });
    return reply.send({ ok: true });
  });

  // Ré-affiche une session masquée.
  app.delete('/api/v1/sessions/:id/hide', async (req, reply) => {
    const { id } = req.params as { id: string };
    const sid = resolveClaudeSid(id);
    if (!sid) {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No session with this id to unhide' } });
    }
    unhideSession(db, sid);
    await discovery.refresh();
    recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.unhide', resourceId: sid });
    return reply.send({ ok: true });
  });

  // Masquage de masse : nettoie d'un coup tout l'historique externe actuellement visible. Les futures
  // sessions externes réapparaîtront (sinon --no-discover). Renvoie le nombre de sessions masquées.
  app.post('/api/v1/sessions/hide-discovered', async (req, reply) => {
    const sids = discovery
      .list()
      .filter((s) => !s.hidden && s.claudeSessionId)
      .map((s) => s.claudeSessionId as string);
    for (const sid of sids) hideSession(db, sid);
    await discovery.refresh();
    if (sids.length > 0) {
      recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.hideDiscovered', resourceId: null, details: { count: sids.length } });
    }
    const res: HideDiscoveredResponse = { hidden: sids.length };
    return reply.send(res);
  });

  // Archive manuellement une session managée terminée (soft-archive). Exclue de la liste par défaut,
  // reste reprenable/forkable. 404 si l'id n'est pas une session managée connue en DB.
  app.post('/api/v1/sessions/:id/archive', async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!sessionExists(db, id)) {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No managed session with this id to archive' } });
    }
    const archivedAt = new Date().toISOString();
    archiveSession(db, id, archivedAt);
    sessionArchive.emitArchived(id, archivedAt); // diffuse aux clients abonnés (session_archived)
    recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.archive', resourceId: id });
    return reply.send({ ok: true });
  });

  // Dés-archive une session : ré-émet un session_update pour que tous les clients ré-affichent le row.
  app.delete('/api/v1/sessions/:id/archive', async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!sessionExists(db, id)) {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No managed session with this id to unarchive' } });
    }
    unarchiveSession(db, id);
    manager.emitHistoricalUpdate(id);
    recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.unarchive', resourceId: id });
    return reply.send({ ok: true });
  });

  // Déclenche un balayage d'archivage immédiat (rétention configurée). Renvoie le nombre archivé.
  // Pratique pour l'UX et pour une acceptance déterministe (sans attendre le scheduler ~6 h).
  app.post('/api/v1/sessions/archive-now', async (req, reply) => {
    const archived = sessionArchive.sweep();
    recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'session.archiveNow', resourceId: null, details: { archived } });
    return reply.send({ archived });
  });

  app.delete('/api/v1/sessions/:id', async (req, reply) => {
    const { id } = req.params as { id: string };
    if (!manager.kill(id)) {
      return reply.status(404).send({ error: { code: 'NOT_FOUND', message: 'No live session with this id' } });
    }
    return reply.send({ ok: true });
  });
}
