// Orchestration des clones (P12). Asynchrone : POST /repos/clone répond 202 avec un operationId,
// la progression est poussée en WS (topic 'clones') et lisible en REST (survit au refresh). À la fin,
// le repo cloné est auto-enregistré via WorktreeManager.addRepo (réutilise unicité/validation/event).
// `dest` est strictement confiné SOUS une racine de scan + non existant (mkdir implicite par git).
import { EventEmitter } from 'node:events';
import { randomUUID } from 'node:crypto';
import { rm, stat } from 'node:fs/promises';
import { resolve, sep } from 'node:path';
import type { CloneOperation, GitService } from '@arboretum/shared';
import type { Db } from '../db/index.js';
import { isSafeAbsolutePath, cloneRepo } from './git.js';
import { readScanRoots } from './scan-settings.js';
import { withGitAuth } from './git-auth.js';
import type { GitAuth } from './git-clients/index.js';
import type { GitCredentialsManager } from './git-credentials.js';
import type { WorktreeManager } from './worktree-manager.js';
import { recordAudit } from './audit-log.js';

export interface CloneManagerEvents {
  clone_update: [CloneOperation];
}

function httpError(statusCode: number, code: string, message: string): Error & { statusCode: number; code: string } {
  return Object.assign(new Error(message), { statusCode, code });
}

export class CloneManager extends EventEmitter<CloneManagerEvents> {
  private readonly ops = new Map<string, CloneOperation>();

  constructor(
    private readonly db: Db,
    private readonly worktrees: WorktreeManager,
    private readonly credentials: GitCredentialsManager,
  ) {
    super();
  }

  get(id: string): CloneOperation | null {
    return this.ops.get(id) ?? null;
  }

  /**
   * Démarre un clone (validation synchrone du dest → throw 4xx ; clone asynchrone ensuite).
   * Retourne l'operationId à suivre via WS/REST. `actor` pour l'audit.
   */
  async start(opts: { credentialId: string; remoteUrl: string; dest: string }, actor: string): Promise<string> {
    const dest = resolve(opts.dest);
    if (!isSafeAbsolutePath(dest)) throw httpError(400, 'BAD_REQUEST', 'dest must be an absolute, normalized path');
    if (typeof opts.remoteUrl !== 'string' || opts.remoteUrl.trim() === '') throw httpError(400, 'BAD_REQUEST', 'remoteUrl is required');
    const ctx = this.credentials.authContext(opts.credentialId);
    if (!ctx) throw httpError(404, 'NOT_FOUND', 'No usable credential with this id');

    // Confinement : dest DOIT être sous une racine de scan configurée (jamais d'écriture arbitraire).
    const roots = readScanRoots(this.db);
    if (roots.length === 0) throw httpError(400, 'NO_SCAN_ROOT', 'Configure a scan folder first (Settings → Discovery)');
    const underRoot = roots.some((r) => {
      const root = resolve(r);
      return dest === root || dest.startsWith(root + sep);
    });
    if (!underRoot) throw httpError(400, 'OUTSIDE_SCAN_ROOT', 'dest must be inside a configured scan folder');

    // Le parent doit exister ; le dest ne doit pas exister (git clone le crée).
    const parent = dest.slice(0, dest.lastIndexOf(sep)) || sep;
    try {
      const st = await stat(parent);
      if (!st.isDirectory()) throw httpError(400, 'BAD_DEST', 'Parent of dest is not a directory');
    } catch (err) {
      if ((err as { statusCode?: number }).statusCode) throw err;
      throw httpError(404, 'NOT_FOUND', `Parent directory does not exist: ${parent}`);
    }
    if (await stat(dest).then(() => true).catch(() => false)) throw httpError(409, 'DEST_EXISTS', `Destination already exists: ${dest}`);

    const id = randomUUID();
    const op: CloneOperation = { id, state: 'pending', progress: null, phase: null, error: null, repoId: null, dest };
    this.ops.set(id, op);
    recordAudit(this.db, { actor, action: 'repo.clone', resourceId: id, details: { service: ctx.service, dest } });
    // Lancement asynchrone (ne bloque pas la réponse 202).
    void this.run(op, opts.remoteUrl, opts.credentialId, ctx);
    return id;
  }

  private update(op: CloneOperation, patch: Partial<CloneOperation>): void {
    Object.assign(op, patch);
    this.emit('clone_update', { ...op });
  }

  private async run(
    op: CloneOperation,
    remoteUrl: string,
    credentialId: string,
    ctx: { service: GitService; baseUrl: string | null; auth: GitAuth },
  ): Promise<void> {
    this.update(op, { state: 'running' });
    let lastPct = -10;
    const onProgress = (p: { phase: string; percent: number | null }): void => {
      // throttle : on ne pousse que sur changement de phase ou +3% pour éviter le flood WS.
      if (p.percent == null || p.percent - lastPct >= 3 || p.phase !== op.phase) {
        lastPct = p.percent ?? lastPct;
        this.update(op, { phase: p.phase, progress: p.percent });
      }
    };
    try {
      await withGitAuth(ctx.service, ctx.auth, (env) =>
        cloneRepo({ url: remoteUrl, dest: op.dest, env, onProgress }),
      );
      // auto-enregistrement du repo cloné + métadonnées de provenance.
      const repo = await this.worktrees.addRepo({ path: op.dest });
      this.db
        .prepare('UPDATE repos SET remote_url = ?, git_service = ?, credential_id = ? WHERE id = ?')
        .run(remoteUrl, ctx.service, credentialId, repo.id);
      this.update(op, { state: 'done', progress: 100, repoId: repo.id });
    } catch (err) {
      // nettoyage du clone partiel (best-effort).
      await rm(op.dest, { recursive: true, force: true }).catch(() => {});
      this.update(op, { state: 'error', error: err instanceof Error ? err.message : String(err) });
    }
  }
}
