package dev.lions.unionflow.server.payment.pispi;

import dev.lions.unionflow.server.api.payment.PaymentException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.io.StringReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

/**
 * Authentification PI-SPI à 3 facteurs : OAuth2 + mTLS + API Key.
 *
 * <p>Conforme à la spec sandbox developer.pispi.bceao.int (vérifiée 2026-04-25). Les 3 facteurs
 * sont systématiquement présents sur tous les appels API Business :
 *
 * <ul>
 *   <li><strong>OAuth2 client_credentials</strong> — clientId + clientSecret pour récupérer un
 *       Bearer token, mis en cache jusqu'à expiration ({@code expires_in - 60s}).
 *   <li><strong>mTLS (mutual TLS)</strong> — certificat client (PKCS12) présenté pendant la
 *       handshake TLS. Configuré via {@link SSLContext} sur le {@link HttpClient}.
 *   <li><strong>API Key</strong> — header {@code X-API-Key} ajouté sur chaque requête (géré par
 *       {@link PispiClient}, exposé par {@link #getApiKey()}).
 * </ul>
 *
 * <p>Configuration ({@code application.properties}) :
 *
 * <pre>{@code
 *   pispi.api.base-url=https://sandbox.pispi.bceao.int/business-api/v1
 *   pispi.api.client-id=<clientId BCEAO>
 *   pispi.api.client-secret=<clientSecret BCEAO>
 *   pispi.api.api-key=<X-API-Key BCEAO>
 *   pispi.api.tls.keystore-path=/secrets/pispi-client.p12
 *   pispi.api.tls.keystore-password=<password>
 *   pispi.api.tls.truststore-path=/secrets/pispi-truststore.p12   # optionnel
 *   pispi.api.tls.truststore-password=<password>                  # optionnel
 * }</pre>
 *
 * <p>En l'absence de credentials (mode dev sans sandbox), {@link #isConfigured()} renvoie
 * {@code false} et {@link PispiPaymentProvider} bascule en mode mock.
 *
 * @since 2026-04-25 — auth 3-facteurs ajoutée (OAuth2 seul auparavant)
 */
@Slf4j
@ApplicationScoped
public class PispiAuth {

    @ConfigProperty(name = "pispi.api.client-id")
    Optional<String> clientIdOpt;

    @ConfigProperty(name = "pispi.api.client-secret")
    Optional<String> clientSecretOpt;

    @ConfigProperty(name = "pispi.api.api-key")
    Optional<String> apiKeyOpt;

    @ConfigProperty(name = "pispi.api.tls.keystore-path")
    Optional<String> keystorePathOpt;

    @ConfigProperty(name = "pispi.api.tls.keystore-password")
    Optional<String> keystorePasswordOpt;

    @ConfigProperty(name = "pispi.api.tls.truststore-path")
    Optional<String> truststorePathOpt;

    @ConfigProperty(name = "pispi.api.tls.truststore-password")
    Optional<String> truststorePasswordOpt;

    @ConfigProperty(name = "pispi.api.base-url", defaultValue = "https://sandbox.pispi.bceao.int/business-api/v1")
    String baseUrl;

    String clientId;
    String clientSecret;
    String apiKey;

    private HttpClient mtlsClient;
    private String cachedToken;
    private Instant cacheExpiry;

    @jakarta.annotation.PostConstruct
    void init() {
        clientId = clientIdOpt.orElse("");
        clientSecret = clientSecretOpt.orElse("");
        apiKey = apiKeyOpt.orElse("");
        // Le client mTLS est construit lazy au premier usage (évite échec au boot si secrets absents)
    }

    /**
     * @return true si tous les facteurs (OAuth2 + mTLS + API Key) sont configurés et que le
     *     provider peut effectivement appeler la production. False = mode mock auto.
     */
    public boolean isConfigured() {
        return !clientId.isEmpty()
                && !clientSecret.isEmpty()
                && !apiKey.isEmpty()
                && keystorePathOpt.isPresent()
                && keystorePasswordOpt.isPresent();
    }

    /** Header {@code X-API-Key} à ajouter sur chaque requête API Business. */
    public String getApiKey() {
        return apiKey;
    }

    // ── Readiness inspection helpers (P1-NEW-15) ────────────────────────────
    public boolean hasClientId() { return clientId != null && !clientId.isEmpty(); }
    public boolean hasClientSecret() { return clientSecret != null && !clientSecret.isEmpty(); }
    public boolean hasApiKey() { return apiKey != null && !apiKey.isEmpty(); }
    public java.util.Optional<String> keystorePath() {
        return keystorePathOpt.filter(s -> !s.isBlank());
    }
    public java.util.Optional<String> keystorePassword() {
        return keystorePasswordOpt.filter(s -> !s.isBlank());
    }
    public java.util.Optional<String> truststorePath() {
        return truststorePathOpt.filter(s -> !s.isBlank());
    }
    public java.util.Optional<String> truststorePassword() {
        return truststorePasswordOpt.filter(s -> !s.isBlank());
    }

    /** Base URL configurée (sandbox ou production). */
    public String getBaseUrl() {
        return baseUrl;
    }

    /**
     * Retourne un {@link HttpClient} configuré avec mTLS (keystore client + truststore optionnel).
     * Construction lazy + cache instance unique.
     */
    public synchronized HttpClient getMtlsHttpClient() throws PaymentException {
        if (mtlsClient != null) {
            return mtlsClient;
        }
        try {
            SSLContext sslContext = buildSSLContext();
            mtlsClient = HttpClient.newBuilder()
                    .sslContext(sslContext)
                    .connectTimeout(Duration.ofSeconds(15))
                    .version(HttpClient.Version.HTTP_2)
                    .build();
            return mtlsClient;
        } catch (Exception e) {
            throw new PaymentException("PISPI",
                    "Impossible d'initialiser le client mTLS PI-SPI : " + e.getMessage(), 503, e);
        }
    }

    /**
     * Construit le {@link SSLContext} avec keystore client (PKCS12) + truststore optionnel.
     * Si truststore absent, utilise le truststore Java par défaut (cacerts JDK).
     */
    private SSLContext buildSSLContext() throws Exception {
        if (keystorePathOpt.isEmpty() || keystorePasswordOpt.isEmpty()) {
            throw new IllegalStateException(
                    "Keystore PI-SPI non configuré (pispi.api.tls.keystore-path / -password)");
        }
        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream(keystorePathOpt.get())) {
            keyStore.load(fis, keystorePasswordOpt.get().toCharArray());
        }
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(keyStore, keystorePasswordOpt.get().toCharArray());

        TrustManagerFactory tmf = null;
        if (truststorePathOpt.isPresent() && truststorePasswordOpt.isPresent()) {
            KeyStore trustStore = KeyStore.getInstance("PKCS12");
            try (FileInputStream fis = new FileInputStream(truststorePathOpt.get())) {
                trustStore.load(fis, truststorePasswordOpt.get().toCharArray());
            }
            tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(trustStore);
        }

        SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
        sslContext.init(kmf.getKeyManagers(),
                tmf != null ? tmf.getTrustManagers() : null,
                null);
        return sslContext;
    }

    public synchronized String getAccessToken() throws PaymentException {
        if (cachedToken != null && Instant.now().isBefore(cacheExpiry)) {
            return cachedToken;
        }
        try {
            String body = "grant_type=client_credentials"
                    + "&client_id=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8)
                    + "&client_secret=" + URLEncoder.encode(clientSecret, StandardCharsets.UTF_8)
                    + "&scope=pispi.transactions";

            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(baseUrl + "/oauth2/token"))
                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("X-API-Key", apiKey)
                    .timeout(Duration.ofSeconds(30))
                    .POST(HttpRequest.BodyPublishers.ofString(body))
                    .build();

            // Le endpoint OAuth2 utilise déjà mTLS — utiliser le client mTLS si configuré,
            // sinon le client par défaut (mode dégradé / dev sans certif)
            HttpClient client = isConfigured() ? getMtlsHttpClient() : HttpClient.newHttpClient();
            HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

            if (response.statusCode() >= 400) {
                throw new PaymentException("PISPI",
                        "Erreur OAuth2 PI-SPI HTTP " + response.statusCode() + " : " + response.body(),
                        503);
            }

            JsonObject json = Json.createReader(new StringReader(response.body())).readObject();
            cachedToken = json.getString("access_token");
            int expiresIn = json.getInt("expires_in", 3600);
            cacheExpiry = Instant.now().plusSeconds(expiresIn - 60);

            log.debug("Token PI-SPI obtenu (expire dans {}s, mTLS={})",
                    expiresIn - 60, isConfigured());
            return cachedToken;
        } catch (PaymentException e) {
            throw e;
        } catch (Exception e) {
            throw new PaymentException("PISPI", "Erreur OAuth2 PI-SPI : " + e.getMessage(), 503, e);
        }
    }
}
