/**
 * EarnedValueService - Earned Value Management (EVM) para Construcción
 *
 * Calcula indicadores de Curva S, SPI, CPI y proyecciones.
 * Basado en PMI PMBOK Earned Value Management.
 *
 * @module Reports (Construction-specific)
 */

import { DataSource, Repository, LessThanOrEqual } from 'typeorm';
import { Estimacion } from '../../estimates/entities/estimacion.entity';
import { KpiSnapshot, KpiCategory } from '../entities/kpi-snapshot.entity';

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

/**
 * Earned Value metrics at a point in time
 */
export interface EarnedValueMetrics {
  date: Date;
  /** Budget at Completion - total presupuesto */
  bac: number;
  /** Planned Value - valor planeado acumulado */
  pv: number;
  /** Earned Value - valor ganado (% avance * BAC) */
  ev: number;
  /** Actual Cost - costo real acumulado */
  ac: number;
  /** Schedule Performance Index = EV / PV */
  spi: number;
  /** Cost Performance Index = EV / AC */
  cpi: number;
  /** Schedule Variance = EV - PV */
  sv: number;
  /** Cost Variance = EV - AC */
  cv: number;
  /** Estimate at Completion = BAC / CPI */
  eac: number;
  /** Estimate to Complete = EAC - AC */
  etc: number;
  /** Variance at Completion = BAC - EAC */
  vac: number;
  /** To Complete Performance Index = (BAC - EV) / (BAC - AC) */
  tcpi: number;
  /** Percent complete = EV / BAC */
  percentComplete: number;
  /** Status color based on SPI and CPI thresholds */
  status: 'green' | 'yellow' | 'red';
}

/**
 * Curva S data point
 */
export interface CurvaSPoint {
  date: Date;
  pv: number;
  ev: number;
  ac: number;
  percentPv: number;
  percentEv: number;
  percentAc: number;
}

/**
 * Project budget data for EVM calculations
 */
export interface ProjectBudget {
  fraccionamientoId: string;
  bac: number;
  startDate: Date;
  endDate: Date;
  plannedCurve: { date: Date; cumulativeValue: number }[];
}

/**
 * Filters for EVM queries
 */
export interface EvmFilters {
  fraccionamientoId?: string;
  dateFrom?: Date;
  dateTo?: Date;
  periodType?: 'daily' | 'weekly' | 'monthly';
}

export class EarnedValueService {
  private estimacionRepo: Repository<Estimacion>;
  private kpiRepo: Repository<KpiSnapshot>;

  constructor(dataSource: DataSource) {
    this.estimacionRepo = dataSource.getRepository(Estimacion);
    this.kpiRepo = dataSource.getRepository(KpiSnapshot);
  }

  /**
   * Calculate current Earned Value metrics for a project
   */
  async getCurrentMetrics(
    ctx: ServiceContext,
    fraccionamientoId: string,
    asOfDate?: Date
  ): Promise<EarnedValueMetrics> {
    const date = asOfDate || new Date();

    // Get budget data (BAC)
    const budget = await this.getProjectBudget(ctx, fraccionamientoId);

    // Get Earned Value (approved estimates sum)
    const ev = await this.calculateEarnedValue(ctx, fraccionamientoId, date);

    // Get Planned Value (scheduled value to date)
    const pv = this.calculatePlannedValue(budget, date);

    // Get Actual Cost (real costs incurred)
    const ac = await this.calculateActualCost(ctx, fraccionamientoId, date);

    // Calculate all EVM indicators
    return this.computeEvmMetrics(budget.bac, pv, ev, ac, date);
  }

  /**
   * Generate Curva S data for a project
   */
  async getCurvaS(
    ctx: ServiceContext,
    fraccionamientoId: string,
    filters?: EvmFilters
  ): Promise<CurvaSPoint[]> {
    const budget = await this.getProjectBudget(ctx, fraccionamientoId);
    const periodType = filters?.periodType || 'weekly';
    const dateFrom = filters?.dateFrom || budget.startDate;
    const dateTo = filters?.dateTo || new Date();

    const points: CurvaSPoint[] = [];
    const dates = this.generateDateSeries(dateFrom, dateTo, periodType);

    for (const date of dates) {
      const pv = this.calculatePlannedValue(budget, date);
      const ev = await this.calculateEarnedValue(ctx, fraccionamientoId, date);
      const ac = await this.calculateActualCost(ctx, fraccionamientoId, date);

      points.push({
        date,
        pv,
        ev,
        ac,
        percentPv: budget.bac > 0 ? (pv / budget.bac) * 100 : 0,
        percentEv: budget.bac > 0 ? (ev / budget.bac) * 100 : 0,
        percentAc: budget.bac > 0 ? (ac / budget.bac) * 100 : 0,
      });
    }

    return points;
  }

  /**
   * Calculate SPI for a specific date
   */
  async calculateSPI(
    ctx: ServiceContext,
    fraccionamientoId: string,
    asOfDate?: Date
  ): Promise<{ spi: number; status: 'green' | 'yellow' | 'red'; interpretation: string }> {
    const metrics = await this.getCurrentMetrics(ctx, fraccionamientoId, asOfDate);

    let interpretation: string;
    if (metrics.spi >= 1.0) {
      interpretation = 'Proyecto adelantado respecto al cronograma';
    } else if (metrics.spi >= 0.95) {
      interpretation = 'Proyecto ligeramente atrasado (tolerable)';
    } else if (metrics.spi >= 0.85) {
      interpretation = 'Proyecto con atraso moderado, requiere atención';
    } else {
      interpretation = 'Proyecto con atraso crítico, requiere acción inmediata';
    }

    return {
      spi: metrics.spi,
      status: metrics.spi >= 0.95 ? 'green' : metrics.spi >= 0.85 ? 'yellow' : 'red',
      interpretation,
    };
  }

  /**
   * Calculate CPI for a specific date
   */
  async calculateCPI(
    ctx: ServiceContext,
    fraccionamientoId: string,
    asOfDate?: Date
  ): Promise<{ cpi: number; status: 'green' | 'yellow' | 'red'; interpretation: string }> {
    const metrics = await this.getCurrentMetrics(ctx, fraccionamientoId, asOfDate);

    let interpretation: string;
    if (metrics.cpi >= 1.0) {
      interpretation = 'Proyecto bajo presupuesto (eficiente)';
    } else if (metrics.cpi >= 0.95) {
      interpretation = 'Proyecto ligeramente sobre presupuesto (tolerable)';
    } else if (metrics.cpi >= 0.85) {
      interpretation = 'Proyecto con sobrecosto moderado, requiere control';
    } else {
      interpretation = 'Proyecto con sobrecosto crítico, requiere acción inmediata';
    }

    return {
      cpi: metrics.cpi,
      status: metrics.cpi >= 0.95 ? 'green' : metrics.cpi >= 0.85 ? 'yellow' : 'red',
      interpretation,
    };
  }

  /**
   * Get EVM summary for multiple projects
   */
  async getPortfolioSummary(
    ctx: ServiceContext,
    fraccionamientoIds?: string[]
  ): Promise<{
    projects: Array<{
      fraccionamientoId: string;
      name: string;
      bac: number;
      percentComplete: number;
      spi: number;
      cpi: number;
      status: 'green' | 'yellow' | 'red';
    }>;
    totals: {
      totalBac: number;
      avgSpi: number;
      avgCpi: number;
      projectsOnTrack: number;
      projectsAtRisk: number;
      projectsCritical: number;
    };
  }> {
    // Get all active projects for tenant
    const projects = await this.getActiveProjects(ctx, fraccionamientoIds);

    const projectMetrics = await Promise.all(
      projects.map(async (p) => {
        const metrics = await this.getCurrentMetrics(ctx, p.fraccionamientoId);
        return {
          fraccionamientoId: p.fraccionamientoId,
          name: p.name,
          bac: metrics.bac,
          percentComplete: metrics.percentComplete,
          spi: metrics.spi,
          cpi: metrics.cpi,
          status: metrics.status,
        };
      })
    );

    const totals = {
      totalBac: projectMetrics.reduce((sum, p) => sum + p.bac, 0),
      avgSpi:
        projectMetrics.length > 0
          ? projectMetrics.reduce((sum, p) => sum + p.spi, 0) / projectMetrics.length
          : 0,
      avgCpi:
        projectMetrics.length > 0
          ? projectMetrics.reduce((sum, p) => sum + p.cpi, 0) / projectMetrics.length
          : 0,
      projectsOnTrack: projectMetrics.filter((p) => p.status === 'green').length,
      projectsAtRisk: projectMetrics.filter((p) => p.status === 'yellow').length,
      projectsCritical: projectMetrics.filter((p) => p.status === 'red').length,
    };

    return { projects: projectMetrics, totals };
  }

  /**
   * Get EVM trend over time
   */
  async getEvmTrend(
    ctx: ServiceContext,
    fraccionamientoId: string,
    periodCount: number = 12,
    periodType: 'weekly' | 'monthly' = 'monthly'
  ): Promise<
    Array<{
      period: Date;
      spi: number;
      cpi: number;
      percentComplete: number;
    }>
  > {
    const trend: Array<{
      period: Date;
      spi: number;
      cpi: number;
      percentComplete: number;
    }> = [];

    const now = new Date();
    for (let i = periodCount - 1; i >= 0; i--) {
      const periodDate = new Date(now);
      if (periodType === 'weekly') {
        periodDate.setDate(periodDate.getDate() - i * 7);
      } else {
        periodDate.setMonth(periodDate.getMonth() - i);
      }

      const metrics = await this.getCurrentMetrics(ctx, fraccionamientoId, periodDate);
      trend.push({
        period: periodDate,
        spi: metrics.spi,
        cpi: metrics.cpi,
        percentComplete: metrics.percentComplete,
      });
    }

    return trend;
  }

  /**
   * Save EVM metrics as KPI snapshots
   */
  async saveEvmSnapshot(
    ctx: ServiceContext,
    fraccionamientoId: string,
    asOfDate?: Date
  ): Promise<void> {
    const metrics = await this.getCurrentMetrics(ctx, fraccionamientoId, asOfDate);
    const date = asOfDate || new Date();

    const kpis = [
      {
        kpiCode: 'EVM_SPI',
        kpiName: 'Schedule Performance Index',
        category: 'progress' as KpiCategory,
        value: metrics.spi,
        targetValue: 1.0,
      },
      {
        kpiCode: 'EVM_CPI',
        kpiName: 'Cost Performance Index',
        category: 'financial' as KpiCategory,
        value: metrics.cpi,
        targetValue: 1.0,
      },
      {
        kpiCode: 'EVM_EV',
        kpiName: 'Earned Value',
        category: 'progress' as KpiCategory,
        value: metrics.ev,
        targetValue: metrics.pv,
      },
      {
        kpiCode: 'EVM_PERCENT',
        kpiName: 'Percent Complete',
        category: 'progress' as KpiCategory,
        value: metrics.percentComplete,
        targetValue: this.calculatePlannedPercent(
          await this.getProjectBudget(ctx, fraccionamientoId),
          date
        ),
      },
    ];

    for (const kpi of kpis) {
      const snapshot = this.kpiRepo.create({
        tenantId: ctx.tenantId,
        kpiCode: kpi.kpiCode,
        kpiName: kpi.kpiName,
        category: kpi.category,
        snapshotDate: date,
        periodType: 'daily',
        fraccionamientoId,
        value: kpi.value,
        targetValue: kpi.targetValue,
        isOnTarget: kpi.value >= (kpi.targetValue || 0) * 0.95,
        statusColor:
          kpi.value >= (kpi.targetValue || 0) * 0.95
            ? 'green'
            : kpi.value >= (kpi.targetValue || 0) * 0.85
              ? 'yellow'
              : 'red',
      });
      await this.kpiRepo.save(snapshot);
    }
  }

  // ============ Private Helper Methods ============

  /**
   * Get project budget data (BAC and planned curve)
   * This would typically come from the budgets/presupuestos module
   */
  private async getProjectBudget(
    ctx: ServiceContext,
    fraccionamientoId: string
  ): Promise<ProjectBudget> {
    // In a real implementation, this would query the presupuesto module
    // For now, we'll calculate from approved estimates as a proxy
    const estimates = await this.estimacionRepo.find({
      where: {
        tenantId: ctx.tenantId,
        fraccionamientoId,
        status: 'approved' as any,
      },
      order: { periodEnd: 'DESC' },
    });

    // Calculate BAC from contract value (placeholder - would come from contracts module)
    const totalEstimated = estimates.reduce((sum, e) => sum + Number(e.totalAmount), 0);
    const bac = totalEstimated * 1.2; // Assume estimates are ~80% of total budget

    // Determine project dates
    const startDate =
      estimates.length > 0
        ? new Date(Math.min(...estimates.map((e) => new Date(e.periodStart).getTime())))
        : new Date();
    const endDate = new Date(startDate);
    endDate.setMonth(endDate.getMonth() + 12); // Default 12 month project

    // Generate planned curve (S-curve typically)
    const plannedCurve = this.generatePlannedCurve(startDate, endDate, bac);

    return { fraccionamientoId, bac, startDate, endDate, plannedCurve };
  }

  /**
   * Calculate Earned Value (sum of approved estimate values to date)
   */
  private async calculateEarnedValue(
    ctx: ServiceContext,
    fraccionamientoId: string,
    asOfDate: Date
  ): Promise<number> {
    const estimates = await this.estimacionRepo.find({
      where: {
        tenantId: ctx.tenantId,
        fraccionamientoId,
        status: 'approved' as any,
        approvedAt: LessThanOrEqual(asOfDate),
      },
    });

    return estimates.reduce((sum, e) => sum + Number(e.subtotal), 0);
  }

  /**
   * Calculate Planned Value from the planned curve
   */
  private calculatePlannedValue(budget: ProjectBudget, asOfDate: Date): number {
    const point = budget.plannedCurve.find(
      (p) => p.date.getTime() >= asOfDate.getTime()
    );
    if (point) {
      return point.cumulativeValue;
    }
    // If past end date, return full BAC
    return budget.bac;
  }

  /**
   * Calculate planned percent complete
   */
  private calculatePlannedPercent(budget: ProjectBudget, asOfDate: Date): number {
    const pv = this.calculatePlannedValue(budget, asOfDate);
    return budget.bac > 0 ? (pv / budget.bac) * 100 : 0;
  }

  /**
   * Calculate Actual Cost (real costs incurred to date)
   * In a real implementation, this would query the financial/costs module
   */
  private async calculateActualCost(
    ctx: ServiceContext,
    fraccionamientoId: string,
    asOfDate: Date
  ): Promise<number> {
    // Sum of paid estimates as a proxy for actual cost
    const estimates = await this.estimacionRepo.find({
      where: {
        tenantId: ctx.tenantId,
        fraccionamientoId,
        status: 'paid' as any,
        paidAt: LessThanOrEqual(asOfDate),
      },
    });

    return estimates.reduce((sum, e) => sum + Number(e.totalAmount), 0);
  }

  /**
   * Compute all EVM metrics from base values
   */
  private computeEvmMetrics(
    bac: number,
    pv: number,
    ev: number,
    ac: number,
    date: Date
  ): EarnedValueMetrics {
    // Avoid division by zero
    const spi = pv > 0 ? ev / pv : 0;
    const cpi = ac > 0 ? ev / ac : 0;
    const sv = ev - pv;
    const cv = ev - ac;
    const eac = cpi > 0 ? bac / cpi : bac;
    const etc = eac - ac;
    const vac = bac - eac;
    const tcpi = bac - ac > 0 ? (bac - ev) / (bac - ac) : 0;
    const percentComplete = bac > 0 ? (ev / bac) * 100 : 0;

    // Determine status based on both SPI and CPI
    const minIndex = Math.min(spi, cpi);
    const status: 'green' | 'yellow' | 'red' =
      minIndex >= 0.95 ? 'green' : minIndex >= 0.85 ? 'yellow' : 'red';

    return {
      date,
      bac,
      pv,
      ev,
      ac,
      spi: Number(spi.toFixed(3)),
      cpi: Number(cpi.toFixed(3)),
      sv: Number(sv.toFixed(2)),
      cv: Number(cv.toFixed(2)),
      eac: Number(eac.toFixed(2)),
      etc: Number(etc.toFixed(2)),
      vac: Number(vac.toFixed(2)),
      tcpi: Number(tcpi.toFixed(3)),
      percentComplete: Number(percentComplete.toFixed(2)),
      status,
    };
  }

  /**
   * Generate S-curve for planned value (typical construction project)
   */
  private generatePlannedCurve(
    startDate: Date,
    endDate: Date,
    bac: number
  ): { date: Date; cumulativeValue: number }[] {
    const curve: { date: Date; cumulativeValue: number }[] = [];
    const totalDays =
      (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24);
    const weekCount = Math.ceil(totalDays / 7);

    for (let week = 0; week <= weekCount; week++) {
      const date = new Date(startDate);
      date.setDate(date.getDate() + week * 7);

      // S-curve formula: cumulative % = 1 / (1 + e^(-k*(t - t0)))
      // Simplified: use sigmoid function
      const t = week / weekCount; // normalized time 0 to 1
      const k = 8; // steepness factor
      const sigmoid = 1 / (1 + Math.exp(-k * (t - 0.5)));
      const cumulativeValue = bac * sigmoid;

      curve.push({ date, cumulativeValue });
    }

    return curve;
  }

  /**
   * Generate date series based on period type
   */
  private generateDateSeries(
    from: Date,
    to: Date,
    periodType: 'daily' | 'weekly' | 'monthly'
  ): Date[] {
    const dates: Date[] = [];
    const current = new Date(from);

    while (current <= to) {
      dates.push(new Date(current));

      switch (periodType) {
        case 'daily':
          current.setDate(current.getDate() + 1);
          break;
        case 'weekly':
          current.setDate(current.getDate() + 7);
          break;
        case 'monthly':
          current.setMonth(current.getMonth() + 1);
          break;
      }
    }

    return dates;
  }

  /**
   * Get active projects for the tenant
   */
  private async getActiveProjects(
    ctx: ServiceContext,
    fraccionamientoIds?: string[]
  ): Promise<{ fraccionamientoId: string; name: string }[]> {
    // Query distinct fraccionamientos from estimates
    const qb = this.estimacionRepo
      .createQueryBuilder('e')
      .select('DISTINCT e.fraccionamiento_id', 'fraccionamientoId')
      .addSelect('f.nombre', 'name')
      .leftJoin('construction.fraccionamientos', 'f', 'f.id = e.fraccionamiento_id')
      .where('e.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('e.deleted_at IS NULL');

    if (fraccionamientoIds && fraccionamientoIds.length > 0) {
      qb.andWhere('e.fraccionamiento_id IN (:...ids)', { ids: fraccionamientoIds });
    }

    const results = await qb.getRawMany();
    return results.map((r) => ({
      fraccionamientoId: r.fraccionamientoId,
      name: r.name || 'Unknown Project',
    }));
  }
}
