import { Injectable, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import {
  createCipheriv,
  createDecipheriv,
  createHash,
  randomBytes,
} from "crypto";

const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;

// Version de FORMATO (algoritmo + layout del blob), separada de la version
// de LLAVE (que llave lo cifro). Son dos cosas distintas: cambiar de
// algoritmo mas adelante bumpea CURRENT_FORMAT_VERSION y puede tener su
// propio parsing en decrypt(); rotar una llave nunca toca esto. Mezclarlas
// en un solo byte, como en la primera version, chocaba las dos rotaciones
// entre si.
const CURRENT_FORMAT_VERSION = 1;

// Cifrado simetrico reversible para credenciales de conexion (password de
// MySQL de cada tenant, access token de WhatsApp por tenant). NUNCA usar
// bcrypt/hash aca: para conectarse a una base hace falta la contraseña, no
// su huella. Las llaves viven en una variable de entorno del proceso, nunca
// en una columna de mbinvtaller.
//
// Formato del blob (antes de base64), version 1:
// [1 byte formatVersion][1 byte keyVersion][12 bytes iv][16 bytes authTag]
// [ciphertext]
//
// keyVersion es el INDICE dentro de la lista ordenada de llaves (ver
// loadKeys): rotar = agregar una llave nueva al final de la lista, nunca
// borrar ni reordenar las anteriores mientras exista algo cifrado con ellas.
//
// DEUDA CONOCIDA — no implementada: no hay camino para RE-cifrar lo ya
// guardado con la llave activa nueva. Hoy, rotar significa ACUMULAR llaves
// para siempre en la lista (todas se necesitan mientras exista un blob
// viejo sin re-cifrar), no reemplazarlas. La forma que deberia tener el
// reemplazo: un job de administrador que, fila por fila (Tenant.dbPasswordEncrypted,
// TenantConfig.whatsappAccessTokenEncrypted), descifre con la keyVersion que
// tenga cada blob y vuelva a cifrar con la keyVersion activa actual,
// dejando un log de cuantas filas se re-cifraron y cuales fallaron. Recien
// cuando ese job confirma que ninguna fila referencia una keyVersion vieja,
// esa llave se puede sacar de la lista de verdad.
@Injectable()
export class CredentialsCryptoService implements OnModuleInit {
  private keys: Buffer[] = [];

  constructor(private readonly configService: ConfigService) {}

  onModuleInit() {
    this.keys = this.loadKeys();
  }

  private loadKeys(): Buffer[] {
    const plural = this.configService.get<string>(
      "CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEYS",
    );
    const singular = this.configService.get<string>(
      "CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY",
    );

    const raw = plural
      ? plural.split(",").map((k) => k.trim()).filter(Boolean)
      : singular
        ? [singular]
        : [];

    if (raw.length === 0) {
      throw new Error(
        "CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY(S) no esta definida. " +
          "Sin ella no se pueden cifrar ni descifrar credenciales de tenants.",
      );
    }

    if (raw.length > 255) {
      throw new Error(
        "Demasiadas llaves de cifrado (>255) — el byte de keyVersion no alcanza.",
      );
    }

    for (const key of raw) {
      if (key.length < 16) {
        throw new Error(
          "Una de las llaves de CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEY(S) es demasiado corta (minimo 16 caracteres).",
        );
      }
    }

    // Derivamos una llave de 32 bytes de un secreto de cualquier longitud.
    return raw.map((k) => createHash("sha256").update(k).digest());
  }

  private activeKeyVersion(): number {
    return this.keys.length - 1;
  }

  encrypt(plaintext: string): string {
    const keyVersion = this.activeKeyVersion();
    const key = this.keys[keyVersion];
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, key, iv);
    const encrypted = Buffer.concat([
      cipher.update(plaintext, "utf8"),
      cipher.final(),
    ]);
    const authTag = cipher.getAuthTag();
    const blob = Buffer.concat([
      Buffer.from([CURRENT_FORMAT_VERSION, keyVersion]),
      iv,
      authTag,
      encrypted,
    ]);
    return blob.toString("base64");
  }

  decrypt(ciphertextBase64: string): string {
    const raw = Buffer.from(ciphertextBase64, "base64");
    if (raw.length < 2 + IV_LENGTH + AUTH_TAG_LENGTH) {
      throw new Error("Blob cifrado invalido: demasiado corto.");
    }

    const formatVersion = raw[0];
    if (formatVersion !== CURRENT_FORMAT_VERSION) {
      // Hoy solo existe la version 1. El dia que haya una version 2 (otro
      // algoritmo/layout), esta rama se convierte en un switch que elige el
      // parsing segun formatVersion — nunca se reutiliza el mismo parsing
      // para dos formatos distintos.
      throw new Error(
        `Version de formato de cifrado no soportada: ${formatVersion}.`,
      );
    }

    const keyVersion = raw[1];
    const key = this.keys[keyVersion];
    if (!key) {
      throw new Error(
        `No hay llave cargada para keyVersion ${keyVersion} de este blob. ` +
          "Si rotaste la llave, la anterior tiene que seguir en " +
          "CONTROL_PLANE_CREDENTIALS_ENCRYPTION_KEYS para poder descifrar datos viejos.",
      );
    }

    const iv = raw.subarray(2, 2 + IV_LENGTH);
    const authTag = raw.subarray(
      2 + IV_LENGTH,
      2 + IV_LENGTH + AUTH_TAG_LENGTH,
    );
    const encrypted = raw.subarray(2 + IV_LENGTH + AUTH_TAG_LENGTH);

    const decipher = createDecipheriv(ALGORITHM, key, iv);
    decipher.setAuthTag(authTag);
    // Si el blob fue alterado, setAuthTag + final() lanza — GCM verifica
    // integridad, no solo confidencialidad.
    const decrypted = Buffer.concat([
      decipher.update(encrypted),
      decipher.final(),
    ]);
    return decrypted.toString("utf8");
  }

  /** Huella corta de la llave activa — no revela la llave, sirve para
   * confirmar en el boot log que dos procesos usan la misma llave (o para
   * detectar un canario que no descifra tras mover/restaurar la base). */
  fingerprint(): string {
    return createHash("sha256")
      .update(this.keys[this.activeKeyVersion()])
      .digest("hex")
      .slice(0, 12);
  }
}
