/**
 * ProgramaObraService - Gestion de Programas de Obra
 *
 * Gestiona programas maestros de obra (planificacion), actividades WBS,
 * versionamiento para reprogramaciones, y datos para Gantt y Curva S.
 *
 * @module Progress
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import { ProgramaObra } from '../entities/programa-obra.entity';
import { ProgramaActividad } from '../entities/programa-actividad.entity';
import { Fraccionamiento } from '../../construction/entities/fraccionamiento.entity';
import { Concepto } from '../../budgets/entities/concepto.entity';
import { AvanceObra } from '../entities/avance-obra.entity';

/**
 * Service context for multi-tenant operations
 */
export interface ServiceContext {
  tenantId: string;
  userId?: string;
}

/**
 * Paginated result
 */
export interface PaginatedResult<T> {
  data: T[];
  total: number;
  page: number;
  limit: number;
  totalPages: number;
}

/**
 * DTO for creating a programa de obra
 */
export interface CreateProgramaDto {
  fraccionamientoId: string;
  name: string;
  startDate: Date;
  endDate: Date;
}

/**
 * DTO for updating a programa de obra
 */
export interface UpdateProgramaDto {
  name?: string;
  startDate?: Date;
  endDate?: Date;
  isActive?: boolean;
}

/**
 * DTO for creating an actividad
 */
export interface CreateActividadDto {
  name: string;
  conceptoId?: string;
  parentId?: string;
  plannedStart?: Date;
  plannedEnd?: Date;
  plannedQuantity?: number;
  plannedWeight?: number;
}

/**
 * DTO for updating an actividad
 */
export interface UpdateActividadDto {
  name?: string;
  conceptoId?: string;
  parentId?: string;
  plannedStart?: Date;
  plannedEnd?: Date;
  plannedQuantity?: number;
  plannedWeight?: number;
}

/**
 * Filters for programa queries
 */
export interface ProgramaFilters {
  fraccionamientoId?: string;
  isActive?: boolean;
  search?: string;
}

/**
 * Gantt chart activity data
 */
export interface GanttActivity {
  id: string;
  name: string;
  wbsCode: string | null;
  parentId: string | null;
  conceptoId: string | null;
  conceptoCode: string | null;
  plannedStart: Date | null;
  plannedEnd: Date | null;
  plannedQuantity: number;
  plannedWeight: number;
  actualProgress: number;
  actualQuantity: number;
  sequence: number;
  children: GanttActivity[];
}

/**
 * Gantt chart data structure
 */
export interface GanttData {
  programaId: string;
  programaName: string;
  programaCode: string;
  version: number;
  startDate: Date;
  endDate: Date;
  activities: GanttActivity[];
  totalPlannedWeight: number;
  totalActualProgress: number;
}

/**
 * S-Curve data point
 */
export interface SCurveDataPoint {
  date: Date;
  plannedCumulative: number;
  actualCumulative: number;
  plannedPercent: number;
  actualPercent: number;
  variance: number;
}

/**
 * S-Curve data structure
 */
export interface SCurveData {
  programaId: string;
  programaName: string;
  totalPlannedWeight: number;
  dataPoints: SCurveDataPoint[];
  currentVariance: number;
  status: 'ahead' | 'on_track' | 'behind' | 'critical';
}

/**
 * Reorder item structure
 */
export interface ReorderItem {
  actividadId: string;
  sequence: number;
  parentId?: string | null;
}

export class ProgramaObraService {
  private readonly programaRepository: Repository<ProgramaObra>;
  private readonly actividadRepository: Repository<ProgramaActividad>;
  private readonly fraccionamientoRepository: Repository<Fraccionamiento>;
  private readonly conceptoRepository: Repository<Concepto>;
  private readonly avanceRepository: Repository<AvanceObra>;
  private readonly dataSource: DataSource;

  constructor(dataSource: DataSource) {
    this.dataSource = dataSource;
    this.programaRepository = dataSource.getRepository(ProgramaObra);
    this.actividadRepository = dataSource.getRepository(ProgramaActividad);
    this.fraccionamientoRepository = dataSource.getRepository(Fraccionamiento);
    this.conceptoRepository = dataSource.getRepository(Concepto);
    this.avanceRepository = dataSource.getRepository(AvanceObra);
  }

  // ============ PROGRAMA METHODS ============

  /**
   * Create a new programa de obra
   * Generates code PRG-YYYY-NNNNN, sets version to 1, isActive to true
   */
  async createPrograma(ctx: ServiceContext, dto: CreateProgramaDto): Promise<ProgramaObra> {
    // Validate fraccionamiento exists
    const fraccionamiento = await this.fraccionamientoRepository.findOne({
      where: {
        id: dto.fraccionamientoId,
        tenantId: ctx.tenantId,
      },
    });

    if (!fraccionamiento) {
      throw new Error(`Fraccionamiento with id '${dto.fraccionamientoId}' not found`);
    }

    // Generate code: PRG-YYYY-NNNNN
    const code = await this.generateProgramaCode(ctx);

    const programa = this.programaRepository.create({
      tenantId: ctx.tenantId,
      fraccionamientoId: dto.fraccionamientoId,
      code,
      name: dto.name,
      startDate: dto.startDate,
      endDate: dto.endDate,
      version: 1,
      isActive: true,
      createdById: ctx.userId,
    });

    return this.programaRepository.save(programa);
  }

  /**
   * Find all programas with filters and pagination
   */
  async findAll(
    ctx: ServiceContext,
    filters: ProgramaFilters,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<ProgramaObra>> {
    const qb = this.programaRepository
      .createQueryBuilder('p')
      .leftJoinAndSelect('p.fraccionamiento', 'f')
      .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('p.deleted_at IS NULL');

    if (filters.fraccionamientoId) {
      qb.andWhere('p.fraccionamiento_id = :fraccionamientoId', {
        fraccionamientoId: filters.fraccionamientoId,
      });
    }

    if (filters.isActive !== undefined) {
      qb.andWhere('p.is_active = :isActive', { isActive: filters.isActive });
    }

    if (filters.search) {
      qb.andWhere('(p.code ILIKE :search OR p.name ILIKE :search)', {
        search: `%${filters.search}%`,
      });
    }

    const skip = (page - 1) * limit;
    qb.orderBy('p.created_at', 'DESC').skip(skip).take(limit);

    const [data, total] = await qb.getManyAndCount();

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

  /**
   * Find programa by ID with actividades
   */
  async findById(ctx: ServiceContext, id: string): Promise<ProgramaObra | null> {
    return this.programaRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['fraccionamiento', 'actividades', 'actividades.concepto'],
    });
  }

  /**
   * Update programa details
   */
  async updatePrograma(
    ctx: ServiceContext,
    id: string,
    dto: UpdateProgramaDto
  ): Promise<ProgramaObra | null> {
    const programa = await this.programaRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!programa) {
      return null;
    }

    if (dto.name !== undefined) {
      programa.name = dto.name;
    }
    if (dto.startDate !== undefined) {
      programa.startDate = dto.startDate;
    }
    if (dto.endDate !== undefined) {
      programa.endDate = dto.endDate;
    }
    if (dto.isActive !== undefined) {
      programa.isActive = dto.isActive;
    }

    programa.updatedById = ctx.userId ?? null;

    return this.programaRepository.save(programa);
  }

  /**
   * Create new version of programa (reprogramming)
   * Copies all actividades from previous version
   */
  async createVersion(ctx: ServiceContext, id: string): Promise<ProgramaObra> {
    return this.dataSource.transaction(async (manager) => {
      const programaRepo = manager.getRepository(ProgramaObra);
      const actividadRepo = manager.getRepository(ProgramaActividad);

      // Find current programa
      const currentPrograma = await programaRepo.findOne({
        where: {
          id,
          tenantId: ctx.tenantId,
          deletedAt: IsNull(),
        },
        relations: ['actividades'],
      });

      if (!currentPrograma) {
        throw new Error(`Programa with id '${id}' not found`);
      }

      // Deactivate current version
      currentPrograma.isActive = false;
      currentPrograma.updatedById = ctx.userId ?? null;
      await programaRepo.save(currentPrograma);

      // Create new version
      const newPrograma = programaRepo.create({
        tenantId: ctx.tenantId,
        fraccionamientoId: currentPrograma.fraccionamientoId,
        code: currentPrograma.code,
        name: currentPrograma.name,
        startDate: currentPrograma.startDate,
        endDate: currentPrograma.endDate,
        version: currentPrograma.version + 1,
        isActive: true,
        createdById: ctx.userId,
      });

      const savedPrograma = await programaRepo.save(newPrograma);

      // Copy actividades with parent-child mapping
      if (currentPrograma.actividades && currentPrograma.actividades.length > 0) {
        const oldToNewIdMap = new Map<string, string>();

        // First pass: create actividades without parent references
        const actividadesSorted = this.sortActividadesByHierarchy(currentPrograma.actividades);

        for (const oldActividad of actividadesSorted) {
          const newActividad = actividadRepo.create({
            tenantId: ctx.tenantId,
            programaId: savedPrograma.id,
            conceptoId: oldActividad.conceptoId,
            parentId: null, // Will be set in second pass
            name: oldActividad.name,
            sequence: oldActividad.sequence,
            plannedStart: oldActividad.plannedStart,
            plannedEnd: oldActividad.plannedEnd,
            plannedQuantity: oldActividad.plannedQuantity,
            plannedWeight: oldActividad.plannedWeight,
            wbsCode: oldActividad.wbsCode,
            createdById: ctx.userId,
          });

          const savedActividad = await actividadRepo.save(newActividad);
          oldToNewIdMap.set(oldActividad.id, savedActividad.id);
        }

        // Second pass: update parent references
        for (const oldActividad of actividadesSorted) {
          if (oldActividad.parentId) {
            const newId = oldToNewIdMap.get(oldActividad.id);
            const newParentId = oldToNewIdMap.get(oldActividad.parentId);

            if (newId && newParentId) {
              await actividadRepo.update(
                { id: newId },
                { parentId: newParentId }
              );
            }
          }
        }
      }

      return savedPrograma;
    });
  }

  // ============ ACTIVIDAD METHODS ============

  /**
   * Add actividad to programa
   * Supports parent-child hierarchy with auto-generated WBS code
   */
  async addActividad(
    ctx: ServiceContext,
    programaId: string,
    dto: CreateActividadDto
  ): Promise<ProgramaActividad> {
    // Validate programa exists
    const programa = await this.programaRepository.findOne({
      where: {
        id: programaId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!programa) {
      throw new Error(`Programa with id '${programaId}' not found`);
    }

    // Validate concepto if provided
    if (dto.conceptoId) {
      const concepto = await this.conceptoRepository.findOne({
        where: {
          id: dto.conceptoId,
          tenantId: ctx.tenantId,
          deletedAt: IsNull(),
        },
      });

      if (!concepto) {
        throw new Error(`Concepto with id '${dto.conceptoId}' not found`);
      }
    }

    // Validate parent if provided
    let parentActividad: ProgramaActividad | null = null;
    if (dto.parentId) {
      parentActividad = await this.actividadRepository.findOne({
        where: {
          id: dto.parentId,
          programaId,
          tenantId: ctx.tenantId,
          deletedAt: IsNull(),
        },
      });

      if (!parentActividad) {
        throw new Error(`Parent actividad with id '${dto.parentId}' not found`);
      }
    }

    // Calculate next sequence
    const maxSequence = await this.getMaxSequence(ctx, programaId, dto.parentId ?? null);

    // Generate WBS code
    const wbsCode = await this.generateWbsCode(ctx, programaId, dto.parentId ?? null);

    const actividad = this.actividadRepository.create({
      tenantId: ctx.tenantId,
      programaId,
      conceptoId: dto.conceptoId ?? null,
      parentId: dto.parentId ?? null,
      name: dto.name,
      sequence: maxSequence + 1,
      plannedStart: dto.plannedStart ?? null,
      plannedEnd: dto.plannedEnd ?? null,
      plannedQuantity: dto.plannedQuantity ?? 0,
      plannedWeight: dto.plannedWeight ?? 0,
      wbsCode,
      createdById: ctx.userId,
    });

    return this.actividadRepository.save(actividad);
  }

  /**
   * Update actividad
   */
  async updateActividad(
    ctx: ServiceContext,
    actividadId: string,
    dto: UpdateActividadDto
  ): Promise<ProgramaActividad | null> {
    const actividad = await this.actividadRepository.findOne({
      where: {
        id: actividadId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!actividad) {
      return null;
    }

    // Validate concepto if changing
    if (dto.conceptoId !== undefined && dto.conceptoId !== null) {
      const concepto = await this.conceptoRepository.findOne({
        where: {
          id: dto.conceptoId,
          tenantId: ctx.tenantId,
          deletedAt: IsNull(),
        },
      });

      if (!concepto) {
        throw new Error(`Concepto with id '${dto.conceptoId}' not found`);
      }
    }

    // Validate parent if changing
    if (dto.parentId !== undefined && dto.parentId !== null) {
      // Prevent self-reference
      if (dto.parentId === actividadId) {
        throw new Error('Actividad cannot be its own parent');
      }

      const parentActividad = await this.actividadRepository.findOne({
        where: {
          id: dto.parentId,
          programaId: actividad.programaId,
          tenantId: ctx.tenantId,
          deletedAt: IsNull(),
        },
      });

      if (!parentActividad) {
        throw new Error(`Parent actividad with id '${dto.parentId}' not found`);
      }

      // Prevent circular references
      const isCircular = await this.checkCircularReference(actividadId, dto.parentId);
      if (isCircular) {
        throw new Error('Cannot set parent: would create circular reference');
      }
    }

    if (dto.name !== undefined) {
      actividad.name = dto.name;
    }
    if (dto.conceptoId !== undefined) {
      actividad.conceptoId = dto.conceptoId;
    }
    if (dto.parentId !== undefined) {
      actividad.parentId = dto.parentId;
      // Regenerate WBS code when parent changes
      actividad.wbsCode = await this.generateWbsCode(
        ctx,
        actividad.programaId,
        dto.parentId
      );
    }
    if (dto.plannedStart !== undefined) {
      actividad.plannedStart = dto.plannedStart;
    }
    if (dto.plannedEnd !== undefined) {
      actividad.plannedEnd = dto.plannedEnd;
    }
    if (dto.plannedQuantity !== undefined) {
      actividad.plannedQuantity = dto.plannedQuantity;
    }
    if (dto.plannedWeight !== undefined) {
      actividad.plannedWeight = dto.plannedWeight;
    }

    actividad.updatedById = ctx.userId ?? null;

    return this.actividadRepository.save(actividad);
  }

  /**
   * Soft delete actividad
   */
  async removeActividad(ctx: ServiceContext, actividadId: string): Promise<boolean> {
    const actividad = await this.actividadRepository.findOne({
      where: {
        id: actividadId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!actividad) {
      return false;
    }

    actividad.deletedAt = new Date();
    actividad.deletedById = ctx.userId ?? null;

    await this.actividadRepository.save(actividad);

    // Also soft delete children
    await this.softDeleteChildren(ctx, actividadId);

    return true;
  }

  /**
   * Reorder actividades
   */
  async reorderActividades(
    ctx: ServiceContext,
    programaId: string,
    orden: ReorderItem[]
  ): Promise<boolean> {
    // Validate programa exists
    const programa = await this.programaRepository.findOne({
      where: {
        id: programaId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!programa) {
      throw new Error(`Programa with id '${programaId}' not found`);
    }

    return this.dataSource.transaction(async (manager) => {
      const actividadRepo = manager.getRepository(ProgramaActividad);

      for (const item of orden) {
        await actividadRepo.update(
          {
            id: item.actividadId,
            programaId,
            tenantId: ctx.tenantId,
          },
          {
            sequence: item.sequence,
            parentId: item.parentId !== undefined ? item.parentId : undefined,
            updatedById: ctx.userId ?? null,
          }
        );
      }

      // Regenerate WBS codes for all actividades
      const actividades = await actividadRepo.find({
        where: {
          programaId,
          tenantId: ctx.tenantId,
          deletedAt: IsNull(),
        },
        order: { sequence: 'ASC' },
      });

      await this.regenerateAllWbsCodes(ctx, programaId, actividades, actividadRepo);

      return true;
    });
  }

  // ============ GANTT METHODS ============

  /**
   * Get data formatted for Gantt chart
   */
  async getGanttData(ctx: ServiceContext, programaId: string): Promise<GanttData | null> {
    const programa = await this.programaRepository.findOne({
      where: {
        id: programaId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['actividades', 'actividades.concepto'],
    });

    if (!programa) {
      return null;
    }

    // Get all actividades with their progress
    const actividades = await this.actividadRepository.find({
      where: {
        programaId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['concepto'],
      order: { sequence: 'ASC' },
    });

    // Calculate progress for each actividad from avances
    const actividadProgress = await this.calculateActividadesProgress(ctx, actividades);

    // Build hierarchical structure
    const ganttActivities = this.buildGanttHierarchy(actividades, actividadProgress);

    // Calculate totals
    const totalPlannedWeight = actividades
      .filter((a) => !a.parentId) // Only root level for total
      .reduce((sum, a) => sum + Number(a.plannedWeight), 0);

    const totalActualProgress = this.calculateWeightedProgress(actividades, actividadProgress);

    return {
      programaId: programa.id,
      programaName: programa.name,
      programaCode: programa.code,
      version: programa.version,
      startDate: programa.startDate,
      endDate: programa.endDate,
      activities: ganttActivities,
      totalPlannedWeight,
      totalActualProgress,
    };
  }

  // ============ S-CURVE METHODS ============

  /**
   * Get S-curve data for programa
   */
  async getSCurveData(
    ctx: ServiceContext,
    programaId: string,
    periodType: 'daily' | 'weekly' | 'monthly' = 'weekly'
  ): Promise<SCurveData | null> {
    const programa = await this.programaRepository.findOne({
      where: {
        id: programaId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!programa) {
      return null;
    }

    // Get all actividades
    const actividades = await this.actividadRepository.find({
      where: {
        programaId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      order: { sequence: 'ASC' },
    });

    if (actividades.length === 0) {
      return {
        programaId: programa.id,
        programaName: programa.name,
        totalPlannedWeight: 0,
        dataPoints: [],
        currentVariance: 0,
        status: 'on_track',
      };
    }

    // Calculate total planned weight
    const totalPlannedWeight = actividades.reduce(
      (sum, a) => sum + Number(a.plannedWeight),
      0
    );

    // Generate date series from programa start to today (or end date if past)
    const today = new Date();
    const endDate = new Date(programa.endDate) < today ? new Date(programa.endDate) : today;
    const dates = this.generateDateSeries(new Date(programa.startDate), endDate, periodType);

    // Calculate planned curve (based on actividad dates and weights)
    const plannedCurve = this.calculatePlannedCurve(actividades, dates, totalPlannedWeight);

    // Calculate actual curve (based on avances)
    const actualCurve = await this.calculateActualCurve(ctx, actividades, dates, totalPlannedWeight);

    // Build data points
    const dataPoints: SCurveDataPoint[] = dates.map((date, index) => {
      const planned = plannedCurve[index] || 0;
      const actual = actualCurve[index] || 0;
      return {
        date,
        plannedCumulative: planned,
        actualCumulative: actual,
        plannedPercent: totalPlannedWeight > 0 ? (planned / totalPlannedWeight) * 100 : 0,
        actualPercent: totalPlannedWeight > 0 ? (actual / totalPlannedWeight) * 100 : 0,
        variance: actual - planned,
      };
    });

    // Calculate current variance
    const lastPoint = dataPoints[dataPoints.length - 1];
    const currentVariance = lastPoint ? lastPoint.actualPercent - lastPoint.plannedPercent : 0;

    // Determine status
    let status: 'ahead' | 'on_track' | 'behind' | 'critical';
    if (currentVariance >= 5) {
      status = 'ahead';
    } else if (currentVariance >= -5) {
      status = 'on_track';
    } else if (currentVariance >= -15) {
      status = 'behind';
    } else {
      status = 'critical';
    }

    return {
      programaId: programa.id,
      programaName: programa.name,
      totalPlannedWeight,
      dataPoints,
      currentVariance,
      status,
    };
  }

  // ============ PRIVATE HELPER METHODS ============

  /**
   * Generate programa code: PRG-YYYY-NNNNN
   */
  private async generateProgramaCode(ctx: ServiceContext): Promise<string> {
    const year = new Date().getFullYear();
    const prefix = `PRG-${year}-`;

    // Find max number for this year
    const lastPrograma = await this.programaRepository
      .createQueryBuilder('p')
      .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('p.code LIKE :prefix', { prefix: `${prefix}%` })
      .orderBy('p.code', 'DESC')
      .getOne();

    let nextNumber = 1;
    if (lastPrograma && lastPrograma.code) {
      const lastNumber = parseInt(lastPrograma.code.replace(prefix, ''), 10);
      if (!isNaN(lastNumber)) {
        nextNumber = lastNumber + 1;
      }
    }

    return `${prefix}${String(nextNumber).padStart(5, '0')}`;
  }

  /**
   * Get max sequence for actividades at a level
   */
  private async getMaxSequence(
    ctx: ServiceContext,
    programaId: string,
    parentId: string | null
  ): Promise<number> {
    const qb = this.actividadRepository
      .createQueryBuilder('a')
      .select('MAX(a.sequence)', 'maxSeq')
      .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('a.programa_id = :programaId', { programaId })
      .andWhere('a.deleted_at IS NULL');

    if (parentId) {
      qb.andWhere('a.parent_id = :parentId', { parentId });
    } else {
      qb.andWhere('a.parent_id IS NULL');
    }

    const result = await qb.getRawOne();
    return result?.maxSeq || 0;
  }

  /**
   * Generate WBS code based on hierarchy
   */
  private async generateWbsCode(
    ctx: ServiceContext,
    programaId: string,
    parentId: string | null
  ): Promise<string> {
    if (!parentId) {
      // Root level: count existing root actividades
      const count = await this.actividadRepository.count({
        where: {
          programaId,
          tenantId: ctx.tenantId,
          parentId: IsNull(),
          deletedAt: IsNull(),
        },
      });
      return String(count + 1);
    }

    // Child level: get parent's WBS and append child number
    const parent = await this.actividadRepository.findOne({
      where: {
        id: parentId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!parent) {
      return '1';
    }

    const childCount = await this.actividadRepository.count({
      where: {
        programaId,
        tenantId: ctx.tenantId,
        parentId,
        deletedAt: IsNull(),
      },
    });

    const parentWbs = parent.wbsCode || '1';
    return `${parentWbs}.${childCount + 1}`;
  }

  /**
   * Check for circular reference in parent chain
   */
  private async checkCircularReference(
    actividadId: string,
    newParentId: string
  ): Promise<boolean> {
    let currentId: string | null = newParentId;
    const visited = new Set<string>();

    while (currentId) {
      if (currentId === actividadId) {
        return true;
      }
      if (visited.has(currentId)) {
        return true;
      }
      visited.add(currentId);

      const parent = await this.actividadRepository.findOne({
        where: { id: currentId },
        select: ['parentId'],
      });

      currentId = parent?.parentId ?? null;
    }

    return false;
  }

  /**
   * Soft delete children of an actividad recursively
   */
  private async softDeleteChildren(ctx: ServiceContext, parentId: string): Promise<void> {
    const children = await this.actividadRepository.find({
      where: {
        parentId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    for (const child of children) {
      child.deletedAt = new Date();
      child.deletedById = ctx.userId ?? null;
      await this.actividadRepository.save(child);
      await this.softDeleteChildren(ctx, child.id);
    }
  }

  /**
   * Sort actividades by hierarchy (parents before children)
   */
  private sortActividadesByHierarchy(actividades: ProgramaActividad[]): ProgramaActividad[] {
    const sorted: ProgramaActividad[] = [];

    const addWithChildren = (parentId: string | null) => {
      const items = actividades.filter((a) => a.parentId === parentId);
      items.sort((a, b) => a.sequence - b.sequence);
      for (const item of items) {
        sorted.push(item);
        addWithChildren(item.id);
      }
    };

    addWithChildren(null);

    // Add any orphans (items whose parent is not in this list)
    for (const a of actividades) {
      if (!sorted.includes(a)) {
        sorted.push(a);
      }
    }

    return sorted;
  }

  /**
   * Regenerate WBS codes for all actividades
   */
  private async regenerateAllWbsCodes(
    _ctx: ServiceContext,
    _programaId: string,
    actividades: ProgramaActividad[],
    repo: Repository<ProgramaActividad>
  ): Promise<void> {
    const wbsMap = new Map<string, string>();

    const generateWbs = (parentId: string | null, parentWbs: string | null): void => {
      const children = actividades.filter((a) => a.parentId === parentId);
      children.sort((a, b) => a.sequence - b.sequence);

      let counter = 1;
      for (const child of children) {
        const wbs = parentWbs ? `${parentWbs}.${counter}` : String(counter);
        wbsMap.set(child.id, wbs);
        generateWbs(child.id, wbs);
        counter++;
      }
    };

    generateWbs(null, null);

    // Update WBS codes
    for (const actividad of actividades) {
      const newWbs = wbsMap.get(actividad.id);
      if (newWbs && newWbs !== actividad.wbsCode) {
        await repo.update({ id: actividad.id }, { wbsCode: newWbs });
      }
    }
  }

  /**
   * Calculate progress for actividades from avances
   */
  private async calculateActividadesProgress(
    ctx: ServiceContext,
    actividades: ProgramaActividad[]
  ): Promise<Map<string, { progress: number; quantity: number }>> {
    const progressMap = new Map<string, { progress: number; quantity: number }>();

    // Get concepto IDs from actividades
    const conceptoIds = actividades
      .filter((a) => a.conceptoId)
      .map((a) => a.conceptoId as string);

    if (conceptoIds.length === 0) {
      return progressMap;
    }

    // Query approved avances grouped by concepto
    const avancesQuery = await this.avanceRepository
      .createQueryBuilder('a')
      .select('a.concepto_id', 'conceptoId')
      .addSelect('SUM(a.quantity_executed)', 'totalQuantity')
      .addSelect('AVG(a.percentage_executed)', 'avgProgress')
      .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('a.concepto_id IN (:...conceptoIds)', { conceptoIds })
      .andWhere('a.status = :status', { status: 'approved' })
      .andWhere('a.deleted_at IS NULL')
      .groupBy('a.concepto_id')
      .getRawMany();

    // Build concepto to progress map
    const conceptoProgressMap = new Map<string, { progress: number; quantity: number }>();
    for (const row of avancesQuery) {
      conceptoProgressMap.set(row.conceptoId, {
        progress: Number(row.avgProgress) || 0,
        quantity: Number(row.totalQuantity) || 0,
      });
    }

    // Map to actividades
    for (const actividad of actividades) {
      if (actividad.conceptoId) {
        const progress = conceptoProgressMap.get(actividad.conceptoId);
        if (progress) {
          progressMap.set(actividad.id, progress);
        }
      }
    }

    return progressMap;
  }

  /**
   * Build hierarchical Gantt activity structure
   */
  private buildGanttHierarchy(
    actividades: ProgramaActividad[],
    progressMap: Map<string, { progress: number; quantity: number }>
  ): GanttActivity[] {
    const buildNode = (actividad: ProgramaActividad): GanttActivity => {
      const children = actividades
        .filter((a) => a.parentId === actividad.id)
        .sort((a, b) => a.sequence - b.sequence)
        .map((child) => buildNode(child));

      const progress = progressMap.get(actividad.id) || { progress: 0, quantity: 0 };

      return {
        id: actividad.id,
        name: actividad.name,
        wbsCode: actividad.wbsCode,
        parentId: actividad.parentId,
        conceptoId: actividad.conceptoId,
        conceptoCode: actividad.concepto?.code ?? null,
        plannedStart: actividad.plannedStart,
        plannedEnd: actividad.plannedEnd,
        plannedQuantity: Number(actividad.plannedQuantity),
        plannedWeight: Number(actividad.plannedWeight),
        actualProgress: progress.progress,
        actualQuantity: progress.quantity,
        sequence: actividad.sequence,
        children,
      };
    };

    return actividades
      .filter((a) => !a.parentId)
      .sort((a, b) => a.sequence - b.sequence)
      .map((a) => buildNode(a));
  }

  /**
   * Calculate weighted average progress
   */
  private calculateWeightedProgress(
    actividades: ProgramaActividad[],
    progressMap: Map<string, { progress: number; quantity: number }>
  ): number {
    let totalWeight = 0;
    let weightedSum = 0;

    for (const actividad of actividades) {
      const weight = Number(actividad.plannedWeight);
      const progress = progressMap.get(actividad.id)?.progress || 0;

      totalWeight += weight;
      weightedSum += weight * progress;
    }

    return totalWeight > 0 ? weightedSum / totalWeight : 0;
  }

  /**
   * Generate date series for S-curve
   */
  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;
  }

  /**
   * Calculate planned cumulative curve based on actividad dates
   */
  private calculatePlannedCurve(
    actividades: ProgramaActividad[],
    dates: Date[],
    _totalWeight: number
  ): number[] {
    const curve: number[] = [];

    for (const date of dates) {
      let cumulativeWeight = 0;

      for (const actividad of actividades) {
        if (!actividad.plannedStart || !actividad.plannedEnd) {
          continue;
        }

        const start = new Date(actividad.plannedStart);
        const end = new Date(actividad.plannedEnd);
        const weight = Number(actividad.plannedWeight);

        if (date < start) {
          // Before activity starts: 0 weight
          continue;
        } else if (date >= end) {
          // After activity ends: full weight
          cumulativeWeight += weight;
        } else {
          // During activity: proportional weight
          const totalDuration = end.getTime() - start.getTime();
          const elapsedDuration = date.getTime() - start.getTime();
          const proportion = totalDuration > 0 ? elapsedDuration / totalDuration : 0;
          cumulativeWeight += weight * proportion;
        }
      }

      curve.push(cumulativeWeight);
    }

    return curve;
  }

  /**
   * Calculate actual cumulative curve based on avances
   */
  private async calculateActualCurve(
    ctx: ServiceContext,
    actividades: ProgramaActividad[],
    dates: Date[],
    _totalWeight: number
  ): Promise<number[]> {
    const conceptoIds = actividades
      .filter((a) => a.conceptoId)
      .map((a) => a.conceptoId as string);

    if (conceptoIds.length === 0) {
      return dates.map(() => 0);
    }

    // Create concepto to weight map
    const conceptoWeightMap = new Map<string, number>();
    for (const actividad of actividades) {
      if (actividad.conceptoId) {
        const existing = conceptoWeightMap.get(actividad.conceptoId) || 0;
        conceptoWeightMap.set(actividad.conceptoId, existing + Number(actividad.plannedWeight));
      }
    }

    const curve: number[] = [];

    for (const date of dates) {
      // Get approved avances up to this date
      const avances = await this.avanceRepository
        .createQueryBuilder('a')
        .select('a.concepto_id', 'conceptoId')
        .addSelect('AVG(a.percentage_executed)', 'avgProgress')
        .where('a.tenant_id = :tenantId', { tenantId: ctx.tenantId })
        .andWhere('a.concepto_id IN (:...conceptoIds)', { conceptoIds })
        .andWhere('a.status = :status', { status: 'approved' })
        .andWhere('a.capture_date <= :date', { date })
        .andWhere('a.deleted_at IS NULL')
        .groupBy('a.concepto_id')
        .getRawMany();

      let cumulativeWeight = 0;

      for (const avance of avances) {
        const weight = conceptoWeightMap.get(avance.conceptoId) || 0;
        const progress = Number(avance.avgProgress) || 0;
        cumulativeWeight += weight * (progress / 100);
      }

      curve.push(cumulativeWeight);
    }

    return curve;
  }
}
