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

const ALGORITHM = "aes-256-gcm";
const ENCRYPTED_PREFIX = "enc:";
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;
const DERIVED_KEY_LENGTH = 32;
const HKDF_SALT = "mosaic.crypto.v1";
const HKDF_INFO = "mosaic-db-secret-encryption";

@Injectable()
export class CryptoService {
  private readonly key: Buffer;

  constructor(private readonly config: ConfigService) {
    const secret = this.config.get<string>("MOSAIC_SECRET_KEY");

    if (!secret) {
      throw new Error("MOSAIC_SECRET_KEY environment variable is required");
    }

    if (secret.length < 32) {
      throw new Error("MOSAIC_SECRET_KEY must be at least 32 characters");
    }

    this.key = Buffer.from(
      hkdfSync(
        "sha256",
        Buffer.from(secret, "utf8"),
        Buffer.from(HKDF_SALT, "utf8"),
        Buffer.from(HKDF_INFO, "utf8"),
        DERIVED_KEY_LENGTH
      )
    );
  }

  encrypt(plaintext: string): string {
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, this.key, iv);
    const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
    const authTag = cipher.getAuthTag();
    const payload = Buffer.concat([iv, ciphertext, authTag]).toString("base64");

    return `${ENCRYPTED_PREFIX}${payload}`;
  }

  decrypt(encrypted: string): string {
    if (!this.isEncrypted(encrypted)) {
      throw new Error("Value is not encrypted");
    }

    const payloadBase64 = encrypted.slice(ENCRYPTED_PREFIX.length);

    try {
      const payload = Buffer.from(payloadBase64, "base64");
      if (payload.length < IV_LENGTH + AUTH_TAG_LENGTH) {
        throw new Error("Encrypted payload is too short");
      }

      const iv = payload.subarray(0, IV_LENGTH);
      const authTag = payload.subarray(payload.length - AUTH_TAG_LENGTH);
      const ciphertext = payload.subarray(IV_LENGTH, payload.length - AUTH_TAG_LENGTH);

      const decipher = createDecipheriv(ALGORITHM, this.key, iv);
      decipher.setAuthTag(authTag);

      return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
    } catch {
      throw new Error("Failed to decrypt value");
    }
  }

  isEncrypted(value: string): boolean {
    return value.startsWith(ENCRYPTED_PREFIX);
  }

  generateToken(): string {
    return randomBytes(32).toString("hex");
  }
}
