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

import dev.lions.unionflow.server.entity.Devise;
import dev.lions.unionflow.server.entity.TauxChange;
import dev.lions.unionflow.server.repository.TauxChangeRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.jboss.logging.Logger;

/**
 * Service de conversion entre devises basé sur la table {@code taux_change}.
 *
 * <p>Stratégie de résolution :
 * <ol>
 *   <li>Cas trivial : devises identiques → identité</li>
 *   <li>Lookup taux exact (devise_source, devise_cible, date) — cache 1h</li>
 *   <li>Lookup taux exact inverse → 1/taux</li>
 *   <li>Pivot via XOF (chaîne) : source → XOF → cible</li>
 *   <li>Fallback : taux le plus récent ≤ date</li>
 *   <li>Sinon : exception</li>
 * </ol>
 *
 * <p>Pas d'appel HTTP automatique ici — la mise à jour des taux est faite par un job batch
 * dédié (à venir). Le seed V49 fournit les paires principales.
 *
 * @since 2026-04-25 (P2-NEW-7)
 */
@ApplicationScoped
public class DeviseConversionService {

  private static final Logger LOG = Logger.getLogger(DeviseConversionService.class);
  private static final Duration CACHE_TTL = Duration.ofHours(1);
  private static final int SCALE_RESULTAT = 4;

  @Inject TauxChangeRepository repository;

  /** Cache thread-safe : (source, cible, date) → (taux, timestamp). */
  private final ConcurrentHashMap<String, CachedTaux> cache = new ConcurrentHashMap<>();

  /** Convertit {@code montant} de {@code source} vers {@code cible} à la date donnée. */
  public BigDecimal convertir(BigDecimal montant, Devise source, Devise cible, LocalDate date) {
    if (montant == null) throw new IllegalArgumentException("montant null");
    if (source == null || cible == null) throw new IllegalArgumentException("devise null");
    if (source == cible) return montant;

    BigDecimal taux = trouverTaux(source, cible, date);
    return montant.multiply(taux).setScale(SCALE_RESULTAT, RoundingMode.HALF_UP);
  }

  /** Convertit à la date du jour. */
  public BigDecimal convertir(BigDecimal montant, Devise source, Devise cible) {
    return convertir(montant, source, cible, LocalDate.now());
  }

  /** Récupère le taux applicable, avec stratégie de fallback. */
  public BigDecimal trouverTaux(Devise source, Devise cible, LocalDate date) {
    if (source == cible) return BigDecimal.ONE;

    String cacheKey = source.name() + "->" + cible.name() + "@" + date;
    CachedTaux cached = cache.get(cacheKey);
    if (cached != null && !cached.expired()) {
      return cached.taux;
    }

    BigDecimal taux = resolveTaux(source, cible, date);
    cache.put(cacheKey, new CachedTaux(taux, Instant.now()));
    return taux;
  }

  private BigDecimal resolveTaux(Devise source, Devise cible, LocalDate date) {
    // 1. Direct exact
    Optional<TauxChange> direct = repository.trouverExact(source, cible, date);
    if (direct.isPresent()) return direct.get().getTaux();

    // 2. Inverse exact
    Optional<TauxChange> inverse = repository.trouverExact(cible, source, date);
    if (inverse.isPresent()) {
      return inverserTaux(inverse.get().getTaux(), cible, source, date);
    }

    // 3. Pivot via XOF
    if (source != Devise.XOF && cible != Devise.XOF) {
      try {
        BigDecimal sourceVersXof = trouverTauxSimple(source, Devise.XOF, date);
        BigDecimal xofVersCible = trouverTauxSimple(Devise.XOF, cible, date);
        return sourceVersXof.multiply(xofVersCible).setScale(8, RoundingMode.HALF_UP);
      } catch (TauxIntrouvableException ignored) {
        // Continue avec fallback
      }
    }

    // 4. Plus récent ≤ date
    Optional<TauxChange> recent = repository.trouverPlusRecent(source, cible, date);
    if (recent.isPresent()) {
      LOG.debugf("Taux %s→%s : utilisation du taux %s du %s (date demandée: %s)",
          source, cible, recent.get().getTaux(), recent.get().getDateValidite(), date);
      return recent.get().getTaux();
    }
    Optional<TauxChange> recentInverse = repository.trouverPlusRecent(cible, source, date);
    if (recentInverse.isPresent()) {
      return inverserTaux(recentInverse.get().getTaux(), cible, source, date);
    }

    throw new TauxIntrouvableException(
        "Aucun taux de change " + source + "→" + cible + " disponible (date: " + date + ")");
  }

  /** Recherche directe sans fallback (utilisée pour le pivot). */
  private BigDecimal trouverTauxSimple(Devise source, Devise cible, LocalDate date) {
    Optional<TauxChange> direct = repository.trouverExact(source, cible, date);
    if (direct.isPresent()) return direct.get().getTaux();

    Optional<TauxChange> inverse = repository.trouverExact(cible, source, date);
    if (inverse.isPresent()) {
      return inverserTaux(inverse.get().getTaux(), cible, source, date);
    }

    Optional<TauxChange> recent = repository.trouverPlusRecent(source, cible, date);
    if (recent.isPresent()) return recent.get().getTaux();

    Optional<TauxChange> recentInverse = repository.trouverPlusRecent(cible, source, date);
    if (recentInverse.isPresent()) {
      return inverserTaux(recentInverse.get().getTaux(), cible, source, date);
    }

    throw new TauxIntrouvableException(
        "Pivot impossible : " + source + "→" + cible + " (date " + date + ")");
  }

  /** Calcule 1/taux en levant TauxIntrouvableException si taux est zéro. */
  private BigDecimal inverserTaux(BigDecimal taux, Devise from, Devise to, LocalDate date) {
    if (taux.compareTo(BigDecimal.ZERO) == 0) {
      throw new TauxIntrouvableException(
          "Taux " + from + "→" + to + " est zéro (date: " + date + ")");
    }
    return BigDecimal.ONE.divide(taux, 8, RoundingMode.HALF_UP);
  }

  /** Vide le cache (utilisé après import batch de nouveaux taux). */
  public void invaliderCache() {
    cache.clear();
  }

  // ── DTOs internes ───────────────────────────────────────────────────────────

  private record CachedTaux(BigDecimal taux, Instant cachedAt) {
    boolean expired() {
      return Duration.between(cachedAt, Instant.now()).compareTo(CACHE_TTL) > 0;
    }
  }

  /** Levée quand aucun taux n'est résolvable. */
  public static class TauxIntrouvableException extends RuntimeException {
    public TauxIntrouvableException(String message) {
      super(message);
    }
  }
}
