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

import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecord;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
import cloud.nstart.terravault.ordinis.restapi.dto.BulkCloseRequest;
import cloud.nstart.terravault.ordinis.restapi.dto.BulkCloseResponse;
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

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

/**
 * Bulk операции над records. Каждый record закрывается в своей транзакции
 * (через инжект {@link DictionaryRecordService}, чтобы Spring AOP proxy
 * подхватил {@code @Transactional} на каждом вызове). Если 3 из 10 уже
 * закрыты — остальные 7 закроются. Atomic guarantee только per-record,
 * не на весь bulk.
 *
 * <p>Не @Transactional само по себе — иначе единственная транзакция накроет
 * весь цикл и любая ошибка откатит всё, что мы НЕ хотим.
 */
@Service
public class BulkRecordService {

  private static final Logger log = LoggerFactory.getLogger(BulkRecordService.class);

  private final DictionaryRecordService recordService;
  private final DictionaryDefinitionService definitionService;
  private final DictionaryRecordRepository repository;

  public BulkRecordService(
      DictionaryRecordService recordService,
      DictionaryDefinitionService definitionService,
      DictionaryRecordRepository repository) {
    this.recordService = recordService;
    this.definitionService = definitionService;
    this.repository = repository;
  }

  /**
   * Закрыть N records по business_key. Дубликаты в списке дедуплицируются.
   * Empty/blank business_key пропускается (попадает в skipped с reason=invalid_key).
   */
  public BulkCloseResponse bulkClose(String dictionaryName, BulkCloseRequest req) {
    List<String> closed = new ArrayList<>();
    List<BulkCloseResponse.SkippedItem> skipped = new ArrayList<>();
    List<BulkCloseResponse.ErrorItem> errors = new ArrayList<>();

    // Дедуп: если admin кликнул bulk close на одном records'е дважды —
    // не пытаться закрыть его повторно во второй итерации.
    var seen = new java.util.LinkedHashSet<String>();
    for (String bk : req.businessKeys()) {
      if (bk == null || bk.isBlank()) {
        skipped.add(new BulkCloseResponse.SkippedItem(String.valueOf(bk), "invalid_key"));
        continue;
      }
      if (!seen.add(bk)) {
        // Дубликат внутри batch — silently skip, не error
        continue;
      }

      try {
        recordService.close(dictionaryName, bk, req.at(), req.reason());
        closed.add(bk);
      } catch (OrdinisException e) {
        // record_not_active = повторный close или race с другим админом
        if ("record_not_active".equals(e.getCode())) {
          skipped.add(new BulkCloseResponse.SkippedItem(bk, "already_closed"));
        } else {
          errors.add(new BulkCloseResponse.ErrorItem(bk, e.getCode() + ": " + e.getMessage()));
        }
      } catch (Exception e) {
        log.warn("Bulk close: unexpected error для business_key={} в словаре {}", bk, dictionaryName, e);
        errors.add(new BulkCloseResponse.ErrorItem(bk, e.getClass().getSimpleName() + ": " + e.getMessage()));
      }
    }

    log.info("Bulk close в {} : closed={} skipped={} errors={}",
        dictionaryName, closed.size(), skipped.size(), errors.size());

    return new BulkCloseResponse(closed, skipped, errors);
  }

  /**
   * Экспорт N записей в CSV. Возвращает строку с заголовком + строками данных.
   * Колонки: {@code business_key, valid_from, valid_to, scope, data} —
   * последняя содержит весь JSONB как одну экранированную ячейку.
   *
   * <p>Решение писать data inline (не flatten'ить per-key): схемы разные между
   * dictionaries, fixed columns не подходит для универсального экспорта.
   * Получатель (Excel/jq/python) распарсит JSON ячейку у себя.
   *
   * <p>Только активные на now версии (не история) — для history использовать
   * {@code GET /records/{key}/history}.
   *
   * @param dictionaryName словарь
   * @param businessKeys список ключей (1..500)
   * @return CSV string
   */
  public String exportCsv(String dictionaryName, List<String> businessKeys) {
    DictionaryDefinition def = definitionService.findByName(dictionaryName);
    OffsetDateTime now = OffsetDateTime.now();

    // Дедуп + skip blank — чтобы не делать N запросов на дублях
    var seen = new java.util.LinkedHashSet<String>();
    for (String bk : businessKeys) {
      if (bk != null && !bk.isBlank()) seen.add(bk);
    }

    // Index активных записей по business_key для O(1) lookup
    Map<String, DictionaryRecord> active = new java.util.HashMap<>();
    for (String bk : seen) {
      repository.findActiveAt(def.getId(), bk, now).ifPresent(r -> active.put(bk, r));
    }

    StringBuilder csv = new StringBuilder("business_key,valid_from,valid_to,scope,data\n");
    // Сохраняем порядок входа (после dedup) — admin ожидает что строки в CSV
    // совпадают с тем что он выбрал
    for (String bk : seen) {
      DictionaryRecord r = active.get(bk);
      csv.append(csvCell(bk)).append(',');
      if (r == null) {
        // Запись не активна / не найдена — отдаём строку с пустыми ячейками,
        // чтобы admin видел в CSV что бизнес-ключ обработан, но данных нет
        csv.append(",,,\n");
        continue;
      }
      csv.append(r.getValidFrom() == null ? "" : r.getValidFrom().toString()).append(',')
          .append(r.getValidTo() == null ? "" : r.getValidTo().toString()).append(',')
          .append(r.getDataScope() == null ? "" : r.getDataScope().name()).append(',')
          .append(csvCell(r.getData() == null ? "" : r.getData().toString()))
          .append('\n');
    }

    log.info("Bulk export CSV в {} : requested={} found={}",
        dictionaryName, seen.size(), active.size());
    return csv.toString();
  }

  /** RFC 4180 экранирование: запятая/кавычка/перенос строки → quote-wrap. */
  private static String csvCell(String s) {
    if (s == null) return "";
    if (s.indexOf(',') < 0 && s.indexOf('"') < 0 && s.indexOf('\n') < 0) return s;
    return "\"" + s.replace("\"", "\"\"") + "\"";
  }
}
