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

import cloud.nstart.terravault.ordinis.auth.ScopeContext;
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
import cloud.nstart.terravault.ordinis.restapi.service.reference.LineageIndexService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Lineage / dependents read-side. Phase 1 dict-relationships-v2 epic.
 *
 * <p>Два endpoint'а:
 * <ul>
 *   <li>{@code GET /api/v1/dictionaries/{name}/dependents} — schema-level
 *       reverse FK list ("какие словари ссылаются на этот словарь и через
 *       какие поля").</li>
 *   <li>{@code GET /api/v1/records/{dict}/{businessKey}/dependents} —
 *       record-level dependents (paginated, per-source counts).</li>
 * </ul>
 *
 * <p>Errors:
 * <ul>
 *   <li>404 {@code dictionary_not_found} — target dict не существует или
 *       не виден caller'у в его scopes (одинаковая ошибка чтобы не leak'ало
 *       existence).</li>
 *   <li>404 {@code record_not_active} — нет active record на now().</li>
 *   <li>400 {@code invalid_pagination} — page/size вне допустимого диапазона.</li>
 * </ul>
 */
@RestController
@RequestMapping("/api/v1")
public class LineageController {

  private final LineageIndexService lineage;
  private final ScopeContext scopeContext;

  public LineageController(LineageIndexService lineage, ScopeContext scopeContext) {
    this.lineage = lineage;
    this.scopeContext = scopeContext;
  }

  /**
   * Schema-level reverse FK list. Empty list если target не существует
   * или не виден — consistent с CRUD endpoints (no leak existence).
   *
   * <p>Возврат: list объектов
   * {@code {sourceDict, sourceDisplayName, sourceField, targetField,
   *  onClose, activeRecordsInSourceDict}}.
   */
  @GetMapping("/dictionaries/{name}/dependents")
  public List<LineageIndexService.SchemaDependent> schemaDependents(
      @PathVariable String name) {
    return lineage.findSchemaDependents(name, scopeContext.currentScopes());
  }

  /**
   * Record-level dependents для конкретной record (target_dict + business_key).
   *
   * <p>Pagination: {@code page} (default 0), {@code size} (default 50, max 200).
   *
   * @return объект {@code {targetDict, targetBusinessKey, items[], perSource[],
   *  total, page, size}}.
   */
  @GetMapping("/records/{dict}/{businessKey}/dependents")
  public LineageIndexService.RecordDependentsPage recordDependents(
      @PathVariable String dict,
      @PathVariable String businessKey,
      @RequestParam(defaultValue = "0") int page,
      @RequestParam(defaultValue = "50") int size) {
    if (page < 0 || size < 1 || size > 200) {
      throw OrdinisException.badRequest(
          "invalid_pagination",
          "page must be >= 0 and size 1..200; got page=" + page + " size=" + size);
    }
    try {
      return lineage.findRecordDependents(
          dict, businessKey, scopeContext.currentScopes(), page, size);
    } catch (IllegalArgumentException e) {
      String msg = e.getMessage() == null ? "" : e.getMessage();
      if (msg.startsWith("dictionary not found")) {
        throw OrdinisException.notFound("dictionary_not_found", msg);
      }
      if (msg.startsWith("active record not found")) {
        throw OrdinisException.notFound("record_not_active", msg);
      }
      throw OrdinisException.badRequest("invalid_request", msg);
    }
  }

  /**
   * Batch outgoing FK summary. Для admin-UI catalog list:
   * frontend нужен сколько outgoing FK refs у каждого dict и список target
   * names — раньше парсил schemaJson per-row на клиенте, теперь один
   * batch endpoint.
   *
   * <p>Query: {@code GET /api/v1/dictionaries/graph/outgoing?dicts=a,b,c}
   *
   * <ul>
   *   <li>{@code dicts} — optional CSV filter. Null/empty = все visible.</li>
   * </ul>
   *
   * <p>Response: {@code { "outgoing": { "<name>": { "fkCount", "targets" } } }}.
   * Dicts вне scope caller'а skip — same hide pattern что и {@code GET /dictionaries}.
   *
   * @return wrapped в outer object для будущего extension (например meta).
   */
  @GetMapping("/dictionaries/graph/outgoing")
  public OutgoingGraphResponse outgoingGraph(
      @RequestParam(required = false) String dicts) {
    Set<String> filter = parseDictsFilter(dicts);
    Map<String, LineageIndexService.OutgoingFkInfo> outgoing =
        lineage.findOutgoingFkSummary(scopeContext.currentScopes(), filter);
    return new OutgoingGraphResponse(outgoing);
  }

  /** Parse CSV "a,b,c" → Set. Returns null если param empty (means "all"). */
  private static Set<String> parseDictsFilter(String dictsCsv) {
    if (dictsCsv == null || dictsCsv.isBlank()) return null;
    return Arrays.stream(dictsCsv.split(","))
        .map(String::trim)
        .filter(s -> !s.isEmpty())
        .collect(Collectors.toSet());
  }

  /**
   * Wrapper для batch outgoing graph response. Inner map не серилизуется
   * напрямую (consumer'у удобнее видеть {@code outgoing: { ... }} ключ —
   * легче добавить sibling fields позже без breaking change).
   */
  public record OutgoingGraphResponse(
      Map<String, LineageIndexService.OutgoingFkInfo> outgoing) {}
}
