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

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.DictionaryRecordRepository;
import cloud.nstart.terravault.ordinis.validation.ValidationError;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.stereotype.Component;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Cross-dictionary FK references — v2 feature. JSON Schema extension
 * {@code x-references: "dict_name.field_name"} объявляет что значение
 * в этом поле должно ссылаться на active record в указанном словаре,
 * где {@code data->>field_name} == referencing value.
 *
 * <p>Пример:
 * <pre>
 * {
 *   "type": "object",
 *   "properties": {
 *     "satelliteTypeCode": {
 *       "type": "string",
 *       "x-references": "satellite_type.code"
 *     }
 *   }
 * }
 * </pre>
 *
 * <p>На POST/PUT record service вызывает {@link #validate} который для
 * каждого x-references поля делает lookup. Если referenced record не
 * найден / не активен / не в доступном scope — ValidationError.
 *
 * <p>v1 ограничения:
 * <ul>
 *   <li>Только top-level fields (nested object refs не поддержаны).</li>
 *   <li>Только string-valued references (number/array — позже).</li>
 *   <li>Без cascade на delete/close: если referenced record закрывается,
 *       referencing records остаются с dangling FK. Отдельный alert
 *       будет в v2.5.</li>
 *   <li>Валидация в момент CRUD, не на schema change. Если меняется
 *       schema с new x-references на existing data — может быть orphan,
 *       проверяем при first re-save.</li>
 * </ul>
 */
@Component
public class ReferenceValidator {

  private final DictionaryDefinitionRepository definitionRepository;
  private final DictionaryRecordRepository recordRepository;

  public ReferenceValidator(
      DictionaryDefinitionRepository definitionRepository,
      DictionaryRecordRepository recordRepository) {
    this.definitionRepository = definitionRepository;
    this.recordRepository = recordRepository;
  }

  /**
   * Validate references. Возвращает список errors (пусто = OK). Не бросает.
   * Caller (DictionaryRecordService) кидает ValidationException если errors > 0.
   *
   * @param schema JSON Schema справочника (содержит x-references markers)
   * @param data payload записи
   * @param allowedScopes scope подмножество которое viewер видит — referenced
   *                      records должны быть в нём (если выше — denied)
   * @param at момент времени (transaction time) — обычно now()
   */
  public List<ValidationError> validate(
      JsonNode schema, JsonNode data, List<DataScope> allowedScopes, OffsetDateTime at) {
    List<ValidationError> errors = new ArrayList<>();
    if (schema == null || data == null || !schema.isObject() || !data.isObject()) {
      return errors;
    }
    JsonNode properties = schema.get("properties");
    if (properties == null || !properties.isObject()) return errors;

    OffsetDateTime when = at == null ? OffsetDateTime.now() : at;

    Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
    while (it.hasNext()) {
      Map.Entry<String, JsonNode> entry = it.next();
      String fieldName = entry.getKey();
      JsonNode propSchema = entry.getValue();
      JsonNode refNode = propSchema.get("x-references");
      if (refNode == null || !refNode.isTextual()) continue;

      String refSpec = refNode.asText();
      ParsedRef parsed;
      try {
        parsed = parseRef(refSpec, propSchema);
      } catch (IllegalArgumentException e) {
        errors.add(new ValidationError(
            "/" + fieldName,
            "x_references_malformed",
            "x-references value malformed: " + e.getMessage()));
        continue;
      }
      // Phase 1 prep: onClose читается, но не используется — runtime cascade
      // engine добавляется Phase 3. Здесь BLOCK (default) совпадает с текущим
      // поведением CRUD validator'а.

      JsonNode value = data.get(fieldName);
      if (value == null || value.isNull()) {
        // Если поле required, JSON Schema валидация уже отлавливает.
        // Если optional — references skip (нет значения = нет проверки).
        continue;
      }
      if (!value.isTextual()) {
        errors.add(new ValidationError(
            "/" + fieldName,
            "x_references_non_string",
            "x-references поддерживает только string values в v1, got: " + value.getNodeType()));
        continue;
      }
      String stringValue = value.asText();

      Optional<DictionaryDefinition> targetOpt =
          definitionRepository.findByName(parsed.dictionaryName());
      if (targetOpt.isEmpty()) {
        errors.add(new ValidationError(
            "/" + fieldName,
            "x_references_dict_not_found",
            "Referenced dictionary '" + parsed.dictionaryName() + "' does not exist"));
        continue;
      }
      DictionaryDefinition target = targetOpt.get();

      if (!allowedScopes.contains(target.getScope())) {
        // Scope hide: referenced dictionary not visible to caller. Возвращаем
        // ту же ошибку что и "not found" чтобы не leak'ало existence.
        errors.add(new ValidationError(
            "/" + fieldName,
            "x_references_target_not_found",
            "Referenced record not found in '" + parsed.dictionaryName()
                + "' for value '" + stringValue + "'"));
        continue;
      }

      List<?> matches = recordRepository.findActiveByJsonField(
          target.getId(), when, parsed.fieldName(), stringValue);

      if (matches.isEmpty()) {
        errors.add(new ValidationError(
            "/" + fieldName,
            "x_references_target_not_found",
            "Referenced record not found in '" + parsed.dictionaryName()
                + "' for " + parsed.fieldName() + "='" + stringValue + "'"));
      }
      // Если matches.size() > 1 — это ситуация когда referenced field не
      // x-unique. Проходим: matches.first работает. Лог-warning отдельно
      // (TODO: emit metric nsi_reference_ambiguous_total).
    }
    return errors;
  }

  /**
   * Parse {@code "dict_name.field"} format. Validates что обе части
   * непустые и не содержат точек (чтобы избежать unexpected nested paths).
   *
   * <p>Default {@link OnCloseAction#BLOCK} — overload без property schema
   * не видит {@code x-references-on-close}. Используется внешними тестами +
   * ситуациями где cascade semantics нерелевантны (orphan scan, simple parse
   * checks). Internal call site в {@link #validate} использует
   * {@link #parseRef(String, JsonNode)} чтобы подхватить onClose из schema.
   */
  public static ParsedRef parseRef(String spec) {
    return parseRef(spec, null);
  }

  /**
   * Parse {@code "dict_name.field"} формат + {@code x-references-on-close}
   * sibling field из property schema. Если sibling отсутствует — onClose
   * defaultится в {@link OnCloseAction#BLOCK} (backward-compat с bundle
   * v1.2.1 где field не объявлен).
   *
   * @param spec       значение field {@code x-references}
   * @param propSchema parent JSON object (the property schema), может быть null
   */
  public static ParsedRef parseRef(String spec, JsonNode propSchema) {
    if (spec == null || spec.isBlank()) {
      throw new IllegalArgumentException("empty x-references");
    }
    int dot = spec.indexOf('.');
    if (dot <= 0 || dot == spec.length() - 1) {
      throw new IllegalArgumentException(
          "x-references must be 'dict_name.field', got: " + spec);
    }
    String dict = spec.substring(0, dot);
    String field = spec.substring(dot + 1);
    if (dict.contains(".") || field.contains(".")) {
      throw new IllegalArgumentException(
          "x-references nested paths not supported in v1: " + spec);
    }
    OnCloseAction onClose = OnCloseAction.BLOCK;
    if (propSchema != null && propSchema.isObject()) {
      JsonNode onCloseNode = propSchema.get("x-references-on-close");
      if (onCloseNode != null && !onCloseNode.isNull()) {
        if (!onCloseNode.isTextual()) {
          throw new IllegalArgumentException(
              "x-references-on-close must be a string, got: " + onCloseNode.getNodeType());
        }
        onClose = OnCloseAction.fromString(onCloseNode.asText());
      }
    }
    return new ParsedRef(dict, field, onClose);
  }

  /**
   * Parsed FK reference. {@code onClose} = что делать при close source record
   * на который ссылаются записи в этом dict (см. {@link OnCloseAction}).
   *
   * <p>Phase 1 prep: field читается parser'ом, но не используется в runtime —
   * cascade engine = Phase 3. Default BLOCK совпадает с текущим v1 поведением.
   */
  public record ParsedRef(String dictionaryName, String fieldName, OnCloseAction onClose) {
    /** Convenience ctor для тестов / call sites что ещё не работают с onClose. */
    public ParsedRef(String dictionaryName, String fieldName) {
      this(dictionaryName, fieldName, OnCloseAction.BLOCK);
    }
  }
}
