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

import cloud.nstart.terravault.ordinis.domain.DataScope;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinitionRepository;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecord;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
import cloud.nstart.terravault.ordinis.readapi.dto.FlattenedRecordResponse;
import cloud.nstart.terravault.ordinis.readapi.locale.LocaleAwareJsonFlattener;
import cloud.nstart.terravault.ordinis.readapi.locale.LocaleNegotiator;
import cloud.nstart.terravault.ordinis.readapi.spatial.BoundingBox;
import com.fasterxml.jackson.databind.JsonNode;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Optional;

/**
 * Read-side. Negotiate locale → load record from replica с фильтрацией по
 * scope → flatten multi-locale → ответ.
 *
 * <p>Подключение через transactional read-only — Hibernate использует replica
 * connection (см. application.yml для read-api).
 */
@Service
public class RecordReadService {

  private final DictionaryDefinitionRepository definitionRepository;
  private final DictionaryRecordRepository recordRepository;
  private final LocaleNegotiator localeNegotiator;
  private final LocaleAwareJsonFlattener flattener;
  private final Timer queryTimer;

  public RecordReadService(
      DictionaryDefinitionRepository definitionRepository,
      DictionaryRecordRepository recordRepository,
      LocaleNegotiator localeNegotiator,
      LocaleAwareJsonFlattener flattener,
      MeterRegistry meterRegistry) {
    this.definitionRepository = definitionRepository;
    this.recordRepository = recordRepository;
    this.localeNegotiator = localeNegotiator;
    this.flattener = flattener;
    this.queryTimer = Timer.builder("ordinis_read_api_query_duration_seconds")
        .description("Время чтения записи через read-api с locale negotiation")
        .register(meterRegistry);
  }

  public record ReadResult(FlattenedRecordResponse response, String chosenLocale) {}

  @Transactional(readOnly = true)
  public ReadResult readActive(
      String dictionaryName,
      String businessKey,
      String acceptLanguage,
      List<DataScope> allowedScopes,
      OffsetDateTime asOf) {

    Timer.Sample sample = Timer.start();
    try {
      DictionaryDefinition def = definitionRepository.findByName(dictionaryName)
          .orElseThrow(() -> new NoSuchElementException("Dictionary not found: " + dictionaryName));

      if (!allowedScopes.contains(def.getScope())) {
        throw new ScopeAccessDeniedException(
            "Scope " + def.getScope() + " not in allowed scopes " + allowedScopes);
      }

      OffsetDateTime when = asOf == null ? OffsetDateTime.now() : asOf;
      Optional<DictionaryRecord> opt = recordRepository.findActiveAt(def.getId(), businessKey, when);
      if (opt.isEmpty()) {
        throw new NoSuchElementException(
            "No active record for business_key=" + businessKey + " at " + when);
      }
      DictionaryRecord record = opt.get();

      if (!allowedScopes.contains(record.getDataScope())) {
        throw new ScopeAccessDeniedException(
            "Record scope " + record.getDataScope() + " not in " + allowedScopes);
      }

      String chosen = localeNegotiator.negotiate(
          acceptLanguage, def.getSupportedLocales(), def.getDefaultLocale());

      JsonNode flat = flattener.flatten(def.getSchemaJson(), record.getData(), chosen, def.getDefaultLocale());

      return new ReadResult(
          FlattenedRecordResponse.of(record, flat, chosen, def.getDefaultLocale(), when),
          chosen);
    } finally {
      sample.stop(queryTimer);
    }
  }

  @Transactional(readOnly = true)
  public List<FlattenedRecordResponse> listActive(
      String dictionaryName,
      String acceptLanguage,
      List<DataScope> allowedScopes,
      OffsetDateTime asOf) {
    return listActive(dictionaryName, acceptLanguage, allowedScopes, asOf, null);
  }

  /**
   * List active records с optional bbox spatial filter.
   * Backward-compat entry — без polygon.
   */
  @Transactional(readOnly = true)
  public List<FlattenedRecordResponse> listActive(
      String dictionaryName,
      String acceptLanguage,
      List<DataScope> allowedScopes,
      OffsetDateTime asOf,
      BoundingBox bbox) {
    return listActive(dictionaryName, acceptLanguage, allowedScopes, asOf, bbox, null);
  }

  /**
   * List active records с optional bbox или polygon GeoJSON spatial filter.
   * Только один из {@code bbox}/{@code polygonGeoJson} может быть non-null
   * (caller валидирует). Используется PostGIS {@code ST_Intersects} с
   * GiST index hit. Записи без geometry колонки автоматически
   * отфильтровываются.
   */
  @Transactional(readOnly = true)
  public List<FlattenedRecordResponse> listActive(
      String dictionaryName,
      String acceptLanguage,
      List<DataScope> allowedScopes,
      OffsetDateTime asOf,
      BoundingBox bbox,
      String polygonGeoJson) {

    DictionaryDefinition def = definitionRepository.findByName(dictionaryName)
        .orElseThrow(() -> new NoSuchElementException("Dictionary not found: " + dictionaryName));

    if (!allowedScopes.contains(def.getScope())) {
      throw new ScopeAccessDeniedException(
          "Scope " + def.getScope() + " not in allowed scopes " + allowedScopes);
    }

    OffsetDateTime when = asOf == null ? OffsetDateTime.now() : asOf;
    List<DictionaryRecord> records;
    String[] scopesCsv = allowedScopes.stream().map(Enum::name).toArray(String[]::new);
    if (bbox != null) {
      records = recordRepository.findActiveByBbox(
          def.getId(), scopesCsv, when,
          bbox.west(), bbox.south(), bbox.east(), bbox.north());
    } else if (polygonGeoJson != null) {
      records = recordRepository.findActiveByPolygonGeoJson(
          def.getId(), scopesCsv, when, polygonGeoJson);
    } else {
      records = recordRepository.findActiveByDictionaryAndScopes(
          def.getId(), allowedScopes, when);
    }

    String chosen = localeNegotiator.negotiate(
        acceptLanguage, def.getSupportedLocales(), def.getDefaultLocale());

    return records.stream()
        .map(r -> FlattenedRecordResponse.of(
            r,
            flattener.flatten(def.getSchemaJson(), r.getData(), chosen, def.getDefaultLocale()),
            chosen,
            def.getDefaultLocale(),
            when))
        .toList();
  }

  public static class ScopeAccessDeniedException extends RuntimeException {
    public ScopeAccessDeniedException(String message) { super(message); }
  }
}
