import type { SecretBackend } from '@prisma/client';
import type { ISecretBackendRepository } from '../repositories/secret-backend.repository.js';
import type { SecretBackendDriver } from './secret-backends/types.js';
import { createDriver, type DriverFactoryDeps } from './secret-backends/factory.js';
import { NotFoundError, ConflictError } from './mcp-server.service.js';

export class SecretBackendInUseError extends Error {
  constructor(backendName: string, count: number) {
    super(`SecretBackend '${backendName}' is still referenced by ${String(count)} secret(s); migrate them first`);
    this.name = 'SecretBackendInUseError';
  }
}

export class SecretBackendService {
  private driverCache = new Map<string, SecretBackendDriver>(); // keyed by backend id

  constructor(
    private readonly repo: ISecretBackendRepository,
    private readonly driverDeps: DriverFactoryDeps,
  ) {}

  async list(): Promise<SecretBackend[]> {
    return this.repo.findAll();
  }

  async getById(id: string): Promise<SecretBackend> {
    const row = await this.repo.findById(id);
    if (row === null) throw new NotFoundError(`SecretBackend not found: ${id}`);
    return row;
  }

  async getByName(name: string): Promise<SecretBackend> {
    const row = await this.repo.findByName(name);
    if (row === null) throw new NotFoundError(`SecretBackend not found: ${name}`);
    return row;
  }

  async getDefault(): Promise<SecretBackend> {
    const row = await this.repo.findDefault();
    if (row === null) {
      throw new Error('No default SecretBackend configured. This shouldn\'t happen — the plaintext row should have been seeded on startup.');
    }
    return row;
  }

  async create(input: {
    name: string;
    type: string;
    config?: Record<string, unknown>;
    isDefault?: boolean;
    description?: string;
  }): Promise<SecretBackend> {
    if (!input.name || !input.type) throw new Error('name and type are required');
    const existing = await this.repo.findByName(input.name);
    if (existing !== null) throw new ConflictError(`SecretBackend already exists: ${input.name}`);
    return this.repo.create(input);
  }

  async update(id: string, input: { config?: Record<string, unknown>; isDefault?: boolean; description?: string }): Promise<SecretBackend> {
    await this.getById(id);
    const row = await this.repo.update(id, input);
    this.driverCache.delete(id); // config may have changed; rebuild lazily
    return row;
  }

  /**
   * Replace `tokenMeta` on a backend row. Called exclusively by the rotator
   * service every time it mints or fails to mint a successor token. The field
   * is runtime state (not user-managed config) so it bypasses the normal
   * update path + doesn't invalidate the driver cache.
   */
  async updateTokenMeta(id: string, tokenMeta: Record<string, unknown>): Promise<SecretBackend> {
    return this.repo.update(id, { tokenMeta });
  }

  async setDefault(id: string): Promise<SecretBackend> {
    await this.getById(id);
    return this.repo.setAsDefault(id);
  }

  async delete(id: string): Promise<void> {
    const row = await this.getById(id);
    const count = await this.repo.countReferencingSecrets(id);
    if (count > 0) throw new SecretBackendInUseError(row.name, count);
    if (row.isDefault) throw new Error(`Cannot delete the default SecretBackend '${row.name}'; promote another one first`);
    await this.repo.delete(id);
    this.driverCache.delete(id);
  }

  /** Get the driver for a given backend id, creating + caching on first call. */
  driverFor(backend: SecretBackend): SecretBackendDriver {
    const cached = this.driverCache.get(backend.id);
    if (cached) return cached;
    const driver = createDriver(backend, this.driverDeps);
    this.driverCache.set(backend.id, driver);
    return driver;
  }
}
