/**
 * NestJS-injectable field encryption service.
 *
 * Wraps the low-level AES-256-GCM encrypt/decrypt functions with:
 *  - Multi-key support for key rotation
 *  - Deterministic hashing for indexed lookups (email, phone)
 *  - Per-model/field configuration
 *  - Access audit logging
 */

import crypto from 'node:crypto';
import { Injectable } from '@nestjs/common';
import {
  encryptField,
  decryptField,
  isEncrypted,
  type FieldEncryptionConfig,
} from './field-encryption';
import { LoggerService } from './logger.service';

// ---------------------------------------------------------------------------
// Configuration types
// ---------------------------------------------------------------------------

export interface EncryptionKeyConfig {
  /** 32-byte hex-encoded encryption key (64 hex chars). */
  key: string;
  /** Key version — newer is higher. */
  version: number;
}

/** Describes which fields on a Prisma model are encrypted. */
export interface ModelEncryptionFieldConfig {
  /** The database field name. */
  field: string;
  /**
   * If true, a deterministic HMAC-SHA256 hash is also maintained in a
   * `{field}Hash` column, enabling indexed lookups on encrypted data.
   */
  searchable?: boolean;
}

export interface ModelEncryptionConfig {
  /** Prisma model name (PascalCase, e.g. "User"). */
  model: string;
  /** Fields to encrypt within this model. */
  fields: ModelEncryptionFieldConfig[];
}

// ---------------------------------------------------------------------------
// Encrypted-field map — the single source of truth
// ---------------------------------------------------------------------------

/**
 * Master configuration of all PII fields that require encryption.
 *
 * - `searchable: true` means a deterministic hash column (`{field}Hash`)
 *   exists to support `WHERE` / unique-index lookups.
 * - JSON/blob fields are never searchable (their data is opaque).
 */
export const PII_FIELD_MAP: ModelEncryptionConfig[] = [
  {
    model: 'User',
    fields: [
      { field: 'email', searchable: true },
      { field: 'phone', searchable: true },
      { field: 'kycData' },
    ],
  },
  {
    model: 'Agent',
    fields: [{ field: 'licenseNumber' }],
  },
  {
    model: 'Payment',
    fields: [
      { field: 'providerTxId' },
      { field: 'callbackData' },
    ],
  },
  {
    model: 'Lead',
    fields: [
      { field: 'phone', searchable: true },
      { field: 'email', searchable: true },
    ],
  },
  {
    model: 'Inquiry',
    fields: [{ field: 'phone' }],
  },
];

// ---------------------------------------------------------------------------
// Service
// ---------------------------------------------------------------------------

@Injectable()
export class FieldEncryptionService {
  private readonly activeConfig: FieldEncryptionConfig | null;
  /** All known key configs, indexed by version — used for decryption. */
  private readonly keysByVersion: Map<number, FieldEncryptionConfig>;
  /** HMAC key derived from the active encryption key (for deterministic hashes). */
  private readonly hmacKey: Buffer | null;
  private readonly enabled: boolean;

  constructor(private readonly logger: LoggerService) {
    const primaryKey = process.env['FIELD_ENCRYPTION_KEY'] ?? process.env['KYC_ENCRYPTION_KEY'];
    const keyVersion = Number(
      process.env['FIELD_ENCRYPTION_KEY_VERSION'] ?? process.env['KYC_ENCRYPTION_KEY_VERSION'] ?? '1',
    );

    if (!primaryKey) {
      this.activeConfig = null;
      this.keysByVersion = new Map();
      this.hmacKey = null;
      this.enabled = false;
      return;
    }

    this.activeConfig = { key: primaryKey, keyVersion: keyVersion };
    this.keysByVersion = new Map([[keyVersion, this.activeConfig]]);

    // Load previous key versions for decryption (FIELD_ENCRYPTION_KEY_PREV_1, _PREV_2, ...)
    for (let i = 1; i <= 10; i++) {
      const prevKey = process.env[`FIELD_ENCRYPTION_KEY_PREV_${i}`];
      const prevVer = Number(process.env[`FIELD_ENCRYPTION_KEY_PREV_${i}_VERSION`] ?? `${keyVersion - i}`);
      if (prevKey) {
        this.keysByVersion.set(prevVer, { key: prevKey, keyVersion: prevVer });
      }
    }

    // Derive a stable HMAC key from the primary encryption key for deterministic hashing.
    // We use HKDF to derive a separate key so the HMAC key is distinct from the encryption key.
    this.hmacKey = crypto.hkdfSync(
      'sha256',
      Buffer.from(primaryKey, 'hex'),
      Buffer.alloc(0), // no salt — deterministic derivation
      Buffer.from('goodgo-field-hash', 'utf8'),
      32,
    ) as unknown as Buffer;

    this.enabled = true;
  }

  /** Whether encryption is configured and active. */
  isEnabled(): boolean {
    return this.enabled;
  }

  /** Encrypt a value using the active key. Returns the `enc:v…:…` string. */
  encrypt(value: unknown): unknown {
    if (!this.activeConfig || value === null || value === undefined) return value;
    return encryptField(value, this.activeConfig);
  }

  /**
   * Decrypt a value. Automatically selects the correct key version from the
   * `enc:v{N}:…` prefix. Falls back to the active key if version lookup fails.
   * Non-encrypted values pass through unchanged (migration-safe).
   */
  decrypt(stored: unknown): unknown {
    if (!this.enabled || stored === null || stored === undefined) return stored;
    if (!isEncrypted(stored)) return stored;

    // Parse version from the stored value
    const version = this.parseVersion(stored as string);
    const config = (version !== null ? this.keysByVersion.get(version) : null) ?? this.activeConfig!;
    return decryptField(stored, config);
  }

  /** Check whether a stored value is already encrypted. */
  isAlreadyEncrypted(value: unknown): boolean {
    return isEncrypted(value);
  }

  /**
   * Compute a deterministic HMAC-SHA256 hash for indexed lookups.
   * The value is normalized (lowercased, trimmed) before hashing.
   */
  computeHash(value: string | null | undefined): string | null {
    if (!this.hmacKey || value === null || value === undefined) return null;
    const normalized = value.toLowerCase().trim();
    return crypto.createHmac('sha256', this.hmacKey).update(normalized).digest('hex');
  }

  /** Log an audit entry for access to encrypted fields. */
  logAccess(
    operation: 'encrypt' | 'decrypt',
    model: string,
    fields: string[],
    recordId?: string,
  ): void {
    this.logger.debug(
      `[field-encryption] ${operation} ${model}.{${fields.join(',')}}${recordId ? ` id=${recordId}` : ''}`,
      'FieldEncryptionService',
    );
  }

  /** Get the full PII field configuration. */
  getFieldMap(): ModelEncryptionConfig[] {
    return PII_FIELD_MAP;
  }

  /** Find encryption config for a specific model. */
  getModelConfig(modelName: string): ModelEncryptionConfig | undefined {
    return PII_FIELD_MAP.find((c) => c.model === modelName);
  }

  // ---------------------------------------------------------------------------
  // Internal
  // ---------------------------------------------------------------------------

  private parseVersion(encrypted: string): number | null {
    // Format: enc:v{N}:{iv}:{authTag}:{ciphertext}
    const match = encrypted.match(/^enc:v(\d+):/);
    return match ? Number(match[1]) : null;
  }
}
