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

import cloud.nstart.terravault.ordinis.domain.idempotency.IdempotencyKey;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;
import java.util.Optional;
import java.util.Set;

/**
 * Servlet filter для exactly-once semantics на write API.
 *
 * <p>Срабатывает только на POST/PUT/PATCH (мутирующие методы) с заголовком
 * {@code Idempotency-Key}. На GET/HEAD/DELETE/OPTIONS — pass-through.
 *
 * <p>Логика:
 * <ol>
 *   <li>Считает sha-256 хэш от request body.</li>
 *   <li>Ищет ключ в БД. Если найден с тем же hash — возвращает cached
 *       {@code response_status + response_body} без выполнения handler.</li>
 *   <li>Если ключ есть с другим hash — 422 (конфликт тел запроса).</li>
 *   <li>Если ключа нет — резервирует строку, передаёт управление вниз, после
 *       успешного 2xx ответа сохраняет body в БД.</li>
 * </ol>
 */
@Component
public class IdempotencyFilter extends OncePerRequestFilter {

  private static final Logger log = LoggerFactory.getLogger(IdempotencyFilter.class);
  private static final String HEADER = "Idempotency-Key";
  private static final Set<String> MUTATING = Set.of("POST", "PUT", "PATCH");

  private final IdempotencyService service;
  private final ObjectMapper objectMapper;

  public IdempotencyFilter(IdempotencyService service, ObjectMapper objectMapper) {
    this.service = service;
    this.objectMapper = objectMapper;
  }

  @Override
  protected void doFilterInternal(
      HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws ServletException, IOException {

    if (!MUTATING.contains(request.getMethod())) {
      chain.doFilter(request, response);
      return;
    }
    String key = request.getHeader(HEADER);
    if (key == null || key.isBlank()) {
      chain.doFilter(request, response);
      return;
    }
    if (key.length() > 128) {
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      writeError(response, 400, "invalid_idempotency_key",
          "Idempotency-Key должен быть короче 128 символов");
      return;
    }

    CachedBodyHttpServletRequest wrappedReq = new CachedBodyHttpServletRequest(request);
    byte[] body = wrappedReq.getCachedBody();
    String hash = service.hashRequest(body);

    try {
      Optional<IdempotencyKey> cached = service.lookup(key, hash);
      if (cached.isPresent()) {
        IdempotencyKey ik = cached.get();
        if (ik.getResponseStatus() == null) {
          // зарезервирован, но обработка не завершилась — клиент должен retry
          response.setStatus(HttpServletResponse.SC_CONFLICT);
          writeError(response, 409, "idempotency_in_progress",
              "Запрос с Idempotency-Key '" + key + "' ещё обрабатывается");
          return;
        }
        log.debug("Idempotency hit для ключа '{}' → status={}", key, ik.getResponseStatus());
        response.setStatus(ik.getResponseStatus());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        if (ik.getResponseBody() != null) {
          byte[] bytes = objectMapper.writeValueAsBytes(ik.getResponseBody());
          response.setContentLength(bytes.length);
          response.getOutputStream().write(bytes);
        }
        return;
      }
      service.reserve(key, hash);
    } catch (IdempotencyConflictException e) {
      response.setStatus(422);
      writeError(response, 422, "idempotency_conflict", e.getMessage());
      return;
    } catch (IdempotencyInProgressException e) {
      response.setStatus(HttpServletResponse.SC_CONFLICT);
      writeError(response, 409, "idempotency_in_progress", e.getMessage());
      return;
    }

    ContentCachingResponseWrapper wrappedResp = new ContentCachingResponseWrapper(response);
    chain.doFilter(wrappedReq, wrappedResp);

    int status = wrappedResp.getStatus();
    byte[] respBody = wrappedResp.getContentAsByteArray();
    // Кэшируем только успешные ответы; ошибки могут быть транзиентными — клиент retry'ит.
    if (status >= 200 && status < 300) {
      try {
        service.saveResponse(key, status, respBody);
      } catch (Exception e) {
        log.warn("Не удалось сохранить idempotent response для ключа '{}': {}", key, e.getMessage());
      }
    }
    wrappedResp.copyBodyToResponse();
  }

  private void writeError(HttpServletResponse response, int status, String code, String message)
      throws IOException {
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    String json = "{\"status\":" + status
        + ",\"code\":\"" + code + "\""
        + ",\"message\":\"" + message.replace("\"", "\\\"") + "\"}";
    response.getOutputStream().write(json.getBytes());
  }
}
