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

import cloud.nstart.terravault.ordinis.auth.ScopeContext;
import cloud.nstart.terravault.ordinis.domain.DataScope;
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookDelivery;
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookDeliveryRepository;
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscription;
import cloud.nstart.terravault.ordinis.domain.webhook.WebhookSubscriptionRepository;
import cloud.nstart.terravault.ordinis.restapi.dto.webhook.CreateWebhookSubscriptionRequest;
import cloud.nstart.terravault.ordinis.restapi.dto.webhook.WebhookDeliveryResponse;
import cloud.nstart.terravault.ordinis.restapi.dto.webhook.WebhookSubscriptionResponse;
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
import cloud.nstart.terravault.ordinis.restapi.service.webhook.WebhookSubscriptionService;
import cloud.nstart.terravault.ordinis.restapi.service.webhook.WebhookTestPingService;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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.util.List;
import java.util.UUID;

/**
 * Webhook subscriptions admin API. RBAC: only RESTRICTED scope can mutate
 * (create/update/delete/rotate-secret), INTERNAL+ может listing'и читать.
 *
 * <p>Endpoints:
 * <pre>
 *   GET    /api/v1/admin/webhooks/subscriptions       — list
 *   POST   /api/v1/admin/webhooks/subscriptions       — create (returns plaintext secret)
 *   GET    /api/v1/admin/webhooks/subscriptions/{id}  — get one
 *   PUT    /api/v1/admin/webhooks/subscriptions/{id}  — update
 *   DELETE /api/v1/admin/webhooks/subscriptions/{id}  — delete
 *   POST   /api/v1/admin/webhooks/subscriptions/{id}/rotate-secret
 *   GET    /api/v1/admin/webhooks/subscriptions/{id}/deliveries
 *   GET    /api/v1/admin/webhooks/deliveries/dlq
 * </pre>
 */
@RestController
@RequestMapping("/api/v1/admin/webhooks")
public class WebhookSubscriptionController {

  private final WebhookSubscriptionService service;
  private final WebhookSubscriptionRepository subscriptionRepository;
  private final WebhookDeliveryRepository deliveryRepository;
  private final WebhookTestPingService testPingService;
  private final ScopeContext scopeContext;

  public WebhookSubscriptionController(
      WebhookSubscriptionService service,
      WebhookSubscriptionRepository subscriptionRepository,
      WebhookDeliveryRepository deliveryRepository,
      WebhookTestPingService testPingService,
      ScopeContext scopeContext) {
    this.service = service;
    this.subscriptionRepository = subscriptionRepository;
    this.deliveryRepository = deliveryRepository;
    this.testPingService = testPingService;
    this.scopeContext = scopeContext;
  }

  // === Subscriptions CRUD ===

  @GetMapping("/subscriptions")
  public List<WebhookSubscriptionResponse> list() {
    requireInternal();
    return subscriptionRepository.findAll().stream()
        .map(WebhookSubscriptionResponse::from)
        .toList();
  }

  @GetMapping("/subscriptions/{id}")
  public WebhookSubscriptionResponse get(@PathVariable UUID id) {
    requireInternal();
    WebhookSubscription sub = service.findById(id);
    return WebhookSubscriptionResponse.from(sub);
  }

  @PostMapping("/subscriptions")
  @ResponseStatus(HttpStatus.CREATED)
  public WebhookSubscriptionResponse create(
      @Valid @RequestBody CreateWebhookSubscriptionRequest req) {
    requireAdmin();
    WebhookSubscriptionService.CreateResult result = service.create(req, currentUserId());
    // Plaintext secret виден ТОЛЬКО здесь — caller должен сохранить.
    return WebhookSubscriptionResponse.fromWithSecret(result.subscription(), result.plaintextSecret());
  }

  @PutMapping("/subscriptions/{id}")
  public WebhookSubscriptionResponse update(
      @PathVariable UUID id,
      @Valid @RequestBody CreateWebhookSubscriptionRequest req) {
    requireAdmin();
    return WebhookSubscriptionResponse.from(service.update(id, req));
  }

  @DeleteMapping("/subscriptions/{id}")
  @ResponseStatus(HttpStatus.NO_CONTENT)
  public void delete(@PathVariable UUID id) {
    requireAdmin();
    service.delete(id);
  }

  @PostMapping("/subscriptions/{id}/rotate-secret")
  public WebhookSubscriptionResponse rotateSecret(@PathVariable UUID id) {
    requireAdmin();
    WebhookSubscriptionService.CreateResult result = service.rotateSecret(id);
    return WebhookSubscriptionResponse.fromWithSecret(result.subscription(), result.plaintextSecret());
  }

  // === Delivery history ===

  @GetMapping("/subscriptions/{id}/deliveries")
  public Page<WebhookDeliveryResponse> deliveries(
      @PathVariable UUID id,
      @RequestParam(defaultValue = "0") int page,
      @RequestParam(defaultValue = "50") int size,
      @RequestParam(value = "status", required = false) String statusFilter) {
    requireInternal();
    PageRequest pr = PageRequest.of(page, Math.min(size, 200));
    Page<WebhookDelivery> deliveries;
    if (statusFilter != null && !statusFilter.isBlank()) {
      WebhookDelivery.Status s;
      try {
        s = WebhookDelivery.Status.valueOf(statusFilter.toLowerCase());
      } catch (IllegalArgumentException e) {
        throw OrdinisException.badRequest(
            "webhook_status_invalid",
            "Unknown delivery status: " + statusFilter
                + ". Allowed: pending, retrying, delivered, dlq.");
      }
      deliveries = deliveryRepository
          .findBySubscriptionIdAndStatusOrderByCreatedAtDesc(id, s, pr);
    } else {
      deliveries = deliveryRepository
          .findBySubscriptionIdOrderByCreatedAtDesc(id, pr);
    }
    return deliveries.map(WebhookDeliveryResponse::from);
  }

  @GetMapping("/deliveries/dlq")
  public Page<WebhookDeliveryResponse> dlq(
      @RequestParam(defaultValue = "0") int page,
      @RequestParam(defaultValue = "50") int size) {
    requireInternal();
    Page<WebhookDelivery> deliveries = deliveryRepository.findByStatus(
        WebhookDelivery.Status.dlq,
        PageRequest.of(page, Math.min(size, 200), Sort.by(Sort.Direction.DESC, "dlqAt")));
    return deliveries.map(WebhookDeliveryResponse::from);
  }

  /**
   * Replay delivery из DLQ или retrying state — сбрасывает attemptCount=0,
   * dlqAt=null, status=retrying, nextAttemptAt=now. Dispatcher подхватит на
   * следующем sendTick.
   *
   * <p>Use case: оператор fixed receiver URL после deploy/cert renewal,
   * хочет replay'нуть failed delivery без ожидания TTL bypass'а.
   *
   * <p>RBAC: RESTRICTED — это destructive op (можно spam'ить receiver).
   */
  /**
   * Synchronous test ping — admin кликает кнопку в UI, receiver получает
   * synthetic event с теми же headers/HMAC что у production delivery.
   * Возвращает result inline для immediate feedback (latency, http status,
   * body preview / error).
   *
   * <p>Не порождает webhook_deliveries row — это test, не business event.
   * RBAC: RESTRICTED — потому что отправляет HTTP request на user-controlled URL.
   */
  @PostMapping("/subscriptions/{id}/test")
  public WebhookTestPingService.Result testSubscription(@PathVariable UUID id) {
    requireAdmin();
    WebhookSubscription sub = service.findById(id);
    return testPingService.testPing(sub);
  }

  @PostMapping("/deliveries/{id}/retry")
  public WebhookDeliveryResponse retryDelivery(@PathVariable Long id) {
    requireAdmin();
    WebhookDelivery delivery = deliveryRepository.findById(id)
        .orElseThrow(() -> OrdinisException.notFound(
            "webhook_delivery_not_found",
            "Webhook delivery " + id + " not found."));
    delivery.resetDlq();
    deliveryRepository.save(delivery);
    return WebhookDeliveryResponse.from(delivery);
  }

  // === Helpers ===

  /** RESTRICTED scope обязателен для mutating endpoints. */
  private void requireAdmin() {
    if (!scopeContext.canAccess(DataScope.RESTRICTED)) {
      throw OrdinisException.forbidden(
          "webhook_admin_required",
          "Webhook subscription management requires RESTRICTED scope (admin role).");
    }
  }

  /** INTERNAL+ scope для read endpoints. PUBLIC отказывается. */
  private void requireInternal() {
    if (!scopeContext.canAccess(DataScope.INTERNAL)) {
      throw OrdinisException.forbidden(
          "webhook_internal_required",
          "Webhook subscriptions accessible only with INTERNAL or RESTRICTED scope.");
    }
  }

  private static String currentUserId() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    return auth == null ? "anonymous" : auth.getName();
  }
}
