/**
 * ContractService - Servicio de gestión de contratos
 *
 * Gestión de contratos con workflow de aprobación.
 *
 * @module Contracts
 */

import { Repository, FindOptionsWhere, LessThan } from 'typeorm';
import { Contract, ContractStatus, ContractType } from '../entities/contract.entity';
import { ContractAddendum } from '../entities/contract-addendum.entity';
import { ContractPartida } from '../entities/contract-partida.entity';

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

export interface CreateContractDto {
  projectId?: string;
  fraccionamientoId?: string;
  contractType: ContractType;
  clientContractType?: string;
  name: string;
  description?: string;
  clientName?: string;
  clientRfc?: string;
  clientAddress?: string;
  subcontractorId?: string;
  specialty?: string;
  startDate: Date;
  endDate: Date;
  contractAmount: number;
  currency?: string;
  paymentTerms?: string;
  retentionPercentage?: number;
  advancePercentage?: number;
  notes?: string;
}

export interface CreateAddendumDto {
  addendumType: string;
  title: string;
  description: string;
  effectiveDate: Date;
  newEndDate?: Date;
  amountChange?: number;
  scopeChanges?: string;
  notes?: string;
}

export interface CreatePartidaDto {
  conceptoId: string;
  quantity: number;
  unitPrice: number;
}

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

export interface ContractFilters {
  projectId?: string;
  fraccionamientoId?: string;
  contractType?: ContractType;
  subcontractorId?: string;
  status?: ContractStatus;
  expiringInDays?: number;
}

export class ContractService {
  constructor(
    private readonly contractRepository: Repository<Contract>,
    private readonly addendumRepository: Repository<ContractAddendum>,
    private readonly partidaRepository?: Repository<ContractPartida>
  ) {}

  private generateContractNumber(type: ContractType): string {
    const now = new Date();
    const year = now.getFullYear().toString().slice(-2);
    const month = (now.getMonth() + 1).toString().padStart(2, '0');
    const random = Math.random().toString(36).substring(2, 6).toUpperCase();
    const prefix = type === 'client' ? 'CTR' : 'SUB';
    return `${prefix}-${year}${month}-${random}`;
  }

  private generateAddendumNumber(contractNumber: string, sequence: number): string {
    return `${contractNumber}-ADD${sequence.toString().padStart(2, '0')}`;
  }

  async findWithFilters(
    ctx: ServiceContext,
    filters: ContractFilters = {},
    page: number = 1,
    limit: number = 20
  ): Promise<{ data: Contract[]; total: number; page: number; limit: number }> {
    const skip = (page - 1) * limit;

    const queryBuilder = this.contractRepository
      .createQueryBuilder('c')
      .leftJoinAndSelect('c.createdBy', 'createdBy')
      .leftJoinAndSelect('c.approvedBy', 'approvedBy')
      .where('c.tenant_id = :tenantId', { tenantId: ctx.tenantId })
      .andWhere('c.deleted_at IS NULL');

    if (filters.projectId) {
      queryBuilder.andWhere('c.project_id = :projectId', { projectId: filters.projectId });
    }

    if (filters.fraccionamientoId) {
      queryBuilder.andWhere('c.fraccionamiento_id = :fraccionamientoId', {
        fraccionamientoId: filters.fraccionamientoId,
      });
    }

    if (filters.contractType) {
      queryBuilder.andWhere('c.contract_type = :contractType', { contractType: filters.contractType });
    }

    if (filters.subcontractorId) {
      queryBuilder.andWhere('c.subcontractor_id = :subcontractorId', {
        subcontractorId: filters.subcontractorId,
      });
    }

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

    if (filters.expiringInDays) {
      const futureDate = new Date();
      futureDate.setDate(futureDate.getDate() + filters.expiringInDays);
      queryBuilder.andWhere('c.end_date <= :futureDate', { futureDate });
      queryBuilder.andWhere('c.end_date >= :today', { today: new Date() });
      queryBuilder.andWhere('c.status = :activeStatus', { activeStatus: 'active' });
    }

    queryBuilder
      .orderBy('c.created_at', 'DESC')
      .skip(skip)
      .take(limit);

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

    return { data, total, page, limit };
  }

  async findById(ctx: ServiceContext, id: string): Promise<Contract | null> {
    return this.contractRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: null,
      } as unknown as FindOptionsWhere<Contract>,
    });
  }

  async findWithDetails(ctx: ServiceContext, id: string): Promise<Contract | null> {
    return this.contractRepository.findOne({
      where: {
        id,
        tenantId: ctx.tenantId,
        deletedAt: null,
      } as unknown as FindOptionsWhere<Contract>,
      relations: ['createdBy', 'approvedBy', 'addendums'],
    });
  }

  async create(ctx: ServiceContext, dto: CreateContractDto): Promise<Contract> {
    const contract = this.contractRepository.create({
      tenantId: ctx.tenantId,
      createdById: ctx.userId,
      contractNumber: this.generateContractNumber(dto.contractType),
      projectId: dto.projectId,
      fraccionamientoId: dto.fraccionamientoId,
      contractType: dto.contractType,
      clientContractType: dto.clientContractType as any,
      name: dto.name,
      description: dto.description,
      clientName: dto.clientName,
      clientRfc: dto.clientRfc?.toUpperCase(),
      clientAddress: dto.clientAddress,
      subcontractorId: dto.subcontractorId,
      specialty: dto.specialty,
      startDate: dto.startDate,
      endDate: dto.endDate,
      contractAmount: dto.contractAmount,
      currency: dto.currency || 'MXN',
      paymentTerms: dto.paymentTerms,
      retentionPercentage: dto.retentionPercentage || 5,
      advancePercentage: dto.advancePercentage || 0,
      notes: dto.notes,
      status: 'draft',
    });

    return this.contractRepository.save(contract);
  }

  async submitForReview(ctx: ServiceContext, id: string): Promise<Contract | null> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return null;
    }

    if (contract.status !== 'draft') {
      throw new Error('Can only submit draft contracts for review');
    }

    contract.status = 'review';
    contract.submittedAt = new Date();
    contract.submittedById = ctx.userId || '';
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }

  async approveLegal(ctx: ServiceContext, id: string): Promise<Contract | null> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return null;
    }

    if (contract.status !== 'review') {
      throw new Error('Can only approve contracts in review');
    }

    contract.legalApprovedAt = new Date();
    contract.legalApprovedById = ctx.userId || '';
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }

  async approve(ctx: ServiceContext, id: string): Promise<Contract | null> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return null;
    }

    if (contract.status !== 'review') {
      throw new Error('Can only approve contracts in review');
    }

    contract.status = 'approved';
    contract.approvedAt = new Date();
    contract.approvedById = ctx.userId || '';
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }

  async activate(ctx: ServiceContext, id: string, signedDocumentUrl?: string): Promise<Contract | null> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return null;
    }

    if (contract.status !== 'approved') {
      throw new Error('Can only activate approved contracts');
    }

    contract.status = 'active';
    contract.signedAt = new Date();
    if (signedDocumentUrl) {
      contract.signedDocumentUrl = signedDocumentUrl;
    }
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }

  async complete(ctx: ServiceContext, id: string): Promise<Contract | null> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return null;
    }

    if (contract.status !== 'active') {
      throw new Error('Can only complete active contracts');
    }

    contract.status = 'completed';
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }

  async terminate(ctx: ServiceContext, id: string, reason: string): Promise<Contract | null> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return null;
    }

    if (contract.status !== 'active') {
      throw new Error('Can only terminate active contracts');
    }

    contract.status = 'terminated';
    contract.terminatedAt = new Date();
    contract.terminationReason = reason;
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }

  async updateProgress(
    ctx: ServiceContext,
    id: string,
    progressPercentage: number,
    invoicedAmount?: number,
    paidAmount?: number
  ): Promise<Contract | null> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return null;
    }

    contract.progressPercentage = progressPercentage;
    if (invoicedAmount !== undefined) {
      contract.invoicedAmount = invoicedAmount;
    }
    if (paidAmount !== undefined) {
      contract.paidAmount = paidAmount;
    }
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }

  async createAddendum(ctx: ServiceContext, contractId: string, dto: CreateAddendumDto): Promise<ContractAddendum> {
    const contract = await this.findWithDetails(ctx, contractId);
    if (!contract) {
      throw new Error('Contract not found');
    }

    if (contract.status !== 'active') {
      throw new Error('Can only add addendums to active contracts');
    }

    const sequence = (contract.addendums?.length || 0) + 1;

    const addendum = this.addendumRepository.create({
      tenantId: ctx.tenantId,
      createdById: ctx.userId,
      contractId,
      addendumNumber: this.generateAddendumNumber(contract.contractNumber, sequence),
      addendumType: dto.addendumType as any,
      title: dto.title,
      description: dto.description,
      effectiveDate: dto.effectiveDate,
      newEndDate: dto.newEndDate,
      amountChange: dto.amountChange || 0,
      newContractAmount: dto.amountChange
        ? Number(contract.contractAmount) + Number(dto.amountChange)
        : undefined,
      scopeChanges: dto.scopeChanges,
      notes: dto.notes,
      status: 'draft',
    });

    return this.addendumRepository.save(addendum);
  }

  async approveAddendum(ctx: ServiceContext, addendumId: string): Promise<ContractAddendum | null> {
    const addendum = await this.addendumRepository.findOne({
      where: {
        id: addendumId,
        tenantId: ctx.tenantId,
      } as FindOptionsWhere<ContractAddendum>,
      relations: ['contract'],
    });

    if (!addendum) {
      return null;
    }

    if (addendum.status !== 'draft' && addendum.status !== 'review') {
      throw new Error('Can only approve draft or review addendums');
    }

    addendum.status = 'approved';
    addendum.approvedAt = new Date();
    addendum.approvedById = ctx.userId || '';
    addendum.updatedById = ctx.userId || '';

    // Apply changes to contract
    if (addendum.newEndDate) {
      addendum.contract.endDate = addendum.newEndDate;
    }
    if (addendum.newContractAmount) {
      addendum.contract.contractAmount = addendum.newContractAmount;
    }
    await this.contractRepository.save(addendum.contract);

    return this.addendumRepository.save(addendum);
  }

  async softDelete(ctx: ServiceContext, id: string): Promise<boolean> {
    const contract = await this.findById(ctx, id);
    if (!contract) {
      return false;
    }

    if (contract.status === 'active') {
      throw new Error('Cannot delete active contracts');
    }

    await this.contractRepository.update(
      { id, tenantId: ctx.tenantId } as FindOptionsWhere<Contract>,
      { deletedAt: new Date(), deletedById: ctx.userId || '' }
    );

    return true;
  }

  async getExpiringContracts(ctx: ServiceContext, days: number = 30): Promise<Contract[]> {
    const futureDate = new Date();
    futureDate.setDate(futureDate.getDate() + days);

    return this.contractRepository.find({
      where: {
        tenantId: ctx.tenantId,
        status: 'active' as ContractStatus,
        endDate: LessThan(futureDate),
        deletedAt: null,
      } as unknown as FindOptionsWhere<Contract>,
      order: { endDate: 'ASC' },
    });
  }

  // Contract Partidas (Items) Management

  async getPartidas(ctx: ServiceContext, contractId: string): Promise<ContractPartida[]> {
    if (!this.partidaRepository) {
      throw new Error('Partida repository not initialized');
    }

    return this.partidaRepository.find({
      where: {
        contractId,
        tenantId: ctx.tenantId,
        deletedAt: null,
      } as unknown as FindOptionsWhere<ContractPartida>,
      order: { createdAt: 'ASC' },
    });
  }

  async addPartida(ctx: ServiceContext, contractId: string, dto: CreatePartidaDto): Promise<ContractPartida> {
    if (!this.partidaRepository) {
      throw new Error('Partida repository not initialized');
    }

    const contract = await this.findById(ctx, contractId);
    if (!contract) {
      throw new Error('Contract not found');
    }

    if (contract.status !== 'draft' && contract.status !== 'review') {
      throw new Error('Can only add partidas to draft or review contracts');
    }

    const partida = this.partidaRepository.create({
      tenantId: ctx.tenantId,
      contractId,
      conceptoId: dto.conceptoId,
      quantity: dto.quantity,
      unitPrice: dto.unitPrice,
      createdById: ctx.userId,
    });

    return this.partidaRepository.save(partida);
  }

  async updatePartida(
    ctx: ServiceContext,
    partidaId: string,
    dto: UpdatePartidaDto
  ): Promise<ContractPartida | null> {
    if (!this.partidaRepository) {
      throw new Error('Partida repository not initialized');
    }

    const partida = await this.partidaRepository.findOne({
      where: {
        id: partidaId,
        tenantId: ctx.tenantId,
        deletedAt: null,
      } as unknown as FindOptionsWhere<ContractPartida>,
      relations: ['contract'],
    });

    if (!partida) {
      return null;
    }

    if (partida.contract.status !== 'draft' && partida.contract.status !== 'review') {
      throw new Error('Can only update partidas of draft or review contracts');
    }

    if (dto.quantity !== undefined) {
      partida.quantity = dto.quantity;
    }
    if (dto.unitPrice !== undefined) {
      partida.unitPrice = dto.unitPrice;
    }
    partida.updatedById = ctx.userId || '';

    return this.partidaRepository.save(partida);
  }

  async removePartida(ctx: ServiceContext, partidaId: string): Promise<boolean> {
    if (!this.partidaRepository) {
      throw new Error('Partida repository not initialized');
    }

    const partida = await this.partidaRepository.findOne({
      where: {
        id: partidaId,
        tenantId: ctx.tenantId,
        deletedAt: null,
      } as unknown as FindOptionsWhere<ContractPartida>,
      relations: ['contract'],
    });

    if (!partida) {
      return false;
    }

    if (partida.contract.status !== 'draft' && partida.contract.status !== 'review') {
      throw new Error('Can only remove partidas from draft or review contracts');
    }

    await this.partidaRepository.update(
      { id: partidaId, tenantId: ctx.tenantId } as FindOptionsWhere<ContractPartida>,
      { deletedAt: new Date(), deletedById: ctx.userId || '' }
    );

    return true;
  }

  async recalculateContractAmount(ctx: ServiceContext, contractId: string): Promise<Contract | null> {
    if (!this.partidaRepository) {
      throw new Error('Partida repository not initialized');
    }

    const contract = await this.findById(ctx, contractId);
    if (!contract) {
      return null;
    }

    const partidas = await this.getPartidas(ctx, contractId);
    const totalAmount = partidas.reduce((sum, p) => sum + Number(p.quantity) * Number(p.unitPrice), 0);

    contract.contractAmount = totalAmount;
    contract.updatedById = ctx.userId || '';

    return this.contractRepository.save(contract);
  }
}
