package dev.lions.unionflow.server.service.mutuelle.credit;

import dev.lions.unionflow.server.api.dto.admin.request.CreateAuditLogRequest;
import dev.lions.unionflow.server.api.dto.mutuelle.credit.DemandeCreditRequest;
import dev.lions.unionflow.server.api.dto.mutuelle.credit.DemandeCreditResponse;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.StatutDemandeCredit;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.StatutEcheanceCredit;
import dev.lions.unionflow.server.api.enums.mutuelle.credit.TypeGarantie;
import dev.lions.unionflow.server.api.enums.mutuelle.epargne.TypeTransactionEpargne;
import dev.lions.unionflow.server.api.enums.membre.StatutKyc;
import dev.lions.unionflow.server.api.dto.mutuelle.epargne.TransactionEpargneRequest;
import dev.lions.unionflow.server.entity.Membre;
import dev.lions.unionflow.server.entity.mutuelle.credit.DemandeCredit;
import dev.lions.unionflow.server.entity.mutuelle.credit.EcheanceCredit;
import dev.lions.unionflow.server.entity.mutuelle.credit.GarantieDemande;
import dev.lions.unionflow.server.entity.mutuelle.epargne.CompteEpargne;
import dev.lions.unionflow.server.mapper.mutuelle.credit.DemandeCreditMapper;
import dev.lions.unionflow.server.mapper.mutuelle.credit.GarantieDemandeMapper;
import dev.lions.unionflow.server.repository.MembreRepository;
import dev.lions.unionflow.server.repository.mutuelle.credit.DemandeCreditRepository;
import dev.lions.unionflow.server.repository.mutuelle.epargne.CompteEpargneRepository;
import dev.lions.unionflow.server.service.mutuelle.epargne.TransactionEpargneService;
import dev.lions.unionflow.server.service.AuditService;

import java.math.BigDecimal;
import java.math.RoundingMode;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * Service métier pour la gestion des demandes de crédit.
 */
@ApplicationScoped
public class DemandeCreditService {

    @Inject
    DemandeCreditRepository demandeCreditRepository;

    @Inject
    MembreRepository membreRepository;

    @Inject
    CompteEpargneRepository compteEpargneRepository;

    @Inject
    DemandeCreditMapper demandeCreditMapper;

    @Inject
    GarantieDemandeMapper garantieDemandeMapper;

    @Inject
    TransactionEpargneService transactionEpargneService;

    @Inject
    AuditService auditService;

    /**
     * Soumet une nouvelle demande de crédit.
     *
     * @param request Le DTO de la demande.
     * @return Le DTO de la demande créée.
     */
    @Transactional
    public DemandeCreditResponse soumettreDemande(DemandeCreditRequest request) {
        Membre membre = membreRepository.findByIdOptional(UUID.fromString(request.getMembreId()))
                .orElseThrow(() -> new NotFoundException("Membre non trouvé avec l'ID: " + request.getMembreId()));

        // Vérification obligatoire de la conformité KYC
        verifierConformiteKyc(membre);

        DemandeCredit demande = demandeCreditMapper.toEntity(request);
        demande.setMembre(membre);

        if (request.getCompteLieId() != null && !request.getCompteLieId().isEmpty()) {
            CompteEpargne compte = compteEpargneRepository.findByIdOptional(UUID.fromString(request.getCompteLieId()))
                    .orElseThrow(() -> new NotFoundException(
                            "Compte épargne non trouvé avec l'ID: " + request.getCompteLieId()));
            demande.setCompteLie(compte);
        }

        // Initialisation des champs techniques
        demande.setStatut(StatutDemandeCredit.SOUMISE);
        demande.setDateSoumission(LocalDate.now());
        demande.setNumeroDossier("CRD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());

        // Traitement des garanties si présentes
        if (request.getGarantiesProposees() != null) {
            List<GarantieDemande> garanties = request.getGarantiesProposees().stream()
                    .map(dto -> {
                        GarantieDemande g = garantieDemandeMapper.toEntity(dto);
                        g.setDemandeCredit(demande);
                        return g;
                    })
                    .collect(Collectors.toList());
            demande.setGaranties(garanties);
        }

        demandeCreditRepository.persist(demande);
        return demandeCreditMapper.toDto(demande);
    }

    /**
     * Récupère une demande par son ID.
     *
     * @param id L'UUID de la demande.
     * @return Le DTO de la demande.
     */
    public DemandeCreditResponse getDemandeById(UUID id) {
        DemandeCredit demande = demandeCreditRepository.findByIdOptional(id)
                .orElseThrow(() -> new NotFoundException("Demande de crédit non trouvée avec l'ID: " + id));
        return demandeCreditMapper.toDto(demande);
    }

    /**
     * Liste les demandes d'un membre.
     *
     * @param membreId L'UUID du membre.
     * @return La liste des demandes.
     */
    public List<DemandeCreditResponse> getDemandesByMembre(UUID membreId) {
        return demandeCreditRepository.find("membre.id = ?1 and actif = true", membreId)
                .stream()
                .map(demandeCreditMapper::toDto)
                .collect(Collectors.toList());
    }

    /**
     * Met à jour le statut d'une demande (Approbation, Rejet, etc.).
     *
     * @param id     L'UUID de la demande.
     * @param statut Le nouveau statut.
     * @param notes  Les notes du comité.
     * @return Le DTO mis à jour.
     */
    @Transactional
    public DemandeCreditResponse changerStatut(UUID id, StatutDemandeCredit statut, String notes) {
        DemandeCredit demande = demandeCreditRepository.findByIdOptional(id)
                .orElseThrow(() -> new NotFoundException("Demande de crédit non trouvée avec l'ID: " + id));

        demande.setStatut(statut);
        demande.setNotesComite(notes);

        if (statut == StatutDemandeCredit.APPROUVEE) {
            demande.setDateValidation(LocalDate.now());

            // Si aucune valeur approuvée n'est fixée, on prend les valeurs demandées par
            // défaut
            if (demande.getMontantApprouve() == null) {
                demande.setMontantApprouve(demande.getMontantDemande());
            }
            if (demande.getDureeMoisApprouvee() == null) {
                demande.setDureeMoisApprouvee(demande.getDureeMoisDemande());
            }

            // Gérer les retenues de garantie si compte d'épargne lié et garantie nantie
            if (demande.getCompteLie() != null) {
                demande.getGaranties().stream()
                        .filter(g -> g.getTypeGarantie() == TypeGarantie.EPARGNE_BLOQUEE)
                        .forEach(g -> {
                            TransactionEpargneRequest holdRequest = TransactionEpargneRequest.builder()
                                    .compteId(demande.getCompteLie().getId().toString())
                                    .typeTransaction(TypeTransactionEpargne.RETENUE_GARANTIE)
                                    .montant(g.getValeurEstimee())
                                    .motif("Garantie pour crédit n° " + demande.getNumeroDossier())
                                    .build();
                            transactionEpargneService.executerTransaction(holdRequest);
                        });
            }
        }

        return demandeCreditMapper.toDto(demande);
    }

    /**
     * Approuve officiellement une demande avec les conditions définitives.
     */
    @Transactional
    public DemandeCreditResponse approuver(UUID id, BigDecimal montant, Integer duree, BigDecimal taux, String notes) {
        DemandeCredit demande = demandeCreditRepository.findByIdOptional(id)
                .orElseThrow(() -> new NotFoundException("Demande de crédit non trouvée avec l'ID: " + id));

        demande.setMontantApprouve(montant);
        demande.setDureeMoisApprouvee(duree);
        demande.setTauxInteretAnnuel(taux);

        return changerStatut(id, StatutDemandeCredit.APPROUVEE, notes);
    }

    /**
     * Effectue le décaissement des fonds sur le compte d'épargne et génère
     * l'échéancier.
     */
    @Transactional
    public DemandeCreditResponse decaisser(UUID id, LocalDate datePremierEcheance) {
        DemandeCredit demande = demandeCreditRepository.findByIdOptional(id)
                .orElseThrow(() -> new NotFoundException("Demande de crédit non trouvée avec l'ID: " + id));

        if (demande.getStatut() != StatutDemandeCredit.APPROUVEE) {
            throw new IllegalStateException("Le crédit doit être au statut APPROUVEE pour être décaissé.");
        }

        // Vérification de sécurité : KYC toujours valide au moment du décaissement
        verifierConformiteKyc(demande.getMembre());

        if (demande.getCompteLie() == null) {
            throw new IllegalStateException("Un compte d'épargne lié est requis pour le décaissement.");
        }

        // 1. Mise à jour du statut
        demande.setStatut(StatutDemandeCredit.DECAISSEE);
        demande.setDatePremierEcheance(datePremierEcheance);

        // 2. Virement des fonds sur le compte d'épargne
        TransactionEpargneRequest creditRequest = TransactionEpargneRequest.builder()
                .compteId(demande.getCompteLie().getId().toString())
                .typeTransaction(TypeTransactionEpargne.DEPOT)
                .montant(demande.getMontantApprouve())
                .motif("Déblocage des fonds - Crédit n° " + demande.getNumeroDossier())
                .build();
        transactionEpargneService.executerTransaction(creditRequest);

        // 3. Génération de l'échéancier (Amortissement à annuités constantes)
        genererEcheancier(demande);

        return demandeCreditMapper.toDto(demande);
    }

    /**
     * Vérifie la conformité KYC du membre avant toute opération de crédit.
     *
     * @param membre Le membre à vérifier
     * @throws IllegalStateException Si le KYC n'est pas conforme
     */
    private void verifierConformiteKyc(Membre membre) {
        // Vérification 1 : Statut KYC doit être VERIFIE
        if (membre.getStatutKyc() == null || !StatutKyc.VERIFIE.name().equals(membre.getStatutKyc())) {
            auditService.enregistrerLog(new CreateAuditLogRequest(
                "CREDIT_KYC_REFUS",
                "WARNING",
                "system",
                null,
                "MUTUELLE_CREDIT",
                "Tentative de crédit refusée : KYC non vérifié",
                String.format("Statut KYC actuel: %s (requis: VERIFIE)", membre.getStatutKyc()),
                null,
                null,
                null,
                LocalDateTime.now(),
                null,
                null,
                membre.getId().toString(),
                "Membre"
            ));
            throw new IllegalStateException(
                "Votre demande de crédit ne peut être traitée. Votre statut KYC doit être vérifié. " +
                "Veuillez contacter l'administration pour mettre à jour vos informations d'identification."
            );
        }

        // Vérification 2 : Date de vérification d'identité doit être présente
        if (membre.getDateVerificationIdentite() == null) {
            auditService.enregistrerLog(new CreateAuditLogRequest(
                "CREDIT_KYC_REFUS",
                "WARNING",
                "system",
                null,
                "MUTUELLE_CREDIT",
                "Tentative de crédit refusée : Date de vérification d'identité absente",
                "Date de vérification non renseignée",
                null,
                null,
                null,
                LocalDateTime.now(),
                null,
                null,
                membre.getId().toString(),
                "Membre"
            ));
            throw new IllegalStateException(
                "Votre demande de crédit ne peut être traitée. Votre identité n'a pas été vérifiée. " +
                "Veuillez vous présenter avec vos pièces d'identité pour finaliser votre dossier KYC."
            );
        }

        // Vérification 3 : La vérification d'identité ne doit pas être expirée (> 1 an)
        LocalDate dateVerification = membre.getDateVerificationIdentite();
        LocalDate dateExpiration = dateVerification.plusYears(1);

        if (LocalDate.now().isAfter(dateExpiration)) {
            auditService.enregistrerLog(new CreateAuditLogRequest(
                "CREDIT_KYC_REFUS",
                "WARNING",
                "system",
                null,
                "MUTUELLE_CREDIT",
                "Tentative de crédit refusée : Vérification d'identité expirée",
                String.format("Date de vérification: %s, Date expiration: %s", dateVerification, dateExpiration),
                null,
                null,
                null,
                LocalDateTime.now(),
                null,
                null,
                membre.getId().toString(),
                "Membre"
            ));
            throw new IllegalStateException(
                String.format(
                    "Votre demande de crédit ne peut être traitée. Votre vérification d'identité a expiré le %s. " +
                    "Une nouvelle vérification est requise. Veuillez contacter l'administration.",
                    dateExpiration
                )
            );
        }

        // Audit positif : KYC conforme
        auditService.enregistrerLog(new CreateAuditLogRequest(
            "CREDIT_KYC_OK",
            "INFO",
            "system",
            null,
            "MUTUELLE_CREDIT",
            "Vérification KYC réussie pour demande de crédit",
            String.format("Statut: %s, Date vérification: %s", membre.getStatutKyc(), dateVerification),
            null,
            null,
            null,
            LocalDateTime.now(),
            null,
            null,
            membre.getId().toString(),
            "Membre"
        ));
    }

    private void genererEcheancier(DemandeCredit demande) {
        BigDecimal capital = demande.getMontantApprouve();
        int n = demande.getDureeMoisApprouvee();
        BigDecimal tauxAnnuel = demande.getTauxInteretAnnuel().divide(new BigDecimal("100"), 10, RoundingMode.HALF_UP);
        BigDecimal tauxMensuel = tauxAnnuel.divide(new BigDecimal("12"), 10, RoundingMode.HALF_UP);

        // Calcul de la mensualité constante : M = P * r / (1 - (1+r)^-n)
        BigDecimal mensualite;
        if (tauxMensuel.compareTo(BigDecimal.ZERO) == 0) {
            mensualite = capital.divide(new BigDecimal(n), 2, RoundingMode.HALF_UP);
        } else {
            double r = tauxMensuel.doubleValue();
            double factor = Math.pow(1 + r, -n);
            mensualite = capital.multiply(new BigDecimal(r))
                    .divide(BigDecimal.valueOf(1 - factor), 2, RoundingMode.HALF_UP);
        }

        BigDecimal capitalRestant = capital;
        LocalDate dateEcheance = demande.getDatePremierEcheance();
        BigDecimal totalInterets = BigDecimal.ZERO;

        for (int i = 1; i <= n; i++) {
            BigDecimal interets = capitalRestant.multiply(tauxMensuel).setScale(2, RoundingMode.HALF_UP);
            BigDecimal principal = mensualite.subtract(interets);

            // Ajustement pour la dernière échéance
            if (i == n) {
                principal = capitalRestant;
                mensualite = principal.add(interets);
            }

            capitalRestant = capitalRestant.subtract(principal);
            totalInterets = totalInterets.add(interets);

            EcheanceCredit echeance = EcheanceCredit.builder()
                    .demandeCredit(demande)
                    .ordre(i)
                    .dateEcheancePrevue(dateEcheance)
                    .capitalAmorti(principal)
                    .interetsDeLaPeriode(interets)
                    .montantTotalExigible(mensualite)
                    .capitalRestantDu(capitalRestant.max(BigDecimal.ZERO))
                    .statut(StatutEcheanceCredit.A_VENIR)
                    .build();

            demande.getEcheancier().add(echeance);
            dateEcheance = dateEcheance.plusMonths(1);
        }

        demande.setCoutTotalCredit(totalInterets);
    }
}
