/**
 * CashFlowService - Servicio de Flujo de Efectivo
 *
 * Gestión de proyecciones y análisis de flujo de efectivo.
 *
 * @module Finance
 */

import { DataSource, Repository, IsNull, Between, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
import {
  CashFlowProjection,
  CashFlowType,
  CashFlowPeriodType,
  AccountPayable,
  AccountReceivable,
  BankAccount,
} from '../entities';

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

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

interface CreateProjectionDto {
  flowType: CashFlowType;
  periodType: CashFlowPeriodType;
  periodStart: Date;
  periodEnd: Date;
  fiscalYear: number;
  fiscalPeriod: number;
  projectId?: string;
  projectCode?: string;
  openingBalance: number;
  incomeEstimations?: number;
  incomeSales?: number;
  incomeAdvances?: number;
  incomeOther?: number;
  expenseSuppliers?: number;
  expenseSubcontractors?: number;
  expensePayroll?: number;
  expenseTaxes?: number;
  expenseOperating?: number;
  expenseOther?: number;
  investingIncome?: number;
  investingExpense?: number;
  financingIncome?: number;
  financingExpense?: number;
  incomeBreakdown?: Record<string, any>[];
  expenseBreakdown?: Record<string, any>[];
  notes?: string;
  metadata?: Record<string, any>;
}

interface CashFlowSummary {
  period: string;
  periodStart: Date;
  periodEnd: Date;
  openingBalance: number;
  totalIncome: number;
  totalExpenses: number;
  netOperatingFlow: number;
  netInvestingFlow: number;
  netFinancingFlow: number;
  netCashFlow: number;
  closingBalance: number;
}

export class CashFlowService {
  private projectionRepository: Repository<CashFlowProjection>;
  private apRepository: Repository<AccountPayable>;
  private arRepository: Repository<AccountReceivable>;
  private bankAccountRepository: Repository<BankAccount>;

  constructor(private dataSource: DataSource) {
    this.projectionRepository = dataSource.getRepository(CashFlowProjection);
    this.apRepository = dataSource.getRepository(AccountPayable);
    this.arRepository = dataSource.getRepository(AccountReceivable);
    this.bankAccountRepository = dataSource.getRepository(BankAccount);
  }

  // ==================== PROYECCIONES ====================

  async findAll(
    ctx: ServiceContext,
    options: {
      page?: number;
      limit?: number;
      flowType?: CashFlowType;
      periodType?: CashFlowPeriodType;
      projectId?: string;
      fiscalYear?: number;
      startDate?: Date;
      endDate?: Date;
    } = {}
  ): Promise<PaginatedResult<CashFlowProjection>> {
    const {
      page = 1,
      limit = 20,
      flowType,
      periodType,
      projectId,
      fiscalYear,
      startDate,
      endDate,
    } = options;

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

    if (flowType) {
      queryBuilder.andWhere('cf.flowType = :flowType', { flowType });
    }

    if (periodType) {
      queryBuilder.andWhere('cf.periodType = :periodType', { periodType });
    }

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

    if (fiscalYear) {
      queryBuilder.andWhere('cf.fiscalYear = :fiscalYear', { fiscalYear });
    }

    if (startDate) {
      queryBuilder.andWhere('cf.periodStart >= :startDate', { startDate });
    }

    if (endDate) {
      queryBuilder.andWhere('cf.periodEnd <= :endDate', { endDate });
    }

    queryBuilder.orderBy('cf.periodStart', 'ASC');

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

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

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

  async create(ctx: ServiceContext, data: CreateProjectionDto): Promise<CashFlowProjection> {
    // Calcular totales
    const totalIncome =
      (data.incomeEstimations ?? 0) +
      (data.incomeSales ?? 0) +
      (data.incomeAdvances ?? 0) +
      (data.incomeOther ?? 0);

    const totalExpenses =
      (data.expenseSuppliers ?? 0) +
      (data.expenseSubcontractors ?? 0) +
      (data.expensePayroll ?? 0) +
      (data.expenseTaxes ?? 0) +
      (data.expenseOperating ?? 0) +
      (data.expenseOther ?? 0);

    const netOperatingFlow = totalIncome - totalExpenses;
    const netInvestingFlow = (data.investingIncome ?? 0) - (data.investingExpense ?? 0);
    const netFinancingFlow = (data.financingIncome ?? 0) - (data.financingExpense ?? 0);
    const netCashFlow = netOperatingFlow + netInvestingFlow + netFinancingFlow;
    const closingBalance = data.openingBalance + netCashFlow;

    const projection = this.projectionRepository.create({
      tenantId: ctx.tenantId,
      flowType: data.flowType,
      periodType: data.periodType,
      periodStart: data.periodStart,
      periodEnd: data.periodEnd,
      fiscalYear: data.fiscalYear,
      fiscalPeriod: data.fiscalPeriod,
      projectId: data.projectId,
      projectCode: data.projectCode,
      openingBalance: data.openingBalance,
      incomeEstimations: data.incomeEstimations ?? 0,
      incomeSales: data.incomeSales ?? 0,
      incomeAdvances: data.incomeAdvances ?? 0,
      incomeOther: data.incomeOther ?? 0,
      totalIncome,
      expenseSuppliers: data.expenseSuppliers ?? 0,
      expenseSubcontractors: data.expenseSubcontractors ?? 0,
      expensePayroll: data.expensePayroll ?? 0,
      expenseTaxes: data.expenseTaxes ?? 0,
      expenseOperating: data.expenseOperating ?? 0,
      expenseOther: data.expenseOther ?? 0,
      totalExpenses,
      netOperatingFlow,
      investingIncome: data.investingIncome ?? 0,
      investingExpense: data.investingExpense ?? 0,
      netInvestingFlow,
      financingIncome: data.financingIncome ?? 0,
      financingExpense: data.financingExpense ?? 0,
      netFinancingFlow,
      netCashFlow,
      closingBalance,
      incomeBreakdown: data.incomeBreakdown,
      expenseBreakdown: data.expenseBreakdown,
      notes: data.notes,
      metadata: data.metadata,
      isLocked: false,
      createdBy: ctx.userId,
    });

    return this.projectionRepository.save(projection);
  }

  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<CreateProjectionDto>
  ): Promise<CashFlowProjection> {
    const projection = await this.findById(ctx, id);
    if (!projection) {
      throw new Error('Proyección no encontrada');
    }

    if (projection.isLocked) {
      throw new Error('La proyección está bloqueada y no se puede modificar');
    }

    Object.assign(projection, data);

    // Recalcular totales
    projection.totalIncome =
      projection.incomeEstimations +
      projection.incomeSales +
      projection.incomeAdvances +
      projection.incomeOther;

    projection.totalExpenses =
      projection.expenseSuppliers +
      projection.expenseSubcontractors +
      projection.expensePayroll +
      projection.expenseTaxes +
      projection.expenseOperating +
      projection.expenseOther;

    projection.netOperatingFlow = projection.totalIncome - projection.totalExpenses;
    projection.netInvestingFlow = projection.investingIncome - projection.investingExpense;
    projection.netFinancingFlow = projection.financingIncome - projection.financingExpense;
    projection.netCashFlow =
      projection.netOperatingFlow + projection.netInvestingFlow + projection.netFinancingFlow;
    projection.closingBalance = projection.openingBalance + projection.netCashFlow;

    projection.updatedBy = ctx.userId;

    return this.projectionRepository.save(projection);
  }

  async lock(ctx: ServiceContext, id: string): Promise<CashFlowProjection> {
    const projection = await this.findById(ctx, id);
    if (!projection) {
      throw new Error('Proyección no encontrada');
    }

    projection.isLocked = true;
    projection.lockedAt = new Date();
    projection.updatedBy = ctx.userId;

    return this.projectionRepository.save(projection);
  }

  async delete(ctx: ServiceContext, id: string): Promise<void> {
    const projection = await this.findById(ctx, id);
    if (!projection) {
      throw new Error('Proyección no encontrada');
    }

    if (projection.isLocked) {
      throw new Error('No se puede eliminar una proyección bloqueada');
    }

    projection.deletedAt = new Date();
    projection.updatedBy = ctx.userId;
    await this.projectionRepository.save(projection);
  }

  // ==================== GENERACIÓN AUTOMÁTICA ====================

  async generateProjection(
    ctx: ServiceContext,
    periodStart: Date,
    periodEnd: Date,
    options: {
      periodType?: CashFlowPeriodType;
      projectId?: string;
      projectCode?: string;
    } = {}
  ): Promise<CashFlowProjection> {
    const { periodType = 'weekly', projectId, projectCode } = options;

    // Obtener saldo inicial de cuentas bancarias
    const bankAccounts = await this.bankAccountRepository.find({
      where: {
        tenantId: ctx.tenantId,
        status: 'active',
        ...(projectId && { projectId }),
      },
    });

    const openingBalance = bankAccounts.reduce(
      (sum, acc) => sum + Number(acc.currentBalance),
      0
    );

    // Obtener cobranza esperada (AR por vencer en el periodo)
    const arQuery = this.arRepository
      .createQueryBuilder('ar')
      .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ar.status IN (:...statuses)', { statuses: ['pending', 'partial'] })
      .andWhere('ar.dueDate BETWEEN :periodStart AND :periodEnd', { periodStart, periodEnd })
      .andWhere('ar.deletedAt IS NULL');

    if (projectId) {
      arQuery.andWhere('ar.projectId = :projectId', { projectId });
    }

    const arRecords = await arQuery.getMany();
    const incomeEstimations = arRecords
      .filter((ar) => ar.documentType === 'estimation')
      .reduce((sum, ar) => sum + Number(ar.balanceAmount), 0);
    const incomeSales = arRecords
      .filter((ar) => ar.documentType !== 'estimation')
      .reduce((sum, ar) => sum + Number(ar.balanceAmount), 0);

    // Obtener pagos esperados (AP por vencer en el periodo)
    const apQuery = this.apRepository
      .createQueryBuilder('ap')
      .where('ap.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('ap.status IN (:...statuses)', { statuses: ['pending', 'partial'] })
      .andWhere('ap.dueDate BETWEEN :periodStart AND :periodEnd', { periodStart, periodEnd })
      .andWhere('ap.deletedAt IS NULL');

    if (projectId) {
      apQuery.andWhere('ap.projectId = :projectId', { projectId });
    }

    const apRecords = await apQuery.getMany();
    const expenseSuppliers = apRecords
      .filter((ap) => ap.documentType === 'invoice')
      .reduce((sum, ap) => sum + Number(ap.balanceAmount), 0);

    // Determinar año y periodo fiscal
    const fiscalYear = periodStart.getFullYear();
    const fiscalPeriod = periodStart.getMonth() + 1;

    // Crear la proyección
    return this.create(ctx, {
      flowType: 'projected',
      periodType,
      periodStart,
      periodEnd,
      fiscalYear,
      fiscalPeriod,
      projectId,
      projectCode,
      openingBalance,
      incomeEstimations,
      incomeSales,
      incomeAdvances: 0,
      incomeOther: 0,
      expenseSuppliers,
      expenseSubcontractors: 0,
      expensePayroll: 0,
      expenseTaxes: 0,
      expenseOperating: 0,
      expenseOther: 0,
      investingIncome: 0,
      investingExpense: 0,
      financingIncome: 0,
      financingExpense: 0,
      incomeBreakdown: arRecords.map((ar) => ({
        id: ar.id,
        documentNumber: ar.documentNumber,
        partnerName: ar.partnerName,
        dueDate: ar.dueDate,
        amount: ar.balanceAmount,
      })),
      expenseBreakdown: apRecords.map((ap) => ({
        id: ap.id,
        documentNumber: ap.documentNumber,
        partnerName: ap.partnerName,
        dueDate: ap.dueDate,
        amount: ap.balanceAmount,
      })),
      notes: `Proyección automática generada el ${new Date().toISOString()}`,
    });
  }

  async generateMultiplePeriods(
    ctx: ServiceContext,
    startDate: Date,
    weeks: number,
    options: { projectId?: string; projectCode?: string } = {}
  ): Promise<CashFlowProjection[]> {
    const projections: CashFlowProjection[] = [];
    let currentStart = new Date(startDate);

    for (let i = 0; i < weeks; i++) {
      const periodEnd = new Date(currentStart);
      periodEnd.setDate(periodEnd.getDate() + 6);

      const projection = await this.generateProjection(ctx, currentStart, periodEnd, {
        periodType: 'weekly',
        ...options,
      });

      projections.push(projection);

      // Siguiente semana
      currentStart = new Date(periodEnd);
      currentStart.setDate(currentStart.getDate() + 1);
    }

    return projections;
  }

  // ==================== COMPARACIÓN PROYECTADO VS REAL ====================

  async createComparison(
    ctx: ServiceContext,
    projectedId: string
  ): Promise<CashFlowProjection> {
    const projected = await this.findById(ctx, projectedId);
    if (!projected) {
      throw new Error('Proyección no encontrada');
    }

    if (projected.flowType !== 'projected') {
      throw new Error('Solo se pueden comparar proyecciones');
    }

    // Generar flujo real para el mismo periodo
    const actual = await this.generateProjection(ctx, projected.periodStart, projected.periodEnd, {
      periodType: projected.periodType,
      projectId: projected.projectId ?? undefined,
      projectCode: projected.projectCode ?? undefined,
    });

    // Actualizar a tipo actual
    actual.flowType = 'actual';
    await this.projectionRepository.save(actual);

    // Crear registro de comparación
    const varianceAmount = actual.netCashFlow - projected.netCashFlow;
    const variancePercentage =
      projected.netCashFlow !== 0
        ? (varianceAmount / Math.abs(projected.netCashFlow)) * 100
        : 0;

    const comparison = this.projectionRepository.create({
      tenantId: ctx.tenantId,
      flowType: 'comparison',
      periodType: projected.periodType,
      periodStart: projected.periodStart,
      periodEnd: projected.periodEnd,
      fiscalYear: projected.fiscalYear,
      fiscalPeriod: projected.fiscalPeriod,
      projectId: projected.projectId,
      projectCode: projected.projectCode,
      openingBalance: projected.openingBalance,
      incomeEstimations: actual.incomeEstimations,
      incomeSales: actual.incomeSales,
      incomeAdvances: actual.incomeAdvances,
      incomeOther: actual.incomeOther,
      totalIncome: actual.totalIncome,
      expenseSuppliers: actual.expenseSuppliers,
      expenseSubcontractors: actual.expenseSubcontractors,
      expensePayroll: actual.expensePayroll,
      expenseTaxes: actual.expenseTaxes,
      expenseOperating: actual.expenseOperating,
      expenseOther: actual.expenseOther,
      totalExpenses: actual.totalExpenses,
      netOperatingFlow: actual.netOperatingFlow,
      investingIncome: actual.investingIncome,
      investingExpense: actual.investingExpense,
      netInvestingFlow: actual.netInvestingFlow,
      financingIncome: actual.financingIncome,
      financingExpense: actual.financingExpense,
      netFinancingFlow: actual.netFinancingFlow,
      netCashFlow: actual.netCashFlow,
      closingBalance: actual.closingBalance,
      projectedAmount: projected.netCashFlow,
      actualAmount: actual.netCashFlow,
      varianceAmount,
      variancePercentage,
      notes: `Comparación: Proyectado vs Real`,
      metadata: {
        projectedId: projected.id,
        actualId: actual.id,
      },
      createdBy: ctx.userId,
    });

    return this.projectionRepository.save(comparison);
  }

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

  async getCashFlowSummary(
    ctx: ServiceContext,
    startDate: Date,
    endDate: Date,
    options: {
      periodType?: CashFlowPeriodType;
      flowType?: CashFlowType;
      projectId?: string;
    } = {}
  ): Promise<CashFlowSummary[]> {
    const { periodType = 'weekly', flowType = 'projected', projectId } = options;

    const queryBuilder = this.projectionRepository
      .createQueryBuilder('cf')
      .where('cf.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('cf.flowType = :flowType', { flowType })
      .andWhere('cf.periodType = :periodType', { periodType })
      .andWhere('cf.periodStart >= :startDate', { startDate })
      .andWhere('cf.periodEnd <= :endDate', { endDate })
      .andWhere('cf.deletedAt IS NULL');

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

    queryBuilder.orderBy('cf.periodStart', 'ASC');

    const projections = await queryBuilder.getMany();

    return projections.map((p) => ({
      period: `${p.periodStart.toISOString().split('T')[0]} - ${p.periodEnd.toISOString().split('T')[0]}`,
      periodStart: p.periodStart,
      periodEnd: p.periodEnd,
      openingBalance: Number(p.openingBalance),
      totalIncome: Number(p.totalIncome),
      totalExpenses: Number(p.totalExpenses),
      netOperatingFlow: Number(p.netOperatingFlow),
      netInvestingFlow: Number(p.netInvestingFlow),
      netFinancingFlow: Number(p.netFinancingFlow),
      netCashFlow: Number(p.netCashFlow),
      closingBalance: Number(p.closingBalance),
    }));
  }

  async getVarianceAnalysis(
    ctx: ServiceContext,
    fiscalYear: number,
    options: { projectId?: string } = {}
  ): Promise<{
    periods: {
      period: string;
      projected: number;
      actual: number;
      variance: number;
      variancePercent: number;
    }[];
    totals: {
      projected: number;
      actual: number;
      variance: number;
      variancePercent: number;
    };
  }> {
    const { projectId } = options;

    const queryBuilder = this.projectionRepository
      .createQueryBuilder('cf')
      .where('cf.tenantId = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('cf.flowType = :flowType', { flowType: 'comparison' })
      .andWhere('cf.fiscalYear = :fiscalYear', { fiscalYear })
      .andWhere('cf.deletedAt IS NULL');

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

    queryBuilder.orderBy('cf.periodStart', 'ASC');

    const comparisons = await queryBuilder.getMany();

    const periods = comparisons.map((c) => ({
      period: `${c.periodStart.toISOString().split('T')[0]} - ${c.periodEnd.toISOString().split('T')[0]}`,
      projected: Number(c.projectedAmount) || 0,
      actual: Number(c.actualAmount) || 0,
      variance: Number(c.varianceAmount) || 0,
      variancePercent: Number(c.variancePercentage) || 0,
    }));

    const totals = periods.reduce(
      (acc, p) => ({
        projected: acc.projected + p.projected,
        actual: acc.actual + p.actual,
        variance: acc.variance + p.variance,
        variancePercent: 0,
      }),
      { projected: 0, actual: 0, variance: 0, variancePercent: 0 }
    );

    totals.variancePercent =
      totals.projected !== 0
        ? (totals.variance / Math.abs(totals.projected)) * 100
        : 0;

    return { periods, totals };
  }

  async getDashboardData(ctx: ServiceContext): Promise<{
    currentBalance: number;
    projectedIncome: number;
    projectedExpenses: number;
    projectedNetFlow: number;
    cashPosition: { date: Date; balance: number }[];
  }> {
    // Saldo actual
    const bankAccounts = await this.bankAccountRepository.find({
      where: {
        tenantId: ctx.tenantId,
        status: 'active',
      },
    });

    const currentBalance = bankAccounts.reduce(
      (sum, acc) => sum + Number(acc.currentBalance),
      0
    );

    // Proyección próximas 4 semanas
    const today = new Date();
    const fourWeeksLater = new Date(today);
    fourWeeksLater.setDate(fourWeeksLater.getDate() + 28);

    const projections = await this.projectionRepository.find({
      where: {
        tenantId: ctx.tenantId,
        flowType: 'projected',
        periodStart: MoreThanOrEqual(today),
        periodEnd: LessThanOrEqual(fourWeeksLater),
        deletedAt: IsNull(),
      },
      order: { periodStart: 'ASC' },
    });

    const projectedIncome = projections.reduce((sum, p) => sum + Number(p.totalIncome), 0);
    const projectedExpenses = projections.reduce((sum, p) => sum + Number(p.totalExpenses), 0);
    const projectedNetFlow = projectedIncome - projectedExpenses;

    // Posición de caja proyectada
    const cashPosition: { date: Date; balance: number }[] = [
      { date: today, balance: currentBalance },
    ];

    let runningBalance = currentBalance;
    for (const p of projections) {
      runningBalance += Number(p.netCashFlow);
      cashPosition.push({
        date: p.periodEnd,
        balance: runningBalance,
      });
    }

    return {
      currentBalance,
      projectedIncome,
      projectedExpenses,
      projectedNetFlow,
      cashPosition,
    };
  }
}
