/**
 * Prisma query-extension that transparently encrypts PII fields on write and
 * decrypts them on read. Works via Prisma `$extends({ query: … })`.
 *
 * Design principles:
 *  - Zero changes required in business logic / repositories
 *  - Searchable fields also get a `{field}Hash` written on create/update
 *  - Decryption is applied to all query results automatically
 *  - Non-encrypted (plaintext) values pass through unchanged — safe for
 *    incremental migration
 */

import { Prisma } from '@prisma/client';
import {
  type FieldEncryptionService,
  type ModelEncryptionConfig,
  type ModelEncryptionFieldConfig,
} from './field-encryption.service';

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

function encryptDataObject(
  data: Record<string, unknown>,
  fields: ModelEncryptionFieldConfig[],
  service: FieldEncryptionService,
): void {
  const encryptedFields: string[] = [];

  for (const fieldConfig of fields) {
    const value = data[fieldConfig.field];
    if (value === undefined || value === null) continue;

    // Skip if already encrypted (idempotent)
    if (service.isAlreadyEncrypted(value)) continue;

    // Compute deterministic hash for searchable fields BEFORE encryption
    if (fieldConfig.searchable && typeof value === 'string') {
      data[`${fieldConfig.field}Hash`] = service.computeHash(value);
    }

    data[fieldConfig.field] = service.encrypt(value);
    encryptedFields.push(fieldConfig.field);
  }

  if (encryptedFields.length > 0) {
    service.logAccess('encrypt', 'write', encryptedFields);
  }
}

function decryptRow(
  row: Record<string, unknown>,
  fields: ModelEncryptionFieldConfig[],
  service: FieldEncryptionService,
): void {
  const decryptedFields: string[] = [];

  for (const fieldConfig of fields) {
    const value = row[fieldConfig.field];
    if (value === undefined || value === null) continue;

    if (service.isAlreadyEncrypted(value)) {
      row[fieldConfig.field] = service.decrypt(value);
      decryptedFields.push(fieldConfig.field);
    }
  }

  if (decryptedFields.length > 0) {
    service.logAccess('decrypt', 'read', decryptedFields);
  }
}

function decryptResult(
  result: unknown,
  config: ModelEncryptionConfig,
  service: FieldEncryptionService,
): void {
  if (Array.isArray(result)) {
    for (const item of result) {
      if (typeof item === 'object' && item !== null) {
        decryptRow(item as Record<string, unknown>, config.fields, service);
      }
    }
  } else if (typeof result === 'object' && result !== null) {
    decryptRow(result as Record<string, unknown>, config.fields, service);
  }
}

// ---------------------------------------------------------------------------
// Write-args encryption
// ---------------------------------------------------------------------------

function encryptWriteArgs(
  args: Record<string, unknown>,
  action: string,
  config: ModelEncryptionConfig,
  service: FieldEncryptionService,
): void {
  if (action === 'createMany' || action === 'createManyAndReturn') {
    const data = args['data'];
    if (Array.isArray(data)) {
      for (const row of data) {
        encryptDataObject(row as Record<string, unknown>, config.fields, service);
      }
    }
    return;
  }

  if (action === 'upsert') {
    const create = args['create'] as Record<string, unknown> | undefined;
    const update = args['update'] as Record<string, unknown> | undefined;
    if (create) encryptDataObject(create, config.fields, service);
    if (update) encryptDataObject(update, config.fields, service);
    return;
  }

  // create, update, updateMany — args.data
  const data = args['data'] as Record<string, unknown> | undefined;
  if (data) {
    encryptDataObject(data, config.fields, service);
  }
}

// Prisma actions that write data
const WRITE_ACTIONS = new Set([
  'create',
  'createMany',
  'createManyAndReturn',
  'update',
  'updateMany',
  'upsert',
]);

// Prisma actions whose results we should decrypt
const READ_ACTIONS = new Set([
  'findUnique',
  'findUniqueOrThrow',
  'findFirst',
  'findFirstOrThrow',
  'findMany',
  'create',
  'createManyAndReturn',
  'update',
  'upsert',
  'delete',
]);

// ---------------------------------------------------------------------------
// Public: create the Prisma extension
// ---------------------------------------------------------------------------

/**
 * Creates a Prisma query extension for field-level encryption.
 *
 * Usage inside PrismaService:
 * ```ts
 * const extended = prisma.$extends(createEncryptionExtension(service));
 * ```
 */
export function createEncryptionExtension(service: FieldEncryptionService) {
  // Build a fast lookup: lowercase model name → config
  const modelLookup = new Map<string, ModelEncryptionConfig>();
  for (const config of service.getFieldMap()) {
    modelLookup.set(config.model.toLowerCase(), config);
  }

  return Prisma.defineExtension({
    name: 'field-encryption',
    query: {
      $allModels: {
        async $allOperations({ model, operation, args, query }) {
          // Look up encryption config for this model
          const config = model ? modelLookup.get(model.toLowerCase()) : undefined;

          if (!config || !service.isEnabled()) {
            return query(args);
          }

          // Encrypt on write
          if (WRITE_ACTIONS.has(operation) && args) {
            encryptWriteArgs(args as Record<string, unknown>, operation, config, service);
          }

          const result = await query(args);

          // Decrypt on read
          if (READ_ACTIONS.has(operation) && result !== null && result !== undefined) {
            decryptResult(result, config, service);
          }

          return result;
        },
      },
    },
  });
}
