// Reconciliation Service
// Generate reconciliation reports for Nostro/Vostro accounts

import { Prisma } from '@prisma/client';
import prisma from '@/shared/database/prisma';
import { Decimal } from '@prisma/client/runtime/library';
import { v4 as uuidv4 } from 'uuid';
import { DbisError, ErrorCode } from '@/shared/types';
import {
  NostroVostroReconciliation,
  ReconciliationRequest,
  ReconciliationBreak,
} from './nostro-vostro.types';

export class ReconciliationService {
  /**
   * Request reconciliation report
   */
  async requestReconciliation(request: ReconciliationRequest): Promise<NostroVostroReconciliation> {
    // Verify participant exists
    const participant = await prisma.nostro_vostro_participants.findUnique({
      where: { participantId: request.participantId },
    });

    if (!participant) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Participant ${request.participantId} not found`);
    }

    const asOfDate = new Date(request.asOfDate);
    const reportId = `REC-${uuidv4()}`;

    // Get accounts for reconciliation
    const accountFilter: any = {
      ownerParticipantId: request.participantId,
    };

    if (request.accountId) {
      accountFilter.accountId = request.accountId;
    }

    const accounts = await prisma.nostro_vostro_accounts.findMany({
      where: accountFilter,
    });

    if (accounts.length === 0) {
      throw new DbisError(ErrorCode.NOT_FOUND, `No accounts found for participant ${request.participantId}`);
    }

    // Calculate reconciliation for each account
    let totalOpeningBalance = new Decimal(0);
    let totalClosingBalance = new Decimal(0);
    let totalDebits = new Decimal(0);
    let totalCredits = new Decimal(0);
    const breaks: ReconciliationBreak[] = [];

    for (const account of accounts) {
      // Get opening balance (balance at start of asOfDate)
      const openingBalance = await this.getOpeningBalance(account.accountId, asOfDate);

      // Get closing balance (current balance)
      const closingBalance = account.currentBalance;

      // Get transactions for the day
      const startOfDay = new Date(asOfDate);
      startOfDay.setHours(0, 0, 0, 0);
      const endOfDay = new Date(asOfDate);
      endOfDay.setHours(23, 59, 59, 999);

      const transactions = await prisma.nostro_vostro_transfers.findMany({
        where: {
          OR: [
            { fromAccountId: account.accountId },
            { toAccountId: account.accountId },
          ],
          valueDate: {
            gte: startOfDay,
            lte: endOfDay,
          },
          status: 'SETTLED',
        },
      });

      // Calculate debits and credits
      let accountDebits = new Decimal(0);
      let accountCredits = new Decimal(0);

      for (const tx of transactions) {
        if (tx.fromAccountId === account.accountId) {
          accountDebits = accountDebits.plus(tx.amount);
        }
        if (tx.toAccountId === account.accountId) {
          accountCredits = accountCredits.plus(tx.amount);
        }
      }

      // Calculate expected closing balance
      const expectedClosing = openingBalance.plus(accountCredits).minus(accountDebits);

      // Check for breaks
      if (!closingBalance.equals(expectedClosing)) {
        const difference = closingBalance.minus(expectedClosing);
        breaks.push({
          breakType: 'BALANCE_MISMATCH',
          description: `Account ${account.accountId} balance mismatch`,
          amount: difference.abs().toString(),
          transactionId: undefined,
          details: {
            accountId: account.accountId,
            expectedBalance: expectedClosing.toString(),
            actualBalance: closingBalance.toString(),
            difference: difference.toString(),
          },
        });
      }

      // Check for pending transfers that should have been settled
      const pendingTransfers = await prisma.nostro_vostro_transfers.findMany({
        where: {
          OR: [
            { fromAccountId: account.accountId },
            { toAccountId: account.accountId },
          ],
          valueDate: {
            lte: asOfDate,
          },
          status: {
            in: ['PENDING', 'ACCEPTED'],
          },
        },
      });

      if (pendingTransfers.length > 0) {
        breaks.push({
          breakType: 'PENDING_SETTLEMENT',
          description: `${pendingTransfers.length} pending transfers found`,
          amount: undefined,
          transactionId: undefined,
          details: {
            accountId: account.accountId,
            pendingCount: pendingTransfers.length,
            transferIds: pendingTransfers.map((t) => t.transferId),
          },
        });
      }

      totalOpeningBalance = totalOpeningBalance.plus(openingBalance);
      totalClosingBalance = totalClosingBalance.plus(closingBalance);
      totalDebits = totalDebits.plus(accountDebits);
      totalCredits = totalCredits.plus(accountCredits);
    }

    // Create reconciliation record
    const reconciliation = await prisma.nostro_vostro_reconciliations.create({
      data: {
        reportId,
        participantId: request.participantId,
        asOfDate,
        openingBalance: totalOpeningBalance,
        closingBalance: totalClosingBalance,
        totalDebits,
        totalCredits,
        breakCount: breaks.length,
        status: breaks.length === 0 ? 'COMPLETED' : 'COMPLETED', // Still completed even with breaks
        breaks: breaks.length > 0 ? breaks : undefined,
        metadata: (request.metadata || {}) as Prisma.InputJsonValue,
        completedAt: new Date(),
      },
    });

    return this.mapToReconciliation(reconciliation);
  }

  /**
   * Get reconciliation report by ID
   */
  async getReconciliation(reportId: string): Promise<NostroVostroReconciliation | null> {
    const reconciliation = await prisma.nostro_vostro_reconciliations.findUnique({
      where: { reportId },
    });

    return reconciliation ? this.mapToReconciliation(reconciliation) : null;
  }

  /**
   * List reconciliations for a participant
   */
  async listReconciliations(
    participantId: string,
    filters?: {
      startDate?: Date;
      endDate?: Date;
      status?: string;
      page?: number;
      pageSize?: number;
    }
  ): Promise<{
    data: NostroVostroReconciliation[];
    pagination: {
      page: number;
      pageSize: number;
      total: number;
      totalPages: number;
    };
  }> {
    const page = filters?.page || 1;
    const pageSize = filters?.pageSize || 50;
    const skip = (page - 1) * pageSize;

    const where: any = {
      participantId,
    };

    if (filters?.startDate || filters?.endDate) {
      where.asOfDate = {};
      if (filters.startDate) where.asOfDate.gte = filters.startDate;
      if (filters.endDate) where.asOfDate.lte = filters.endDate;
    }

    if (filters?.status) {
      where.status = filters.status;
    }

    const [reconciliations, total] = await Promise.all([
      prisma.nostro_vostro_reconciliations.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { asOfDate: 'desc' },
      }),
      prisma.nostro_vostro_reconciliations.count({ where }),
    ]);

    return {
      data: reconciliations.map((r) => this.mapToReconciliation(r)),
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  /**
   * Get opening balance for an account at a specific date
   */
  private async getOpeningBalance(accountId: string, asOfDate: Date): Promise<Decimal> {
    // Get the last balance history entry before asOfDate
    const startOfDay = new Date(asOfDate);
    startOfDay.setHours(0, 0, 0, 0);

    const lastHistory = await prisma.nostroVostroBalanceHistory.findFirst({
      where: {
        accountId,
        recordedAt: {
          lt: startOfDay,
        },
      },
      orderBy: {
        recordedAt: 'desc',
      },
    });

    if (lastHistory) {
      return lastHistory.balance;
    }

    // If no history, get account's current balance and subtract transactions from that day
    const account = await prisma.nostro_vostro_accounts.findUnique({
      where: { accountId },
    });

    if (!account) {
      return new Decimal(0);
    }

    // Calculate opening balance by reversing transactions from asOfDate
    const startOfDayDate = new Date(asOfDate);
    startOfDayDate.setHours(0, 0, 0, 0);
    const endOfDayDate = new Date(asOfDate);
    endOfDayDate.setHours(23, 59, 59, 999);

    const transactions = await prisma.nostro_vostro_transfers.findMany({
      where: {
        OR: [
          { fromAccountId: accountId },
          { toAccountId: accountId },
        ],
        valueDate: {
          gte: startOfDayDate,
          lte: endOfDayDate,
        },
        status: 'SETTLED',
      },
    });

    let openingBalance = account.currentBalance;

    for (const tx of transactions) {
      if (tx.fromAccountId === accountId) {
        openingBalance = openingBalance.plus(tx.amount); // Reverse debit
      }
      if (tx.toAccountId === accountId) {
        openingBalance = openingBalance.minus(tx.amount); // Reverse credit
      }
    }

    return openingBalance;
  }

  /**
   * Map Prisma model to Reconciliation type
   */
  private mapToReconciliation(reconciliation: any): NostroVostroReconciliation {
    return {
      reportId: reconciliation.reportId,
      participantId: reconciliation.participantId,
      asOfDate: reconciliation.asOfDate,
      openingBalance: reconciliation.openingBalance.toString(),
      closingBalance: reconciliation.closingBalance.toString(),
      totalDebits: reconciliation.totalDebits.toString(),
      totalCredits: reconciliation.totalCredits.toString(),
      breakCount: reconciliation.breakCount,
      status: reconciliation.status as 'PENDING' | 'COMPLETED' | 'FAILED',
      breaks: reconciliation.breaks as ReconciliationBreak[] | undefined,
      metadata: reconciliation.metadata as Record<string, unknown> | undefined,
      completedAt: reconciliation.completedAt || undefined,
      createdAt: reconciliation.createdAt,
      updatedAt: reconciliation.updatedAt,
    };
  }
}

export const reconciliationService = new ReconciliationService();

