/**
 * Account Encryption Middleware
 *
 * Prisma middleware that transparently encrypts/decrypts OAuth tokens
 * in the Account table using AES-256-GCM encryption.
 *
 * Encryption happens on:
 * - create: New account records
 * - update/updateMany: Token updates
 * - upsert: Both create and update data
 *
 * Decryption happens on:
 * - findUnique/findMany/findFirst: Read operations
 *
 * Format detection:
 * - encryptionVersion field is the primary discriminator
 * - `aes` = AES-256-GCM encrypted
 * - `vault` = OpenBao Transit encrypted (future, Phase 2)
 * - null/undefined = Legacy plaintext (backward compatible)
 */

import { Logger } from "@nestjs/common";
import type { PrismaClient } from "@prisma/client";
import type { VaultService } from "../vault/vault.service";
import { TransitKey } from "../vault/vault.constants";

/**
 * Token fields to encrypt/decrypt in Account model
 */
const TOKEN_FIELDS = ["accessToken", "refreshToken", "idToken"] as const;

/**
 * Prisma middleware parameters interface
 */
interface MiddlewareParams {
  model?: string;
  action: string;
  args: {
    data?: Record<string, unknown>;
    where?: Record<string, unknown>;
    select?: Record<string, unknown>;
    create?: Record<string, unknown>;
    update?: Record<string, unknown>;
  };
  dataPath: string[];
  runInTransaction: boolean;
}

/**
 * Account data with token fields
 */
interface AccountData extends Record<string, unknown> {
  accessToken?: string | null;
  refreshToken?: string | null;
  idToken?: string | null;
  encryptionVersion?: string | null;
}

/**
 * Register account encryption middleware on Prisma client
 *
 * @param prisma - Prisma client instance
 * @param vaultService - Vault service for encryption/decryption
 */
export function registerAccountEncryptionMiddleware(
  prisma: PrismaClient,
  vaultService: VaultService
): void {
  const logger = new Logger("AccountEncryptionMiddleware");

  // TODO: Replace with Prisma Client Extensions (https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions)
  // when stable. Client extensions provide a type-safe alternative to middleware without requiring
  // type assertions or eslint-disable directives. Migration path:
  // 1. Wait for Prisma 6.x stable release with full extension support
  // 2. Create extension using prisma.$extends({ query: { account: { ... } } })
  // 3. Remove this middleware and eslint-disable comments
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
  (prisma as any).$use(
    async (params: MiddlewareParams, next: (params: MiddlewareParams) => Promise<unknown>) => {
      // Only process Account model operations
      if (params.model !== "Account") {
        return next(params);
      }

      // Encrypt on write operations
      if (
        params.action === "create" ||
        params.action === "update" ||
        params.action === "updateMany"
      ) {
        if (params.args.data) {
          await encryptTokens(params.args.data as AccountData, vaultService);
        }
      } else if (params.action === "upsert") {
        // Handle upsert - encrypt both create and update data
        if (params.args.create) {
          await encryptTokens(params.args.create as AccountData, vaultService);
        }
        if (params.args.update) {
          await encryptTokens(params.args.update as AccountData, vaultService);
        }
      }

      // Execute query
      const result = await next(params);

      // Decrypt on read operations
      if (params.action === "findUnique" || params.action === "findFirst") {
        if (result && typeof result === "object") {
          await decryptTokens(result as AccountData, vaultService, logger);
        }
      } else if (params.action === "findMany") {
        if (Array.isArray(result)) {
          for (const account of result) {
            if (account && typeof account === "object") {
              await decryptTokens(account as AccountData, vaultService, logger);
            }
          }
        }
      }

      return result;
    }
  );
}

/**
 * Encrypt token fields in account data
 * Modifies data in-place
 *
 * @param data - Account data object
 * @param vaultService - Vault service
 */
async function encryptTokens(data: AccountData, vaultService: VaultService): Promise<void> {
  let encrypted = false;
  let encryptionVersion: "aes" | "vault" | null = null;

  for (const field of TOKEN_FIELDS) {
    const value = data[field];

    // Skip null/undefined values
    if (value == null) {
      continue;
    }

    // Skip if already encrypted (idempotent)
    if (typeof value === "string" && isEncrypted(value)) {
      continue;
    }

    // Encrypt plaintext value
    if (typeof value === "string") {
      const ciphertext = await vaultService.encrypt(value, TransitKey.ACCOUNT_TOKENS);
      data[field] = ciphertext;
      encrypted = true;

      // Determine encryption version from ciphertext format
      if (ciphertext.startsWith("vault:v1:")) {
        encryptionVersion = "vault";
      } else {
        encryptionVersion = "aes";
      }
    }
  }

  // Mark encryption version if any tokens were encrypted
  if (encrypted && encryptionVersion) {
    data.encryptionVersion = encryptionVersion;
  }
}

/**
 * Decrypt token fields in account record
 * Modifies record in-place
 *
 * Uses encryptionVersion field as primary discriminator to determine
 * if decryption is needed, falling back to pattern matching for
 * records without the field (migration compatibility).
 *
 * Throws errors on decryption failure to prevent silent corruption.
 *
 * @param account - Account record
 * @param vaultService - Vault service
 * @param _logger - NestJS logger (unused, kept for compatibility with middleware signature)
 * @throws Error with user-facing message when decryption fails
 */
async function decryptTokens(
  account: AccountData,
  vaultService: VaultService,
  _logger: Logger
): Promise<void> {
  // Check encryptionVersion field first (primary discriminator)
  const shouldDecrypt =
    account.encryptionVersion === "aes" || account.encryptionVersion === "vault";

  for (const field of TOKEN_FIELDS) {
    const value = account[field];

    // Skip null/undefined values
    if (value == null) {
      continue;
    }

    if (typeof value === "string") {
      // Primary path: Use encryptionVersion field
      if (shouldDecrypt) {
        try {
          account[field] = await vaultService.decrypt(value, TransitKey.ACCOUNT_TOKENS);
        } catch (error) {
          const errorMsg = error instanceof Error ? error.message : "Unknown error";
          throw new Error(
            `Failed to decrypt account credentials. Please reconnect this account. Details: ${errorMsg}`
          );
        }
      }
      // Fallback: For records without encryptionVersion (migration compatibility)
      else if (!account.encryptionVersion && isEncrypted(value)) {
        try {
          account[field] = await vaultService.decrypt(value, TransitKey.ACCOUNT_TOKENS);
        } catch (error) {
          const errorMsg = error instanceof Error ? error.message : "Unknown error";
          throw new Error(
            `Failed to decrypt account credentials. Please reconnect this account. Details: ${errorMsg}`
          );
        }
      }
      // Legacy plaintext (no encryptionVersion) - pass through unchanged
    }
  }
}

/**
 * Check if a value is encrypted (any format)
 *
 * @param value - String value to check
 * @returns true if value appears to be encrypted
 */
function isEncrypted(value: string): boolean {
  if (!value || typeof value !== "string") {
    return false;
  }

  // AES format: iv:authTag:encrypted (3 colon-separated hex parts)
  if (isAESEncrypted(value)) {
    return true;
  }

  // Vault format: vault:v1:...
  if (value.startsWith("vault:v1:")) {
    return true;
  }

  return false;
}

/**
 * Check if a value is AES-256-GCM encrypted
 *
 * @param value - String value to check
 * @returns true if value is in AES format
 */
function isAESEncrypted(value: string): boolean {
  if (!value || typeof value !== "string") {
    return false;
  }

  // AES format: iv:authTag:encrypted (3 parts, all hex)
  const parts = value.split(":");
  if (parts.length !== 3) {
    return false;
  }

  // Verify all parts are hex strings
  return parts.every((part) => /^[0-9a-f]+$/i.test(part));
}
