import { Repository, FindOptionsWhere, DataSource } from 'typeorm';
import { MercanciaCartaPorte } from '../entities';
import { CartaPorte, EstadoCartaPorte } from '../entities/carta-porte.entity';

/**
 * DTO para crear mercancia
 */
export interface CreateMercanciaDto {
  bienesTransp: string;
  descripcion: string;
  cantidad: number;
  claveUnidad: string;
  unidad?: string;
  pesoEnKg: number;
  largoCm?: number;
  anchoCm?: number;
  altoCm?: number;
  valorMercancia?: number;
  moneda?: string;
  materialPeligroso?: boolean;
  cveMaterialPeligroso?: string;
  tipoEmbalaje?: string;
  descripcionEmbalaje?: string;
  fraccionArancelaria?: string;
  uuidComercioExt?: string;
  pedimentos?: string[];
  guias?: string[];
  secuencia?: number;
}

/**
 * DTO para actualizar mercancia
 */
export interface UpdateMercanciaDto extends Partial<CreateMercanciaDto> {}

/**
 * Resultado de validacion de pesos
 */
export interface ValidacionPesos {
  valid: boolean;
  pesoTotalMercancias: number;
  pesoBrutoDeclarado: number | null;
  diferencia: number;
  porcentajeDiferencia: number;
  toleranciaPorcentaje: number;
  errors: string[];
}

/**
 * Resumen de valor de mercancias
 */
export interface ValorMercanciaResumen {
  valorTotal: number;
  moneda: string;
  cantidadMercancias: number;
  valorPromedioPorMercancia: number;
  mercanciaMayorValor: {
    id: string;
    descripcion: string;
    valor: number;
  } | null;
}

/**
 * Servicio para gestion de mercancias de Carta Porte
 * CFDI 3.1 Compliance
 */
export class MercanciaService {
  private mercanciaRepository: Repository<MercanciaCartaPorte>;
  private cartaPorteRepository: Repository<CartaPorte>;

  constructor(private readonly dataSource: DataSource) {
    this.mercanciaRepository = dataSource.getRepository(MercanciaCartaPorte);
    this.cartaPorteRepository = dataSource.getRepository(CartaPorte);
  }

  /**
   * Verifica que la Carta Porte exista y pertenezca al tenant
   */
  private async getCartaPorteOrFail(
    cartaPorteId: string,
    tenantId: string
  ): Promise<CartaPorte> {
    const cartaPorte = await this.cartaPorteRepository.findOne({
      where: { id: cartaPorteId, tenantId },
    });

    if (!cartaPorte) {
      throw new Error('Carta Porte no encontrada');
    }

    return cartaPorte;
  }

  /**
   * Verifica que la Carta Porte este en estado editable
   */
  private assertEditable(cartaPorte: CartaPorte): void {
    if (cartaPorte.estado !== EstadoCartaPorte.BORRADOR) {
      throw new Error(
        `No se pueden modificar mercancias en estado ${cartaPorte.estado}`
      );
    }
  }

  /**
   * Obtiene el siguiente numero de secuencia para mercancias
   */
  private async getNextSecuencia(cartaPorteId: string): Promise<number> {
    const result = await this.mercanciaRepository
      .createQueryBuilder('m')
      .select('COALESCE(MAX(m.secuencia), 0)', 'maxSecuencia')
      .where('m.cartaPorteId = :cartaPorteId', { cartaPorteId })
      .getRawOne();

    return (result?.maxSecuencia || 0) + 1;
  }

  /**
   * Crea una nueva mercancia para una Carta Porte
   */
  async create(
    tenantId: string,
    cartaPorteId: string,
    data: CreateMercanciaDto
  ): Promise<MercanciaCartaPorte> {
    const cartaPorte = await this.getCartaPorteOrFail(cartaPorteId, tenantId);
    this.assertEditable(cartaPorte);

    // Validar datos requeridos por SAT
    this.validateMercanciaData(data);

    const secuencia = data.secuencia ?? (await this.getNextSecuencia(cartaPorteId));

    const mercancia = this.mercanciaRepository.create({
      tenantId,
      cartaPorteId,
      bienesTransp: data.bienesTransp,
      descripcion: data.descripcion,
      cantidad: data.cantidad,
      claveUnidad: data.claveUnidad,
      unidad: data.unidad || null,
      pesoEnKg: data.pesoEnKg,
      largoCm: data.largoCm || null,
      anchoCm: data.anchoCm || null,
      altoCm: data.altoCm || null,
      valorMercancia: data.valorMercancia || null,
      moneda: data.moneda || 'MXN',
      materialPeligroso: data.materialPeligroso || false,
      cveMaterialPeligroso: data.cveMaterialPeligroso || null,
      tipoEmbalaje: data.tipoEmbalaje || null,
      descripcionEmbalaje: data.descripcionEmbalaje || null,
      fraccionArancelaria: data.fraccionArancelaria || null,
      uuidComercioExt: data.uuidComercioExt || null,
      pedimentos: data.pedimentos || null,
      guias: data.guias || null,
      secuencia,
    });

    const saved = await this.mercanciaRepository.save(mercancia);

    // Actualizar contador en Carta Porte
    await this.updateCartaPorteTotals(cartaPorteId, tenantId);

    return saved;
  }

  /**
   * Obtiene todas las mercancias de una Carta Porte
   */
  async findByCartaParte(
    tenantId: string,
    cartaPorteId: string
  ): Promise<MercanciaCartaPorte[]> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);

    return this.mercanciaRepository.find({
      where: { cartaPorteId, tenantId },
      order: { secuencia: 'ASC' },
    });
  }

  /**
   * Obtiene una mercancia por ID
   */
  async findById(
    tenantId: string,
    id: string
  ): Promise<MercanciaCartaPorte | null> {
    return this.mercanciaRepository.findOne({
      where: { id, tenantId },
    });
  }

  /**
   * Actualiza una mercancia existente
   */
  async update(
    tenantId: string,
    id: string,
    data: UpdateMercanciaDto
  ): Promise<MercanciaCartaPorte | null> {
    const mercancia = await this.findById(tenantId, id);
    if (!mercancia) return null;

    const cartaPorte = await this.getCartaPorteOrFail(
      mercancia.cartaPorteId,
      tenantId
    );
    this.assertEditable(cartaPorte);

    // Validar datos si se actualizan campos requeridos
    if (data.bienesTransp || data.descripcion || data.cantidad || data.claveUnidad || data.pesoEnKg) {
      this.validateMercanciaData({
        bienesTransp: data.bienesTransp ?? mercancia.bienesTransp,
        descripcion: data.descripcion ?? mercancia.descripcion,
        cantidad: data.cantidad ?? mercancia.cantidad,
        claveUnidad: data.claveUnidad ?? mercancia.claveUnidad,
        pesoEnKg: data.pesoEnKg ?? mercancia.pesoEnKg,
      } as CreateMercanciaDto);
    }

    // Validar material peligroso
    if (data.materialPeligroso === true && !data.cveMaterialPeligroso && !mercancia.cveMaterialPeligroso) {
      throw new Error('Se requiere clave de material peligroso cuando materialPeligroso es true');
    }

    Object.assign(mercancia, data);
    const updated = await this.mercanciaRepository.save(mercancia);

    // Actualizar totales
    await this.updateCartaPorteTotals(mercancia.cartaPorteId, tenantId);

    return updated;
  }

  /**
   * Elimina una mercancia
   */
  async delete(tenantId: string, id: string): Promise<boolean> {
    const mercancia = await this.findById(tenantId, id);
    if (!mercancia) return false;

    const cartaPorte = await this.getCartaPorteOrFail(
      mercancia.cartaPorteId,
      tenantId
    );
    this.assertEditable(cartaPorte);

    const cartaPorteId = mercancia.cartaPorteId;
    const result = await this.mercanciaRepository.delete(id);

    if ((result.affected ?? 0) > 0) {
      // Reordenar secuencias
      await this.reorderSecuencias(cartaPorteId);
      // Actualizar totales
      await this.updateCartaPorteTotals(cartaPorteId, tenantId);
      return true;
    }

    return false;
  }

  /**
   * Valida que los pesos totales de mercancias coincidan con el declarado
   */
  async validatePesosTotales(
    tenantId: string,
    cartaPorteId: string
  ): Promise<ValidacionPesos> {
    const cartaPorte = await this.getCartaPorteOrFail(cartaPorteId, tenantId);
    const mercancias = await this.findByCartaParte(tenantId, cartaPorteId);

    const errors: string[] = [];
    const toleranciaPorcentaje = 2; // 2% de tolerancia

    if (mercancias.length === 0) {
      return {
        valid: false,
        pesoTotalMercancias: 0,
        pesoBrutoDeclarado: cartaPorte.pesoBrutoTotal,
        diferencia: 0,
        porcentajeDiferencia: 0,
        toleranciaPorcentaje,
        errors: ['No hay mercancias registradas'],
      };
    }

    const pesoTotalMercancias = mercancias.reduce(
      (sum, m) => sum + Number(m.pesoEnKg) * Number(m.cantidad),
      0
    );

    const pesoBrutoDeclarado = cartaPorte.pesoBrutoTotal
      ? Number(cartaPorte.pesoBrutoTotal)
      : null;

    if (pesoBrutoDeclarado === null) {
      errors.push('No se ha declarado peso bruto total en la Carta Porte');
      return {
        valid: false,
        pesoTotalMercancias,
        pesoBrutoDeclarado: null,
        diferencia: 0,
        porcentajeDiferencia: 0,
        toleranciaPorcentaje,
        errors,
      };
    }

    const diferencia = Math.abs(pesoTotalMercancias - pesoBrutoDeclarado);
    const porcentajeDiferencia =
      pesoBrutoDeclarado > 0 ? (diferencia / pesoBrutoDeclarado) * 100 : 0;

    if (porcentajeDiferencia > toleranciaPorcentaje) {
      errors.push(
        `Diferencia de peso (${porcentajeDiferencia.toFixed(2)}%) excede tolerancia de ${toleranciaPorcentaje}%`
      );
    }

    // Validar pesos individuales
    for (const m of mercancias) {
      if (Number(m.pesoEnKg) <= 0) {
        errors.push(`Mercancia "${m.descripcion}" tiene peso invalido`);
      }
      if (Number(m.cantidad) <= 0) {
        errors.push(`Mercancia "${m.descripcion}" tiene cantidad invalida`);
      }
    }

    return {
      valid: errors.length === 0,
      pesoTotalMercancias,
      pesoBrutoDeclarado,
      diferencia,
      porcentajeDiferencia,
      toleranciaPorcentaje,
      errors,
    };
  }

  /**
   * Calcula el valor total de las mercancias
   */
  async calculateValorMercancia(
    tenantId: string,
    cartaPorteId: string
  ): Promise<ValorMercanciaResumen> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);
    const mercancias = await this.findByCartaParte(tenantId, cartaPorteId);

    if (mercancias.length === 0) {
      return {
        valorTotal: 0,
        moneda: 'MXN',
        cantidadMercancias: 0,
        valorPromedioPorMercancia: 0,
        mercanciaMayorValor: null,
      };
    }

    // Asumimos misma moneda (SAT requiere MXN para valores)
    const moneda = mercancias[0]?.moneda || 'MXN';

    let valorTotal = 0;
    let mercanciaMayorValor: { id: string; descripcion: string; valor: number } | null = null;

    for (const m of mercancias) {
      const valorItem = m.valorMercancia ? Number(m.valorMercancia) * Number(m.cantidad) : 0;
      valorTotal += valorItem;

      if (!mercanciaMayorValor || valorItem > mercanciaMayorValor.valor) {
        mercanciaMayorValor = {
          id: m.id,
          descripcion: m.descripcion,
          valor: valorItem,
        };
      }
    }

    return {
      valorTotal,
      moneda,
      cantidadMercancias: mercancias.length,
      valorPromedioPorMercancia: mercancias.length > 0 ? valorTotal / mercancias.length : 0,
      mercanciaMayorValor,
    };
  }

  /**
   * Obtiene solo las mercancias peligrosas de una Carta Porte
   */
  async getMercanciasPeligrosas(
    tenantId: string,
    cartaPorteId: string
  ): Promise<MercanciaCartaPorte[]> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);

    return this.mercanciaRepository.find({
      where: {
        cartaPorteId,
        tenantId,
        materialPeligroso: true,
      },
      order: { secuencia: 'ASC' },
    });
  }

  /**
   * Obtiene estadisticas de mercancias peligrosas
   */
  async getEstadisticasMercanciasPeligrosas(
    tenantId: string,
    cartaPorteId: string
  ): Promise<{
    totalMercancias: number;
    mercanciasPeligrosas: number;
    porcentajePeligrosas: number;
    clavesMaterialPeligroso: string[];
  }> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);
    const todas = await this.findByCartaParte(tenantId, cartaPorteId);
    const peligrosas = todas.filter(m => m.materialPeligroso);

    const clavesMaterialPeligroso = Array.from(
      new Set(
        peligrosas
          .map(m => m.cveMaterialPeligroso)
          .filter((c): c is string => c !== null)
      )
    );

    return {
      totalMercancias: todas.length,
      mercanciasPeligrosas: peligrosas.length,
      porcentajePeligrosas: todas.length > 0 ? (peligrosas.length / todas.length) * 100 : 0,
      clavesMaterialPeligroso,
    };
  }

  /**
   * Valida datos de mercancia segun requerimientos SAT
   */
  private validateMercanciaData(data: CreateMercanciaDto): void {
    const errors: string[] = [];

    if (!data.bienesTransp || data.bienesTransp.length === 0) {
      errors.push('bienesTransp es requerido');
    }

    if (!data.descripcion || data.descripcion.length === 0) {
      errors.push('descripcion es requerida');
    }

    if (!data.cantidad || data.cantidad <= 0) {
      errors.push('cantidad debe ser mayor a 0');
    }

    if (!data.claveUnidad || data.claveUnidad.length === 0) {
      errors.push('claveUnidad es requerida');
    }

    if (!data.pesoEnKg || data.pesoEnKg <= 0) {
      errors.push('pesoEnKg debe ser mayor a 0');
    }

    // Validaciones material peligroso
    if (data.materialPeligroso === true) {
      if (!data.cveMaterialPeligroso) {
        errors.push('cveMaterialPeligroso es requerido para material peligroso');
      }
      if (!data.tipoEmbalaje) {
        errors.push('tipoEmbalaje es requerido para material peligroso');
      }
    }

    if (errors.length > 0) {
      throw new Error(`Datos de mercancia invalidos: ${errors.join(', ')}`);
    }
  }

  /**
   * Reordena las secuencias despues de eliminar
   */
  private async reorderSecuencias(cartaPorteId: string): Promise<void> {
    const mercancias = await this.mercanciaRepository.find({
      where: { cartaPorteId },
      order: { secuencia: 'ASC' },
    });

    for (let i = 0; i < mercancias.length; i++) {
      if (mercancias[i].secuencia !== i + 1) {
        mercancias[i].secuencia = i + 1;
        await this.mercanciaRepository.save(mercancias[i]);
      }
    }
  }

  /**
   * Actualiza totales en la Carta Porte
   */
  private async updateCartaPorteTotals(
    cartaPorteId: string,
    tenantId: string
  ): Promise<void> {
    const mercancias = await this.mercanciaRepository.find({
      where: { cartaPorteId, tenantId },
    });

    const numTotalMercancias = mercancias.length;
    const pesoBrutoTotal = mercancias.reduce(
      (sum, m) => sum + Number(m.pesoEnKg) * Number(m.cantidad),
      0
    );

    await this.cartaPorteRepository.update(
      { id: cartaPorteId, tenantId },
      { numTotalMercancias, pesoBrutoTotal }
    );
  }
}
