package sn.ladoum.bergerie.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import sn.ladoum.bergerie.dto.MoutonDto;
import sn.ladoum.bergerie.entity.Mouton;
import sn.ladoum.bergerie.entity.Utilisateur;
import sn.ladoum.bergerie.entity.enums.Role;
import sn.ladoum.bergerie.entity.enums.Sexe;
import sn.ladoum.bergerie.entity.enums.StatutMouton;
import sn.ladoum.bergerie.exception.ResourceNotFoundException;
import sn.ladoum.bergerie.repository.MoutonRepository;
import sn.ladoum.bergerie.repository.UtilisateurRepository;
import sn.ladoum.bergerie.storage.FileStorageService;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
public class MoutonService {

    private final MoutonRepository moutonRepository;
    private final UtilisateurRepository utilisateurRepository;
    private final FileStorageService fileStorageService;

    @Transactional(readOnly = true)
    public List<MoutonDto> findAll() {
        Long eleveurId = eleveurFilterId();
        List<Mouton> moutons = (eleveurId != null)
                ? moutonRepository.findByEleveurId(eleveurId)
                : moutonRepository.findAll();
        return moutons.stream().map(this::toDto).toList();
    }

    @Transactional(readOnly = true)
    public MoutonDto findById(Long id) {
        Mouton m = getEntity(id);
        checkAccess(m);
        return toDto(m);
    }

    /**
     * Fiche publique d'un mouton : accessible à tout utilisateur authentifié,
     * sans contrôle de propriété. Utilisée pour le scan QR code.
     */
    @Transactional(readOnly = true)
    public MoutonDto findPublicInfo(Long id) {
        return toDto(getEntity(id));
    }

    /**
     * Descendance directe d'un mouton (ses enfants).
     * Liste publique accessible à tout utilisateur authentifié.
     */
    @Transactional(readOnly = true)
    public List<MoutonDto> descendanceDirecte(Long id) {
        Mouton m = getEntity(id);
        List<Mouton> enfants = (m.getSexe() == Sexe.MALE)
                ? moutonRepository.findByPereId(id)
                : moutonRepository.findByMereId(id);
        return enfants.stream().map(this::toDto).toList();
    }

    private static final DateTimeFormatter FORMAT_NUMERO = DateTimeFormatter.ofPattern("ddMMyyyy");

    /** Construit l'identifiant fonctionnel JJMMYYYY#id à partir de la date de naissance et de l'id. */
    public static String genererNumero(LocalDate dateNaissance, Long id) {
        if (dateNaissance == null || id == null) return null;
        return dateNaissance.format(FORMAT_NUMERO) + "#" + id;
    }

    public MoutonDto create(MoutonDto dto) {
        Mouton mouton = toEntity(dto, new Mouton());
        mouton.setDateEntreeSysteme(LocalDate.now());
        mouton = moutonRepository.save(mouton);
        // Génération du numéro (nécessite l'id, donc après la première persistance)
        mouton.setNumeroIdentification(genererNumero(mouton.getDateNaissance(), mouton.getId()));
        return toDto(moutonRepository.save(mouton));
    }

    public MoutonDto update(Long id, MoutonDto dto) {
        Mouton mouton = getEntity(id);
        checkAccess(mouton);
        // Si la photo est explicitement retirée (dto.photo null alors qu'elle existait), on supprime le fichier
        String anciennePhoto = mouton.getPhoto();
        if (anciennePhoto != null && dto.getPhoto() == null) {
            fileStorageService.delete(anciennePhoto);
        }
        return toDto(moutonRepository.save(toEntity(dto, mouton)));
    }

    public void delete(Long id) {
        Mouton mouton = getEntity(id);
        checkAccess(mouton);
        if (mouton.getPhoto() != null) {
            fileStorageService.delete(mouton.getPhoto());
        }
        moutonRepository.delete(mouton);
    }

    public MoutonDto uploadPhoto(Long id, MultipartFile file) {
        Mouton mouton = getEntity(id);
        checkAccess(mouton);
        if (mouton.getPhoto() != null) {
            fileStorageService.delete(mouton.getPhoto());
        }
        String url = fileStorageService.store(file, "moutons");
        mouton.setPhoto(url);
        return toDto(moutonRepository.save(mouton));
    }

    @Transactional(readOnly = true)
    public List<MoutonDto> searchByNom(String nom) {
        Long eleveurId = eleveurFilterId();
        List<Mouton> resultat = (eleveurId != null)
                ? moutonRepository.findByEleveurIdAndNomContainingIgnoreCase(eleveurId, nom)
                : moutonRepository.findByNomContainingIgnoreCase(nom);
        return resultat.stream().map(this::toDto).toList();
    }

    @Transactional(readOnly = true)
    public List<MoutonDto> findBySexe(Sexe sexe) {
        Long eleveurId = eleveurFilterId();
        List<Mouton> resultat = (eleveurId != null)
                ? moutonRepository.findByEleveurIdAndSexe(eleveurId, sexe)
                : moutonRepository.findBySexe(sexe);
        return resultat.stream().map(this::toDto).toList();
    }

    @Transactional(readOnly = true)
    public List<MoutonDto> findByStatut(StatutMouton statut) {
        Long eleveurId = eleveurFilterId();
        List<Mouton> resultat = (eleveurId != null)
                ? moutonRepository.findByEleveurIdAndStatut(eleveurId, statut)
                : moutonRepository.findByStatut(statut);
        return resultat.stream().map(this::toDto).toList();
    }

    /**
     * Pool public de reproducteurs : mâles vivants explicitement désignés comme
     * reproducteurs (flag reproducteur=true), toutes bergeries confondues.
     */
    @Transactional(readOnly = true)
    public List<MoutonDto> listReproducteursMales() {
        return moutonRepository.findBySexe(Sexe.MALE).stream()
                .filter(m -> m.getStatut() == StatutMouton.VIVANT)
                .filter(m -> Boolean.TRUE.equals(m.getReproducteur()))
                .map(this::toDto)
                .toList();
    }

    /**
     * Pool public de femelles vivantes, toutes bergeries confondues.
     * Utilisé pour renseigner la mère d'un mouton dont la mère appartient
     * à une autre bergerie.
     */
    @Transactional(readOnly = true)
    public List<MoutonDto> listReproductricesFemelles() {
        return moutonRepository.findBySexe(Sexe.FEMELLE).stream()
                .filter(m -> m.getStatut() == StatutMouton.VIVANT)
                .map(this::toDto)
                .toList();
    }

    @Transactional(readOnly = true)
    public Mouton getEntity(Long id) {
        return moutonRepository.findById(id).orElseThrow(() -> ResourceNotFoundException.of("Mouton", id));
    }

    /** Retourne l'id de l'éleveur connecté si filtrage nécessaire, sinon null (admin = pas de filtre). */
    private Long eleveurFilterId() {
        Utilisateur current = currentUser();
        if (current != null && current.getRole() == Role.ELEVEUR) {
            return current.getId();
        }
        return null;
    }

    /** Rejette l'accès si l'utilisateur connecté est un éleveur non-propriétaire du mouton. */
    public void checkAccess(Mouton mouton) {
        Utilisateur current = currentUser();
        if (current == null || current.getRole() != Role.ELEVEUR) return;
        Long proprio = mouton.getEleveur() != null ? mouton.getEleveur().getId() : null;
        if (!current.getId().equals(proprio)) {
            throw new AccessDeniedException("Ce mouton n'est pas rattaché à votre compte.");
        }
    }

    private Mouton toEntity(MoutonDto dto, Mouton mouton) {
        mouton.setNom(dto.getNom());
        mouton.setSexe(dto.getSexe());
        mouton.setDateNaissance(dto.getDateNaissance());
        mouton.setRace(dto.getRace());
        mouton.setCouleur(dto.getCouleur());
        mouton.setCaracteristiques(dto.getCaracteristiques());
        mouton.setPhoto(dto.getPhoto());
        mouton.setStatut(dto.getStatut());

        // Le flag reproducteur ne concerne que les mâles
        boolean flagReproducteur = dto.getReproducteur() != null && dto.getReproducteur();
        mouton.setReproducteur(dto.getSexe() == Sexe.MALE && flagReproducteur);

        mouton.setPere(resolveMouton(dto.getPereId(), Sexe.MALE, "père"));
        mouton.setMere(resolveMouton(dto.getMereId(), Sexe.FEMELLE, "mère"));
        mouton.setEleveur(resolveEleveur(dto.getEleveurId()));
        return mouton;
    }

    private Mouton resolveMouton(Long id, Sexe sexeAttendu, String label) {
        if (id == null) return null;
        Mouton parent = moutonRepository.findById(id)
                .orElseThrow(() -> ResourceNotFoundException.of("Mouton " + label, id));
        if (parent.getSexe() != sexeAttendu) {
            throw new IllegalArgumentException("Le " + label + " doit être de sexe " + sexeAttendu);
        }
        return parent;
    }

    private Utilisateur resolveEleveur(Long id) {
        Utilisateur current = currentUser();
        // Un éleveur ne peut rattacher un mouton qu'à lui-même
        if (current != null && current.getRole() == Role.ELEVEUR) {
            return current;
        }
        // ADMIN : doit obligatoirement choisir un éleveur (pas d'auto-rattachement)
        if (current != null && current.getRole() == Role.ADMIN) {
            if (id == null) {
                throw new IllegalArgumentException(
                        "L'administrateur doit choisir l'éleveur à rattacher.");
            }
            return utilisateurRepository.findById(id)
                    .orElseThrow(() -> ResourceNotFoundException.of("Utilisateur", id));
        }
        // Garde-fou : tout autre cas, on suit l'id fourni s'il existe
        if (id != null) {
            return utilisateurRepository.findById(id)
                    .orElseThrow(() -> ResourceNotFoundException.of("Utilisateur", id));
        }
        return current;
    }

    private Utilisateur currentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated()) return null;
        String email = auth.getName();
        if (email == null || "anonymousUser".equals(email)) return null;
        return utilisateurRepository.findByEmail(email).orElse(null);
    }

    private MoutonDto toDto(Mouton m) {
        return MoutonDto.builder()
                .id(m.getId())
                .numeroIdentification(m.getNumeroIdentification())
                .nom(m.getNom())
                .sexe(m.getSexe())
                .dateNaissance(m.getDateNaissance())
                .dateEntreeSysteme(m.getDateEntreeSysteme())
                .race(m.getRace())
                .couleur(m.getCouleur())
                .caracteristiques(m.getCaracteristiques())
                .photo(m.getPhoto())
                .statut(m.getStatut())
                .reproducteur(m.getReproducteur())
                .pereId(m.getPere() != null ? m.getPere().getId() : null)
                .pereNom(m.getPere() != null ? m.getPere().getNom() : null)
                .mereId(m.getMere() != null ? m.getMere().getId() : null)
                .mereNom(m.getMere() != null ? m.getMere().getNom() : null)
                .eleveurId(m.getEleveur() != null ? m.getEleveur().getId() : null)
                .eleveurNom(m.getEleveur() != null ? m.getEleveur().getNom() : null)
                .build();
    }
}
