// Ledger Engine - Double-Entry and Multi-Entry Ledger

import { Decimal } from '@prisma/client/runtime/library';
import {
  LedgerEntry,
  LedgerEntryType,
  LedgerEntryStatus,
  AssetType,
  MultiEntryTransaction,
} from '@/shared/types';
import { encryptionService } from '@/infrastructure/encryption/encryption.service';
import { DbisError, ErrorCode } from '@/shared/types';
import prisma from '@/shared/database/prisma';

export interface LedgerPostingResult {
  entryIds: string[];
  blockHash: string;
  status: LedgerEntryStatus;
}

/**
 * Ledger Service
 * Handles double-entry and multi-entry ledger operations with hash chaining
 */
export class LedgerService {
  /**
   * Post a single double-entry transaction
   */
  async postDoubleEntry(
    ledgerId: string,
    debitAccountId: string,
    creditAccountId: string,
    amount: string,
    currencyCode: string,
    assetType: AssetType,
    transactionType: LedgerEntryType,
    referenceId: string,
    fxRate?: string,
    metadata?: Record<string, unknown>
  ): Promise<LedgerPostingResult> {
    // Validate accounts exist
    const debitAccount = await prisma.bank_accounts.findUnique({
      where: { id: debitAccountId },
    });
    const creditAccount = await prisma.bank_accounts.findUnique({
      where: { id: creditAccountId },
    });

    if (!debitAccount || !creditAccount) {
      throw new DbisError(ErrorCode.NOT_FOUND, 'Account not found');
    }

    // Validate balance (for debit account)
    const amountDecimal = new Decimal(amount);
    if (debitAccount.availableBalance.lt(amountDecimal)) {
      throw new DbisError(ErrorCode.VALIDATION_ERROR, 'Insufficient balance');
    }

    // Get previous entry for hash chaining
    const previousEntry = await prisma.ledger_entries.findFirst({
      where: { ledgerId },
      orderBy: { timestampUtc: 'desc' },
    });

    const previousHash = previousEntry?.blockHash || null;

    // Create ledger entry
    const timestampUtc = new Date();
    const entryData = {
      id: '',
      ledgerId,
      debitAccountId,
      creditAccountId,
      amount: amountDecimal,
      currencyCode,
      timestampUtc,
      previousHash: previousHash || '',
    };

    // Calculate block hash
    const blockHash = encryptionService.hashLedgerEntry(entryData);

    // Create entry in database
    const entry = await prisma.ledger_entries.create({
      data: {
        id: uuidv4(),
        ledgerId,
        debitAccountId,
        creditAccountId,
        amount: amountDecimal,
        currencyCode,
        fxRate: fxRate ? new Decimal(fxRate) : null,
        assetType,
        transactionType,
        referenceId,
        timestampUtc,
        blockHash,
        previousHash,
        status: LedgerEntryStatus.POSTED,
        metadata: metadata ? JSON.parse(JSON.stringify(metadata)) : null,
        createdAt: new Date(),
        updatedAt: new Date(),
      },
    });

    // Update account balances
    await prisma.bank_accounts.update({
      where: { id: debitAccountId },
      data: {
        balance: debitAccount.balance.minus(amountDecimal),
        availableBalance: debitAccount.availableBalance.minus(amountDecimal),
      },
    });

    await prisma.bank_accounts.update({
      where: { id: creditAccountId },
      data: {
        balance: creditAccount.balance.plus(amountDecimal),
        availableBalance: creditAccount.availableBalance.plus(amountDecimal),
      },
    });

    return {
      entryIds: [entry.id],
      blockHash,
      status: LedgerEntryStatus.POSTED,
    };
  }

  /**
   * Post a multi-entry transaction
   */
  async postMultiEntry(
    ledgerId: string,
    transaction: MultiEntryTransaction,
    transactionType: LedgerEntryType,
    fxRates?: Record<string, string>,
    metadata?: Record<string, unknown>
  ): Promise<LedgerPostingResult> {
    // Validate all accounts exist and have sufficient balance
    for (const entry of transaction.entries) {
      const account = await prisma.bank_accounts.findUnique({
        where: { id: entry.debitAccountId },
      });

      if (!account) {
        throw new DbisError(ErrorCode.NOT_FOUND, `Account not found: ${entry.debitAccountId}`);
      }

      const amountDecimal = new Decimal(entry.amount);
      if (account.availableBalance.lt(amountDecimal)) {
        throw new DbisError(
          ErrorCode.VALIDATION_ERROR,
          `Insufficient balance in account: ${entry.debitAccountId}`
        );
      }
    }

    // Get previous entry for hash chaining
    const previousEntry = await prisma.ledger_entries.findFirst({
      where: { ledgerId },
      orderBy: { timestampUtc: 'desc' },
    });

    const previousHash = previousEntry?.blockHash || null;
    const timestampUtc = new Date();
    const entryIds: string[] = [];

    // Create all entries in a transaction
    await prisma.$transaction(async (tx) => {
      let currentPreviousHash = previousHash;

      for (const entry of transaction.entries) {
        const debitAccount = await tx.bankAccount.findUnique({
          where: { id: entry.debitAccountId },
        });
        const creditAccount = await tx.bankAccount.findUnique({
          where: { id: entry.creditAccountId },
        });

        if (!debitAccount || !creditAccount) {
          throw new DbisError(ErrorCode.NOT_FOUND, 'Account not found');
        }

        const amountDecimal = new Decimal(entry.amount);
        const fxRate = fxRates?.[entry.currencyCode];

        const entryData = {
          id: '',
          ledgerId,
          debitAccountId: entry.debitAccountId,
          creditAccountId: entry.creditAccountId,
          amount: entry.amount,
          currencyCode: entry.currencyCode,
          timestampUtc,
          previousHash: currentPreviousHash || '',
        };

        const blockHash = encryptionService.hashLedgerEntry(entryData);

        const ledgerEntry = await tx.ledgerEntry.create({
          data: {
            ledgerId,
            debitAccountId: entry.debitAccountId,
            creditAccountId: entry.creditAccountId,
            amount: amountDecimal,
            currencyCode: entry.currencyCode,
            fxRate: fxRate ? new Decimal(fxRate) : null,
            assetType: entry.assetType,
            transactionType,
            referenceId: transaction.referenceId,
            timestampUtc,
            blockHash,
            previousHash: currentPreviousHash,
            status: LedgerEntryStatus.POSTED,
            metadata: metadata ? JSON.parse(JSON.stringify(metadata)) : null,
          },
        });

        entryIds.push(ledgerEntry.id);

        // Update account balances
        await tx.bankAccount.update({
          where: { id: entry.debitAccountId },
          data: {
            balance: debitAccount.balance.minus(amountDecimal),
            availableBalance: debitAccount.availableBalance.minus(amountDecimal),
          },
        });

        await tx.bankAccount.update({
          where: { id: entry.creditAccountId },
          data: {
            balance: creditAccount.balance.plus(amountDecimal),
            availableBalance: creditAccount.availableBalance.plus(amountDecimal),
          },
        });

        currentPreviousHash = blockHash;
      }
    });

    // Calculate final block hash for the multi-entry transaction
    const finalBlockHash = encryptionService.hash(
      JSON.stringify({ referenceId: transaction.referenceId, entryIds })
    );

    return {
      entryIds,
      blockHash: finalBlockHash,
      status: LedgerEntryStatus.POSTED,
    };
  }

  /**
   * Get ledger entries by reference ID
   */
  async getEntriesByReference(referenceId: string): Promise<LedgerEntry[]> {
    const entries = await prisma.ledger_entries.findMany({
      where: { referenceId },
      orderBy: { timestampUtc: 'asc' },
    });

    return entries.map(this.mapToLedgerEntry);
  }

  /**
   * Get ledger entries by ledger ID
   */
  async getEntriesByLedger(
    ledgerId: string,
    limit: number = 100,
    offset: number = 0
  ): Promise<LedgerEntry[]> {
    const entries = await prisma.ledger_entries.findMany({
      where: { ledgerId },
      orderBy: { timestampUtc: 'desc' },
      take: limit,
      skip: offset,
    });

    return entries.map(this.mapToLedgerEntry);
  }

  /**
   * Verify ledger integrity by checking hash chain
   */
  async verifyLedgerIntegrity(ledgerId: string): Promise<boolean> {
    const entries = await prisma.ledger_entries.findMany({
      where: { ledgerId },
      orderBy: { timestampUtc: 'asc' },
    });

    if (entries.length === 0) {
      return true;
    }

    let previousHash: string | null = null;

    for (const entry of entries) {
      const entryData = {
        id: entry.id,
        ledgerId: entry.ledgerId,
        debitAccountId: entry.debitAccountId,
        creditAccountId: entry.creditAccountId,
        amount: entry.amount.toString(),
        currencyCode: entry.currencyCode,
        timestampUtc: entry.timestampUtc,
        previousHash: previousHash || '',
      };

      const computedHash = encryptionService.hashLedgerEntry(entryData);

      if (computedHash !== entry.blockHash) {
        return false;
      }

      if (entry.previousHash !== previousHash) {
        return false;
      }

      previousHash = entry.blockHash;
    }

    return true;
  }

  /**
   * Reverse a ledger entry
   */
  async reverseEntry(entryId: string, reason: string): Promise<LedgerPostingResult> {
    const entry = await prisma.ledger_entries.findUnique({
      where: { id: entryId },
    });

    if (!entry) {
      throw new DbisError(ErrorCode.NOT_FOUND, 'Ledger entry not found');
    }

    if (entry.status === LedgerEntryStatus.REVERSED) {
      throw new DbisError(ErrorCode.VALIDATION_ERROR, 'Entry already reversed');
    }

    // Create reversal entry
    const reversalResult = await this.postDoubleEntry(
      entry.ledgerId,
      entry.creditAccountId, // Swap debit and credit
      entry.debitAccountId,
      entry.amount.toString(),
      entry.currencyCode,
      entry.assetType as AssetType,
      entry.transactionType as LedgerEntryType,
      `REV-${entry.referenceId}`,
      entry.fxRate?.toString(),
      { originalEntryId: entryId, reason }
    );

    // Mark original entry as reversed
    await prisma.ledger_entries.update({
      where: { id: entryId },
      data: { status: LedgerEntryStatus.REVERSED },
    });

    return reversalResult;
  }

  /**
   * Map Prisma model to LedgerEntry type
   */
  private mapToLedgerEntry(entry: any): LedgerEntry {
    return {
      id: entry.id,
      ledgerId: entry.ledgerId,
      debitAccountId: entry.debitAccountId,
      creditAccountId: entry.creditAccountId,
      amount: entry.amount.toString(),
      currencyCode: entry.currencyCode,
      fxRate: entry.fxRate?.toString(),
      assetType: entry.assetType as AssetType,
      transactionType: entry.transactionType as LedgerEntryType,
      referenceId: entry.referenceId,
      timestampUtc: entry.timestampUtc,
      blockHash: entry.blockHash,
      previousHash: entry.previousHash || undefined,
      auditFlag: entry.auditFlag,
      amlRiskScore: entry.amlRiskScore || undefined,
      status: entry.status as LedgerEntryStatus,
      metadata: entry.metadata as Record<string, unknown> | undefined,
    };
  }
}

export const ledgerService = new LedgerService();

