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

import cloud.nstart.terravault.ordinis.domain.userdisplay.UserDisplayCacheEntry;
import cloud.nstart.terravault.ordinis.domain.userdisplay.UserDisplayCacheEntry.Source;
import cloud.nstart.terravault.ordinis.domain.userdisplay.UserDisplayCacheRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.OffsetDateTime;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Three-tier resolve sub → display info:
 * <ol>
 *   <li>JVM ConcurrentHashMap hot cache (~µs).</li>
 *   <li>{@code user_display_cache} Postgres table (~ms, persistent across pod restart).</li>
 *   <li>Keycloak Admin API on-demand fallback (~10-50ms, Phase 2 — wired
 *       via optional {@link KeycloakUserResolver} bean).</li>
 * </ol>
 *
 * <p>JwtUserCaptureFilter calls {@link #put} on every authenticated request
 * → updates both hot cache and DB (idempotent merge).
 *
 * <p>Phase 3 (TBD): scheduled bulk sync from Keycloak will call
 * {@link #upsertFromKeycloak} for every realm user every 6h to refill
 * stale entries.
 *
 * <p>Hot cache reads from DB lazily on miss — no warm-up at startup
 * (would block readiness probe + Keycloak might be cold). First request
 * for each sub after restart pays a single DB hit.
 */
@Service
public class UserDisplayService {

  private static final Logger log = LoggerFactory.getLogger(UserDisplayService.class);

  /**
   * Cap чтобы не держать миллионы entries в RAM при abuse. ~100 байт per
   * entry → 10k = 1MB. Eviction policy — LRU не нужен, hash без bound — fine.
   * DB table сама разрастается без cap (см. migration 0023 design notes).
   */
  private static final int HOT_CACHE_CAP = 10_000;

  private final ConcurrentHashMap<String, UserDisplayInfo> hotCache = new ConcurrentHashMap<>();
  private final UserDisplayCacheRepository repository;
  private final KeycloakUserResolver keycloakResolver;

  public UserDisplayService(UserDisplayCacheRepository repository,
                            @Autowired(required = false) KeycloakUserResolver keycloakResolver) {
    this.repository = repository;
    this.keycloakResolver = keycloakResolver;
    if (keycloakResolver == null) {
      log.info("UserDisplayService: KeycloakUserResolver not wired — three-tier resolve "
          + "will fall back to short-UUID for users that never made an authenticated "
          + "request to ordinis.");
    }
  }

  /**
   * Capture from JWT (called per-request by JwtUserCaptureFilter). Cheap and
   * idempotent. Writes to hot cache + DB. Source is always JWT_CAPTURE here —
   * KEYCLOAK_SYNC / KEYCLOAK_ON_DEMAND have their own write paths.
   */
  @Transactional
  public void put(String sub, String preferredUsername, String name, String email) {
    if (sub == null || sub.isBlank()) return;
    upsert(sub, preferredUsername, name, email, Source.JWT_CAPTURE);
  }

  /**
   * Three-tier lookup. Returns empty only if sub absent in all three tiers
   * AND Keycloak is unreachable (transient — caller's UserCell will fall
   * back to short UUID, retry next time it renders).
   */
  @Transactional
  public Optional<UserDisplayInfo> find(String sub) {
    if (sub == null || sub.isBlank()) return Optional.empty();

    // Tier 1: hot cache
    UserDisplayInfo hot = hotCache.get(sub);
    if (hot != null) return Optional.of(hot);

    // Tier 2: DB
    Optional<UserDisplayCacheEntry> dbHit = repository.findById(sub);
    if (dbHit.isPresent()) {
      UserDisplayInfo info = toInfo(dbHit.get());
      cacheHot(sub, info);
      return Optional.of(info);
    }

    // Tier 3: Keycloak Admin on-demand (Phase 2)
    if (keycloakResolver != null) {
      try {
        Optional<KeycloakUserResolver.KeycloakUser> kc = keycloakResolver.findById(sub);
        if (kc.isPresent()) {
          KeycloakUserResolver.KeycloakUser u = kc.get();
          UserDisplayInfo info = upsert(sub, u.preferredUsername(), u.name(), u.email(),
              Source.KEYCLOAK_ON_DEMAND);
          return Optional.of(info);
        }
      } catch (RuntimeException e) {
        log.warn("Keycloak on-demand lookup failed for sub={}: {}", sub, e.getMessage());
      }
    }

    return Optional.empty();
  }

  /**
   * Phase 3 entry point — scheduled sync writes every realm user with
   * Source.KEYCLOAK_SYNC. Same idempotent merge as {@link #put}.
   */
  @Transactional
  public void upsertFromKeycloak(String sub, String preferredUsername, String name, String email) {
    if (sub == null || sub.isBlank()) return;
    upsert(sub, preferredUsername, name, email, Source.KEYCLOAK_SYNC);
  }

  /** Stats для observability. */
  public int hotCacheSize() {
    return hotCache.size();
  }

  // --- internals ---

  private UserDisplayInfo upsert(String sub, String preferredUsername, String name,
                                 String email, Source source) {
    OffsetDateTime now = OffsetDateTime.now();
    UserDisplayCacheEntry entry = repository.findById(sub).orElse(null);
    if (entry == null) {
      entry = new UserDisplayCacheEntry(sub,
          nullIfBlank(preferredUsername),
          nullIfBlank(name),
          nullIfBlank(email),
          source,
          now);
    } else {
      entry.mergeFrom(preferredUsername, name, email, source, now);
    }
    repository.save(entry);
    UserDisplayInfo info = toInfo(entry);
    cacheHot(sub, info);
    return info;
  }

  private void cacheHot(String sub, UserDisplayInfo info) {
    if (hotCache.size() < HOT_CACHE_CAP) {
      hotCache.put(sub, info);
    }
  }

  private static UserDisplayInfo toInfo(UserDisplayCacheEntry e) {
    return new UserDisplayInfo(
        e.getSub(),
        e.getPreferredUsername(),
        e.getName(),
        e.getEmail(),
        e.getUpdatedAt());
  }

  private static String nullIfBlank(String s) {
    return (s == null || s.isBlank()) ? null : s;
  }

  public record UserDisplayInfo(
      String sub,
      String preferredUsername,
      String name,
      String email,
      OffsetDateTime updatedAt) {}
}
