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

/**
 * DTO para crear ubicacion
 */
export interface CreateUbicacionDto {
  tipoUbicacion: string;
  idUbicacion?: string;
  rfcRemitenteDestinatario?: string;
  nombreRemitenteDestinatario?: string;
  pais?: string;
  estado?: string;
  municipio?: string;
  localidad?: string;
  codigoPostal: string;
  colonia?: string;
  calle?: string;
  numeroExterior?: string;
  numeroInterior?: string;
  referencia?: string;
  fechaHoraSalidaLlegada?: Date;
  distanciaRecorrida?: number;
  secuencia?: number;
}

/**
 * DTO para actualizar ubicacion
 */
export interface UpdateUbicacionDto extends Partial<CreateUbicacionDto> {}

/**
 * Resultado de validacion de secuencia
 */
export interface ValidacionSecuencia {
  valid: boolean;
  tieneOrigen: boolean;
  tieneDestino: boolean;
  secuenciaCorrecta: boolean;
  distanciaTotalKm: number;
  errors: string[];
}

/**
 * Servicio para gestion de ubicaciones de Carta Porte
 * CFDI 3.1 Compliance
 */
export class UbicacionCartaPorteService {
  private ubicacionRepository: Repository<UbicacionCartaPorte>;
  private cartaPorteRepository: Repository<CartaPorte>;

  constructor(private readonly dataSource: DataSource) {
    this.ubicacionRepository = dataSource.getRepository(UbicacionCartaPorte);
    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 ubicaciones en estado ${cartaPorte.estado}`
      );
    }
  }

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

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

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

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

    // Validar reglas de negocio
    await this.validateBusinessRules(cartaPorteId, data);

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

    const ubicacion = this.ubicacionRepository.create({
      tenantId,
      cartaPorteId,
      tipoUbicacion: data.tipoUbicacion,
      idUbicacion: data.idUbicacion || null,
      rfcRemitenteDestinatario: data.rfcRemitenteDestinatario || null,
      nombreRemitenteDestinatario: data.nombreRemitenteDestinatario || null,
      pais: data.pais || 'MEX',
      estado: data.estado || null,
      municipio: data.municipio || null,
      localidad: data.localidad || null,
      codigoPostal: data.codigoPostal,
      colonia: data.colonia || null,
      calle: data.calle || null,
      numeroExterior: data.numeroExterior || null,
      numeroInterior: data.numeroInterior || null,
      referencia: data.referencia || null,
      fechaHoraSalidaLlegada: data.fechaHoraSalidaLlegada || null,
      distanciaRecorrida: data.distanciaRecorrida || null,
      secuencia,
    });

    return this.ubicacionRepository.save(ubicacion);
  }

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

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

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

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

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

    // Validar datos si se actualizan campos requeridos
    if (data.tipoUbicacion || data.codigoPostal) {
      this.validateUbicacionData({
        tipoUbicacion: data.tipoUbicacion ?? ubicacion.tipoUbicacion,
        codigoPostal: data.codigoPostal ?? ubicacion.codigoPostal,
      } as CreateUbicacionDto);
    }

    Object.assign(ubicacion, data);
    return this.ubicacionRepository.save(ubicacion);
  }

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

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

    const cartaPorteId = ubicacion.cartaPorteId;
    const result = await this.ubicacionRepository.delete(id);

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

    return false;
  }

  /**
   * Reordena las ubicaciones segun un nuevo orden
   */
  async reorder(
    tenantId: string,
    cartaPorteId: string,
    newOrder: string[]
  ): Promise<UbicacionCartaPorte[]> {
    const cartaPorte = await this.getCartaPorteOrFail(cartaPorteId, tenantId);
    this.assertEditable(cartaPorte);

    const ubicaciones = await this.findByCartaParte(tenantId, cartaPorteId);

    // Validar que todos los IDs existan
    const existingIds = new Set(ubicaciones.map(u => u.id));
    for (const id of newOrder) {
      if (!existingIds.has(id)) {
        throw new Error(`Ubicacion con ID ${id} no encontrada`);
      }
    }

    // Validar que todos los IDs esten presentes
    if (newOrder.length !== ubicaciones.length) {
      throw new Error('El nuevo orden debe contener todas las ubicaciones');
    }

    // Actualizar secuencias
    const updates: Promise<UbicacionCartaPorte>[] = [];
    for (let i = 0; i < newOrder.length; i++) {
      const ubicacion = ubicaciones.find(u => u.id === newOrder[i]);
      if (ubicacion && ubicacion.secuencia !== i + 1) {
        ubicacion.secuencia = i + 1;
        updates.push(this.ubicacionRepository.save(ubicacion));
      }
    }

    await Promise.all(updates);

    // Validar que el nuevo orden tenga sentido (origen primero, destino ultimo)
    const reordered = await this.findByCartaParte(tenantId, cartaPorteId);
    await this.validateSecuenciaAfterReorder(reordered);

    return reordered;
  }

  /**
   * Valida la integridad de la secuencia de ubicaciones
   */
  async validateSecuencia(
    tenantId: string,
    cartaPorteId: string
  ): Promise<ValidacionSecuencia> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);
    const ubicaciones = await this.findByCartaParte(tenantId, cartaPorteId);

    const errors: string[] = [];

    if (ubicaciones.length === 0) {
      return {
        valid: false,
        tieneOrigen: false,
        tieneDestino: false,
        secuenciaCorrecta: false,
        distanciaTotalKm: 0,
        errors: ['No hay ubicaciones registradas'],
      };
    }

    // Verificar origen y destino
    const tieneOrigen = ubicaciones.some(
      u => u.tipoUbicacion === TipoUbicacionCartaPorte.ORIGEN
    );
    const tieneDestino = ubicaciones.some(
      u => u.tipoUbicacion === TipoUbicacionCartaPorte.DESTINO
    );

    if (!tieneOrigen) {
      errors.push('Se requiere al menos una ubicacion de origen');
    }

    if (!tieneDestino) {
      errors.push('Se requiere al menos una ubicacion de destino');
    }

    // Verificar que origen sea primero y destino sea ultimo
    const primera = ubicaciones[0];
    const ultima = ubicaciones[ubicaciones.length - 1];

    let secuenciaCorrecta = true;

    if (primera.tipoUbicacion !== TipoUbicacionCartaPorte.ORIGEN) {
      errors.push('La primera ubicacion debe ser de tipo Origen');
      secuenciaCorrecta = false;
    }

    if (ultima.tipoUbicacion !== TipoUbicacionCartaPorte.DESTINO) {
      errors.push('La ultima ubicacion debe ser de tipo Destino');
      secuenciaCorrecta = false;
    }

    // Verificar secuencia de numeros
    for (let i = 0; i < ubicaciones.length; i++) {
      if (ubicaciones[i].secuencia !== i + 1) {
        errors.push(`Secuencia incorrecta en ubicacion ${i + 1}`);
        secuenciaCorrecta = false;
      }
    }

    // Verificar fechas en orden cronologico
    for (let i = 1; i < ubicaciones.length; i++) {
      const fechaAnterior = ubicaciones[i - 1].fechaHoraSalidaLlegada;
      const fechaActual = ubicaciones[i].fechaHoraSalidaLlegada;

      if (fechaAnterior && fechaActual && fechaActual < fechaAnterior) {
        errors.push(
          `Fecha de ubicacion ${i + 1} es anterior a ubicacion ${i}`
        );
        secuenciaCorrecta = false;
      }
    }

    // Calcular distancia total
    const distanciaTotalKm = ubicaciones.reduce(
      (sum, u) => sum + (u.distanciaRecorrida ? Number(u.distanciaRecorrida) : 0),
      0
    );

    // Validar que haya al menos 2 ubicaciones (SAT requirement)
    if (ubicaciones.length < 2) {
      errors.push('Se requieren al menos 2 ubicaciones (origen y destino)');
    }

    return {
      valid: errors.length === 0,
      tieneOrigen,
      tieneDestino,
      secuenciaCorrecta,
      distanciaTotalKm,
      errors,
    };
  }

  /**
   * Obtiene la ubicacion de origen
   */
  async getOrigen(
    tenantId: string,
    cartaPorteId: string
  ): Promise<UbicacionCartaPorte | null> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);

    return this.ubicacionRepository.findOne({
      where: {
        cartaPorteId,
        tenantId,
        tipoUbicacion: TipoUbicacionCartaPorte.ORIGEN,
      },
      order: { secuencia: 'ASC' },
    });
  }

  /**
   * Obtiene la ubicacion de destino final
   */
  async getDestinoFinal(
    tenantId: string,
    cartaPorteId: string
  ): Promise<UbicacionCartaPorte | null> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);

    return this.ubicacionRepository.findOne({
      where: {
        cartaPorteId,
        tenantId,
        tipoUbicacion: TipoUbicacionCartaPorte.DESTINO,
      },
      order: { secuencia: 'DESC' },
    });
  }

  /**
   * Obtiene la distancia total del recorrido
   */
  async getDistanciaTotal(
    tenantId: string,
    cartaPorteId: string
  ): Promise<number> {
    await this.getCartaPorteOrFail(cartaPorteId, tenantId);

    const result = await this.ubicacionRepository
      .createQueryBuilder('u')
      .select('COALESCE(SUM(u.distanciaRecorrida), 0)', 'total')
      .where('u.cartaPorteId = :cartaPorteId', { cartaPorteId })
      .andWhere('u.tenantId = :tenantId', { tenantId })
      .getRawOne();

    return Number(result?.total || 0);
  }

  /**
   * Valida datos de ubicacion segun requerimientos SAT
   */
  private validateUbicacionData(data: CreateUbicacionDto): void {
    const errors: string[] = [];

    if (!data.tipoUbicacion) {
      errors.push('tipoUbicacion es requerido');
    }

    if (
      data.tipoUbicacion &&
      !Object.values(TipoUbicacionCartaPorte).includes(
        data.tipoUbicacion as TipoUbicacionCartaPorte
      )
    ) {
      errors.push(
        `tipoUbicacion debe ser: ${Object.values(TipoUbicacionCartaPorte).join(', ')}`
      );
    }

    if (!data.codigoPostal) {
      errors.push('codigoPostal es requerido');
    }

    if (data.codigoPostal && !/^\d{5}$/.test(data.codigoPostal)) {
      errors.push('codigoPostal debe tener 5 digitos');
    }

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

  /**
   * Valida reglas de negocio para ubicaciones
   */
  private async validateBusinessRules(
    cartaPorteId: string,
    data: CreateUbicacionDto
  ): Promise<void> {
    const existingUbicaciones = await this.ubicacionRepository.find({
      where: { cartaPorteId },
      order: { secuencia: 'ASC' },
    });

    // Si es la primera ubicacion, debe ser origen
    if (
      existingUbicaciones.length === 0 &&
      data.tipoUbicacion !== TipoUbicacionCartaPorte.ORIGEN
    ) {
      throw new Error('La primera ubicacion debe ser de tipo Origen');
    }
  }

  /**
   * Valida secuencia despues de reordenar
   */
  private async validateSecuenciaAfterReorder(
    ubicaciones: UbicacionCartaPorte[]
  ): Promise<void> {
    if (ubicaciones.length < 2) return;

    const primera = ubicaciones[0];
    const ultima = ubicaciones[ubicaciones.length - 1];

    const warnings: string[] = [];

    if (primera.tipoUbicacion !== TipoUbicacionCartaPorte.ORIGEN) {
      warnings.push(
        'Advertencia: La primera ubicacion no es de tipo Origen'
      );
    }

    if (ultima.tipoUbicacion !== TipoUbicacionCartaPorte.DESTINO) {
      warnings.push(
        'Advertencia: La ultima ubicacion no es de tipo Destino'
      );
    }

    // Log warnings but don't throw - let user decide
    if (warnings.length > 0) {
      console.warn('Ubicaciones reordenadas con advertencias:', warnings);
    }
  }

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

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