/**
 * Move secrets from one SecretBackend to another.
 *
 * Per-secret atomicity: for each secret we
 *   1. resolve the data via the source driver,
 *   2. write it to the destination driver,
 *   3. update the Secret row (flip backendId + set new externalRef, clear data),
 *   4. optionally delete from source.
 *
 * If the process dies between 2 and 3, the destination has an orphan entry
 * but the row still points at the source — restart is idempotent (skips rows
 * already on destination). We never run a batch-wide transaction because each
 * remote driver write is a real HTTP call that can't roll back.
 */
import type { Secret } from '@prisma/client';
import type { ISecretRepository } from '../repositories/interfaces.js';
import type { SecretBackendService } from './secret-backend.service.js';

export interface MigrateOptions {
  /** Source backend name. */
  from: string;
  /** Destination backend name. */
  to: string;
  /** If provided, only migrate secrets with these names. Otherwise migrate all. */
  names?: string[];
  /** Leave the source copy intact after migration. Default false. */
  keepSource?: boolean;
}

export interface MigrateResult {
  migrated: Array<{ name: string }>;
  skipped: Array<{ name: string; reason: string }>;
  failed: Array<{ name: string; error: string }>;
}

export class SecretMigrateService {
  constructor(
    private readonly secretRepo: ISecretRepository,
    private readonly backends: SecretBackendService,
  ) {}

  async migrate(opts: MigrateOptions): Promise<MigrateResult> {
    const source = await this.backends.getByName(opts.from);
    const dest = await this.backends.getByName(opts.to);
    if (source.id === dest.id) {
      return { migrated: [], skipped: [], failed: [{ name: '*', error: 'source and destination are the same backend' }] };
    }

    const sourceDriver = this.backends.driverFor(source);
    const destDriver = this.backends.driverFor(dest);

    let secrets = await this.secretRepo.findByBackend(source.id);
    if (opts.names && opts.names.length > 0) {
      const wanted = new Set(opts.names);
      secrets = secrets.filter((s) => wanted.has(s.name));
    }

    // Protect bootstrap tokens: any Secret named in a SecretBackend's
    // `config.tokenSecretRef.name` is the credential that lets mcpd reach
    // that backend at all. Moving it off the plaintext backend (the trust
    // root) breaks the chain — the destination backend would need its own
    // token to read its own token. Skip silently unless the user explicitly
    // names it via --names.
    const result: MigrateResult = { migrated: [], skipped: [], failed: [] };
    if (!(opts.names && opts.names.length > 0)) {
      const bootstrapNames = await this.collectBootstrapTokenNames();
      const kept: typeof secrets = [];
      for (const s of secrets) {
        if (bootstrapNames.has(s.name)) {
          result.skipped.push({ name: s.name, reason: 'bootstrap token for a SecretBackend — kept on source backend by design' });
        } else {
          kept.push(s);
        }
      }
      secrets = kept;
    }

    for (const secret of secrets) {
      try {
        // Skip if somehow already on destination (re-run safety).
        if (secret.backendId === dest.id) {
          result.skipped.push({ name: secret.name, reason: 'already on destination' });
          continue;
        }

        const data = await sourceDriver.read({
          name: secret.name,
          externalRef: secret.externalRef,
          data: secret.data as Record<string, string>,
        });
        const written = await destDriver.write({ name: secret.name, data });

        await this.secretRepo.update(secret.id, {
          backendId: dest.id,
          data: written.storedData,
          externalRef: written.externalRef,
          // Keys travel with the secret — populate now so list/describe
          // doesn't need a per-row backend read just to display key names.
          keyNames: Object.keys(data).sort(),
        });

        if (opts.keepSource !== true) {
          await sourceDriver.delete({ name: secret.name, externalRef: secret.externalRef })
            .catch((err: unknown) => {
              // Destination is intact; best-effort source cleanup. Log + continue.
              const msg = err instanceof Error ? err.message : String(err);
              result.skipped.push({ name: secret.name, reason: `migrated OK; source cleanup failed: ${msg}` });
            });
        }

        result.migrated.push({ name: secret.name });
      } catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        result.failed.push({ name: secret.name, error: msg });
      }
    }

    return result;
  }

  /** Track which secrets would be touched by a migrate run, without performing it. */
  async dryRun(opts: MigrateOptions): Promise<Array<Secret>> {
    const source = await this.backends.getByName(opts.from);
    let secrets = await this.secretRepo.findByBackend(source.id);
    if (opts.names && opts.names.length > 0) {
      const wanted = new Set(opts.names);
      secrets = secrets.filter((s) => wanted.has(s.name));
    } else {
      // Same bootstrap-token protection as `migrate()` — dry-run MUST report
      // the same candidate list the real run would move.
      const bootstrapNames = await this.collectBootstrapTokenNames();
      secrets = secrets.filter((s) => !bootstrapNames.has(s.name));
    }
    return secrets;
  }

  /**
   * Return the set of Secret names that are referenced by any SecretBackend's
   * `config.tokenSecretRef.name`. These are the bootstrap credentials that let
   * mcpd reach the backend itself; moving them anywhere off their current
   * backend would break the auth chain.
   */
  private async collectBootstrapTokenNames(): Promise<Set<string>> {
    const all = await this.backends.list();
    const names = new Set<string>();
    for (const b of all) {
      const cfg = b.config as { tokenSecretRef?: { name?: string } } | null;
      const name = cfg?.tokenSecretRef?.name;
      if (typeof name === 'string' && name !== '') names.add(name);
    }
    return names;
  }
}

export interface SecretMigrateRouteDeps {
  migrateService: SecretMigrateService;
}
