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

import jakarta.enterprise.context.ApplicationScoped;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.Base64;
import java.util.Optional;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;

/**
 * Génération et validation des tokens signés pour partage KPI public (Sprint 17).
 *
 * <p>Format token : {@code base64url(orgId)} + {@code .} + {@code base64url(expiryEpochMs)}
 * + {@code .} + {@code base64url(HMAC-SHA256(orgId.expiryEpochMs))}.
 *
 * <p>Vérification : recalcule HMAC, compare en time-constant ({@link MessageDigest#isEqual}),
 * vérifie expiry. Aucune base de données — token autosuffisant et révocable uniquement
 * par rotation du secret.
 *
 * @since 2026-04-25 (Sprint 17)
 */
@ApplicationScoped
public class KpiShareTokenService {

  private static final Logger LOG = Logger.getLogger(KpiShareTokenService.class);
  private static final String HMAC_ALGO = "HmacSHA256";
  public static final long DEFAULT_TTL_SECONDS = 7 * 24 * 3600L; // 7 jours

  @ConfigProperty(name = "unionflow.kpi.share.secret",
      defaultValue = "CHANGE-ME-IN-PROD-32-CHARS-MIN-OK")
  String secret;

  /**
   * Génère un token signé pour partage KPI d'une organisation pendant {@code ttlSeconds}.
   * Si {@code ttlSeconds <= 0}, applique le défaut (7 jours).
   */
  public String generer(UUID organisationId, long ttlSeconds) {
    if (organisationId == null) throw new IllegalArgumentException("organisationId null");
    long ttl = ttlSeconds > 0 ? ttlSeconds : DEFAULT_TTL_SECONDS;
    long expiryMs = Instant.now().plusSeconds(ttl).toEpochMilli();

    String orgEncoded = encode(organisationId.toString());
    String expEncoded = encode(Long.toString(expiryMs));
    String payload = orgEncoded + "." + expEncoded;
    String signature = encode(hmac(payload));
    return payload + "." + signature;
  }

  /**
   * Vérifie un token. Retourne l'orgId si valide et non expiré.
   * Sinon {@link Optional#empty()}.
   */
  public Optional<UUID> verifier(String token) {
    if (token == null || token.isBlank()) return Optional.empty();
    String[] parts = token.split("\\.");
    if (parts.length != 3) {
      LOG.debugf("Token KPI invalide : nombre de parts = %d", parts.length);
      return Optional.empty();
    }
    String orgEncoded = parts[0];
    String expEncoded = parts[1];
    String signature = parts[2];

    // Vérifie HMAC en time-constant
    String expected = encode(hmac(orgEncoded + "." + expEncoded));
    byte[] expectedBytes = expected.getBytes(StandardCharsets.UTF_8);
    byte[] receivedBytes = signature.getBytes(StandardCharsets.UTF_8);
    if (!MessageDigest.isEqual(expectedBytes, receivedBytes)) {
      LOG.debugf("Token KPI signature invalide");
      return Optional.empty();
    }

    // Vérifie expiry
    long expiryMs;
    UUID orgId;
    try {
      expiryMs = Long.parseLong(decode(expEncoded));
      orgId = UUID.fromString(decode(orgEncoded));
    } catch (Exception e) {
      LOG.debugf("Token KPI payload corrompu : %s", e.getMessage());
      return Optional.empty();
    }
    if (Instant.now().toEpochMilli() > expiryMs) {
      LOG.debugf("Token KPI expiré (epochMs=%d, now=%d)", expiryMs, Instant.now().toEpochMilli());
      return Optional.empty();
    }

    return Optional.of(orgId);
  }

  // ── Helpers ────────────────────────────────────────────────────────────

  private byte[] hmac(String data) {
    try {
      Mac mac = Mac.getInstance(HMAC_ALGO);
      mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGO));
      return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
    } catch (Exception e) {
      throw new IllegalStateException("HMAC-SHA256 indisponible", e);
    }
  }

  static String encode(String s) {
    return Base64.getUrlEncoder().withoutPadding()
        .encodeToString(s.getBytes(StandardCharsets.UTF_8));
  }

  static String encode(byte[] bytes) {
    return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
  }

  static String decode(String s) {
    return new String(Base64.getUrlDecoder().decode(s), StandardCharsets.UTF_8);
  }
}
