/**
 * PresupuestoService - Gestión de Presupuestos de Obra
 *
 * Gestiona presupuestos de obra con sus partidas.
 * Soporta versionamiento y aprobación.
 *
 * @module Budgets
 */

import { DataSource, Repository, IsNull } from 'typeorm';
import { Presupuesto } from '../entities/presupuesto.entity';
import { PresupuestoPartida } from '../entities/presupuesto-partida.entity';

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

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

export interface CreatePresupuestoDto {
  code: string;
  name: string;
  description?: string;
  fraccionamientoId?: string;
  prototipoId?: string;
  currencyId?: string;
}

export interface AddPartidaDto {
  conceptoId: string;
  quantity: number;
  unitPrice: number;
  sequence?: number;
}

export interface UpdatePartidaDto {
  quantity?: number;
  unitPrice?: number;
  sequence?: number;
}

export class PresupuestoService {
  private repository: Repository<Presupuesto>;
  private partidaRepository: Repository<PresupuestoPartida>;

  constructor(dataSource: DataSource) {
    this.repository = dataSource.getRepository(Presupuesto);
    this.partidaRepository = dataSource.getRepository(PresupuestoPartida);
  }

  /**
   * Busca un presupuesto por ID
   */
  async findById(ctx: ServiceContext, id: string): Promise<Presupuesto | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });
  }

  /**
   * Busca todos los presupuestos con paginación
   */
  async findAll(
    ctx: ServiceContext,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Presupuesto>> {
    const skip = (page - 1) * limit;

    const [data, total] = await this.repository.findAndCount({
      where: {
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      order: { createdAt: 'DESC' },
      skip,
      take: limit,
    });

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

  /**
   * Crea un nuevo registro
   */
  async create(ctx: ServiceContext, data: Partial<Presupuesto>): Promise<Presupuesto> {
    const entity = this.repository.create({
      ...data,
      tenantId: ctx.tenantId,
      createdById: ctx.userId,
    });
    return this.repository.save(entity);
  }

  /**
   * Actualiza un registro
   */
  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<Presupuesto>
  ): Promise<Presupuesto | null> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return null;
    }

    Object.assign(entity, data);
    entity.updatedById = ctx.userId || null;

    return this.repository.save(entity);
  }

  /**
   * Soft delete de un registro
   */
  async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
    const entity = await this.findById(ctx, id);
    if (!entity) {
      return false;
    }

    entity.deletedAt = new Date();
    entity.deletedById = ctx.userId || null;
    await this.repository.save(entity);
    return true;
  }

  /**
   * Crear nuevo presupuesto
   */
  async createPresupuesto(
    ctx: ServiceContext,
    data: CreatePresupuestoDto
  ): Promise<Presupuesto> {
    return this.create(ctx, {
      ...data,
      version: 1,
      isActive: true,
      totalAmount: 0,
    });
  }

  /**
   * Obtener presupuestos por fraccionamiento
   */
  async findByFraccionamiento(
    ctx: ServiceContext,
    fraccionamientoId: string,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Presupuesto>> {
    const skip = (page - 1) * limit;

    const [data, total] = await this.repository.findAndCount({
      where: {
        tenantId: ctx.tenantId,
        fraccionamientoId,
        isActive: true,
        deletedAt: IsNull(),
      },
      order: { createdAt: 'DESC' },
      skip,
      take: limit,
    });

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

  /**
   * Obtener presupuesto con sus partidas
   */
  async findWithPartidas(
    ctx: ServiceContext,
    id: string
  ): Promise<Presupuesto | null> {
    return this.repository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
      relations: ['partidas', 'partidas.concepto'],
    });
  }

  /**
   * Agregar partida al presupuesto
   */
  async addPartida(
    ctx: ServiceContext,
    presupuestoId: string,
    data: AddPartidaDto
  ): Promise<PresupuestoPartida> {
    const presupuesto = await this.findById(ctx, presupuestoId);
    if (!presupuesto) {
      throw new Error('Presupuesto not found');
    }

    const partida = this.partidaRepository.create({
      tenantId: ctx.tenantId,
      presupuestoId,
      conceptoId: data.conceptoId,
      quantity: data.quantity,
      unitPrice: data.unitPrice,
      sequence: data.sequence || 0,
      createdById: ctx.userId,
    });

    const savedPartida = await this.partidaRepository.save(partida);
    await this.recalculateTotal(ctx, presupuestoId);

    return savedPartida;
  }

  /**
   * Actualizar partida
   */
  async updatePartida(
    ctx: ServiceContext,
    partidaId: string,
    data: UpdatePartidaDto
  ): Promise<PresupuestoPartida | null> {
    const partida = await this.partidaRepository.findOne({
      where: {
        id: partidaId,
        tenantId: ctx.tenantId,
        deletedAt: IsNull(),
      },
    });

    if (!partida) {
      return null;
    }

    const updated = this.partidaRepository.merge(partida, {
      ...data,
      updatedById: ctx.userId,
    });

    const saved = await this.partidaRepository.save(updated);
    await this.recalculateTotal(ctx, partida.presupuestoId);

    return saved;
  }

  /**
   * Eliminar partida
   */
  async removePartida(ctx: ServiceContext, partidaId: string): Promise<boolean> {
    const partida = await this.partidaRepository.findOne({
      where: {
        id: partidaId,
        tenantId: ctx.tenantId,
      },
    });

    if (!partida) {
      return false;
    }

    await this.partidaRepository.update(
      { id: partidaId },
      {
        deletedAt: new Date(),
        deletedById: ctx.userId,
      }
    );

    await this.recalculateTotal(ctx, partida.presupuestoId);
    return true;
  }

  /**
   * Recalcular total del presupuesto
   */
  async recalculateTotal(ctx: ServiceContext, presupuestoId: string): Promise<void> {
    const result = await this.partidaRepository
      .createQueryBuilder('p')
      .select('SUM(p.quantity * p.unit_price)', 'total')
      .where('p.presupuesto_id = :presupuestoId', { presupuestoId })
      .andWhere('p.deleted_at IS NULL')
      .getRawOne();

    const total = parseFloat(result?.total || '0');

    await this.repository.update(
      { id: presupuestoId } as any,
      { totalAmount: total, updatedById: ctx.userId }
    );
  }

  /**
   * Crear nueva versión del presupuesto
   */
  async createNewVersion(
    ctx: ServiceContext,
    presupuestoId: string
  ): Promise<Presupuesto> {
    const original = await this.findWithPartidas(ctx, presupuestoId);
    if (!original) {
      throw new Error('Presupuesto not found');
    }

    // Desactivar versión anterior
    await this.repository.update(
      { id: presupuestoId } as any,
      { isActive: false, updatedById: ctx.userId }
    );

    // Crear nueva versión
    const newVersion = await this.create(ctx, {
      code: original.code,
      name: original.name,
      description: original.description,
      fraccionamientoId: original.fraccionamientoId,
      prototipoId: original.prototipoId,
      currencyId: original.currencyId,
      version: original.version + 1,
      isActive: true,
      totalAmount: original.totalAmount,
    });

    // Copiar partidas
    for (const partida of original.partidas) {
      await this.partidaRepository.save(
        this.partidaRepository.create({
          tenantId: ctx.tenantId,
          presupuestoId: newVersion.id,
          conceptoId: partida.conceptoId,
          quantity: partida.quantity,
          unitPrice: partida.unitPrice,
          sequence: partida.sequence,
          createdById: ctx.userId,
        })
      );
    }

    return newVersion;
  }

  /**
   * Aprobar presupuesto
   */
  async approve(ctx: ServiceContext, presupuestoId: string): Promise<Presupuesto | null> {
    const presupuesto = await this.findById(ctx, presupuestoId);
    if (!presupuesto) {
      return null;
    }

    return this.update(ctx, presupuestoId, {
      approvedAt: new Date(),
      approvedById: ctx.userId,
    });
  }

  /**
   * Obtener presupuestos por prototipo
   */
  async findByPrototipo(
    ctx: ServiceContext,
    prototipoId: string,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Presupuesto>> {
    const skip = (page - 1) * limit;

    const [data, total] = await this.repository.findAndCount({
      where: {
        tenantId: ctx.tenantId,
        prototipoId,
        isActive: true,
        deletedAt: IsNull(),
      },
      order: { version: 'DESC' },
      skip,
      take: limit,
    });

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

  /**
   * Obtener historial de versiones de un presupuesto
   */
  async getVersionHistory(
    ctx: ServiceContext,
    code: string
  ): Promise<Presupuesto[]> {
    return this.repository.find({
      where: {
        tenantId: ctx.tenantId,
        code,
        deletedAt: IsNull(),
      },
      order: { version: 'DESC' },
    });
  }

  /**
   * Comparar dos presupuestos (versiones o diferentes presupuestos)
   * Retorna las diferencias entre partidas
   */
  async comparePresupuestos(
    ctx: ServiceContext,
    presupuestoId1: string,
    presupuestoId2: string
  ): Promise<BudgetComparison> {
    const [budget1, budget2] = await Promise.all([
      this.findWithPartidas(ctx, presupuestoId1),
      this.findWithPartidas(ctx, presupuestoId2),
    ]);

    if (!budget1 || !budget2) {
      throw new Error('One or both budgets not found');
    }

    const partidas1Map = new Map(
      budget1.partidas.map((p) => [p.conceptoId, p])
    );
    const partidas2Map = new Map(
      budget2.partidas.map((p) => [p.conceptoId, p])
    );

    const comparison: PartidaComparison[] = [];

    // Items in budget1
    for (const [conceptoId, partida1] of partidas1Map) {
      const partida2 = partidas2Map.get(conceptoId);
      if (partida2) {
        // Item exists in both
        const quantityDiff = Number(partida2.quantity) - Number(partida1.quantity);
        const priceDiff = Number(partida2.unitPrice) - Number(partida1.unitPrice);
        const total1 = Number(partida1.quantity) * Number(partida1.unitPrice);
        const total2 = Number(partida2.quantity) * Number(partida2.unitPrice);

        comparison.push({
          conceptoId,
          conceptoCode: partida1.concepto?.code,
          conceptoName: partida1.concepto?.name,
          status: quantityDiff === 0 && priceDiff === 0 ? 'unchanged' : 'modified',
          budget1: {
            quantity: Number(partida1.quantity),
            unitPrice: Number(partida1.unitPrice),
            total: total1,
          },
          budget2: {
            quantity: Number(partida2.quantity),
            unitPrice: Number(partida2.unitPrice),
            total: total2,
          },
          difference: {
            quantity: quantityDiff,
            unitPrice: priceDiff,
            total: total2 - total1,
            percentageChange: total1 > 0 ? ((total2 - total1) / total1) * 100 : 0,
          },
        });
      } else {
        // Item only in budget1 (removed in budget2)
        const total1 = Number(partida1.quantity) * Number(partida1.unitPrice);
        comparison.push({
          conceptoId,
          conceptoCode: partida1.concepto?.code,
          conceptoName: partida1.concepto?.name,
          status: 'removed',
          budget1: {
            quantity: Number(partida1.quantity),
            unitPrice: Number(partida1.unitPrice),
            total: total1,
          },
          budget2: null,
          difference: {
            quantity: -Number(partida1.quantity),
            unitPrice: -Number(partida1.unitPrice),
            total: -total1,
            percentageChange: -100,
          },
        });
      }
    }

    // Items only in budget2 (added)
    for (const [conceptoId, partida2] of partidas2Map) {
      if (!partidas1Map.has(conceptoId)) {
        const total2 = Number(partida2.quantity) * Number(partida2.unitPrice);
        comparison.push({
          conceptoId,
          conceptoCode: partida2.concepto?.code,
          conceptoName: partida2.concepto?.name,
          status: 'added',
          budget1: null,
          budget2: {
            quantity: Number(partida2.quantity),
            unitPrice: Number(partida2.unitPrice),
            total: total2,
          },
          difference: {
            quantity: Number(partida2.quantity),
            unitPrice: Number(partida2.unitPrice),
            total: total2,
            percentageChange: 100,
          },
        });
      }
    }

    // Calculate summary
    const totalBudget1 = Number(budget1.totalAmount);
    const totalBudget2 = Number(budget2.totalAmount);

    return {
      budget1: {
        id: budget1.id,
        code: budget1.code,
        name: budget1.name,
        version: budget1.version,
        totalAmount: totalBudget1,
        partidasCount: budget1.partidas.length,
      },
      budget2: {
        id: budget2.id,
        code: budget2.code,
        name: budget2.name,
        version: budget2.version,
        totalAmount: totalBudget2,
        partidasCount: budget2.partidas.length,
      },
      summary: {
        totalDifference: totalBudget2 - totalBudget1,
        percentageChange: totalBudget1 > 0 ? ((totalBudget2 - totalBudget1) / totalBudget1) * 100 : 0,
        addedItems: comparison.filter((c) => c.status === 'added').length,
        removedItems: comparison.filter((c) => c.status === 'removed').length,
        modifiedItems: comparison.filter((c) => c.status === 'modified').length,
        unchangedItems: comparison.filter((c) => c.status === 'unchanged').length,
      },
      partidas: comparison,
    };
  }

  /**
   * Comparar versiones de un mismo presupuesto
   */
  async compareVersions(
    ctx: ServiceContext,
    code: string,
    version1: number,
    version2: number
  ): Promise<BudgetComparison> {
    const budgets = await this.repository.find({
      where: {
        tenantId: ctx.tenantId,
        code,
        version: version1,
        deletedAt: IsNull(),
      },
    });

    const budget1 = budgets.find((b) => b.version === version1);
    const budget2 = await this.repository.findOne({
      where: {
        tenantId: ctx.tenantId,
        code,
        version: version2,
        deletedAt: IsNull(),
      },
    });

    if (!budget1 || !budget2) {
      throw new Error('One or both budget versions not found');
    }

    return this.comparePresupuestos(ctx, budget1.id, budget2.id);
  }

  /**
   * Duplicar presupuesto (sin versionar, crea uno nuevo)
   */
  async duplicate(
    ctx: ServiceContext,
    presupuestoId: string,
    newCode: string,
    newName: string
  ): Promise<Presupuesto> {
    const original = await this.findWithPartidas(ctx, presupuestoId);
    if (!original) {
      throw new Error('Presupuesto not found');
    }

    // Create new budget
    const newBudget = await this.create(ctx, {
      code: newCode,
      name: newName,
      description: original.description,
      fraccionamientoId: original.fraccionamientoId,
      prototipoId: original.prototipoId,
      currencyId: original.currencyId,
      version: 1,
      isActive: true,
      totalAmount: 0,
    });

    // Copy partidas
    for (const partida of original.partidas) {
      await this.partidaRepository.save(
        this.partidaRepository.create({
          tenantId: ctx.tenantId,
          presupuestoId: newBudget.id,
          conceptoId: partida.conceptoId,
          quantity: partida.quantity,
          unitPrice: partida.unitPrice,
          sequence: partida.sequence,
          createdById: ctx.userId,
        })
      );
    }

    await this.recalculateTotal(ctx, newBudget.id);

    return this.findById(ctx, newBudget.id) as Promise<Presupuesto>;
  }

  /**
   * Obtener resumen de presupuesto
   */
  async getSummary(
    ctx: ServiceContext,
    presupuestoId: string
  ): Promise<BudgetSummary | null> {
    const presupuesto = await this.findWithPartidas(ctx, presupuestoId);
    if (!presupuesto) {
      return null;
    }

    const partidasByLevel = new Map<number, PartidaSummary[]>();

    for (const partida of presupuesto.partidas) {
      const level = partida.concepto?.level || 0;
      if (!partidasByLevel.has(level)) {
        partidasByLevel.set(level, []);
      }
      partidasByLevel.get(level)!.push({
        conceptoId: partida.conceptoId,
        conceptoCode: partida.concepto?.code || '',
        conceptoName: partida.concepto?.name || '',
        quantity: Number(partida.quantity),
        unitPrice: Number(partida.unitPrice),
        total: Number(partida.quantity) * Number(partida.unitPrice),
      });
    }

    return {
      id: presupuesto.id,
      code: presupuesto.code,
      name: presupuesto.name,
      version: presupuesto.version,
      totalAmount: Number(presupuesto.totalAmount),
      partidasCount: presupuesto.partidas.length,
      isApproved: presupuesto.approvedAt !== null,
      approvedAt: presupuesto.approvedAt,
      createdAt: presupuesto.createdAt,
      partidasByLevel: Object.fromEntries(partidasByLevel),
    };
  }

  /**
   * Buscar presupuestos con filtros avanzados
   */
  async search(
    ctx: ServiceContext,
    filters: PresupuestoFilters,
    page = 1,
    limit = 20
  ): Promise<PaginatedResult<Presupuesto>> {
    const qb = this.repository
      .createQueryBuilder('p')
      .where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('p.deleted_at IS NULL');

    if (filters.code) {
      qb.andWhere('p.code ILIKE :code', { code: `%${filters.code}%` });
    }
    if (filters.name) {
      qb.andWhere('p.name ILIKE :name', { name: `%${filters.name}%` });
    }
    if (filters.fraccionamientoId) {
      qb.andWhere('p.fraccionamiento_id = :fraccionamientoId', {
        fraccionamientoId: filters.fraccionamientoId,
      });
    }
    if (filters.prototipoId) {
      qb.andWhere('p.prototipo_id = :prototipoId', {
        prototipoId: filters.prototipoId,
      });
    }
    if (filters.isActive !== undefined) {
      qb.andWhere('p.is_active = :isActive', { isActive: filters.isActive });
    }
    if (filters.isApproved !== undefined) {
      if (filters.isApproved) {
        qb.andWhere('p.approved_at IS NOT NULL');
      } else {
        qb.andWhere('p.approved_at IS NULL');
      }
    }
    if (filters.minAmount !== undefined) {
      qb.andWhere('p.total_amount >= :minAmount', { minAmount: filters.minAmount });
    }
    if (filters.maxAmount !== undefined) {
      qb.andWhere('p.total_amount <= :maxAmount', { maxAmount: filters.maxAmount });
    }

    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),
    };
  }
}

// Types for comparison results
export interface BudgetComparisonItem {
  quantity: number;
  unitPrice: number;
  total: number;
}

export interface PartidaComparison {
  conceptoId: string;
  conceptoCode?: string;
  conceptoName?: string;
  status: 'added' | 'removed' | 'modified' | 'unchanged';
  budget1: BudgetComparisonItem | null;
  budget2: BudgetComparisonItem | null;
  difference: {
    quantity: number;
    unitPrice: number;
    total: number;
    percentageChange: number;
  };
}

export interface BudgetComparison {
  budget1: {
    id: string;
    code: string;
    name: string;
    version: number;
    totalAmount: number;
    partidasCount: number;
  };
  budget2: {
    id: string;
    code: string;
    name: string;
    version: number;
    totalAmount: number;
    partidasCount: number;
  };
  summary: {
    totalDifference: number;
    percentageChange: number;
    addedItems: number;
    removedItems: number;
    modifiedItems: number;
    unchangedItems: number;
  };
  partidas: PartidaComparison[];
}

export interface PartidaSummary {
  conceptoId: string;
  conceptoCode: string;
  conceptoName: string;
  quantity: number;
  unitPrice: number;
  total: number;
}

export interface BudgetSummary {
  id: string;
  code: string;
  name: string;
  version: number;
  totalAmount: number;
  partidasCount: number;
  isApproved: boolean;
  approvedAt: Date | null;
  createdAt: Date;
  partidasByLevel: Record<number, PartidaSummary[]>;
}

export interface PresupuestoFilters {
  code?: string;
  name?: string;
  fraccionamientoId?: string;
  prototipoId?: string;
  isActive?: boolean;
  isApproved?: boolean;
  minAmount?: number;
  maxAmount?: number;
}
