/**
 * SecretService — CRUD over `Secret` rows.
 *
 * Dispatches storage I/O through the `SecretBackendService`: on create/update
 * the default backend's driver writes, and the resulting {externalRef,
 * storedData} is persisted on the row. On read (`resolveData`) the row's
 * `backendId` selects the driver, which fetches the actual data.
 */
import type { Secret } from '@prisma/client';
import type { ISecretRepository } from '../repositories/interfaces.js';
import type { SecretBackendService } from './secret-backend.service.js';
import { CreateSecretSchema, UpdateSecretSchema } from '../validation/secret.schema.js';
import { NotFoundError, ConflictError } from './mcp-server.service.js';
import type { SecretRefResolver } from './secret-backends/types.js';

export class SecretService implements SecretRefResolver {
  constructor(
    private readonly repo: ISecretRepository,
    private readonly backends: SecretBackendService,
  ) {}

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

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

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

  /** Return the secret's actual data by dispatching through its backend driver. */
  async resolveData(secret: Secret): Promise<Record<string, string>> {
    const backend = await this.backends.getById(secret.backendId);
    const driver = this.backends.driverFor(backend);
    return driver.read({
      name: secret.name,
      externalRef: secret.externalRef,
      data: secret.data as Record<string, string>,
    });
  }

  /** Convenience: resolve {secretName, key} → string. Implements SecretRefResolver. */
  async resolve(secretName: string, key: string): Promise<string> {
    const secret = await this.getByName(secretName);
    const data = await this.resolveData(secret);
    const value = data[key];
    if (value === undefined) {
      throw new NotFoundError(`Secret '${secretName}' has no key '${key}'`);
    }
    return value;
  }

  async create(input: unknown): Promise<Secret> {
    const data = CreateSecretSchema.parse(input);
    const existing = await this.repo.findByName(data.name);
    if (existing !== null) {
      throw new ConflictError(`Secret already exists: ${data.name}`);
    }
    const backend = await this.backends.getDefault();
    const driver = this.backends.driverFor(backend);
    const written = await driver.write({ name: data.name, data: data.data });
    return this.repo.create({
      name: data.name,
      backendId: backend.id,
      data: written.storedData,
      externalRef: written.externalRef,
      keyNames: keysOf(data.data),
    });
  }

  async update(id: string, input: unknown): Promise<Secret> {
    const data = UpdateSecretSchema.parse(input);
    const existing = await this.getById(id);
    const backend = await this.backends.getById(existing.backendId);
    const driver = this.backends.driverFor(backend);
    const written = await driver.write({ name: existing.name, data: data.data });
    return this.repo.update(id, {
      data: written.storedData,
      externalRef: written.externalRef,
      keyNames: keysOf(data.data),
    });
  }

  async delete(id: string): Promise<void> {
    const existing = await this.getById(id);
    const backend = await this.backends.getById(existing.backendId);
    const driver = this.backends.driverFor(backend);
    await driver.delete({ name: existing.name, externalRef: existing.externalRef });
    await this.repo.delete(id);
  }

  // ── Backup/restore helpers (preserved) ──

  async upsertByName(data: Record<string, unknown>): Promise<Secret> {
    const name = data['name'] as string;
    const existing = await this.repo.findByName(name);
    if (existing !== null) {
      return this.update(existing.id, data);
    }
    return this.create(data);
  }

  async deleteByName(name: string): Promise<void> {
    const existing = await this.repo.findByName(name);
    if (existing === null) return;
    await this.delete(existing.id);
  }

  /**
   * If a row's `keyNames` is empty (pre-Phase-5 migrated rows), fetch the
   * data from the backend once and persist the keys back to the row. Means
   * the next read of this secret takes the cheap path. We never store values
   * here — only the sorted list of key names.
   *
   * Best-effort: if the backend is unreachable we just return the row as-is;
   * the next read will retry.
   */
  private async maybeBackfillKeyNames(secret: Secret): Promise<Secret> {
    const existingKeys = secret.keyNames as unknown;
    if (Array.isArray(existingKeys) && existingKeys.length > 0) return secret;

    // Plaintext secrets carry their values directly — derive keys from `data`
    // without touching the backend.
    const data = secret.data as Record<string, unknown>;
    if (data && Object.keys(data).length > 0) {
      const keys = keysOf(data as Record<string, string>);
      const updated = await this.repo.update(secret.id, { keyNames: keys })
        .catch(() => secret);
      return updated;
    }

    // Non-plaintext + empty data: ask the backend for the actual values just
    // to learn the keys. Cheap (single backend read) and only happens once
    // per row.
    try {
      const resolved = await this.resolveData(secret);
      const keys = keysOf(resolved);
      if (keys.length === 0) return secret;
      return await this.repo.update(secret.id, { keyNames: keys });
    } catch {
      return secret;
    }
  }
}

/** Sorted list of keys from a data object — stable serialization for diffing. */
function keysOf(data: Record<string, string>): string[] {
  return Object.keys(data).sort();
}
