// Nostro/Vostro Service
// Core business logic for participant, account, and transfer management

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 {
  Participant,
  ParticipantCreateRequest,
  NostroVostroAccount,
  AccountCreateRequest,
  BalanceResponse,
  NostroVostroTransfer,
  TransferCreateRequest,
  TransferStatus,
  SettlementAsset,
} from './nostro-vostro.types';

export class NostroVostroService {
  // ============================================================================
  // Participant Management
  // ============================================================================

  /**
   * Create a new participant
   */
  async createParticipant(request: ParticipantCreateRequest): Promise<Participant> {
    const participantId = request.participantId || `PART-${uuidv4()}`;

    // Check if participantId already exists
    const existing = await prisma.nostro_vostro_participants.findUnique({
      where: { participantId },
    });

    if (existing) {
      throw new DbisError(ErrorCode.VALIDATION_ERROR, `Participant with ID ${participantId} already exists`);
    }

    // Check BIC uniqueness if provided
    if (request.bic) {
      const existingBic = await prisma.nostro_vostro_participants.findUnique({
        where: { bic: request.bic },
      });
      if (existingBic) {
        throw new DbisError(ErrorCode.VALIDATION_ERROR, `Participant with BIC ${request.bic} already exists`);
      }
    }

    // Check LEI uniqueness if provided
    if (request.lei) {
      const existingLei = await prisma.nostro_vostro_participants.findUnique({
        where: { lei: request.lei },
      });
      if (existingLei) {
        throw new DbisError(ErrorCode.VALIDATION_ERROR, `Participant with LEI ${request.lei} already exists`);
      }
    }

    const participant = await prisma.nostro_vostro_participants.create({
      data: {
        participantId,
        name: request.name,
        bic: request.bic,
        lei: request.lei,
        country: request.country,
        regulatoryTier: request.regulatoryTier,
        sovereignBankId: request.sovereignBankId,
        status: 'active',
        metadata: (request.metadata || {}) as Prisma.InputJsonValue,
      },
    });

    return this.mapToParticipant(participant);
  }

  /**
   * Get participant by ID
   */
  async getParticipant(participantId: string): Promise<Participant | null> {
    const participant = await prisma.nostro_vostro_participants.findUnique({
      where: { participantId },
    });

    return participant ? this.mapToParticipant(participant) : null;
  }

  /**
   * List participants with optional filters
   */
  async listParticipants(filters?: {
    regulatoryTier?: string;
    country?: string;
    status?: string;
    page?: number;
    pageSize?: number;
  }): Promise<{
    data: Participant[];
    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 = {};
    if (filters?.regulatoryTier) where.regulatoryTier = filters.regulatoryTier;
    if (filters?.country) where.country = filters.country;
    if (filters?.status) where.status = filters.status;

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

    return {
      data: participants.map((p) => this.mapToParticipant(p)),
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  // ============================================================================
  // Account Management
  // ============================================================================

  /**
   * Create a new Nostro/Vostro account
   */
  async createAccount(request: AccountCreateRequest): Promise<NostroVostroAccount> {
    // Verify participants exist
    const ownerParticipant = await prisma.nostro_vostro_participants.findUnique({
      where: { participantId: request.ownerParticipantId },
    });

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

    const counterpartyParticipant = await prisma.nostro_vostro_participants.findUnique({
      where: { participantId: request.counterpartyParticipantId },
    });

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

    const accountId = `ACC-${uuidv4()}`;

    const account = await prisma.nostro_vostro_accounts.create({
      data: {
        accountId,
        ownerParticipantId: request.ownerParticipantId,
        counterpartyParticipantId: request.counterpartyParticipantId,
        ibanOrLocalAccount: request.ibanOrLocalAccount,
        currency: request.currency,
        accountType: request.accountType,
        status: 'ACTIVE',
        currentBalance: new Decimal(0),
        availableLiquidity: new Decimal(0),
        holdAmount: new Decimal(0),
        metadata: (request.metadata || {}) as Prisma.InputJsonValue,
      },
    });

    return this.mapToAccount(account);
  }

  /**
   * Get account by ID
   */
  async getAccount(accountId: string): Promise<NostroVostroAccount | null> {
    const account = await prisma.nostro_vostro_accounts.findUnique({
      where: { accountId },
    });

    return account ? this.mapToAccount(account) : null;
  }

  /**
   * List accounts with optional filters
   */
  async listAccounts(filters?: {
    ownerParticipantId?: string;
    counterpartyParticipantId?: string;
    accountType?: string;
    currency?: string;
    status?: string;
    page?: number;
    pageSize?: number;
  }): Promise<{
    data: NostroVostroAccount[];
    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 = {};
    if (filters?.ownerParticipantId) where.ownerParticipantId = filters.ownerParticipantId;
    if (filters?.counterpartyParticipantId) where.counterpartyParticipantId = filters.counterpartyParticipantId;
    if (filters?.accountType) where.accountType = filters.accountType;
    if (filters?.currency) where.currency = filters.currency;
    if (filters?.status) where.status = filters.status;

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

    return {
      data: accounts.map((a) => this.mapToAccount(a)),
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  /**
   * Get account balances and liquidity
   */
  async getAccountBalances(accountId: string): Promise<BalanceResponse> {
    const account = await prisma.nostro_vostro_accounts.findUnique({
      where: { accountId },
    });

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

    return {
      accountId: account.accountId,
      currentBalance: account.currentBalance.toString(),
      availableLiquidity: account.availableLiquidity.toString(),
      holdAmount: account.holdAmount.toString(),
      currency: account.currency,
      lastUpdatedAt: account.lastUpdatedAt,
    };
  }

  // ============================================================================
  // Transfer Management
  // ============================================================================

  /**
   * Create a transfer/instruction with idempotency support
   */
  async createTransfer(
    request: TransferCreateRequest,
    idempotencyKey?: string
  ): Promise<NostroVostroTransfer> {
    // Check idempotency if key provided
    if (idempotencyKey) {
      const existing = await prisma.nostro_vostro_transfers.findUnique({
        where: { idempotencyKey },
      });

      if (existing) {
        return this.mapToTransfer(existing);
      }
    }

    // Verify accounts exist
    const fromAccount = await prisma.nostro_vostro_accounts.findUnique({
      where: { accountId: request.fromAccountId },
      include: { ownerParticipant: true },
    });

    if (!fromAccount) {
      throw new DbisError(ErrorCode.NOT_FOUND, `From account ${request.fromAccountId} not found`);
    }

    const toAccount = await prisma.nostro_vostro_accounts.findUnique({
      where: { accountId: request.toAccountId },
      include: { ownerParticipant: true },
    });

    if (!toAccount) {
      throw new DbisError(ErrorCode.NOT_FOUND, `To account ${request.toAccountId} not found`);
    }

    // Verify currency match
    if (fromAccount.currency !== request.currency) {
      throw new DbisError(
        ErrorCode.VALIDATION_ERROR,
        `Currency mismatch: from account uses ${fromAccount.currency}, transfer uses ${request.currency}`
      );
    }

    // Verify sufficient balance (if not GRU/SSU/CBDC which may have different rules)
    if (request.settlementAsset === SettlementAsset.FIAT) {
      const availableBalance = fromAccount.availableLiquidity;
      const transferAmount = new Decimal(request.amount);

      if (availableBalance.lessThan(transferAmount)) {
        throw new DbisError(
          ErrorCode.VALIDATION_ERROR,
          `Insufficient balance. Available: ${availableBalance.toString()}, Required: ${request.amount}`
        );
      }
    }

    const transferId = `TRF-${uuidv4()}`;
    const valueDate = request.valueDate ? new Date(request.valueDate) : new Date();

    // Create transfer in a transaction
    const transfer = await prisma.$transaction(async (tx) => {
      // Create transfer record
      const newTransfer = await tx.nostroVostroTransfer.create({
        data: {
          transferId,
          fromAccountId: request.fromAccountId,
          toAccountId: request.toAccountId,
          fromParticipantId: fromAccount.ownerParticipantId,
          toParticipantId: toAccount.ownerParticipantId,
          amount: new Decimal(request.amount),
          currency: request.currency,
          settlementAsset: request.settlementAsset || SettlementAsset.FIAT,
          valueDate,
          fxDetails: (request.fxDetails || {}) as Prisma.InputJsonValue,
          status: TransferStatus.PENDING,
          idempotencyKey: idempotencyKey || null,
          reference: request.reference,
          metadata: (request.metadata || {}) as Prisma.InputJsonValue,
        },
      });

      // Hold amount on from account if FIAT
      if (request.settlementAsset === SettlementAsset.FIAT) {
        await tx.nostroVostroAccount.update({
          where: { accountId: request.fromAccountId },
          data: {
            holdAmount: {
              increment: new Decimal(request.amount),
            },
            availableLiquidity: {
              decrement: new Decimal(request.amount),
            },
          },
        });
      }

      return newTransfer;
    });

    return this.mapToTransfer(transfer);
  }

  /**
   * Get transfer by ID
   */
  async getTransfer(transferId: string): Promise<NostroVostroTransfer | null> {
    const transfer = await prisma.nostro_vostro_transfers.findUnique({
      where: { transferId },
    });

    return transfer ? this.mapToTransfer(transfer) : null;
  }

  /**
   * List transfers with optional filters
   */
  async listTransfers(filters?: {
    fromAccountId?: string;
    toAccountId?: string;
    fromParticipantId?: string;
    toParticipantId?: string;
    status?: string;
    settlementAsset?: string;
    page?: number;
    pageSize?: number;
  }): Promise<{
    data: NostroVostroTransfer[];
    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 = {};
    if (filters?.fromAccountId) where.fromAccountId = filters.fromAccountId;
    if (filters?.toAccountId) where.toAccountId = filters.toAccountId;
    if (filters?.fromParticipantId) where.fromParticipantId = filters.fromParticipantId;
    if (filters?.toParticipantId) where.toParticipantId = filters.toParticipantId;
    if (filters?.status) where.status = filters.status;
    if (filters?.settlementAsset) where.settlementAsset = filters.settlementAsset;

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

    return {
      data: transfers.map((t) => this.mapToTransfer(t)),
      pagination: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  /**
   * Update transfer status (typically for settlement)
   */
  async updateTransferStatus(
    transferId: string,
    status: TransferStatus,
    rejectionReason?: string
  ): Promise<NostroVostroTransfer> {
    const transfer = await prisma.nostro_vostro_transfers.findUnique({
      where: { transferId },
    });

    if (!transfer) {
      throw new DbisError(ErrorCode.NOT_FOUND, `Transfer ${transferId} not found`);
    }

    // Handle settlement
    if (status === TransferStatus.SETTLED) {
      return await prisma.$transaction(async (tx) => {
        // Update transfer
        const updated = await tx.nostroVostroTransfer.update({
          where: { transferId },
          data: {
            status: TransferStatus.SETTLED,
            settledAt: new Date(),
          },
        });

        // Update balances
        if (transfer.settlementAsset === SettlementAsset.FIAT) {
          // Debit from account
          await tx.nostroVostroAccount.update({
            where: { accountId: transfer.fromAccountId },
            data: {
              currentBalance: { decrement: transfer.amount },
              holdAmount: { decrement: transfer.amount },
            },
          });

          // Credit to account
          await tx.nostroVostroAccount.update({
            where: { accountId: transfer.toAccountId },
            data: {
              currentBalance: { increment: transfer.amount },
              availableLiquidity: { increment: transfer.amount },
            },
          });
        }

        return updated;
      }).then((t) => this.mapToTransfer(t));
    }

    // Handle rejection/cancellation - release hold
    if (status === TransferStatus.REJECTED || status === TransferStatus.CANCELLED) {
      return await prisma.$transaction(async (tx) => {
        const updated = await tx.nostroVostroTransfer.update({
          where: { transferId },
          data: {
            status,
            rejectionReason: rejectionReason || null,
          },
        });

        // Release hold if FIAT
        if (transfer.settlementAsset === SettlementAsset.FIAT) {
          await tx.nostroVostroAccount.update({
            where: { accountId: transfer.fromAccountId },
            data: {
              holdAmount: { decrement: transfer.amount },
              availableLiquidity: { increment: transfer.amount },
            },
          });
        }

        return updated;
      }).then((t) => this.mapToTransfer(t));
    }

    // Simple status update
    const updated = await prisma.nostro_vostro_transfers.update({
      where: { transferId },
      data: {
        status,
        rejectionReason: rejectionReason || null,
      },
    });

    return this.mapToTransfer(updated);
  }

  // ============================================================================
  // Mapping Functions
  // ============================================================================

  private mapToParticipant(participant: any): Participant {
    return {
      participantId: participant.participantId,
      name: participant.name,
      bic: participant.bic || undefined,
      lei: participant.lei || undefined,
      country: participant.country,
      regulatoryTier: participant.regulatoryTier as any,
      sovereignBankId: participant.sovereignBankId || undefined,
      status: participant.status as 'active' | 'suspended' | 'inactive',
      metadata: participant.metadata as Record<string, unknown> | undefined,
      createdAt: participant.createdAt,
      updatedAt: participant.updatedAt,
    };
  }

  private mapToAccount(account: any): NostroVostroAccount {
    return {
      accountId: account.accountId,
      ownerParticipantId: account.ownerParticipantId,
      counterpartyParticipantId: account.counterpartyParticipantId,
      ibanOrLocalAccount: account.ibanOrLocalAccount || undefined,
      currency: account.currency,
      accountType: account.accountType as any,
      status: account.status as any,
      currentBalance: account.currentBalance.toString(),
      availableLiquidity: account.availableLiquidity.toString(),
      holdAmount: account.holdAmount.toString(),
      lastUpdatedAt: account.lastUpdatedAt,
      metadata: account.metadata as Record<string, unknown> | undefined,
      createdAt: account.createdAt,
      updatedAt: account.updatedAt,
    };
  }

  private mapToTransfer(transfer: any): NostroVostroTransfer {
    return {
      transferId: transfer.transferId,
      fromAccountId: transfer.fromAccountId,
      toAccountId: transfer.toAccountId,
      fromParticipantId: transfer.fromParticipantId,
      toParticipantId: transfer.toParticipantId,
      amount: transfer.amount.toString(),
      currency: transfer.currency,
      settlementAsset: transfer.settlementAsset as SettlementAsset,
      valueDate: transfer.valueDate,
      fxDetails: transfer.fxDetails as any,
      status: transfer.status as TransferStatus,
      rejectionReason: transfer.rejectionReason || undefined,
      idempotencyKey: transfer.idempotencyKey || undefined,
      reference: transfer.reference || undefined,
      metadata: transfer.metadata as Record<string, unknown> | undefined,
      settledAt: transfer.settledAt || undefined,
      createdAt: transfer.createdAt,
      updatedAt: transfer.updatedAt,
    };
  }
}

export const nostroVostroService = new NostroVostroService();

