package dev.lions.unionflow.server.service;

import dev.lions.unionflow.server.api.dto.comptabilite.request.*;
import dev.lions.unionflow.server.api.dto.comptabilite.response.*;
import dev.lions.unionflow.server.api.enums.comptabilite.TypeJournalComptable;
import dev.lions.unionflow.server.entity.*;
import dev.lions.unionflow.server.entity.mutuelle.epargne.TransactionEpargne;
import dev.lions.unionflow.server.repository.*;
import dev.lions.unionflow.server.service.KeycloakService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import org.jboss.logging.Logger;

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

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

  @Inject
  CompteComptableRepository compteComptableRepository;

  @Inject
  JournalComptableRepository journalComptableRepository;

  @Inject
  EcritureComptableRepository ecritureComptableRepository;

  @Inject
  LigneEcritureRepository ligneEcritureRepository;

  @Inject
  OrganisationRepository organisationRepository;

  @Inject
  PaiementRepository paiementRepository;

  @Inject
  KeycloakService keycloakService;

  // ========================================
  // COMPTES COMPTABLES
  // ========================================

  /**
   * Crée un nouveau compte comptable
   *
   * @param compteDTO DTO du compte à créer
   * @return DTO du compte créé
   */
  @Transactional
  public CompteComptableResponse creerCompteComptable(CreateCompteComptableRequest request) {
    LOG.infof("Création d'un nouveau compte comptable: %s", request.numeroCompte());

    // Vérifier l'unicité du numéro
    if (compteComptableRepository.findByNumeroCompte(request.numeroCompte()).isPresent()) {
      throw new IllegalArgumentException("Un compte avec ce numéro existe déjà: " + request.numeroCompte());
    }

    CompteComptable compte = convertToEntity(request);
    compte.setCreePar(keycloakService.getCurrentUserEmail());

    compteComptableRepository.persist(compte);
    LOG.infof("Compte comptable créé avec succès: ID=%s, Numéro=%s", compte.getId(), compte.getNumeroCompte());

    return convertToResponse(compte);
  }

  /**
   * Trouve un compte comptable par son ID
   *
   * @param id ID du compte
   * @return DTO du compte
   */
  public CompteComptableResponse trouverCompteParId(UUID id) {
    return compteComptableRepository
        .findCompteComptableById(id)
        .map(this::convertToResponse)
        .orElseThrow(() -> new NotFoundException("Compte comptable non trouvé avec l'ID: " + id));
  }

  /**
   * Liste tous les comptes comptables actifs
   *
   * @return Liste des comptes
   */
  public List<CompteComptableResponse> listerTousLesComptes() {
    return compteComptableRepository.findAllActifs().stream()
        .map(this::convertToResponse)
        .collect(Collectors.toList());
  }

  // ========================================
  // JOURNAUX COMPTABLES
  // ========================================

  /**
   * Crée un nouveau journal comptable
   *
   * @param journalDTO DTO du journal à créer
   * @return DTO du journal créé
   */
  @Transactional
  public JournalComptableResponse creerJournalComptable(CreateJournalComptableRequest request) {
    LOG.infof("Création d'un nouveau journal comptable: %s", request.code());

    // Vérifier l'unicité du code
    if (journalComptableRepository.findByCode(request.code()).isPresent()) {
      throw new IllegalArgumentException("Un journal avec ce code existe déjà: " + request.code());
    }

    JournalComptable journal = convertToEntity(request);
    journal.setCreePar(keycloakService.getCurrentUserEmail());

    journalComptableRepository.persist(journal);
    LOG.infof("Journal comptable créé avec succès: ID=%s, Code=%s", journal.getId(), journal.getCode());

    return convertToResponse(journal);
  }

  /**
   * Trouve un journal comptable par son ID
   *
   * @param id ID du journal
   * @return DTO du journal
   */
  public JournalComptableResponse trouverJournalParId(UUID id) {
    return journalComptableRepository
        .findJournalComptableById(id)
        .map(this::convertToResponse)
        .orElseThrow(() -> new NotFoundException("Journal comptable non trouvé avec l'ID: " + id));
  }

  /**
   * Liste tous les journaux comptables actifs
   *
   * @return Liste des journaux
   */
  public List<JournalComptableResponse> listerTousLesJournaux() {
    return journalComptableRepository.findAllActifs().stream()
        .map(this::convertToResponse)
        .collect(Collectors.toList());
  }

  // ========================================
  // ÉCRITURES COMPTABLES
  // ========================================

  /**
   * Crée une nouvelle écriture comptable avec validation de l'équilibre
   *
   * @param ecritureDTO DTO de l'écriture à créer
   * @return DTO de l'écriture créée
   */
  @Transactional
  public EcritureComptableResponse creerEcritureComptable(CreateEcritureComptableRequest request) {
    LOG.infof("Création d'une nouvelle écriture comptable: %s", request.numeroPiece());

    // Vérifier l'équilibre
    if (!isEcritureEquilibree(request)) {
      throw new IllegalArgumentException("L'écriture n'est pas équilibrée (Débit ≠ Crédit)");
    }

    EcritureComptable ecriture = convertToEntity(request);
    ecriture.setCreePar(keycloakService.getCurrentUserEmail());

    // Calculer les totaux
    ecriture.calculerTotaux();

    ecritureComptableRepository.persist(ecriture);
    LOG.infof("Écriture comptable créée avec succès: ID=%s, Numéro=%s", ecriture.getId(), ecriture.getNumeroPiece());

    return convertToResponse(ecriture);
  }

  /**
   * Trouve une écriture comptable par son ID
   *
   * @param id ID de l'écriture
   * @return DTO de l'écriture
   */
  public EcritureComptableResponse trouverEcritureParId(UUID id) {
    return ecritureComptableRepository
        .findEcritureComptableById(id)
        .map(this::convertToResponse)
        .orElseThrow(() -> new NotFoundException("Écriture comptable non trouvée avec l'ID: " + id));
  }

  /**
   * Liste les écritures d'un journal
   *
   * @param journalId ID du journal
   * @return Liste des écritures
   */
  public List<EcritureComptableResponse> listerEcrituresParJournal(UUID journalId) {
    return ecritureComptableRepository.findByJournalId(journalId).stream()
        .map(this::convertToResponse)
        .collect(Collectors.toList());
  }

  /**
   * Liste les écritures d'une organisation
   *
   * @param organisationId ID de l'organisation
   * @return Liste des écritures
   */
  public List<EcritureComptableResponse> listerEcrituresParOrganisation(UUID organisationId) {
    return ecritureComptableRepository.findByOrganisationId(organisationId).stream()
        .map(this::convertToResponse)
        .collect(Collectors.toList());
  }

  // ========================================
  // MÉTHODES SYSCOHADA — Génération automatique d'écritures depuis les opérations métier
  // Débit/Crédit selon les règles SYSCOHADA révisé (UEMOA, applicable depuis 2018)
  // ========================================

  /**
   * Génère l'écriture comptable SYSCOHADA pour une cotisation payée.
   * Schéma : Débit 5121xx (trésorerie provider) ; Crédit 706100 (cotisations ordinaires).
   * Appeler depuis CotisationService.marquerPaye() après confirmation du paiement.
   */
  @Transactional
  public EcritureComptable enregistrerCotisation(Cotisation cotisation) {
    if (cotisation == null || cotisation.getOrganisation() == null) {
      LOG.warn("enregistrerCotisation : cotisation ou organisation null — écriture ignorée");
      return null;
    }

    UUID orgId = cotisation.getOrganisation().getId();
    BigDecimal montant = cotisation.getMontantPaye();
    if (montant == null || montant.compareTo(BigDecimal.ZERO) == 0) {
      return null;
    }

    // Choix du compte de trésorerie selon le provider (Wave par défaut)
    String numeroTresorerie = resolveCompteTresorerie(cotisation.getCodeDevise());
    CompteComptable compteTresorerie = compteComptableRepository
        .findByOrganisationAndNumero(orgId, numeroTresorerie)
        .orElseGet(() -> compteComptableRepository.findByOrganisationAndNumero(orgId, "512500").orElse(null));

    // Compte produit cotisations ordinaires
    String numeroCompteType = "ORDINAIRE".equals(cotisation.getTypeCotisation()) ? "706100" : "706200";
    CompteComptable compteProduit = compteComptableRepository
        .findByOrganisationAndNumero(orgId, numeroCompteType)
        .orElseGet(() -> compteComptableRepository.findByOrganisationAndNumero(orgId, "706100").orElse(null));

    if (compteTresorerie == null || compteProduit == null) {
      LOG.warnf("Comptes SYSCOHADA manquants pour org %s — plan comptable non initialisé ?", orgId);
      return null;
    }

    JournalComptable journal = journalComptableRepository
        .findByOrganisationAndType(orgId, TypeJournalComptable.VENTES)
        .orElse(null);
    if (journal == null) {
      LOG.warnf("Journal VENTES absent pour org %s — écriture ignorée", orgId);
      return null;
    }

    EcritureComptable ecriture = construireEcriture(
        journal,
        cotisation.getOrganisation(),
        LocalDate.now(),
        String.format("Cotisation %s - %s", cotisation.getTypeCotisation(), cotisation.getNumeroReference()),
        cotisation.getNumeroReference(),
        montant,
        compteTresorerie,
        compteProduit
    );

    ecritureComptableRepository.persist(ecriture);
    LOG.infof("Écriture SYSCOHADA cotisation créée : %s | montant %s XOF", ecriture.getNumeroPiece(), montant);
    return ecriture;
  }

  /**
   * Génère l'écriture SYSCOHADA pour un dépôt épargne.
   * Schéma : Débit 5121xx (trésorerie) ; Crédit 421000 (dette mutuelle envers membre).
   */
  @Transactional
  public EcritureComptable enregistrerDepotEpargne(TransactionEpargne transaction, Organisation organisation) {
    if (transaction == null || organisation == null) return null;

    UUID orgId = organisation.getId();
    BigDecimal montant = transaction.getMontant();
    if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) return null;

    CompteComptable compteTresorerie = compteComptableRepository
        .findByOrganisationAndNumero(orgId, "512100")
        .orElseGet(() -> compteComptableRepository.findByOrganisationAndNumero(orgId, "512500").orElse(null));

    CompteComptable compteEpargne = compteComptableRepository
        .findByOrganisationAndNumero(orgId, "421000").orElse(null);

    if (compteTresorerie == null || compteEpargne == null) return null;

    JournalComptable journal = journalComptableRepository
        .findByOrganisationAndType(orgId, TypeJournalComptable.BANQUE)
        .orElse(null);
    if (journal == null) return null;

    EcritureComptable ecriture = construireEcriture(
        journal, organisation, LocalDate.now(),
        "Dépôt épargne - " + (transaction.getReferenceExterne() != null ? transaction.getReferenceExterne() : ""),
        transaction.getReferenceExterne(),
        montant, compteTresorerie, compteEpargne
    );

    ecritureComptableRepository.persist(ecriture);
    LOG.infof("Écriture SYSCOHADA dépôt épargne : %s | %s XOF", ecriture.getNumeroPiece(), montant);
    return ecriture;
  }

  /**
   * Génère l'écriture SYSCOHADA pour un retrait épargne.
   * Schéma : Débit 421000 (dette mutuelle) ; Crédit 5121xx (trésorerie sortante).
   */
  @Transactional
  public EcritureComptable enregistrerRetraitEpargne(TransactionEpargne transaction, Organisation organisation) {
    if (transaction == null || organisation == null) return null;

    UUID orgId = organisation.getId();
    BigDecimal montant = transaction.getMontant();
    if (montant == null || montant.compareTo(BigDecimal.ZERO) <= 0) return null;

    CompteComptable compteEpargne = compteComptableRepository
        .findByOrganisationAndNumero(orgId, "421000").orElse(null);

    CompteComptable compteTresorerie = compteComptableRepository
        .findByOrganisationAndNumero(orgId, "512100")
        .orElseGet(() -> compteComptableRepository.findByOrganisationAndNumero(orgId, "512500").orElse(null));

    if (compteEpargne == null || compteTresorerie == null) return null;

    JournalComptable journal = journalComptableRepository
        .findByOrganisationAndType(orgId, TypeJournalComptable.BANQUE)
        .orElse(null);
    if (journal == null) return null;

    // Retrait : débit = 421000 (dette diminue), crédit = 512xxx (cash sort)
    EcritureComptable ecriture = construireEcriture(
        journal, organisation, LocalDate.now(),
        "Retrait épargne - " + (transaction.getReferenceExterne() != null ? transaction.getReferenceExterne() : ""),
        transaction.getReferenceExterne(),
        montant, compteEpargne, compteTresorerie
    );

    ecritureComptableRepository.persist(ecriture);
    return ecriture;
  }

  // ========================================
  // MÉTHODES PRIVÉES - HELPERS SYSCOHADA
  // ========================================

  /**
   * Détermine le compte de trésorerie selon le code devise / provider.
   * Par défaut 512100 (Wave) pour XOF en UEMOA.
   */
  private String resolveCompteTresorerie(String codeDevise) {
    // Pour l'instant Wave = 512100 par défaut. Sera enrichi avec multi-provider P1.3.
    return "512100";
  }

  /**
   * Construit une écriture comptable à 2 lignes (débit/crédit) équilibrée.
   */
  private EcritureComptable construireEcriture(
      JournalComptable journal,
      Organisation organisation,
      LocalDate date,
      String libelle,
      String reference,
      BigDecimal montant,
      CompteComptable compteDebit,
      CompteComptable compteCredit) {

    LigneEcriture ligneDebit = new LigneEcriture();
    ligneDebit.setNumeroLigne(1);
    ligneDebit.setCompteComptable(compteDebit);
    ligneDebit.setMontantDebit(montant);
    ligneDebit.setMontantCredit(BigDecimal.ZERO);
    ligneDebit.setLibelle(libelle);
    ligneDebit.setReference(reference);

    LigneEcriture ligneCredit = new LigneEcriture();
    ligneCredit.setNumeroLigne(2);
    ligneCredit.setCompteComptable(compteCredit);
    ligneCredit.setMontantDebit(BigDecimal.ZERO);
    ligneCredit.setMontantCredit(montant);
    ligneCredit.setLibelle(libelle);
    ligneCredit.setReference(reference);

    EcritureComptable ecriture = EcritureComptable.builder()
        .journal(journal)
        .organisation(organisation)
        .dateEcriture(date)
        .libelle(libelle)
        .reference(reference)
        .montantDebit(montant)
        .montantCredit(montant)
        .pointe(false)
        .build();

    ecriture.getLignes().add(ligneDebit);
    ecriture.getLignes().add(ligneCredit);
    ligneDebit.setEcriture(ecriture);
    ligneCredit.setEcriture(ecriture);

    return ecriture;
  }

  // ========================================
  // MÉTHODES PRIVÉES - CONVERSIONS
  // ========================================

  /** Vérifie si une écriture est équilibrée */
  private boolean isEcritureEquilibree(CreateEcritureComptableRequest request) {
    if (request.lignes() == null || request.lignes().isEmpty()) {
      return false;
    }

    BigDecimal totalDebit = request.lignes().stream()
        .map(CreateLigneEcritureRequest::montantDebit)
        .filter(amount -> amount != null)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

    BigDecimal totalCredit = request.lignes().stream()
        .map(CreateLigneEcritureRequest::montantCredit)
        .filter(amount -> amount != null)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

    return totalDebit.compareTo(totalCredit) == 0;
  }

  /** Convertit une entité CompteComptable en DTO */
  private CompteComptableResponse convertToResponse(CompteComptable compte) {
    if (compte == null) {
      return null;
    }

    CompteComptableResponse dto = new CompteComptableResponse();
    dto.setId(compte.getId());
    dto.setNumeroCompte(compte.getNumeroCompte());
    dto.setLibelle(compte.getLibelle());
    dto.setTypeCompte(compte.getTypeCompte());
    dto.setClasseComptable(compte.getClasseComptable());
    dto.setSoldeInitial(compte.getSoldeInitial());
    dto.setSoldeActuel(compte.getSoldeActuel());
    dto.setCompteCollectif(compte.getCompteCollectif());
    dto.setCompteAnalytique(compte.getCompteAnalytique());
    dto.setDescription(compte.getDescription());
    dto.setDateCreation(compte.getDateCreation());
    dto.setDateModification(compte.getDateModification());
    dto.setActif(compte.getActif());

    return dto;
  }

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

    CompteComptable compte = new CompteComptable();
    compte.setNumeroCompte(dto.numeroCompte());
    compte.setLibelle(dto.libelle());
    compte.setTypeCompte(dto.typeCompte());
    compte.setClasseComptable(dto.classeComptable());
    compte.setSoldeInitial(dto.soldeInitial() != null ? dto.soldeInitial() : BigDecimal.ZERO);
    compte.setSoldeActuel(dto.soldeActuel() != null ? dto.soldeActuel() : dto.soldeInitial());
    compte.setCompteCollectif(dto.compteCollectif() != null ? dto.compteCollectif() : false);
    compte.setCompteAnalytique(dto.compteAnalytique() != null ? dto.compteAnalytique() : false);
    compte.setDescription(dto.description());

    return compte;
  }

  /** Convertit une entité JournalComptable en DTO */
  private JournalComptableResponse convertToResponse(JournalComptable journal) {
    if (journal == null) {
      return null;
    }

    JournalComptableResponse dto = new JournalComptableResponse();
    dto.setId(journal.getId());
    dto.setCode(journal.getCode());
    dto.setLibelle(journal.getLibelle());
    dto.setTypeJournal(journal.getTypeJournal());
    dto.setDateDebut(journal.getDateDebut());
    dto.setDateFin(journal.getDateFin());
    dto.setStatut(journal.getStatut());
    dto.setDescription(journal.getDescription());
    dto.setDateCreation(journal.getDateCreation());
    dto.setDateModification(journal.getDateModification());
    dto.setActif(journal.getActif());

    return dto;
  }

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

    JournalComptable journal = new JournalComptable();
    journal.setCode(dto.code());
    journal.setLibelle(dto.libelle());
    journal.setTypeJournal(dto.typeJournal());
    journal.setDateDebut(dto.dateDebut());
    journal.setDateFin(dto.dateFin());
    journal.setStatut(dto.statut() != null ? dto.statut() : "OUVERT");
    journal.setDescription(dto.description());

    return journal;
  }

  /** Convertit une entité EcritureComptable en DTO */
  private EcritureComptableResponse convertToResponse(EcritureComptable ecriture) {
    if (ecriture == null) {
      return null;
    }

    EcritureComptableResponse dto = new EcritureComptableResponse();
    dto.setId(ecriture.getId());
    dto.setNumeroPiece(ecriture.getNumeroPiece());
    dto.setDateEcriture(ecriture.getDateEcriture());
    dto.setLibelle(ecriture.getLibelle());
    dto.setReference(ecriture.getReference());
    dto.setLettrage(ecriture.getLettrage());
    dto.setPointe(ecriture.getPointe());
    dto.setMontantDebit(ecriture.getMontantDebit());
    dto.setMontantCredit(ecriture.getMontantCredit());
    dto.setCommentaire(ecriture.getCommentaire());

    if (ecriture.getJournal() != null) {
      dto.setJournalId(ecriture.getJournal().getId());
    }
    if (ecriture.getOrganisation() != null) {
      dto.setOrganisationId(ecriture.getOrganisation().getId());
    }
    if (ecriture.getPaiement() != null) {
      dto.setPaiementId(ecriture.getPaiement().getId());
    }

    // Convertir les lignes
    if (ecriture.getLignes() != null) {
      dto.setLignes(
          ecriture.getLignes().stream().map(this::convertToResponse).collect(Collectors.toList()));
    }

    dto.setDateCreation(ecriture.getDateCreation());
    dto.setDateModification(ecriture.getDateModification());
    dto.setActif(ecriture.getActif());

    return dto;
  }

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

    EcritureComptable ecriture = new EcritureComptable();
    ecriture.setNumeroPiece(dto.numeroPiece());
    ecriture.setDateEcriture(dto.dateEcriture() != null ? dto.dateEcriture() : LocalDate.now());
    ecriture.setLibelle(dto.libelle());
    ecriture.setReference(dto.reference());
    ecriture.setLettrage(dto.lettrage());
    ecriture.setPointe(dto.pointe() != null ? dto.pointe() : false);
    ecriture.setCommentaire(dto.commentaire());

    // Relations
    if (dto.journalId() != null) {
      JournalComptable journal = journalComptableRepository
          .findJournalComptableById(dto.journalId())
          .orElseThrow(
              () -> new NotFoundException("Journal comptable non trouvé avec l'ID: " + dto.journalId()));
      ecriture.setJournal(journal);
    }

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

    if (dto.paiementId() != null) {
      Paiement paiement = paiementRepository
          .findPaiementById(dto.paiementId())
          .orElseThrow(
              () -> new NotFoundException("Paiement non trouvé avec l'ID: " + dto.paiementId()));
      ecriture.setPaiement(paiement);
    }

    // Convertir les lignes
    if (dto.lignes() != null) {
      for (CreateLigneEcritureRequest ligneDTO : dto.lignes()) {
        LigneEcriture ligne = convertToEntity(ligneDTO);
        ligne.setEcriture(ecriture);
        ecriture.getLignes().add(ligne);
      }
    }

    return ecriture;
  }

  /** Convertit une entité LigneEcriture en DTO */
  private LigneEcritureResponse convertToResponse(LigneEcriture ligne) {
    if (ligne == null) {
      return null;
    }

    LigneEcritureResponse dto = new LigneEcritureResponse();
    dto.setId(ligne.getId());
    dto.setNumeroLigne(ligne.getNumeroLigne());
    dto.setMontantDebit(ligne.getMontantDebit());
    dto.setMontantCredit(ligne.getMontantCredit());
    dto.setLibelle(ligne.getLibelle());
    dto.setReference(ligne.getReference());

    if (ligne.getEcriture() != null) {
      dto.setEcritureId(ligne.getEcriture().getId());
    }
    if (ligne.getCompteComptable() != null) {
      dto.setCompteComptableId(ligne.getCompteComptable().getId());
    }

    dto.setDateCreation(ligne.getDateCreation());
    dto.setDateModification(ligne.getDateModification());
    dto.setActif(ligne.getActif());

    return dto;
  }

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

    LigneEcriture ligne = new LigneEcriture();
    ligne.setNumeroLigne(dto.numeroLigne());
    ligne.setMontantDebit(dto.montantDebit() != null ? dto.montantDebit() : BigDecimal.ZERO);
    ligne.setMontantCredit(dto.montantCredit() != null ? dto.montantCredit() : BigDecimal.ZERO);
    ligne.setLibelle(dto.libelle());
    ligne.setReference(dto.reference());

    // Relation CompteComptable
    if (dto.compteComptableId() != null) {
      CompteComptable compte = compteComptableRepository
          .findCompteComptableById(dto.compteComptableId())
          .orElseThrow(
              () -> new NotFoundException(
                  "Compte comptable non trouvé avec l'ID: " + dto.compteComptableId()));
      ligne.setCompteComptable(compte);
    }

    return ligne;
  }
}
