package cloud.nstart.terravault.ordinis.validation;

import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
import com.fasterxml.jackson.databind.JsonNode;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SchemaValidatorsConfig;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Двухступенчатая валидация записи:
 *
 * <ol>
 *   <li>Стандартный JSON Schema (Draft-7) через networknt — типы, обязательные
 *       поля, форматы, etc.</li>
 *   <li>Custom правило для полей с extension {@code x-localized: true}: значение
 *       должно быть object {@code {locale: text}}, обязательно содержит
 *       {@code default_locale} справочника, ключи matchать pattern локали.</li>
 * </ol>
 */
@Component
public class RecordValidator {

  private static final Pattern LOCALE_TAG = Pattern.compile("^[a-z]{2}-[A-Z]{2}$");

  private final JsonSchemaFactory schemaFactory =
      JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);

  /**
   * Валидирует {@code data} запись справочника против определения.
   * Возвращает {@link ValidationResult}, не бросает.
   */
  public ValidationResult validate(DictionaryDefinition definition, JsonNode data) {
    List<ValidationError> errors = new ArrayList<>();

    SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build();
    JsonSchema schema = schemaFactory.getSchema(definition.getSchemaJson(), config);

    Set<ValidationMessage> messages = schema.validate(data);
    for (ValidationMessage m : messages) {
      errors.add(new ValidationError(
          m.getInstanceLocation().toString(),
          m.getCode(),
          m.getMessage()));
    }

    Set<String> supportedLocales = new HashSet<>(List.of(definition.getSupportedLocales()));
    String defaultLocale = definition.getDefaultLocale();
    walkLocalizedFields(definition.getSchemaJson(), data, "", supportedLocales, defaultLocale, errors);

    return errors.isEmpty() ? ValidationResult.ok() : ValidationResult.failed(errors);
  }

  /**
   * Рекурсивный обход schema. Для каждого {@code properties[name]} с
   * {@code x-localized: true} — проверяет значение в data, учитывая required
   * массив родительской схемы (optional поля не требуют значения).
   */
  private void walkLocalizedFields(
      JsonNode schema,
      JsonNode data,
      String path,
      Set<String> supported,
      String defaultLocale,
      List<ValidationError> errors) {

    if (schema == null || !schema.isObject()) return;

    JsonNode properties = schema.get("properties");
    if (properties == null || !properties.isObject()) return;

    Set<String> requiredFields = new HashSet<>();
    JsonNode required = schema.get("required");
    if (required != null && required.isArray()) {
      required.forEach(n -> { if (n.isTextual()) requiredFields.add(n.asText()); });
    }

    Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
    while (it.hasNext()) {
      Map.Entry<String, JsonNode> entry = it.next();
      String fieldName = entry.getKey();
      JsonNode fieldSchema = entry.getValue();
      String fieldPath = path.isEmpty() ? "/" + fieldName : path + "/" + fieldName;

      JsonNode dataField = data == null ? null : data.get(fieldName);
      boolean fieldIsRequired = requiredFields.contains(fieldName);

      JsonNode xLocalized = fieldSchema.get("x-localized");
      if (xLocalized != null && xLocalized.isBoolean() && xLocalized.asBoolean()) {
        validateLocalizedValue(fieldPath, dataField, fieldIsRequired, supported, defaultLocale, errors);
      } else if (fieldSchema.has("properties")) {
        walkLocalizedFields(fieldSchema, dataField, fieldPath, supported, defaultLocale, errors);
      }
    }
  }

  private void validateLocalizedValue(
      String path,
      JsonNode value,
      boolean isRequired,
      Set<String> supported,
      String defaultLocale,
      List<ValidationError> errors) {

    if (value == null || value.isNull()) {
      if (isRequired) {
        errors.add(new ValidationError(path, "x-localized.missing",
            "Localized field is required (must be object with locale keys)"));
      }
      return;
    }
    if (!value.isObject()) {
      errors.add(new ValidationError(path, "x-localized.not_object",
          "Localized field must be an object {locale: text}"));
      return;
    }

    boolean hasDefault = false;
    Iterator<String> keys = value.fieldNames();
    while (keys.hasNext()) {
      String locale = keys.next();
      if (!LOCALE_TAG.matcher(locale).matches()) {
        errors.add(new ValidationError(path + "/" + locale, "x-localized.invalid_tag",
            "Locale tag must match pattern xx-XX (BCP 47), got: " + locale));
        continue;
      }
      if (!supported.contains(locale)) {
        errors.add(new ValidationError(path + "/" + locale, "x-localized.unsupported_locale",
            "Locale " + locale + " is not in supported_locales of dictionary"));
        continue;
      }
      JsonNode text = value.get(locale);
      if (text == null || !text.isTextual() || text.asText().isBlank()) {
        errors.add(new ValidationError(path + "/" + locale, "x-localized.empty_value",
            "Locale " + locale + " value must be non-empty string"));
      }
      if (locale.equals(defaultLocale)) hasDefault = true;
    }

    if (!hasDefault) {
      errors.add(new ValidationError(path, "x-localized.missing_default",
          "Default locale '" + defaultLocale + "' is required"));
    }
  }
}
