import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import * as crypto from 'crypto'
import { setPiiEncryption } from './pii-plugin'

const PREFIX = 'enc:'

@Injectable()
export class EncryptionService implements OnModuleInit {
  private readonly logger = new Logger(EncryptionService.name)
  private readonly key:     Buffer   // 32 bytes — AES-256
  private readonly hmacKey: Buffer   // 32 bytes — HMAC-SHA256

  constructor(private readonly config: ConfigService) {
    const keyHex  = config.get<string>('ENCRYPTION_KEY')  ?? ''
    const hmacHex = config.get<string>('ENCRYPTION_HMAC_KEY') ?? ''

    if (!keyHex || !hmacHex) {
      throw new Error(
        'ENCRYPTION_KEY and ENCRYPTION_HMAC_KEY must be set in environment. ' +
        'Generate with: node -e "const c=require(\'crypto\'); ' +
        'console.log(c.randomBytes(32).toString(\'hex\'))"',
      )
    }

    this.key     = Buffer.from(keyHex,  'hex')
    this.hmacKey = Buffer.from(hmacHex, 'hex')

    if (this.key.length !== 32 || this.hmacKey.length !== 32) {
      throw new Error('Encryption keys must be exactly 32 bytes (64 hex characters)')
    }
  }

  onModuleInit() {
    // Register this instance as the singleton used by the Mongoose plugin
    setPiiEncryption(this)
    this.logger.log('PII encryption initialised — AES-256-GCM active')
  }

  // ── Encrypt ──────────────────────────────────────────────────────────────

  encrypt(plaintext: string): string {
    if (!plaintext || this.isEncrypted(plaintext)) return plaintext
    const iv         = crypto.randomBytes(12)                           // 96-bit IV for GCM
    const cipher     = crypto.createCipheriv('aes-256-gcm', this.key, iv)
    const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
    const authTag    = cipher.getAuthTag()
    return `${PREFIX}${iv.toString('hex')}:${authTag.toString('hex')}:${ciphertext.toString('hex')}`
  }

  // ── Decrypt ──────────────────────────────────────────────────────────────

  decrypt(value: string): string {
    if (!value || !this.isEncrypted(value)) return value
    try {
      const rest    = value.slice(PREFIX.length)
      const [ivHex, tagHex, ctHex] = rest.split(':')
      const iv         = Buffer.from(ivHex,  'hex')
      const authTag    = Buffer.from(tagHex,  'hex')
      const ciphertext = Buffer.from(ctHex,   'hex')
      const decipher   = crypto.createDecipheriv('aes-256-gcm', this.key, iv)
      decipher.setAuthTag(authTag)
      return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8')
    } catch {
      this.logger.warn('Decryption failed — returning raw value (may be legacy unencrypted)')
      return value
    }
  }

  // ── Hash (deterministic, for search index) ────────────────────────────────

  /**
   * HMAC-SHA256 of a normalised (lowercase + trimmed) value.
   * Same input always produces the same output — safe for keyword index lookups.
   */
  hash(value: string): string {
    if (!value) return ''
    return crypto.createHmac('sha256', this.hmacKey)
      .update(value.toLowerCase().trim())
      .digest('hex')
  }

  /**
   * Tokenise a string (split on whitespace + punctuation) and return
   * an array of HMAC hashes — one per token.  Used to build keyword indexes.
   */
  tokenHashes(value: string): string[] {
    if (!value) return []
    return value
      .toLowerCase()
      .split(/[\s,.\-–—/\\|]+/)
      .filter(t => t.length > 1)
      .map(t => this.hash(t))
  }

  isEncrypted(value: string): boolean {
    return typeof value === 'string' && value.startsWith(PREFIX)
  }

  // ── Helpers for services ─────────────────────────────────────────────────

  /** Encrypt every non-null string value in an object (shallow). */
  encryptObject<T extends Record<string, any>>(obj: T, fields: (keyof T)[]): T {
    const result = { ...obj } as any
    for (const field of fields) {
      if (typeof result[field] === 'string' && result[field]) {
        result[field] = this.encrypt(result[field])
      }
    }
    return result
  }

  /** Decrypt every specified field in an object (shallow). */
  decryptObject<T extends Record<string, any>>(obj: T, fields: (keyof T)[]): T {
    const result = { ...obj } as any
    for (const field of fields) {
      if (typeof result[field] === 'string') {
        result[field] = this.decrypt(result[field])
      }
    }
    return result
  }
}
