/**
 * Mongoose PII plugin
 *
 * Applies transparent field-level AES-256-GCM encryption to specified
 * schema fields.  A keyword index (HMAC token array) is maintained so
 * search works without exposing plaintext values in the database.
 *
 * Usage:
 *   schema.plugin(piiPlugin, {
 *     fields:      ['displayName', 'email', 'phone'],   // fields to encrypt
 *     searchable:  ['displayName', 'email'],            // subset that get keyword tokens
 *   })
 *
 * The EncryptionService singleton must be registered (via setPiiEncryption)
 * before any document is saved or retrieved.  EncryptionService.onModuleInit
 * does this automatically.
 */

import type { Schema } from 'mongoose'
import type { EncryptionService } from './encryption.service'

// ── Singleton reference ───────────────────────────────────────────────────────

let _enc: EncryptionService | null = null

export function setPiiEncryption(svc: EncryptionService): void {
  _enc = svc
}

export function _getEnc(): EncryptionService | null {
  return _enc
}

// ── Plugin ────────────────────────────────────────────────────────────────────

export interface PiiPluginOptions {
  fields:     string[]    // all fields to encrypt
  searchable: string[]    // subset whose tokens contribute to the keywords index
}

export function piiPlugin(schema: Schema, opts: PiiPluginOptions): void {
  const { fields, searchable } = opts

  // Add keyword index field if any searchable fields declared
  if (searchable.length > 0) {
    schema.add({ keywords: { type: [String], default: [], index: true, select: false } })
  }

  // ── pre-save: encrypt + rebuild keyword index ─────────────────────────────
  schema.pre('save', function (next) {
    if (!_enc) return next()

    const kw = new Set<string>()

    for (const field of fields) {
      const val: any = (this as any)[field]
      if (typeof val === 'string' && val) {
        if (searchable.includes(field)) {
          _enc.tokenHashes(val).forEach(h => kw.add(h))
        }
        ;(this as any)[field] = _enc.encrypt(val)
      }
    }

    if (searchable.length > 0) {
      ;(this as any).keywords = Array.from(kw)
    }

    next()
  })

  // ── post-init: decrypt when document is hydrated from DB ─────────────────
  schema.post('init', function (doc: any) {
    if (!_enc) return
    for (const field of fields) {
      if (typeof doc[field] === 'string') {
        doc[field] = _enc.decrypt(doc[field])
      }
    }
  })
}

// ── Utility: decrypt a lean() result (plain object) ──────────────────────────

export function decryptLean<T extends Record<string, any>>(
  obj: T,
  fields: string[],
): T {
  if (!_enc || !obj) return obj
  const result = { ...obj } as any
  for (const field of fields) {
    if (typeof result[field] === 'string') {
      result[field] = _enc.decrypt(result[field])
    }
  }
  return result as T
}

export function decryptLeanArray<T extends Record<string, any>>(
  arr: T[],
  fields: string[],
): T[] {
  return arr.map(item => decryptLean(item, fields))
}
