import { credentialEncryption } from '../classes/encryption.js';
import * as plugins from '../plugins.js';

export interface IServiceSecretsStore {
  getRawServiceSecrets(serviceIdArg: number): Promise<string | null> | string | null;
  setRawServiceSecrets(serviceIdArg: number, encryptedValuesArg: string): Promise<void> | void;
  deleteRawServiceSecrets(serviceIdArg: number): Promise<void> | void;
}

const canonicalSecretKeyRegex = /^[A-Z_][A-Z0-9_]*$/;

export class ServiceSecretsManager {
  constructor(private store: IServiceSecretsStore) {}

  public async get(serviceIdArg: number): Promise<Record<string, string>> {
    this.assertServiceId(serviceIdArg);
    const encryptedValues = await this.store.getRawServiceSecrets(serviceIdArg);
    if (!encryptedValues) return {};
    return await this.decryptExported(encryptedValues);
  }

  public async set(serviceIdArg: number, valuesArg: Record<string, string>): Promise<void> {
    this.assertServiceId(serviceIdArg);
    const values = this.validate(valuesArg);
    if (Object.keys(values).length === 0) {
      await this.clear(serviceIdArg);
      return;
    }
    const encryptedValues = await credentialEncryption.encrypt(values);
    await this.store.setRawServiceSecrets(serviceIdArg, encryptedValues);
  }

  public async clear(serviceIdArg: number): Promise<void> {
    this.assertServiceId(serviceIdArg);
    await this.store.deleteRawServiceSecrets(serviceIdArg);
  }

  public async exportEncrypted(serviceIdArg: number): Promise<string | undefined> {
    this.assertServiceId(serviceIdArg);
    return (await this.store.getRawServiceSecrets(serviceIdArg)) || undefined;
  }

  public async decryptExported(encryptedValuesArg: string): Promise<Record<string, string>> {
    if (typeof encryptedValuesArg !== 'string' || encryptedValuesArg.length === 0) {
      throw new Error('Encrypted service secret bundle is missing');
    }
    try {
      const values = await credentialEncryption.decrypt<Record<string, string>>(encryptedValuesArg);
      return this.validate(values);
    } catch (error) {
      throw new Error(
        'Could not decrypt service secrets. Cross-host restore requires the same ONEBOX_ENCRYPTION_KEY or machine identity.',
        { cause: error },
      );
    }
  }

  private assertServiceId(serviceIdArg: number): void {
    if (!Number.isSafeInteger(serviceIdArg) || serviceIdArg < 1) {
      throw new Error(`Invalid service secret owner id: ${serviceIdArg}`);
    }
  }

  public validate(valuesArg: Record<string, string>): Record<string, string> {
    if (!valuesArg || typeof valuesArg !== 'object' || Array.isArray(valuesArg)) {
      throw new Error('Service secret bundle must be an object');
    }
    const values: Record<string, string> = {};
    let totalBytes = 0;
    for (const [key, value] of Object.entries(valuesArg)) {
      if (!canonicalSecretKeyRegex.test(key)) {
        throw new Error(`Invalid service secret key: ${key}`);
      }
      if (typeof value !== 'string') {
        throw new Error(`Service secret '${key}' must be a string`);
      }
      const valueBytes = new TextEncoder().encode(value).byteLength;
      if (valueBytes > plugins.servezoneInterfaces.data.serviceSecretFileLimits.maximumValueBytes) {
        throw new Error(`Service secret '${key}' exceeds the value size limit`);
      }
      totalBytes += valueBytes;
      values[key] = value;
    }
    if (totalBytes > 1024 * 1024) {
      throw new Error('Service secret bundle exceeds the total size limit');
    }
    return values;
  }
}
