/**
 * BankReconciliationService - Servicio de Conciliación Bancaria
 *
 * Gestión de cuentas bancarias, movimientos y conciliación.
 *
 * @module Finance
 */

import { DataSource, Repository, IsNull, Between } from 'typeorm';
import {
  BankAccount,
  BankAccountType,
  BankAccountStatus,
  BankMovement,
  MovementType,
  MovementStatus,
  MovementSource,
  BankReconciliation,
  ReconciliationStatus,
} from '../entities';

interface ServiceContext {
  tenantId: string;
  userId?: string;
}

interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

interface CreateBankAccountDto {
  name: string;
  accountNumber: string;
  clabe?: string;
  accountType: BankAccountType;
  bankName: string;
  bankCode?: string;
  branchName?: string;
  branchCode?: string;
  currency?: string;
  initialBalance?: number;
  projectId?: string;
  projectCode?: string;
  ledgerAccountId?: string;
  bankContactName?: string;
  bankContactPhone?: string;
  bankContactEmail?: string;
  creditLimit?: number;
  minimumBalance?: number;
  isDefault?: boolean;
  allowsPayments?: boolean;
  allowsCollections?: boolean;
  notes?: string;
  metadata?: Record<string, any>;
}

interface CreateMovementDto {
  bankAccountId: string;
  movementReference?: string;
  bankReference?: string;
  movementType: MovementType;
  movementDate: Date;
  valueDate?: Date;
  description: string;
  bankDescription?: string;
  amount: number;
  currency?: string;
  balanceAfter?: number;
  source?: MovementSource;
  category?: string;
  subcategory?: string;
  partnerId?: string;
  partnerName?: string;
  notes?: string;
  rawData?: Record<string, any>;
  metadata?: Record<string, any>;
}

interface CreateReconciliationDto {
  bankAccountId: string;
  periodStart: Date;
  periodEnd: Date;
  bankOpeningBalance: number;
  bankClosingBalance: number;
  statementFilePath?: string;
  notes?: string;
}

interface ImportBankStatementDto {
  bankAccountId: string;
  movements: {
    date: Date;
    reference?: string;
    description: string;
    amount: number;
    type: MovementType;
    balance?: number;
    rawData?: Record<string, any>;
  }[];
}

export class BankReconciliationService {
  private bankAccountRepository: Repository<BankAccount>;
  private movementRepository: Repository<BankMovement>;
  private reconciliationRepository: Repository<BankReconciliation>;
  dataSource: DataSource;

  constructor(dataSource: DataSource) {
    this.dataSource = dataSource;
    this.bankAccountRepository = dataSource.getRepository(BankAccount);
    this.movementRepository = dataSource.getRepository(BankMovement);
    this.reconciliationRepository = dataSource.getRepository(BankReconciliation);
  }

  // ==================== CUENTAS BANCARIAS ====================

  async findAllAccounts(
    ctx: ServiceContext,
    options: {
      page?: number;
      limit?: number;
      accountType?: BankAccountType;
      status?: BankAccountStatus;
      projectId?: string;
      search?: string;
    } = {}
  ): Promise<PaginatedResult<BankAccount>> {
    const { page = 1, limit = 20, accountType, status, projectId, search } = options;

    const queryBuilder = this.bankAccountRepository
      .createQueryBuilder('ba')
      .where('ba.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ba.deletedAt IS NULL');

    if (accountType) {
      queryBuilder.andWhere('ba.accountType = :accountType', { accountType });
    }

    if (status) {
      queryBuilder.andWhere('ba.status = :status', { status });
    }

    if (projectId) {
      queryBuilder.andWhere('ba.projectId = :projectId', { projectId });
    }

    if (search) {
      queryBuilder.andWhere(
        '(ba.name ILIKE :search OR ba.accountNumber ILIKE :search OR ba.bankName ILIKE :search)',
        { search: `%${search}%` }
      );
    }

    queryBuilder.orderBy('ba.name', 'ASC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder
      .skip((page - 1) * limit)
      .take(limit)
      .getMany();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async findAccountById(ctx: ServiceContext, id: string): Promise<BankAccount | null> {
    return this.bankAccountRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });
  }

  async createAccount(ctx: ServiceContext, data: CreateBankAccountDto): Promise<BankAccount> {
    // Verificar número de cuenta único
    const existing = await this.bankAccountRepository.findOne({
      where: {
        tenantId: ctx.tenantId,
        accountNumber: data.accountNumber,
        deletedAt: IsNull(),
      },
    });

    if (existing) {
      throw new Error(`Ya existe una cuenta con el número ${data.accountNumber}`);
    }

    // Si es cuenta por defecto, quitar el flag de las demás
    if (data.isDefault) {
      await this.bankAccountRepository.update(
        { tenantId: ctx.tenantId, isDefault: true },
        { isDefault: false }
      );
    }

    const account = this.bankAccountRepository.create({
      tenantId: ctx.tenantId,
      name: data.name,
      accountNumber: data.accountNumber,
      clabe: data.clabe,
      accountType: data.accountType,
      bankName: data.bankName,
      bankCode: data.bankCode,
      branchName: data.branchName,
      branchCode: data.branchCode,
      currency: data.currency ?? 'MXN',
      initialBalance: data.initialBalance ?? 0,
      currentBalance: data.initialBalance ?? 0,
      availableBalance: data.initialBalance ?? 0,
      projectId: data.projectId,
      projectCode: data.projectCode,
      ledgerAccountId: data.ledgerAccountId,
      bankContactName: data.bankContactName,
      bankContactPhone: data.bankContactPhone,
      bankContactEmail: data.bankContactEmail,
      creditLimit: data.creditLimit,
      minimumBalance: data.minimumBalance,
      isDefault: data.isDefault ?? false,
      allowsPayments: data.allowsPayments ?? true,
      allowsCollections: data.allowsCollections ?? true,
      status: 'active',
      notes: data.notes,
      metadata: data.metadata,
      createdBy: ctx.userId,
    });

    return this.bankAccountRepository.save(account);
  }

  async updateAccount(
    ctx: ServiceContext,
    id: string,
    data: Partial<CreateBankAccountDto>
  ): Promise<BankAccount> {
    const account = await this.findAccountById(ctx, id);
    if (!account) {
      throw new Error('Cuenta bancaria no encontrada');
    }

    // Si es cuenta por defecto, quitar el flag de las demás
    if (data.isDefault && !account.isDefault) {
      await this.bankAccountRepository.update(
        { tenantId: ctx.tenantId, isDefault: true },
        { isDefault: false }
      );
    }

    Object.assign(account, {
      ...data,
      updatedBy: ctx.userId,
    });

    return this.bankAccountRepository.save(account);
  }

  async updateAccountBalance(
    ctx: ServiceContext,
    id: string,
    currentBalance: number,
    availableBalance?: number
  ): Promise<BankAccount> {
    const account = await this.findAccountById(ctx, id);
    if (!account) {
      throw new Error('Cuenta bancaria no encontrada');
    }

    account.currentBalance = currentBalance;
    account.availableBalance = availableBalance ?? currentBalance;
    account.balanceUpdatedAt = new Date();
    account.updatedBy = ctx.userId;

    return this.bankAccountRepository.save(account);
  }

  // ==================== MOVIMIENTOS BANCARIOS ====================

  async findAllMovements(
    ctx: ServiceContext,
    options: {
      page?: number;
      limit?: number;
      bankAccountId?: string;
      movementType?: MovementType;
      status?: MovementStatus;
      startDate?: Date;
      endDate?: Date;
      search?: string;
    } = {}
  ): Promise<PaginatedResult<BankMovement>> {
    const {
      page = 1,
      limit = 50,
      bankAccountId,
      movementType,
      status,
      startDate,
      endDate,
      search,
    } = options;

    const queryBuilder = this.movementRepository
      .createQueryBuilder('mv')
      .leftJoinAndSelect('mv.bankAccount', 'bankAccount')
      .where('mv.tenantId = :tenantId', { tenantId: ctx.tenantId });

    if (bankAccountId) {
      queryBuilder.andWhere('mv.bankAccountId = :bankAccountId', { bankAccountId });
    }

    if (movementType) {
      queryBuilder.andWhere('mv.movementType = :movementType', { movementType });
    }

    if (status) {
      queryBuilder.andWhere('mv.status = :status', { status });
    }

    if (startDate) {
      queryBuilder.andWhere('mv.movementDate >= :startDate', { startDate });
    }

    if (endDate) {
      queryBuilder.andWhere('mv.movementDate <= :endDate', { endDate });
    }

    if (search) {
      queryBuilder.andWhere(
        '(mv.description ILIKE :search OR mv.movementReference ILIKE :search OR mv.bankReference ILIKE :search)',
        { search: `%${search}%` }
      );
    }

    queryBuilder.orderBy('mv.movementDate', 'DESC').addOrderBy('mv.createdAt', 'DESC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder
      .skip((page - 1) * limit)
      .take(limit)
      .getMany();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async findMovementById(ctx: ServiceContext, id: string): Promise<BankMovement | null> {
    return this.movementRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
      },
      relations: ['bankAccount'],
    });
  }

  async createMovement(ctx: ServiceContext, data: CreateMovementDto): Promise<BankMovement> {
    const bankAccount = await this.findAccountById(ctx, data.bankAccountId);
    if (!bankAccount) {
      throw new Error('Cuenta bancaria no encontrada');
    }

    const movement = this.movementRepository.create({
      tenantId: ctx.tenantId,
      bankAccountId: data.bankAccountId,
      movementReference: data.movementReference,
      bankReference: data.bankReference,
      movementType: data.movementType,
      movementDate: data.movementDate,
      valueDate: data.valueDate,
      description: data.description,
      bankDescription: data.bankDescription,
      amount: data.amount,
      currency: data.currency ?? bankAccount.currency,
      balanceAfter: data.balanceAfter,
      source: data.source ?? 'manual',
      status: 'pending',
      category: data.category,
      subcategory: data.subcategory,
      partnerId: data.partnerId,
      partnerName: data.partnerName,
      notes: data.notes,
      rawData: data.rawData,
      metadata: data.metadata,
      createdBy: ctx.userId,
    });

    return this.movementRepository.save(movement);
  }

  async importBankStatement(
    ctx: ServiceContext,
    data: ImportBankStatementDto
  ): Promise<{ imported: number; duplicates: number; movements: BankMovement[] }> {
    const bankAccount = await this.findAccountById(ctx, data.bankAccountId);
    if (!bankAccount) {
      throw new Error('Cuenta bancaria no encontrada');
    }

    const importBatchId = crypto.randomUUID();
    const importedMovements: BankMovement[] = [];
    let duplicates = 0;

    for (const mv of data.movements) {
      // Verificar duplicado por fecha, monto y referencia
      const existing = await this.movementRepository.findOne({
        where: {
          tenantId: ctx.tenantId,
          bankAccountId: data.bankAccountId,
          movementDate: mv.date,
          amount: mv.amount,
          movementType: mv.type,
        },
      });

      if (existing) {
        duplicates++;
        continue;
      }

      const movement = await this.createMovement(ctx, {
        bankAccountId: data.bankAccountId,
        movementReference: mv.reference,
        movementType: mv.type,
        movementDate: mv.date,
        description: mv.description,
        amount: mv.amount,
        balanceAfter: mv.balance,
        source: 'import_file',
        rawData: mv.rawData,
        metadata: { importBatchId },
      });

      importedMovements.push(movement);
    }

    return {
      imported: importedMovements.length,
      duplicates,
      movements: importedMovements,
    };
  }

  async matchMovement(
    ctx: ServiceContext,
    movementId: string,
    matchData: {
      paymentId?: string;
      collectionId?: string;
      entryId?: string;
      confidence?: number;
    }
  ): Promise<BankMovement> {
    const movement = await this.findMovementById(ctx, movementId);
    if (!movement) {
      throw new Error('Movimiento no encontrado');
    }

    movement.matchedPaymentId = matchData.paymentId;
    movement.matchedCollectionId = matchData.collectionId;
    movement.matchedEntryId = matchData.entryId;
    movement.matchConfidence = matchData.confidence;
    movement.status = 'matched';
    movement.updatedBy = ctx.userId;

    return this.movementRepository.save(movement);
  }

  async ignoreMovement(ctx: ServiceContext, movementId: string): Promise<BankMovement> {
    const movement = await this.findMovementById(ctx, movementId);
    if (!movement) {
      throw new Error('Movimiento no encontrado');
    }

    movement.status = 'ignored';
    movement.updatedBy = ctx.userId;

    return this.movementRepository.save(movement);
  }

  // ==================== CONCILIACIÓN ====================

  async findAllReconciliations(
    ctx: ServiceContext,
    options: {
      page?: number;
      limit?: number;
      bankAccountId?: string;
      status?: ReconciliationStatus;
    } = {}
  ): Promise<PaginatedResult<BankReconciliation>> {
    const { page = 1, limit = 20, bankAccountId, status } = options;

    const queryBuilder = this.reconciliationRepository
      .createQueryBuilder('rec')
      .leftJoinAndSelect('rec.bankAccount', 'bankAccount')
      .where('rec.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('rec.deletedAt IS NULL');

    if (bankAccountId) {
      queryBuilder.andWhere('rec.bankAccountId = :bankAccountId', { bankAccountId });
    }

    if (status) {
      queryBuilder.andWhere('rec.status = :status', { status });
    }

    queryBuilder.orderBy('rec.periodEnd', 'DESC');

    const total = await queryBuilder.getCount();
    const data = await queryBuilder
      .skip((page - 1) * limit)
      .take(limit)
      .getMany();

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async findReconciliationById(
    ctx: ServiceContext,
    id: string
  ): Promise<BankReconciliation | null> {
    return this.reconciliationRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['bankAccount', 'approvedBy'],
    });
  }

  async createReconciliation(
    ctx: ServiceContext,
    data: CreateReconciliationDto
  ): Promise<BankReconciliation> {
    const bankAccount = await this.findAccountById(ctx, data.bankAccountId);
    if (!bankAccount) {
      throw new Error('Cuenta bancaria no encontrada');
    }

    // Obtener saldo en libros
    const bookBalance = bankAccount.currentBalance;

    // Contar movimientos en el periodo
    const movements = await this.movementRepository.count({
      where: {
        tenantId: ctx.tenantId,
        bankAccountId: data.bankAccountId,
        movementDate: Between(data.periodStart, data.periodEnd),
      },
    });

    const reconciliation = this.reconciliationRepository.create({
      tenantId: ctx.tenantId,
      bankAccountId: data.bankAccountId,
      periodStart: data.periodStart,
      periodEnd: data.periodEnd,
      bankOpeningBalance: data.bankOpeningBalance,
      bankClosingBalance: data.bankClosingBalance,
      bookOpeningBalance: bookBalance, // TODO: Calcular saldo de apertura
      bookClosingBalance: bookBalance,
      depositsInTransit: 0,
      checksInTransit: 0,
      bankChargesNotRecorded: 0,
      interestNotRecorded: 0,
      otherAdjustments: 0,
      reconciledBalance: 0,
      difference: data.bankClosingBalance - bookBalance,
      isBalanced: false,
      totalMovements: movements,
      reconciledMovements: 0,
      pendingMovements: movements,
      statementFilePath: data.statementFilePath,
      statementImportDate: data.statementFilePath ? new Date() : undefined,
      notes: data.notes,
      status: 'draft',
      createdBy: ctx.userId,
    });

    return this.reconciliationRepository.save(reconciliation);
  }

  async startReconciliation(ctx: ServiceContext, id: string): Promise<BankReconciliation> {
    const reconciliation = await this.findReconciliationById(ctx, id);
    if (!reconciliation) {
      throw new Error('Conciliación no encontrada');
    }

    if (reconciliation.status !== 'draft') {
      throw new Error('La conciliación ya fue iniciada');
    }

    reconciliation.status = 'in_progress';
    reconciliation.startedAt = new Date();
    reconciliation.updatedBy = ctx.userId;

    return this.reconciliationRepository.save(reconciliation);
  }

  async reconcileMovement(
    ctx: ServiceContext,
    reconciliationId: string,
    movementId: string
  ): Promise<BankReconciliation> {
    const reconciliation = await this.findReconciliationById(ctx, reconciliationId);
    if (!reconciliation) {
      throw new Error('Conciliación no encontrada');
    }

    const movement = await this.findMovementById(ctx, movementId);
    if (!movement) {
      throw new Error('Movimiento no encontrado');
    }

    // Marcar movimiento como conciliado
    movement.status = 'reconciled';
    movement.reconciliationId = reconciliationId;
    movement.reconciledAt = new Date();
    movement.reconciledById = ctx.userId;
    movement.updatedBy = ctx.userId;
    await this.movementRepository.save(movement);

    // Actualizar contadores
    reconciliation.reconciledMovements++;
    reconciliation.pendingMovements--;
    reconciliation.updatedBy = ctx.userId;

    // Recalcular diferencia
    await this.recalculateReconciliation(ctx, reconciliationId);

    return this.findReconciliationById(ctx, reconciliationId) as Promise<BankReconciliation>;
  }

  private async recalculateReconciliation(
    ctx: ServiceContext,
    reconciliationId: string
  ): Promise<void> {
    const reconciliation = await this.findReconciliationById(ctx, reconciliationId);
    if (!reconciliation) return;

    // Obtener movimientos pendientes de conciliación
    const pendingDeposits = await this.movementRepository
      .createQueryBuilder('mv')
      .where('mv.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('mv.bankAccountId = :bankAccountId', { bankAccountId: reconciliation.bankAccountId })
      .andWhere('mv.movementDate <= :periodEnd', { periodEnd: reconciliation.periodEnd })
      .andWhere('mv.status IN (:...statuses)', { statuses: ['pending', 'matched'] })
      .andWhere('mv.movementType = :type', { type: 'credit' })
      .select('SUM(mv.amount)', 'total')
      .getRawOne();

    const pendingChecks = await this.movementRepository
      .createQueryBuilder('mv')
      .where('mv.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('mv.bankAccountId = :bankAccountId', { bankAccountId: reconciliation.bankAccountId })
      .andWhere('mv.movementDate <= :periodEnd', { periodEnd: reconciliation.periodEnd })
      .andWhere('mv.status IN (:...statuses)', { statuses: ['pending', 'matched'] })
      .andWhere('mv.movementType = :type', { type: 'debit' })
      .select('SUM(mv.amount)', 'total')
      .getRawOne();

    reconciliation.depositsInTransit = parseFloat(pendingDeposits?.total) || 0;
    reconciliation.checksInTransit = parseFloat(pendingChecks?.total) || 0;

    // Calcular saldo conciliado
    const reconciledBalance =
      reconciliation.bankClosingBalance -
      reconciliation.depositsInTransit +
      reconciliation.checksInTransit -
      reconciliation.bankChargesNotRecorded +
      reconciliation.interestNotRecorded +
      reconciliation.otherAdjustments;

    reconciliation.reconciledBalance = reconciledBalance;
    reconciliation.difference = Math.abs(reconciliation.bookClosingBalance - reconciledBalance);
    reconciliation.isBalanced = reconciliation.difference < 0.01;

    await this.reconciliationRepository.save(reconciliation);
  }

  async completeReconciliation(ctx: ServiceContext, id: string): Promise<BankReconciliation> {
    const reconciliation = await this.findReconciliationById(ctx, id);
    if (!reconciliation) {
      throw new Error('Conciliación no encontrada');
    }

    if (!reconciliation.isBalanced) {
      throw new Error('La conciliación no está balanceada');
    }

    reconciliation.status = 'completed';
    reconciliation.completedAt = new Date();
    reconciliation.updatedBy = ctx.userId;

    // Actualizar cuenta bancaria
    const bankAccount = await this.findAccountById(ctx, reconciliation.bankAccountId);
    if (bankAccount) {
      bankAccount.lastReconciliationDate = reconciliation.periodEnd;
      bankAccount.lastReconciledBalance = reconciliation.bankClosingBalance;
      await this.bankAccountRepository.save(bankAccount);
    }

    return this.reconciliationRepository.save(reconciliation);
  }

  async approveReconciliation(ctx: ServiceContext, id: string): Promise<BankReconciliation> {
    const reconciliation = await this.findReconciliationById(ctx, id);
    if (!reconciliation) {
      throw new Error('Conciliación no encontrada');
    }

    if (reconciliation.status !== 'completed') {
      throw new Error('Solo se pueden aprobar conciliaciones completadas');
    }

    reconciliation.status = 'approved';
    reconciliation.approvedById = ctx.userId;
    reconciliation.approvedAt = new Date();
    reconciliation.updatedBy = ctx.userId;

    return this.reconciliationRepository.save(reconciliation);
  }

  // ==================== REPORTES ====================

  async getBankAccountSummary(ctx: ServiceContext): Promise<{
    totalBalance: number;
    byAccount: { id: string; name: string; bankName: string; balance: number; currency: string }[];
    byCurrency: { currency: string; total: number }[];
  }> {
    const accounts = await this.bankAccountRepository.find({
      where: {
        tenantId: ctx.tenantId,
        status: 'active',
        deletedAt: IsNull(),
      },
    });

    const byAccount = accounts.map((acc) => ({
      id: acc.id,
      name: acc.name,
      bankName: acc.bankName,
      balance: Number(acc.currentBalance),
      currency: acc.currency,
    }));

    const byCurrencyMap = new Map<string, number>();
    for (const acc of accounts) {
      const current = byCurrencyMap.get(acc.currency) || 0;
      byCurrencyMap.set(acc.currency, current + Number(acc.currentBalance));
    }

    const byCurrency = Array.from(byCurrencyMap.entries()).map(([currency, total]) => ({
      currency,
      total,
    }));

    const totalBalance = accounts
      .filter((acc) => acc.currency === 'MXN')
      .reduce((sum, acc) => sum + Number(acc.currentBalance), 0);

    return {
      totalBalance,
      byAccount,
      byCurrency,
    };
  }

  async getReconciliationStatus(ctx: ServiceContext): Promise<{
    pendingCount: number;
    inProgressCount: number;
    accountsNotReconciled: { id: string; name: string; lastReconciliationDate?: Date }[];
  }> {
    const pending = await this.reconciliationRepository.count({
      where: {
        tenantId: ctx.tenantId,
        status: 'draft',
        deletedAt: IsNull(),
      },
    });

    const inProgress = await this.reconciliationRepository.count({
      where: {
        tenantId: ctx.tenantId,
        status: 'in_progress',
        deletedAt: IsNull(),
      },
    });

    // Cuentas sin conciliación reciente (más de 30 días)
    const thirtyDaysAgo = new Date();
    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);

    const accounts = await this.bankAccountRepository.find({
      where: {
        tenantId: ctx.tenantId,
        status: 'active',
        deletedAt: IsNull(),
      },
    });

    const accountsNotReconciled = accounts
      .filter(
        (acc) =>
          !acc.lastReconciliationDate || new Date(acc.lastReconciliationDate) < thirtyDaysAgo
      )
      .map((acc) => ({
        id: acc.id,
        name: acc.name,
        lastReconciliationDate: acc.lastReconciliationDate,
      }));

    return {
      pendingCount: pending,
      inProgressCount: inProgress,
      accountsNotReconciled,
    };
  }
}
