package cloud.nstart.terravault.ordinis.notifications.dispatcher;

import cloud.nstart.terravault.ordinis.domain.notification.NotificationLog;
import cloud.nstart.terravault.ordinis.domain.notification.NotificationLogRepository;
import cloud.nstart.terravault.ordinis.notifications.channel.ChannelKind;
import cloud.nstart.terravault.ordinis.notifications.event.NotificationEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

/**
 * Закрывает CRITICAL gap (idempotency) из v3 review.
 *
 * <p>Использует UNIQUE {@code (event_id, channel, recipient)} на
 * {@code notification_log} (см. migration 0021) + PostgreSQL
 * {@code INSERT ... ON CONFLICT DO NOTHING RETURNING} pattern.
 *
 * <p>Concurrent semantics:
 * <ul>
 *   <li>Two consumer threads parallel'но обрабатывают same event → один claim'ит row,
 *       другой получает empty Optional → skip.</li>
 *   <li>Consumer crash после email_OK + before slack → replay → idempotency guard
 *       видит email row (status=SENT) → skip; express row отсутствует → claim + send.</li>
 *   <li>Pod crash mid-INSERT-pending → row застряла PENDING → {@code PendingSweepJob}
 *       через 5 min DELETE → next consumer poll → re-claim + send.</li>
 * </ul>
 *
 * <p>{@code @Transactional(REQUIRES_NEW)} — claim isolated от outer dispatcher transaction
 * чтобы commit'ить claim immediately, before actual send. Send fail'ит → UPDATE status='FAILED'
 * via {@code markFailed()} в отдельной tx.
 */
// Gate must mirror NotificationsAutoConfiguration. Writer's @SpringBootApplication
// scanBasePackages = "cloud.nstart.terravault.ordinis" — superset, picks up
// этот @Component даже когда notifications module dormant. Без явного
// ConditionalOnProperty bean регается до того, как notification_log table
// создан → при первом invocation NPE / table-not-found.
@Component
@ConditionalOnProperty(name = "ordinis.notifications.enabled", havingValue = "true", matchIfMissing = false)
public class IdempotencyGuard {

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

  private final NotificationLogRepository repo;

  public IdempotencyGuard(NotificationLogRepository repo) {
    this.repo = repo;
  }

  /**
   * Try to claim the (event_id, channel, recipient) slot. Returns the claimed
   * row if INSERT succeeded, empty if already claimed (другой consumer / replay).
   *
   * @param event source event для idempotency key + audit context
   * @param channel target channel
   * @param recipient target recipient identifier (email / huid / chat_id)
   * @return populated NotificationLog if claimed (PENDING status), empty if conflict
   */
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public Optional<NotificationLog> tryClaim(
      NotificationEvent event, ChannelKind channel, String recipient) {

    Optional<Long> claimedId = repo.tryClaim(
        event.eventId(),
        event.eventType(),
        event.draft() != null ? event.draft().id() : null,
        channel.wireName(),
        recipient
    );

    if (claimedId.isEmpty()) {
      log.debug("Idempotency conflict — already claimed: event={}, channel={}, recipient={}",
          event.eventId(), channel, recipient);
      return Optional.empty();
    }

    return repo.findById(claimedId.get());
  }

  /**
   * Mark claimed row as SENT. Called после successful channel.dispatch().
   * @Transactional(REQUIRES_NEW) для commit'а independent от dispatcher loop.
   */
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void markSent(Long rowId) {
    repo.findById(rowId).ifPresent(row -> {
      row.markSent();
      repo.save(row);
    });
  }

  /** Mark claimed row as FAILED with error message. */
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void markFailed(Long rowId, String errorMsg) {
    repo.findById(rowId).ifPresent(row -> {
      row.markFailed(errorMsg);
      repo.save(row);
    });
  }

  /** Mark claimed row as RATE_LIMITED — recipient hit per-hour cap. */
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void markRateLimited(Long rowId) {
    repo.findById(rowId).ifPresent(row -> {
      row.markRateLimited();
      repo.save(row);
    });
  }

  /** Mark claimed row as SKIPPED_PREF — user opted out. */
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void markSkippedPref(Long rowId) {
    repo.findById(rowId).ifPresent(row -> {
      row.markSkippedPref();
      repo.save(row);
    });
  }

  /** Mark claimed row as SKIPPED_TOO_OLD — backlog burst guard fired. */
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void markSkippedTooOld(Long rowId) {
    repo.findById(rowId).ifPresent(row -> {
      row.markSkippedTooOld();
      repo.save(row);
    });
  }
}
