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

import cloud.nstart.terravault.ordinis.auth.ScopeContext;
import cloud.nstart.terravault.ordinis.domain.DataScope;
import cloud.nstart.terravault.ordinis.restapi.dto.BulkCloseRequest;
import cloud.nstart.terravault.ordinis.restapi.dto.BulkCloseResponse;
import cloud.nstart.terravault.ordinis.restapi.dto.BulkExportRequest;
import cloud.nstart.terravault.ordinis.restapi.dto.CreateRecordRequest;
import cloud.nstart.terravault.ordinis.restapi.dto.RecordResponse;
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
import cloud.nstart.terravault.ordinis.restapi.service.BulkRecordService;
import cloud.nstart.terravault.ordinis.restapi.service.DictionaryDefinitionService;
import cloud.nstart.terravault.ordinis.restapi.service.DictionaryRecordService;
import cloud.nstart.terravault.ordinis.restapi.service.reference.CascadeCloseService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequestMapping("/api/v1/dictionaries/{name}/records")
public class DictionaryRecordController {

  private final DictionaryRecordService service;
  private final BulkRecordService bulkService;
  private final DictionaryDefinitionService definitionService;
  private final ScopeContext scopeContext;
  private final CascadeCloseService cascadeService;

  public DictionaryRecordController(
      DictionaryRecordService service,
      BulkRecordService bulkService,
      DictionaryDefinitionService definitionService,
      ScopeContext scopeContext,
      CascadeCloseService cascadeService) {
    this.service = service;
    this.bulkService = bulkService;
    this.definitionService = definitionService;
    this.scopeContext = scopeContext;
    this.cascadeService = cascadeService;
  }

  @GetMapping("/{businessKey}")
  public RecordResponse getActive(
      @PathVariable String name,
      @PathVariable String businessKey,
      @RequestParam(required = false) OffsetDateTime at) {
    requireReadAccess(name);
    OffsetDateTime when = at == null ? OffsetDateTime.now() : at;
    return service.findActive(name, businessKey, when)
        .map(RecordResponse::from)
        .orElseThrow(() -> OrdinisException.notFound(
            "record_not_active",
            "Нет активной записи business_key=" + businessKey + " на " + when));
  }

  @GetMapping("/{businessKey}/history")
  public List<RecordResponse> getHistory(
      @PathVariable String name,
      @PathVariable String businessKey) {
    requireReadAccess(name);
    return service.findHistory(name, businessKey).stream().map(RecordResponse::from).toList();
  }

  @PostMapping
  @ResponseStatus(HttpStatus.CREATED)
  public RecordResponse create(
      @PathVariable String name,
      @Valid @RequestBody CreateRecordRequest req) {
    requireWriteAccess(name);
    return RecordResponse.from(service.create(name, req));
  }

  @PutMapping("/{businessKey}")
  public RecordResponse update(
      @PathVariable String name,
      @PathVariable String businessKey,
      @Valid @RequestBody CreateRecordRequest req) {
    requireWriteAccess(name);
    return RecordResponse.from(service.update(name, businessKey, req));
  }

  @DeleteMapping("/{businessKey}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void close(
      @PathVariable String name,
      @PathVariable String businessKey,
      @RequestParam(required = false) OffsetDateTime at,
      @RequestParam(required = false) String reason) {
    requireWriteAccess(name);
    service.close(name, businessKey, at, reason);
  }

  /**
   * Cascade preview (Phase 3): READ-ONLY оценка cascade plan. Ничего не
   * меняет. UI вызывает перед close чтобы показать "this will close N
   * records: ...".
   *
   * <p>Возвращает 200 с {@link CascadeCloseService.CascadePlan} независимо
   * от того есть ли blockers — UI сам отображает разные иконки per mode.
   */
  @GetMapping("/{businessKey}/cascade-preview")
  public CascadeCloseService.CascadePlan cascadePreview(
      @PathVariable String name,
      @PathVariable String businessKey,
      @RequestParam(required = false) OffsetDateTime at) {
    requireReadAccess(name);
    return cascadeService.evaluatePlan(name, businessKey, at);
  }

  /**
   * Cascade close (Phase 3): atomic close target + all CASCADE dependents.
   * Требует {@code confirmed=true} в query params когда план содержит
   * cascade entries (UX guard против accidental cascade).
   *
   * <p>Errors:
   * <ul>
   *   <li>409 {@code x_references_blocked_by_dependents} — есть BLOCK deps.</li>
   *   <li>409 {@code x_references_cascade_required} — есть CASCADE deps,
   *       но {@code confirmed=false}.</li>
   *   <li>400 {@code x_references_cascade_too_large} — cascade size {@literal >} 500.</li>
   *   <li>503 {@code x_references_cascade_timeout} — tx exceeded 30s.</li>
   * </ul>
   */
  @PostMapping("/{businessKey}/cascade-close")
  public CascadeCloseService.CascadeCloseResult cascadeClose(
      @PathVariable String name,
      @PathVariable String businessKey,
      @RequestParam(required = false) OffsetDateTime at,
      @RequestParam(required = false) String reason,
      @RequestParam(defaultValue = "false") boolean confirmed) {
    requireWriteAccess(name);
    return cascadeService.executeCascade(name, businessKey, at, reason, confirmed);
  }

  /**
   * Bulk close: закрыть N записей одним запросом. Per-record транзакция —
   * partial success возможен (e.g. 7 из 10 закрылись, 3 уже были закрыты).
   * Используется multi-select toolbar в admin UI.
   */
  @PostMapping("/bulk-close")
  public BulkCloseResponse bulkClose(
      @PathVariable String name,
      @Valid @RequestBody BulkCloseRequest req) {
    requireWriteAccess(name);
    return bulkService.bulkClose(name, req);
  }

  /**
   * Bulk export: выгрузить N выбранных записей в CSV. POST (не GET) потому что
   * списком >100 ключей URL ломается. Auth header attached через axios →
   * frontend получает blob и триггерит download.
   *
   * <p>RequireReadAccess: для экспорта достаточно прав чтения.
   */
  @PostMapping(value = "/export-selected.csv", produces = "text/csv")
  public org.springframework.http.ResponseEntity<String> bulkExportCsv(
      @PathVariable String name,
      @Valid @RequestBody BulkExportRequest req) {
    requireReadAccess(name);
    String csv = bulkService.exportCsv(name, req.businessKeys());
    String filename = name + "-selected-" + OffsetDateTime.now().toLocalDate() + ".csv";
    return org.springframework.http.ResponseEntity.ok()
        .header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
        .header("Content-Type", "text/csv; charset=UTF-8")
        .body(csv);
  }

  /**
   * 404 если scope словаря недоступен текущему пользователю — намеренно скрываем
   * существование записи (security through obscurity), как делает read-api.
   */
  private void requireReadAccess(String dictionaryName) {
    DataScope dictScope = definitionService.findByName(dictionaryName).getScope();
    if (!scopeContext.canAccess(dictScope)) {
      throw OrdinisException.notFound(
          "dictionary_not_found", "Dictionary not found: " + dictionaryName);
    }
  }

  /**
   * 403 для записи: пользователь должен иметь scope ≥ scope словаря. PUBLIC-юзер
   * не имеет права создавать/менять/закрывать INTERNAL/RESTRICTED записи даже
   * если знает имя словаря.
   */
  private void requireWriteAccess(String dictionaryName) {
    DataScope dictScope = definitionService.findByName(dictionaryName).getScope();
    if (!scopeContext.canAccess(dictScope)) {
      throw OrdinisException.forbidden(
          "scope_insufficient",
          "Недостаточно прав scope для записи в словарь '" + dictionaryName + "'.");
    }
  }
}
