package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.finance.AdhesionDTO;
import dev.lions.unionflow.server.entity.Adhesion;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.AdhesionRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import io.quarkus.panache.common.Page;
import io.quarkus.panache.common.Sort;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;

/**
 * Service métier pour la gestion des adhésions
 * Contient la logique métier et les règles de validation
 *
 * @author UnionFlow Team
 * @version 1.0
 * @since 2025-01-17
 */
@ApplicationScoped
@Slf4j
public class AdhesionService {

  @Inject AdhesionRepository adhesionRepository;

  @Inject MembreRepository membreRepository;

  @Inject OrganisationRepository organisationRepository;

  /**
   * Récupère toutes les adhésions avec pagination
   *
   * @param page numéro de page (0-based)
   * @param size taille de la page
   * @return liste des adhésions converties en DTO
   */
  public List<AdhesionDTO> getAllAdhesions(int page, int size) {
    log.debug("Récupération des adhésions - page: {}, size: {}", page, size);

    jakarta.persistence.TypedQuery<Adhesion> query =
        adhesionRepository
            .getEntityManager()
            .createQuery(
                "SELECT a FROM Adhesion a ORDER BY a.dateDemande DESC", Adhesion.class);
    query.setFirstResult(page * size);
    query.setMaxResults(size);
    List<Adhesion> adhesions = query.getResultList();

    return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
  }

  /**
   * Récupère une adhésion par son ID
   *
   * @param id identifiant UUID de l'adhésion
   * @return DTO de l'adhésion
   * @throws NotFoundException si l'adhésion n'existe pas
   */
  public AdhesionDTO getAdhesionById(@NotNull UUID id) {
    log.debug("Récupération de l'adhésion avec ID: {}", id);

    Adhesion adhesion =
        adhesionRepository
            .findByIdOptional(id)
            .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));

    return convertToDTO(adhesion);
  }

  /**
   * Récupère une adhésion par son numéro de référence
   *
   * @param numeroReference numéro de référence unique
   * @return DTO de l'adhésion
   * @throws NotFoundException si l'adhésion n'existe pas
   */
  public AdhesionDTO getAdhesionByReference(@NotNull String numeroReference) {
    log.debug("Récupération de l'adhésion avec référence: {}", numeroReference);

    Adhesion adhesion =
        adhesionRepository
            .findByNumeroReference(numeroReference)
            .orElseThrow(
                () ->
                    new NotFoundException(
                        "Adhésion non trouvée avec la référence: " + numeroReference));

    return convertToDTO(adhesion);
  }

  /**
   * Crée une nouvelle adhésion
   *
   * @param adhesionDTO données de l'adhésion à créer
   * @return DTO de l'adhésion créée
   */
  @Transactional
  public AdhesionDTO createAdhesion(@Valid AdhesionDTO adhesionDTO) {
    log.info(
        "Création d'une nouvelle adhésion pour le membre: {} et l'organisation: {}",
        adhesionDTO.getMembreId(),
        adhesionDTO.getOrganisationId());

    // Validation du membre
    Membre membre =
        membreRepository
            .findByIdOptional(adhesionDTO.getMembreId())
            .orElseThrow(
                () ->
                    new NotFoundException(
                        "Membre non trouvé avec l'ID: " + adhesionDTO.getMembreId()));

    // Validation de l'organisation
    Organisation organisation =
        organisationRepository
            .findByIdOptional(adhesionDTO.getOrganisationId())
            .orElseThrow(
                () ->
                    new NotFoundException(
                        "Organisation non trouvée avec l'ID: " + adhesionDTO.getOrganisationId()));

    // Conversion DTO vers entité
    Adhesion adhesion = convertToEntity(adhesionDTO);
    adhesion.setMembre(membre);
    adhesion.setOrganisation(organisation);

    // Génération automatique du numéro de référence si absent
    if (adhesion.getNumeroReference() == null || adhesion.getNumeroReference().isEmpty()) {
      adhesion.setNumeroReference(genererNumeroReference());
    }

    // Initialisation par défaut
    if (adhesion.getDateDemande() == null) {
      adhesion.setDateDemande(LocalDate.now());
    }
    if (adhesion.getStatut() == null || adhesion.getStatut().isEmpty()) {
      adhesion.setStatut("EN_ATTENTE");
    }
    if (adhesion.getMontantPaye() == null) {
      adhesion.setMontantPaye(BigDecimal.ZERO);
    }
    if (adhesion.getCodeDevise() == null || adhesion.getCodeDevise().isEmpty()) {
      adhesion.setCodeDevise("XOF");
    }

    // Persistance
    adhesionRepository.persist(adhesion);

    log.info(
        "Adhésion créée avec succès - ID: {}, Référence: {}",
        adhesion.getId(),
        adhesion.getNumeroReference());

    return convertToDTO(adhesion);
  }

  /**
   * Met à jour une adhésion existante
   *
   * @param id identifiant UUID de l'adhésion
   * @param adhesionDTO nouvelles données
   * @return DTO de l'adhésion mise à jour
   */
  @Transactional
  public AdhesionDTO updateAdhesion(@NotNull UUID id, @Valid AdhesionDTO adhesionDTO) {
    log.info("Mise à jour de l'adhésion avec ID: {}", id);

    Adhesion adhesionExistante =
        adhesionRepository
            .findByIdOptional(id)
            .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));

    // Mise à jour des champs modifiables
    updateAdhesionFields(adhesionExistante, adhesionDTO);

    log.info("Adhésion mise à jour avec succès - ID: {}", id);

    return convertToDTO(adhesionExistante);
  }

  /**
   * Supprime (désactive) une adhésion
   *
   * @param id identifiant UUID de l'adhésion
   */
  @Transactional
  public void deleteAdhesion(@NotNull UUID id) {
    log.info("Suppression de l'adhésion avec ID: {}", id);

    Adhesion adhesion =
        adhesionRepository
            .findByIdOptional(id)
            .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));

    // Vérification si l'adhésion peut être supprimée
    if ("PAYEE".equals(adhesion.getStatut())) {
      throw new IllegalStateException("Impossible de supprimer une adhésion déjà payée");
    }

    adhesion.setStatut("ANNULEE");

    log.info("Adhésion supprimée avec succès - ID: {}", id);
  }

  /**
   * Approuve une adhésion
   *
   * @param id identifiant UUID de l'adhésion
   * @param approuvePar nom de l'utilisateur qui approuve
   * @return DTO de l'adhésion approuvée
   */
  @Transactional
  public AdhesionDTO approuverAdhesion(@NotNull UUID id, String approuvePar) {
    log.info("Approbation de l'adhésion avec ID: {}", id);

    Adhesion adhesion =
        adhesionRepository
            .findByIdOptional(id)
            .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));

    if (!"EN_ATTENTE".equals(adhesion.getStatut())) {
      throw new IllegalStateException(
          "Seules les adhésions en attente peuvent être approuvées");
    }

    adhesion.setStatut("APPROUVEE");
    adhesion.setDateApprobation(LocalDate.now());
    adhesion.setApprouvePar(approuvePar);
    adhesion.setDateValidation(LocalDate.now());

    log.info("Adhésion approuvée avec succès - ID: {}", id);

    return convertToDTO(adhesion);
  }

  /**
   * Rejette une adhésion
   *
   * @param id identifiant UUID de l'adhésion
   * @param motifRejet motif du rejet
   * @return DTO de l'adhésion rejetée
   */
  @Transactional
  public AdhesionDTO rejeterAdhesion(@NotNull UUID id, String motifRejet) {
    log.info("Rejet de l'adhésion avec ID: {}", id);

    Adhesion adhesion =
        adhesionRepository
            .findByIdOptional(id)
            .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));

    if (!"EN_ATTENTE".equals(adhesion.getStatut())) {
      throw new IllegalStateException("Seules les adhésions en attente peuvent être rejetées");
    }

    adhesion.setStatut("REJETEE");
    adhesion.setMotifRejet(motifRejet);

    log.info("Adhésion rejetée avec succès - ID: {}", id);

    return convertToDTO(adhesion);
  }

  /**
   * Enregistre un paiement pour une adhésion
   *
   * @param id identifiant UUID de l'adhésion
   * @param montantPaye montant payé
   * @param methodePaiement méthode de paiement
   * @param referencePaiement référence du paiement
   * @return DTO de l'adhésion mise à jour
   */
  @Transactional
  public AdhesionDTO enregistrerPaiement(
      @NotNull UUID id,
      BigDecimal montantPaye,
      String methodePaiement,
      String referencePaiement) {
    log.info("Enregistrement du paiement pour l'adhésion avec ID: {}", id);

    Adhesion adhesion =
        adhesionRepository
            .findByIdOptional(id)
            .orElseThrow(() -> new NotFoundException("Adhésion non trouvée avec l'ID: " + id));

    if (!"APPROUVEE".equals(adhesion.getStatut()) && !"EN_PAIEMENT".equals(adhesion.getStatut())) {
      throw new IllegalStateException(
          "Seules les adhésions approuvées peuvent recevoir un paiement");
    }

    BigDecimal nouveauMontantPaye =
        adhesion.getMontantPaye() != null
            ? adhesion.getMontantPaye().add(montantPaye)
            : montantPaye;

    adhesion.setMontantPaye(nouveauMontantPaye);
    adhesion.setMethodePaiement(methodePaiement);
    adhesion.setReferencePaiement(referencePaiement);
    adhesion.setDatePaiement(java.time.LocalDateTime.now());

    // Mise à jour du statut si payée intégralement
    if (adhesion.isPayeeIntegralement()) {
      adhesion.setStatut("PAYEE");
    } else {
      adhesion.setStatut("EN_PAIEMENT");
    }

    log.info("Paiement enregistré avec succès pour l'adhésion - ID: {}", id);

    return convertToDTO(adhesion);
  }

  /**
   * Récupère les adhésions d'un membre
   *
   * @param membreId identifiant UUID du membre
   * @param page numéro de page
   * @param size taille de la page
   * @return liste des adhésions du membre
   */
  public List<AdhesionDTO> getAdhesionsByMembre(@NotNull UUID membreId, int page, int size) {
    log.debug("Récupération des adhésions du membre: {}", membreId);

    if (!membreRepository.findByIdOptional(membreId).isPresent()) {
      throw new NotFoundException("Membre non trouvé avec l'ID: " + membreId);
    }

    List<Adhesion> adhesions =
        adhesionRepository.findByMembreId(membreId).stream()
            .skip(page * size)
            .limit(size)
            .collect(Collectors.toList());

    return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
  }

  /**
   * Récupère les adhésions d'une organisation
   *
   * @param organisationId identifiant UUID de l'organisation
   * @param page numéro de page
   * @param size taille de la page
   * @return liste des adhésions de l'organisation
   */
  public List<AdhesionDTO> getAdhesionsByOrganisation(
      @NotNull UUID organisationId, int page, int size) {
    log.debug("Récupération des adhésions de l'organisation: {}", organisationId);

    if (!organisationRepository.findByIdOptional(organisationId).isPresent()) {
      throw new NotFoundException("Organisation non trouvée avec l'ID: " + organisationId);
    }

    List<Adhesion> adhesions =
        adhesionRepository.findByOrganisationId(organisationId).stream()
            .skip(page * size)
            .limit(size)
            .collect(Collectors.toList());

    return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
  }

  /**
   * Récupère les adhésions par statut
   *
   * @param statut statut recherché
   * @param page numéro de page
   * @param size taille de la page
   * @return liste des adhésions avec le statut spécifié
   */
  public List<AdhesionDTO> getAdhesionsByStatut(@NotNull String statut, int page, int size) {
    log.debug("Récupération des adhésions avec statut: {}", statut);

    List<Adhesion> adhesions =
        adhesionRepository.findByStatut(statut).stream()
            .skip(page * size)
            .limit(size)
            .collect(Collectors.toList());

    return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
  }

  /**
   * Récupère les adhésions en attente
   *
   * @param page numéro de page
   * @param size taille de la page
   * @return liste des adhésions en attente
   */
  public List<AdhesionDTO> getAdhesionsEnAttente(int page, int size) {
    log.debug("Récupération des adhésions en attente");

    List<Adhesion> adhesions =
        adhesionRepository.findEnAttente().stream()
            .skip(page * size)
            .limit(size)
            .collect(Collectors.toList());

    return adhesions.stream().map(this::convertToDTO).collect(Collectors.toList());
  }

  /**
   * Récupère les statistiques des adhésions
   *
   * @return map contenant les statistiques
   */
  public Map<String, Object> getStatistiquesAdhesions() {
    log.debug("Calcul des statistiques des adhésions");

    long totalAdhesions = adhesionRepository.count();
    long adhesionsApprouvees = adhesionRepository.findByStatut("APPROUVEE").size();
    long adhesionsEnAttente = adhesionRepository.findEnAttente().size();
    long adhesionsPayees = adhesionRepository.findByStatut("PAYEE").size();

    return Map.of(
        "totalAdhesions", totalAdhesions,
        "adhesionsApprouvees", adhesionsApprouvees,
        "adhesionsEnAttente", adhesionsEnAttente,
        "adhesionsPayees", adhesionsPayees,
        "tauxApprobation",
            totalAdhesions > 0 ? (adhesionsApprouvees * 100.0 / totalAdhesions) : 0.0,
        "tauxPaiement",
            adhesionsApprouvees > 0
                ? (adhesionsPayees * 100.0 / adhesionsApprouvees)
                : 0.0);
  }

  /** Génère un numéro de référence unique pour une adhésion */
  private String genererNumeroReference() {
    return "ADH-" + System.currentTimeMillis() + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
  }

  /** Convertit une entité Adhesion en DTO */
  private AdhesionDTO convertToDTO(Adhesion adhesion) {
    if (adhesion == null) {
      return null;
    }

    AdhesionDTO dto = new AdhesionDTO();

    dto.setId(adhesion.getId());
    dto.setNumeroReference(adhesion.getNumeroReference());

    // Conversion du membre associé
    if (adhesion.getMembre() != null) {
      dto.setMembreId(adhesion.getMembre().getId());
      dto.setNomMembre(adhesion.getMembre().getNomComplet());
      dto.setNumeroMembre(adhesion.getMembre().getNumeroMembre());
      dto.setEmailMembre(adhesion.getMembre().getEmail());
    }

    // Conversion de l'organisation
    if (adhesion.getOrganisation() != null) {
      dto.setOrganisationId(adhesion.getOrganisation().getId());
      dto.setNomOrganisation(adhesion.getOrganisation().getNom());
    }

    // Propriétés de l'adhésion
    dto.setDateDemande(adhesion.getDateDemande());
    dto.setFraisAdhesion(adhesion.getFraisAdhesion());
    dto.setMontantPaye(adhesion.getMontantPaye());
    dto.setCodeDevise(adhesion.getCodeDevise());
    dto.setStatut(adhesion.getStatut());
    dto.setDateApprobation(adhesion.getDateApprobation());
    dto.setDatePaiement(adhesion.getDatePaiement());
    dto.setMethodePaiement(adhesion.getMethodePaiement());
    dto.setReferencePaiement(adhesion.getReferencePaiement());
    dto.setMotifRejet(adhesion.getMotifRejet());
    dto.setObservations(adhesion.getObservations());
    dto.setApprouvePar(adhesion.getApprouvePar());
    dto.setDateValidation(adhesion.getDateValidation());

    // Métadonnées de BaseEntity
    dto.setDateCreation(adhesion.getDateCreation());
    dto.setDateModification(adhesion.getDateModification());
    dto.setCreePar(adhesion.getCreePar());
    dto.setModifiePar(adhesion.getModifiePar());
    dto.setActif(adhesion.getActif());

    return dto;
  }

  /** Convertit un DTO en entité Adhesion */
  private Adhesion convertToEntity(AdhesionDTO dto) {
    if (dto == null) {
      return null;
    }

    Adhesion adhesion = new Adhesion();

    adhesion.setNumeroReference(dto.getNumeroReference());
    adhesion.setDateDemande(dto.getDateDemande());
    adhesion.setFraisAdhesion(dto.getFraisAdhesion());
    adhesion.setMontantPaye(dto.getMontantPaye() != null ? dto.getMontantPaye() : BigDecimal.ZERO);
    adhesion.setCodeDevise(dto.getCodeDevise());
    adhesion.setStatut(dto.getStatut());
    adhesion.setDateApprobation(dto.getDateApprobation());
    adhesion.setDatePaiement(dto.getDatePaiement());
    adhesion.setMethodePaiement(dto.getMethodePaiement());
    adhesion.setReferencePaiement(dto.getReferencePaiement());
    adhesion.setMotifRejet(dto.getMotifRejet());
    adhesion.setObservations(dto.getObservations());
    adhesion.setApprouvePar(dto.getApprouvePar());
    adhesion.setDateValidation(dto.getDateValidation());

    return adhesion;
  }

  /** Met à jour les champs modifiables d'une adhésion existante */
  private void updateAdhesionFields(Adhesion adhesion, AdhesionDTO dto) {
    if (dto.getFraisAdhesion() != null) {
      adhesion.setFraisAdhesion(dto.getFraisAdhesion());
    }
    if (dto.getMontantPaye() != null) {
      adhesion.setMontantPaye(dto.getMontantPaye());
    }
    if (dto.getStatut() != null) {
      adhesion.setStatut(dto.getStatut());
    }
    if (dto.getDateApprobation() != null) {
      adhesion.setDateApprobation(dto.getDateApprobation());
    }
    if (dto.getDatePaiement() != null) {
      adhesion.setDatePaiement(dto.getDatePaiement());
    }
    if (dto.getMethodePaiement() != null) {
      adhesion.setMethodePaiement(dto.getMethodePaiement());
    }
    if (dto.getReferencePaiement() != null) {
      adhesion.setReferencePaiement(dto.getReferencePaiement());
    }
    if (dto.getMotifRejet() != null) {
      adhesion.setMotifRejet(dto.getMotifRejet());
    }
    if (dto.getObservations() != null) {
      adhesion.setObservations(dto.getObservations());
    }
    if (dto.getApprouvePar() != null) {
      adhesion.setApprouvePar(dto.getApprouvePar());
    }
    if (dto.getDateValidation() != null) {
      adhesion.setDateValidation(dto.getDateValidation());
    }
  }
}

