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

import cloud.nstart.terravault.ordinis.domain.idempotency.IdempotencyKey;
import cloud.nstart.terravault.ordinis.domain.idempotency.IdempotencyKeyRepository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.OffsetDateTime;
import java.util.HexFormat;
import java.util.Optional;

/**
 * Хранение и lookup для Idempotency-Key.
 *
 * <p>Контракт:
 * <ul>
 *   <li>{@link #lookup(String, String)} — возвращает существующий ключ если он уже
 *       видел тот же request hash. Если hash не совпадает — кидает
 *       {@link IdempotencyConflictException} (HTTP 422 у клиента).</li>
 *   <li>{@link #reserve(String, String)} — пытается атомарно зарегистрировать
 *       новый ключ. Бросает {@link IdempotencyConflictException} если ключ уже
 *       занят чужим hash (race-safe через unique PK constraint).</li>
 *   <li>{@link #saveResponse(String, int, JsonNode)} — после успешной обработки
 *       сохраняет response для повтора.</li>
 * </ul>
 */
@Service
public class IdempotencyService {

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

  /** Хранилище: 30 дней (соответствует комментарию в migration 0006). */
  private static final long RETENTION_DAYS = 30;

  private final IdempotencyKeyRepository repository;
  private final ObjectMapper objectMapper;

  public IdempotencyService(IdempotencyKeyRepository repository, ObjectMapper objectMapper) {
    this.repository = repository;
    this.objectMapper = objectMapper;
  }

  /** Sha-256 hex от тела запроса; идентичен только для байт-в-байт идентичных тел. */
  public String hashRequest(byte[] body) {
    try {
      MessageDigest md = MessageDigest.getInstance("SHA-256");
      return HexFormat.of().formatHex(md.digest(body == null ? new byte[0] : body));
    } catch (NoSuchAlgorithmException e) {
      throw new IllegalStateException("SHA-256 not available", e);
    }
  }

  @Transactional(readOnly = true)
  public Optional<IdempotencyKey> lookup(String key, String requestHash) {
    Optional<IdempotencyKey> existing = repository.findById(key);
    if (existing.isEmpty()) {
      return Optional.empty();
    }
    IdempotencyKey ik = existing.get();
    if (!ik.getRequestHash().equals(requestHash)) {
      throw new IdempotencyConflictException(
          "Idempotency-Key '" + key + "' уже использовался с другим телом запроса");
    }
    return existing;
  }

  /**
   * Резервирует ключ перед обработкой. Если запись уже создана — поднимает 422
   * (клиент видит, что мы только что начали обрабатывать тот же ключ; обычно это
   * следствие гонки double-submit, и второй запрос должен retry'ить).
   */
  @Transactional
  public void reserve(String key, String requestHash) {
    OffsetDateTime expires = OffsetDateTime.now().plusDays(RETENTION_DAYS);
    IdempotencyKey reserved = new IdempotencyKey(key, requestHash, expires);
    try {
      repository.saveAndFlush(reserved);
    } catch (DataIntegrityViolationException e) {
      // Кто-то параллельный успел зарезервировать тот же ключ.
      Optional<IdempotencyKey> existing = repository.findById(key);
      if (existing.isPresent() && existing.get().getRequestHash().equals(requestHash)) {
        // То же тело — клиент может retry'ить, мы вернём 409 чтобы он понял что обработка ещё идёт.
        throw new IdempotencyInProgressException(
            "Запрос с Idempotency-Key '" + key + "' ещё обрабатывается");
      }
      throw new IdempotencyConflictException(
          "Idempotency-Key '" + key + "' уже использовался с другим телом запроса");
    }
  }

  @Transactional
  public void saveResponse(String key, int status, byte[] body) {
    IdempotencyKey ik = repository.findById(key).orElse(null);
    if (ik == null) {
      log.warn("saveResponse для отсутствующего ключа '{}', пропускаем", key);
      return;
    }
    JsonNode parsed = null;
    if (body != null && body.length > 0) {
      try {
        parsed = objectMapper.readTree(body);
      } catch (Exception e) {
        log.debug("Тело ответа для ключа '{}' не JSON, не кэшируем", key);
      }
    }
    ik.setResponse(status, parsed);
    repository.save(ik);
  }
}
