package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.adresse.request.CreateAdresseRequest;
import dev.lions.unionflow.server.api.dto.adresse.request.UpdateAdresseRequest;
import dev.lions.unionflow.server.api.dto.adresse.response.AdresseResponse;
import dev.lions.unionflow.server.entity.Adresse;
import dev.lions.unionflow.server.entity.Evenement;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.Organisation;
import dev.lions.unionflow.server.repository.AdresseRepository;
import dev.lions.unionflow.server.repository.EvenementRepository;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.OrganisationRepository;
import dev.lions.unionflow.server.repository.TypeReferenceRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;

/**
 * Service métier pour la gestion des adresses
 *
 * @author UnionFlow Team
 * @version 3.0
 * @since 2025-01-29
 */
@ApplicationScoped
public class AdresseService {

  private static final Logger LOG = Logger.getLogger(AdresseService.class);

  @Inject
  AdresseRepository adresseRepository;
  @Inject
  OrganisationRepository organisationRepository;
  @Inject
  MembreRepository membreRepository;
  @Inject
  EvenementRepository evenementRepository;
  @Inject
  TypeReferenceRepository typeReferenceRepository;

  /**
   * Crée une nouvelle adresse
   *
   * @param request DTO de l'adresse à créer
   * @return DTO de l'adresse créée
   */
  @Transactional
  public AdresseResponse creerAdresse(CreateAdresseRequest request) {
    LOG.infof("Création d'une nouvelle adresse de type: %s", request.typeAdresse());

    Adresse adresse = convertToEntity(request);

    // Gestion de l'adresse principale
    if (Boolean.TRUE.equals(request.principale())) {
      desactiverAutresPrincipales(
          request.organisationId(), request.membreId(), request.evenementId());
    }

    adresseRepository.persist(adresse);
    LOG.infof("Adresse créée avec succès: ID=%s", adresse.getId());

    return convertToDTO(adresse);
  }

  /**
   * Met à jour une adresse existante
   *
   * @param id      ID de l'adresse
   * @param request DTO avec les nouvelles données
   * @return DTO de l'adresse mise à jour
   */
  @Transactional
  public AdresseResponse mettreAJourAdresse(UUID id, UpdateAdresseRequest request) {
    LOG.infof("Mise à jour de l'adresse ID: %s", id);

    Adresse adresse = adresseRepository
        .findAdresseById(id)
        .orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));

    // Gestion de l'adresse principale AVANT la mise à jour pour éviter l'auto-flush Hibernate
    // qui inclurait l'entité courante dans la requête JPQL si elle est déjà dirty
    if (Boolean.TRUE.equals(request.principale())) {
      desactiverAutresPrincipales(
          adresse.getOrganisation() != null ? adresse.getOrganisation().getId() : null,
          adresse.getMembre() != null ? adresse.getMembre().getId() : null,
          adresse.getEvenement() != null ? adresse.getEvenement().getId() : null);
    }

    // Mise à jour des champs
    updateFromDTO(adresse, request);

    adresseRepository.persist(adresse);
    LOG.infof("Adresse mise à jour avec succès: ID=%s", id);

    return convertToDTO(adresse);
  }

  /**
   * Supprime une adresse
   *
   * @param id ID de l'adresse
   */
  @Transactional
  public void supprimerAdresse(UUID id) {
    LOG.infof("Suppression de l'adresse ID: %s", id);

    Adresse adresse = adresseRepository
        .findAdresseById(id)
        .orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));

    adresseRepository.delete(adresse);
    LOG.infof("Adresse supprimée avec succès: ID=%s", id);
  }

  /**
   * Trouve une adresse par son ID
   *
   * @param id ID de l'adresse
   * @return DTO de l'adresse
   */
  public AdresseResponse trouverParId(UUID id) {
    return adresseRepository
        .findAdresseById(id)
        .map(this::convertToDTO)
        .orElseThrow(() -> new NotFoundException("Adresse non trouvée avec l'ID: " + id));
  }

  /**
   * Trouve toutes les adresses d'une organisation
   *
   * @param organisationId ID de l'organisation
   * @return Liste des adresses
   */
  public List<AdresseResponse> trouverParOrganisation(UUID organisationId) {
    return adresseRepository.findByOrganisationId(organisationId).stream()
        .map(this::convertToDTO)
        .collect(Collectors.toList());
  }

  /**
   * Trouve toutes les adresses d'un membre
   *
   * @param membreId ID du membre
   * @return Liste des adresses
   */
  public List<AdresseResponse> trouverParMembre(UUID membreId) {
    return adresseRepository.findByMembreId(membreId).stream()
        .map(this::convertToDTO)
        .collect(Collectors.toList());
  }

  /**
   * Trouve l'adresse d'un événement
   *
   * @param evenementId ID de l'événement
   * @return DTO de l'adresse ou null
   */
  public AdresseResponse trouverParEvenement(UUID evenementId) {
    return adresseRepository
        .findByEvenementId(evenementId)
        .map(this::convertToDTO)
        .orElse(null);
  }

  /**
   * Trouve l'adresse principale d'une organisation
   *
   * @param organisationId ID de l'organisation
   * @return DTO de l'adresse principale ou null
   */
  public AdresseResponse trouverPrincipaleParOrganisation(UUID organisationId) {
    return adresseRepository
        .findPrincipaleByOrganisationId(organisationId)
        .map(this::convertToDTO)
        .orElse(null);
  }

  /**
   * Trouve l'adresse principale d'un membre
   *
   * @param membreId ID du membre
   * @return DTO de l'adresse principale ou null
   */
  public AdresseResponse trouverPrincipaleParMembre(UUID membreId) {
    return adresseRepository
        .findPrincipaleByMembreId(membreId)
        .map(this::convertToDTO)
        .orElse(null);
  }

  // ========================================
  // MÉTHODES PRIVÉES
  // ========================================

  /** Désactive les autres adresses principales pour la même entité */
  private void desactiverAutresPrincipales(UUID organisationId, UUID membreId, UUID evenementId) {
    List<Adresse> autresPrincipales;

    if (organisationId != null) {
      autresPrincipales = adresseRepository
          .find("organisation.id = ?1 AND principale = true", organisationId)
          .list();
    } else if (membreId != null) {
      autresPrincipales = adresseRepository
          .find("membre.id = ?1 AND principale = true", membreId)
          .list();
    } else if (evenementId != null) {
      autresPrincipales = adresseRepository
          .find("evenement.id = ?1 AND principale = true", evenementId)
          .list();
    } else {
      return; // Pas d'entité associée
    }

    autresPrincipales.forEach(adr -> adr.setPrincipale(false));
  }

  /** Convertit une entité en Response DTO */
  private AdresseResponse convertToDTO(Adresse adresse) {
    if (adresse == null) {
      return null;
    }

    AdresseResponse dto = new AdresseResponse();
    dto.setId(adresse.getId());

    dto.setTypeAdresse(adresse.getTypeAdresse());
    dto.setTypeAdresseLibelle(resolveLibelle("TYPE_ADRESSE", adresse.getTypeAdresse()));
    // L'icône pourrait venir du champ severity ou option des requêtes
    dto.setTypeAdresseIcone(resolveIcone("TYPE_ADRESSE", adresse.getTypeAdresse()));

    dto.setAdresse(adresse.getAdresse());
    dto.setComplementAdresse(adresse.getComplementAdresse());
    dto.setCodePostal(adresse.getCodePostal());
    dto.setVille(adresse.getVille());
    dto.setRegion(adresse.getRegion());
    dto.setPays(adresse.getPays());
    dto.setLatitude(adresse.getLatitude());
    dto.setLongitude(adresse.getLongitude());
    dto.setPrincipale(adresse.getPrincipale());
    dto.setLibelle(adresse.getLibelle());
    dto.setNotes(adresse.getNotes());

    if (adresse.getOrganisation() != null) {
      dto.setOrganisationId(adresse.getOrganisation().getId());
    }
    if (adresse.getMembre() != null) {
      dto.setMembreId(adresse.getMembre().getId());
    }
    if (adresse.getEvenement() != null) {
      dto.setEvenementId(adresse.getEvenement().getId());
    }

    dto.setAdresseComplete(adresse.getAdresseComplete());
    dto.setDateCreation(adresse.getDateCreation());
    dto.setDateModification(adresse.getDateModification());
    dto.setActif(adresse.getActif());

    return dto;
  }

  /** Convertit un Create Request en entité */
  private Adresse convertToEntity(CreateAdresseRequest request) {
    if (request == null) {
      return null;
    }

    Adresse adresse = new Adresse();
    // Valeur par défaut si non fourni
    adresse.setTypeAdresse(request.typeAdresse() != null ? request.typeAdresse() : "AUTRE");
    adresse.setAdresse(request.adresse());
    adresse.setComplementAdresse(request.complementAdresse());
    adresse.setCodePostal(request.codePostal());
    adresse.setVille(request.ville());
    adresse.setRegion(request.region());
    adresse.setPays(request.pays());
    adresse.setLatitude(request.latitude());
    adresse.setLongitude(request.longitude());
    adresse.setPrincipale(Boolean.TRUE.equals(request.principale()));
    adresse.setLibelle(request.libelle());
    adresse.setNotes(request.notes());

    // Relations
    if (request.organisationId() != null) {
      Organisation org = organisationRepository
          .findByIdOptional(request.organisationId())
          .orElseThrow(
              () -> new NotFoundException(
                  "Organisation non trouvée avec l'ID: " + request.organisationId()));
      adresse.setOrganisation(org);
    }

    if (request.membreId() != null) {
      Membre membre = membreRepository
          .findByIdOptional(request.membreId())
          .orElseThrow(
              () -> new NotFoundException("Membre non trouvé avec l'ID: " + request.membreId()));
      adresse.setMembre(membre);
    }

    if (request.evenementId() != null) {
      Evenement evenement = evenementRepository
          .findByIdOptional(request.evenementId())
          .orElseThrow(
              () -> new NotFoundException(
                  "Événement non trouvé avec l'ID: " + request.evenementId()));
      adresse.setEvenement(evenement);
    }

    return adresse;
  }

  /** Met à jour une entité à partir d'un Update Request */
  private void updateFromDTO(Adresse adresse, UpdateAdresseRequest request) {
    if (request.typeAdresse() != null) {
      adresse.setTypeAdresse(request.typeAdresse());
    }
    if (request.adresse() != null) {
      adresse.setAdresse(request.adresse());
    }
    if (request.complementAdresse() != null) {
      adresse.setComplementAdresse(request.complementAdresse());
    }
    if (request.codePostal() != null) {
      adresse.setCodePostal(request.codePostal());
    }
    if (request.ville() != null) {
      adresse.setVille(request.ville());
    }
    if (request.region() != null) {
      adresse.setRegion(request.region());
    }
    if (request.pays() != null) {
      adresse.setPays(request.pays());
    }
    if (request.latitude() != null) {
      adresse.setLatitude(request.latitude());
    }
    if (request.longitude() != null) {
      adresse.setLongitude(request.longitude());
    }
    if (request.principale() != null) {
      adresse.setPrincipale(request.principale());
    }
    if (request.libelle() != null) {
      adresse.setLibelle(request.libelle());
    }
    if (request.notes() != null) {
      adresse.setNotes(request.notes());
    }
  }

  private String resolveLibelle(String domaine, String code) {
    if (code == null)
      return null;
    return typeReferenceRepository.findByDomaineAndCode(domaine, code)
        .map(dev.lions.unionflow.server.entity.TypeReference::getLibelle)
        .orElse(code);
  }

  private String resolveIcone(String domaine, String code) {
    if (code == null)
      return null;
    return typeReferenceRepository.findByDomaineAndCode(domaine, code)
        .map(dev.lions.unionflow.server.entity.TypeReference::getIcone) // Ou un champ icone si dispo, sinon null
        .orElse(null);
  }
}
