package ai.sentryagent.idp;

import ai.sentryagent.idp.models.TokenResponse;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
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.time.Duration;
import java.time.Instant;

/**
 * Obtains and caches OAuth 2.0 client credentials tokens.
 * Thread-safe: all cache access is synchronized.
 * Tokens are refreshed 60 seconds before they expire.
 */
public final class TokenManager {

    private static final int REFRESH_BUFFER_SECONDS = 60;
    private static final ObjectMapper MAPPER = new ObjectMapper();

    private final String baseUrl;
    private final String clientId;
    private final String clientSecret;
    private final String scope;
    private final HttpClient httpClient;

    private String cachedToken;
    private Instant tokenExpiresAt;

    public TokenManager(String baseUrl, String clientId, String clientSecret, String scope) {
        this(baseUrl, clientId, clientSecret, scope,
            HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build());
    }

    /** Package-visible constructor for injecting a custom HttpClient in tests. */
    TokenManager(String baseUrl, String clientId, String clientSecret, String scope, HttpClient httpClient) {
        this.baseUrl      = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
        this.clientId     = clientId;
        this.clientSecret = clientSecret;
        this.scope        = scope;
        this.httpClient   = httpClient;
    }

    /**
     * Returns a valid access token, fetching a new one if the cache is empty
     * or within the 60-second refresh buffer.
     */
    public synchronized String getToken() {
        if (cachedToken != null && tokenExpiresAt != null
                && Instant.now().plusSeconds(REFRESH_BUFFER_SECONDS).isBefore(tokenExpiresAt)) {
            return cachedToken;
        }
        TokenResponse tr = fetchToken();
        cachedToken    = tr.getAccessToken();
        tokenExpiresAt = Instant.now().plusSeconds(tr.getExpiresIn());
        return cachedToken;
    }

    /** Invalidates the cached token. The next call to {@link #getToken()} fetches a fresh one. */
    public synchronized void clearCache() {
        cachedToken    = null;
        tokenExpiresAt = null;
    }

    private TokenResponse fetchToken() {
        String form = "grant_type=client_credentials"
            + "&client_id=" + encode(clientId)
            + "&client_secret=" + encode(clientSecret)
            + "&scope=" + encode(scope);

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(baseUrl + "/api/v1/token"))
            .POST(HttpRequest.BodyPublishers.ofString(form))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .build();

        try {
            HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
            if (resp.statusCode() != 200) {
                throw AgentIdPException.fromOAuth2Error(resp.body(), resp.statusCode());
            }
            return MAPPER.readValue(resp.body(), TokenResponse.class);
        } catch (AgentIdPException e) {
            throw e;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw AgentIdPException.networkError(e);
        } catch (IOException e) {
            throw AgentIdPException.networkError(e);
        }
    }

    private static String encode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }
}
