package dev.lions.unionflow.server.service.delegation;

import dev.lions.unionflow.server.api.dto.delegation.request.CreateRoleDelegationRequest;
import dev.lions.unionflow.server.api.dto.delegation.response.RoleDelegationResponse;
import dev.lions.unionflow.server.entity.RoleDelegation;
import dev.lions.unionflow.server.repository.RoleDelegationRepository;
import dev.lions.unionflow.server.security.AuditTrailService;
import dev.lions.unionflow.server.security.SoDPermissionChecker;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.WebApplicationException;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;

/**
 * Service de gestion des délégations temporaires de rôles.
 *
 * <p>Cas d'usage : un trésorier en congé délègue son rôle pendant 2 semaines au trésorier
 * adjoint. Le {@link SoDPermissionChecker} consulte ensuite les délégations actives pour
 * calculer le rôle effectif d'un utilisateur.
 *
 * <p>Règles :
 *
 * <ul>
 *   <li>Une délégation ne peut être créée que par le délégant lui-même (vérifié par l'endpoint).
 *   <li>Le délégataire ne peut pas avoir un rôle en conflit SoD avec le rôle délégué (vérifié à
 *       la création).
 *   <li>Une délégation expire automatiquement à {@code dateFin} (scheduler quotidien).
 *   <li>Le délégant peut révoquer manuellement à tout moment.
 * </ul>
 *
 * @since 2026-04-25 (P1-NEW-5)
 */
@ApplicationScoped
public class RoleDelegationService {

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

  @Inject RoleDelegationRepository repository;
  @Inject AuditTrailService auditTrail;
  @Inject SoDPermissionChecker sodChecker;

  /**
   * Crée une délégation temporaire après vérification SoD.
   *
   * @throws WebApplicationException 400 si la combinaison rôle délégué + rôles existants du
   *     délégataire viole le SoD.
   */
  @Transactional
  public RoleDelegation creer(RoleDelegation delegation, Set<String> rolesDelegataire) {
    if (delegation.getDelegantUserId().equals(delegation.getDelegataireUserId())) {
      throw new WebApplicationException("Le délégant et le délégataire ne peuvent être identiques", 400);
    }
    if (delegation.getDateFin().isBefore(delegation.getDateDebut())
        || delegation.getDateFin().isEqual(delegation.getDateDebut())) {
      throw new WebApplicationException("dateFin doit être strictement après dateDebut", 400);
    }

    // Vérifie que la combinaison roles délégataire + role délégué ne crée pas de conflit SoD
    Set<String> rolesEffectifsApresDelegation = new HashSet<>(rolesDelegataire);
    rolesEffectifsApresDelegation.add(delegation.getRoleDelegue());
    SoDPermissionChecker.SoDCheckResult sodResult =
        sodChecker.checkRoleCombination(delegation.getDelegataireUserId(),
            rolesEffectifsApresDelegation);
    if (sodResult.isViolation()) {
      throw new WebApplicationException(
          "Délégation refusée — violation SoD : " + sodResult.violationReason(), 400);
    }

    delegation.setStatut("ACTIVE");
    repository.persist(delegation);
    auditTrail.log("RoleDelegation", delegation.getId(), "CREATE",
        "Délégation " + delegation.getRoleDelegue() + " : "
            + delegation.getDelegantUserId() + " → " + delegation.getDelegataireUserId(),
        null, delegation, null, true, null);
    return delegation;
  }

  /** Révoque une délégation manuellement. */
  @Transactional
  public RoleDelegation revoquer(UUID delegationId) {
    RoleDelegation d = repository.findById(delegationId);
    if (d == null) throw new NotFoundException("Délégation introuvable : " + delegationId);
    if (!"ACTIVE".equals(d.getStatut())) {
      throw new WebApplicationException(
          "Délégation non active (statut: " + d.getStatut() + ")", 400);
    }
    d.setStatut("REVOQUEE");
    d.setDateRevocation(LocalDateTime.now());
    repository.persist(d);
    auditTrail.logSimple("RoleDelegation", d.getId(), "DELETE", "Révocation délégation");
    return d;
  }

  /** Retourne les rôles effectivement détenus par un user à un instant donné (directs ∪ délégués). */
  public Set<String> rolesEffectifs(UUID userId, UUID organisationId, Set<String> rolesDirects) {
    LocalDateTime now = LocalDateTime.now();
    List<RoleDelegation> actives = repository.findActiveByDelegataire(userId, organisationId, now);
    Set<String> roles = new HashSet<>(rolesDirects);
    roles.addAll(actives.stream().map(RoleDelegation::getRoleDelegue)
        .collect(Collectors.toSet()));
    return roles;
  }

  /** Scheduler à appeler quotidiennement : marque comme EXPIREE les délégations dont dateFin est passée. */
  @Transactional
  public int marquerExpirees() {
    List<RoleDelegation> expired = repository.findExpired(LocalDateTime.now());
    for (RoleDelegation d : expired) {
      d.setStatut("EXPIREE");
      repository.persist(d);
    }
    if (!expired.isEmpty()) {
      LOG.infof("RoleDelegationService: %d délégations marquées EXPIREE", expired.size());
    }
    return expired.size();
  }

  // ────────────────────────────────────────────────────────────
  // API publique orientée DTO (Sprint 10)
  // ────────────────────────────────────────────────────────────

  /** Crée une délégation à partir du DTO + rôles connus du délégataire. */
  @Transactional
  public RoleDelegationResponse creerDepuisRequest(
      CreateRoleDelegationRequest req, Set<String> rolesDelegataire) {
    RoleDelegation entity = RoleDelegation.builder()
        .organisationId(req.organisationId())
        .delegantUserId(req.delegantUserId())
        .delegataireUserId(req.delegataireUserId())
        .roleDelegue(req.roleDelegue())
        .dateDebut(req.dateDebut())
        .dateFin(req.dateFin())
        .motif(req.motif())
        .statut("ACTIVE")
        .build();
    entity.setActif(true);
    return toResponse(creer(entity, rolesDelegataire));
  }

  /** Révoque et retourne le DTO. */
  @Transactional
  public RoleDelegationResponse revoquerEtRetourner(UUID delegationId) {
    return toResponse(revoquer(delegationId));
  }

  /** Liste les délégations d'une organisation. */
  public List<RoleDelegationResponse> listerParOrganisation(UUID organisationId) {
    return repository.findByOrganisation(organisationId).stream()
        .map(this::toResponse).collect(Collectors.toList());
  }

  // ────────────────────────────────────────────────────────────
  // Mapping Entity ↔ DTO (centralisé ici par contrat architecture)
  // ────────────────────────────────────────────────────────────

  RoleDelegationResponse toResponse(RoleDelegation d) {
    return RoleDelegationResponse.builder()
        .id(d.getId())
        .organisationId(d.getOrganisationId())
        .delegantUserId(d.getDelegantUserId())
        .delegataireUserId(d.getDelegataireUserId())
        .roleDelegue(d.getRoleDelegue())
        .dateDebut(d.getDateDebut())
        .dateFin(d.getDateFin())
        .motif(d.getMotif())
        .statut(d.getStatut())
        .dateRevocation(d.getDateRevocation())
        .dateCreation(d.getDateCreation())
        .estActive(d.isActiveAt(LocalDateTime.now()))
        .build();
  }
}
