import { credentialEncryption } from '../classes/encryption.ts';
import type { AuthRepository } from './repositories/auth.repository.ts';

const encryptedSecretPrefix = 'enc:v1:';

const secretSettingAliases = {
  backupPassword: ['backup_encryption_password'],
  cloudflareToken: ['cloudflareAPIKey'],
  dcrouterGatewayApiToken: ['externalGatewayApiToken'],
  dcrouterManagedAdminApiToken: [],
} as const;

type TCanonicalSecretSettingKey = keyof typeof secretSettingAliases;

export class SecretSettingsManager {
  constructor(private authRepo: AuthRepository) {}

  public isSecretKey(key: string): boolean {
    return this.resolveCanonicalKey(key) !== null;
  }

  public getCanonicalKeys(): TCanonicalSecretSettingKey[] {
    return Object.keys(secretSettingAliases) as TCanonicalSecretSettingKey[];
  }

  public async get(key: string): Promise<string | null> {
    const canonicalKey = this.resolveCanonicalKey(key);
    if (!canonicalKey) {
      return null;
    }

    for (const candidateKey of this.getCandidateKeys(canonicalKey)) {
      const secretValue = this.authRepo.getSecretSetting(candidateKey);
      if (secretValue !== null) {
        const decryptedValue = await this.decodeStoredValue(secretValue);
        await this.normalizeStoredSecret(canonicalKey, candidateKey, secretValue, decryptedValue);
        return decryptedValue;
      }

      const legacyValue = this.authRepo.getSetting(candidateKey);
      if (legacyValue !== null) {
        await this.set(canonicalKey, legacyValue);
        if (candidateKey !== canonicalKey) {
          this.authRepo.deleteSetting(candidateKey);
        }
        this.authRepo.deleteSetting(canonicalKey);
        return legacyValue;
      }
    }

    return null;
  }

  public async set(key: string, value: string | null): Promise<void> {
    const canonicalKey = this.resolveCanonicalKey(key);
    if (!canonicalKey) {
      throw new Error(`Unsupported secret setting key: ${key}`);
    }

    if (!value) {
      this.clear(canonicalKey);
      return;
    }

    const encryptedValue = await credentialEncryption.encrypt({ value });
    this.authRepo.setSecretSetting(canonicalKey, `${encryptedSecretPrefix}${encryptedValue}`);

    for (const aliasKey of secretSettingAliases[canonicalKey]) {
      this.authRepo.deleteSecretSetting(aliasKey);
      this.authRepo.deleteSetting(aliasKey);
    }

    this.authRepo.deleteSetting(canonicalKey);
  }

  public async has(key: string): Promise<boolean> {
    return (await this.get(key)) !== null;
  }

  public clear(key: string): void {
    const canonicalKey = this.resolveCanonicalKey(key);
    if (!canonicalKey) {
      return;
    }

    this.authRepo.deleteSecretSetting(canonicalKey);
    this.authRepo.deleteSetting(canonicalKey);

    for (const aliasKey of secretSettingAliases[canonicalKey]) {
      this.authRepo.deleteSecretSetting(aliasKey);
      this.authRepo.deleteSetting(aliasKey);
    }
  }

  private resolveCanonicalKey(key: string): TCanonicalSecretSettingKey | null {
    if (key in secretSettingAliases) {
      return key as TCanonicalSecretSettingKey;
    }

    for (const [canonicalKey, aliases] of Object.entries(secretSettingAliases)) {
      if ((aliases as readonly string[]).includes(key)) {
        return canonicalKey as TCanonicalSecretSettingKey;
      }
    }

    return null;
  }

  private getCandidateKeys(canonicalKey: TCanonicalSecretSettingKey): string[] {
    return [canonicalKey, ...secretSettingAliases[canonicalKey]];
  }

  private async decodeStoredValue(value: string): Promise<string> {
    if (value.startsWith(encryptedSecretPrefix)) {
      const decrypted = await credentialEncryption.decrypt<{ value: string }>(
        value.slice(encryptedSecretPrefix.length),
      );
      return decrypted.value;
    }

    // Compatibility for any earlier secret_settings rows stored without encryption.
    return value;
  }

  private async normalizeStoredSecret(
    canonicalKey: TCanonicalSecretSettingKey,
    sourceKey: string,
    storedValue: string,
    decryptedValue: string,
  ): Promise<void> {
    if (sourceKey !== canonicalKey || !storedValue.startsWith(encryptedSecretPrefix)) {
      await this.set(canonicalKey, decryptedValue);
      if (sourceKey !== canonicalKey) {
        this.authRepo.deleteSecretSetting(sourceKey);
      }
    }

    this.authRepo.deleteSetting(canonicalKey);
    for (const aliasKey of secretSettingAliases[canonicalKey]) {
      this.authRepo.deleteSetting(aliasKey);
    }
  }
}
