package cloud.nstart.terravault.ordinis.restapi.service;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;
import org.springframework.web.util.UriComponentsBuilder;

import java.time.Duration;
import java.time.Instant;
import java.util.Optional;

/**
 * Phase 2: resolve {@code sub} via Keycloak Admin REST API. Activated when
 * {@code ordinis.keycloak.admin.enabled=true} (and required env vars set).
 *
 * <p>Auth flow: client_credentials grant against
 * {@code {issuer}/protocol/openid-connect/token}. The service-account
 * client must have realm role {@code view-users} (composite of
 * {@code realm-management/view-users}).
 *
 * <p>Token caching: the access_token from Keycloak typically lives
 * 60-300s. We cache it minus 30s safety margin and refresh on miss.
 *
 * <p>Lookup uses {@code GET /admin/realms/{realm}/users/{sub}} which
 * returns 404 cleanly when the sub is unknown. Network/5xx are surfaced
 * as RuntimeException so {@link UserDisplayService} can log and skip.
 */
@Service
@ConditionalOnProperty(name = "ordinis.keycloak.admin.enabled", havingValue = "true")
public class KeycloakAdminUserResolver implements KeycloakUserResolver {

  private static final Logger log = LoggerFactory.getLogger(KeycloakAdminUserResolver.class);
  private static final Duration TOKEN_REFRESH_MARGIN = Duration.ofSeconds(30);

  private final String adminBaseUrl;
  private final String realm;
  private final String clientId;
  private final String clientSecret;
  private final RestClient http;

  // Token cache — Keycloak access_token + expiry. Access is single-threaded
  // in practice (one resolver call at a time); volatile is enough.
  private volatile String cachedToken;
  private volatile Instant cachedTokenExpiresAt = Instant.EPOCH;

  public KeycloakAdminUserResolver(
      @Value("${ordinis.keycloak.admin.url}") String adminBaseUrl,
      @Value("${ordinis.keycloak.admin.realm}") String realm,
      @Value("${ordinis.keycloak.admin.client-id}") String clientId,
      @Value("${ordinis.keycloak.admin.client-secret}") String clientSecret) {
    this.adminBaseUrl = trimTrailingSlash(adminBaseUrl);
    this.realm = realm;
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.http = RestClient.builder()
        .baseUrl(this.adminBaseUrl)
        .build();
    log.info("KeycloakAdminUserResolver active — base={} realm={} client={}",
        this.adminBaseUrl, realm, clientId);
  }

  @Override
  public Optional<KeycloakUser> findById(String sub) {
    if (sub == null || sub.isBlank()) return Optional.empty();
    String token = currentToken();
    String url = UriComponentsBuilder.fromHttpUrl(adminBaseUrl)
        .pathSegment("admin", "realms", realm, "users", sub)
        .toUriString();
    try {
      KeycloakUserDto dto = http.get()
          .uri(url)
          .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
          .retrieve()
          .body(KeycloakUserDto.class);
      if (dto == null || dto.id == null) return Optional.empty();
      String name = composeName(dto.firstName, dto.lastName);
      return Optional.of(new KeycloakUser(
          dto.id,
          dto.username,
          name,
          dto.email));
    } catch (HttpClientErrorException.NotFound e) {
      // Sub doesn't exist in this realm — definitive miss. Don't cache the
      // negative; UserDisplayService.find returns Optional.empty and UserCell
      // falls back to short UUID. Negative cache TTL would help latency on
      // bots/scanners but adds complexity not justified yet.
      return Optional.empty();
    } catch (HttpClientErrorException.Unauthorized e) {
      // Cached token rejected — wipe and retry once. If still 401, surface.
      cachedToken = null;
      cachedTokenExpiresAt = Instant.EPOCH;
      throw new RuntimeException("Keycloak admin auth rejected (token wiped, will retry)", e);
    }
  }

  // --- token management ---

  private synchronized String currentToken() {
    if (cachedToken != null && Instant.now().isBefore(cachedTokenExpiresAt)) {
      return cachedToken;
    }
    return fetchAndCacheToken();
  }

  private String fetchAndCacheToken() {
    String tokenUrl = UriComponentsBuilder.fromHttpUrl(adminBaseUrl)
        .pathSegment("realms", realm, "protocol", "openid-connect", "token")
        .toUriString();
    MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
    form.add("grant_type", "client_credentials");
    form.add("client_id", clientId);
    form.add("client_secret", clientSecret);
    TokenResponse tr = http.post()
        .uri(tokenUrl)
        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
        .body(form)
        .retrieve()
        .onStatus(s -> s.is4xxClientError() || s.is5xxServerError(), (req, res) -> {
          throw new RuntimeException("Keycloak admin token request failed: HTTP "
              + res.getStatusCode() + " — check client_id/secret + realm");
        })
        .body(TokenResponse.class);
    if (tr == null || tr.accessToken == null) {
      throw new RuntimeException("Keycloak admin token response missing access_token");
    }
    cachedToken = tr.accessToken;
    int expiresIn = tr.expiresIn != null ? tr.expiresIn : 60;
    cachedTokenExpiresAt = Instant.now()
        .plusSeconds(expiresIn)
        .minus(TOKEN_REFRESH_MARGIN);
    return cachedToken;
  }

  // --- helpers ---

  private static String trimTrailingSlash(String s) {
    return (s != null && s.endsWith("/")) ? s.substring(0, s.length() - 1) : s;
  }

  private static String composeName(String first, String last) {
    String f = (first == null || first.isBlank()) ? "" : first.trim();
    String l = (last == null || last.isBlank()) ? "" : last.trim();
    String joined = (f + " " + l).trim();
    return joined.isEmpty() ? null : joined;
  }

  // --- DTOs (Keycloak 26 admin API) ---

  @JsonIgnoreProperties(ignoreUnknown = true)
  private record TokenResponse(
      @com.fasterxml.jackson.annotation.JsonProperty("access_token") String accessToken,
      @com.fasterxml.jackson.annotation.JsonProperty("expires_in") Integer expiresIn) {}

  @JsonIgnoreProperties(ignoreUnknown = true)
  private record KeycloakUserDto(
      String id,
      String username,
      String firstName,
      String lastName,
      String email) {}
}
