package cloud.nstart.terravault.ordinis.domain.notification;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;

import java.time.OffsetDateTime;
import java.util.UUID;

/**
 * Per-recipient notification delivery audit row.
 *
 * <p>UNIQUE {@code (event_id, channel, recipient)} — primary idempotency guard
 * (см. migration 0021 + design doc D2). Insert pattern в
 * {@code IdempotencyGuard.shouldSend()}:
 *
 * <pre>{@code
 * INSERT INTO notification_log (event_id, channel, recipient, status, sent_at)
 * VALUES (?, ?, ?, 'PENDING', NOW())
 * ON CONFLICT (event_id, channel, recipient) DO NOTHING
 * RETURNING id;
 * }</pre>
 *
 * <p>Если RETURNING пустой — другой consumer уже claim'ил отправку (или sweep
 * job не освободил). Else — мы owner, dispatch + UPDATE status final.
 *
 * <p>Status enum UPPERCASE (Hibernate {@code @Enumerated(EnumType.STRING)}
 * lesson из v1.1.0 P3 — migration 0020 fix). DB CHECK constraint matches
 * Java enum names exactly.
 */
@Entity
@Table(
    name = "notification_log",
    uniqueConstraints = {
        // Mirrors notification_log_idempotency UNIQUE constraint в migration 0021.
        @UniqueConstraint(
            name = "notification_log_idempotency",
            columnNames = {"event_id", "channel", "recipient"}
        )
    },
    indexes = {
        @Index(name = "idx_notif_log_draft", columnList = "draft_id"),
        @Index(name = "idx_notif_log_recipient_time", columnList = "recipient,sent_at DESC"),
        @Index(name = "idx_notif_log_event", columnList = "event_id")
    }
)
public class NotificationLog {

  /** Lifecycle status. UPPERCASE — matches CHECK constraint в 0021. */
  public enum Status {
    /** Inserted but not yet dispatched (или crashed mid-dispatch). Pending sweep job
     *  cleans up stuck rows старше {@code notifications.pendingSweep.olderThanMs}. */
    PENDING,
    /** Successfully sent через channel. Idempotency lock — не replay'ится. */
    SENT,
    /** Channel returned non-retryable error (auth, malformed, etc). */
    FAILED,
    /** Rate limiter denied — recipient hit per-hour cap. */
    RATE_LIMITED,
    /** User opted out этого channel в user_notification_prefs. */
    SKIPPED_PREF,
    /** Event старше maxEventAgeHours — backlog burst guard fired. */
    SKIPPED_TOO_OLD,
    /** One-click unsubscribe — токен HMAC validated, prefs.enabled=false. */
    UNSUBSCRIBED
  }

  /** Wire format channel — lowercase strings ('email', 'express', 'telegram')
   *  matches CHECK constraint и Helm config keys. NOT EnumType.STRING на channel
   *  потому что хотим lowercase (брендовые токены), не Java UPPERCASE conv. */
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  @Column(name = "event_id", nullable = false)
  private UUID eventId;

  @Column(name = "event_type", nullable = false, length = 64)
  private String eventType;

  /** Nullable: некоторые events без draft контекста (future expansion). */
  @Column(name = "draft_id")
  private UUID draftId;

  /** Lower-case wire format. См. {@code ChannelKind.wireName()} в notifications module. */
  @Column(name = "channel", nullable = false, length = 16)
  private String channel;

  /** Email / express chat_id (UUID string) / telegram chat_id (numeric string). */
  @Column(name = "recipient", nullable = false, length = 256)
  private String recipient;

  @Enumerated(EnumType.STRING)
  @Column(name = "status", nullable = false, length = 20)
  private Status status;

  @Column(name = "sent_at", nullable = false)
  private OffsetDateTime sentAt;

  @Column(name = "error_msg", columnDefinition = "TEXT")
  private String errorMsg;

  protected NotificationLog() {}

  /** Constructs row in PENDING state — typical entry point для IdempotencyGuard. */
  public NotificationLog(UUID eventId, String eventType, UUID draftId,
                         String channel, String recipient) {
    this.eventId = eventId;
    this.eventType = eventType;
    this.draftId = draftId;
    this.channel = channel;
    this.recipient = recipient;
    this.status = Status.PENDING;
    this.sentAt = OffsetDateTime.now();
  }

  public void markSent() {
    this.status = Status.SENT;
    this.errorMsg = null;
  }

  public void markFailed(String error) {
    this.status = Status.FAILED;
    this.errorMsg = error;
  }

  public void markRateLimited() {
    this.status = Status.RATE_LIMITED;
  }

  public void markSkippedPref() {
    this.status = Status.SKIPPED_PREF;
  }

  public void markSkippedTooOld() {
    this.status = Status.SKIPPED_TOO_OLD;
  }

  public void markUnsubscribed() {
    this.status = Status.UNSUBSCRIBED;
  }

  // Getters (no setters — entity мутирует через named transitions)
  public Long getId() { return id; }
  public UUID getEventId() { return eventId; }
  public String getEventType() { return eventType; }
  public UUID getDraftId() { return draftId; }
  public String getChannel() { return channel; }
  public String getRecipient() { return recipient; }
  public Status getStatus() { return status; }
  public OffsetDateTime getSentAt() { return sentAt; }
  public String getErrorMsg() { return errorMsg; }
}
