from django.conf import settings
from django.db import models
from django.utils import timezone


FREE_DRAW_COOLDOWN_HOURS = 24

# Total hand size per spread — matches DRAW_ORDER in my_sea.html JS.
# Used by `is_hand_complete` to decide whether the picker UX is in
# "still drawing" (AUTO DRAW visible, DEL disabled, FLIPs enabled)
# vs "completed" (GATE VIEW visible, DEL enabled, FLIPs disabled) state.
HAND_SIZE_BY_SPREAD = {
    "past-present-future":      3,
    "situation-action-outcome": 3,
    "mind-body-spirit":         3,
    "desire-obstacle-solution": 3,
    "waite-smith":              6,
    "escape-velocity":          6,
}


class MySeaDraw(models.Model):
    """Persisted Celtic-Cross-style tarot draw for the solo-user My Sea
    feature. Each row is the user's active draw — both the hand state
    AND the 24h quota tracker.

    Sprint 5 iter 4c of [[project-my-sea-roadmap]] — refactor of the
    iter-4b LOCK-HAND model. The row is created on the FIRST card draw
    (manual or AUTO DRAW), not on a separate LOCK action. Quota is
    committed at first-card moment + survives a DEL (DEL clears the
    `hand` JSON but preserves the row — so `created_at` keeps running
    the 24h clock + landing renders GATE VIEW instead of FREE DRAW
    until the row expires).

    `hand` is an ordered list of position-dicts in draw order — Sprint 7's
    applet renders them left-to-right in that order. Each entry shape:

        {"position": "lay", "card_id": 42, "reversed": false, "polarity": "gravity"}

    For mid-draw rows, `hand` is partial (1+ entries < HAND_SIZE_BY_SPREAD).
    For DEL'd rows, `hand` is `[]` but `created_at` still anchors the
    quota window.

    `significator_id` + `significator_reversed` snapshot the user's sig
    at first-card-draw time so a subsequent `User.significator = None`
    (via my-sign DEL) doesn't invalidate the saved draw.
    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="my_sea_draws",
    )
    spread = models.CharField(max_length=40)
    hand = models.JSONField(default=list)
    significator_id = models.IntegerField()
    significator_reversed = models.BooleanField(default=False)
    created_at = models.DateTimeField(default=timezone.now)
    # Sprint 5 iter 6a — gatekeeper deposit lifecycle. A token can be
    # RESERVED on the row (refundable until the user clicks PAID DRAW)
    # via the my-sea gatekeeper at /gameboard/my-sea/gate/. PAID DRAW
    # commits the deposit + resets the row (`hand=[]`, `created_at=now`,
    # both deposit_* fields back to None), starting a fresh 24h quota
    # cycle paid for by the deposited token. See `_select_my_sea_token`
    # + `debit_my_sea_token` for the priority chain + per-type rules.
    deposit_token_id = models.IntegerField(null=True, blank=True)
    deposit_reserved_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return f"MySeaDraw({self.user_id}, {self.spread}, {self.created_at:%Y-%m-%d %H:%M})"

    @property
    def next_free_draw_at(self):
        """Datetime when the user's next free draw will be available
        (created_at + FREE_DRAW_COOLDOWN_HOURS)."""
        return self.created_at + timezone.timedelta(hours=FREE_DRAW_COOLDOWN_HOURS)

    @property
    def is_within_quota_window(self):
        """True iff this draw was created within the last
        FREE_DRAW_COOLDOWN_HOURS — i.e., it currently occupies the user's
        free-draw slot."""
        return self.created_at >= (
            timezone.now() - timezone.timedelta(hours=FREE_DRAW_COOLDOWN_HOURS)
        )

    @property
    def is_hand_complete(self):
        """True iff the hand has filled every position for its spread.
        Drives the picker-form-col button state (AUTO DRAW → GATE VIEW
        + DEL enable + FLIPs disable on the deck stacks)."""
        return len(self.hand or []) >= HAND_SIZE_BY_SPREAD.get(self.spread, 0)

    @property
    def is_hand_empty(self):
        """True iff the hand carries no entries — the post-DEL state,
        where the row stays as a quota tracker but the cards are gone."""
        return not self.hand

    @classmethod
    def delete_stale(cls):
        """Hard-delete every draw older than FREE_DRAW_COOLDOWN_HOURS.
        Returns the deletion count.

        Called lazily from `active_draw_for` on every view access (so
        the cleanup naturally rides user traffic — no scheduler needed
        for the common case) AND from the `delete_stale_my_sea_draws`
        management command (cron backstop for inactive periods)."""
        cutoff = timezone.now() - timezone.timedelta(hours=FREE_DRAW_COOLDOWN_HOURS)
        deleted, _ = cls.objects.filter(created_at__lt=cutoff).delete()
        return deleted


def _select_my_sea_token(user):
    """Token-picker for the my-sea gatekeeper. Mirrors `apps.epic.models.
    select_token` priority chain (PASS → COIN → FREE → TITHE) w. two
    iter-6a tweaks per user spec 2026-05-20:

    - **CARTE excluded.** The door-spell trinket isn't a valid token
      for paying my-sea draws.
    - **COIN cooldown-respecting.** After a my-sea PAID DRAW debits a
      COIN, `debit_my_sea_token` sets `next_ready_at = now + 24h` (not
      the room's 7-day window). A cooldown'd COIN is unavailable to
      this picker; standard `select_token` ignores `next_ready_at` so
      room logic stays untouched (intentional: my-sea is the only path
      that respects this cooldown).
    """
    from django.db.models import Q
    from apps.lyric.models import Token
    now = timezone.now()
    if user.is_staff:
        pass_token = user.tokens.filter(token_type=Token.PASS).first()
        if pass_token:
            return pass_token
    coin = user.tokens.filter(
        token_type=Token.COIN, current_room__isnull=True,
    ).filter(
        Q(next_ready_at__isnull=True) | Q(next_ready_at__lte=now),
    ).first()
    if coin:
        return coin
    free = user.tokens.filter(
        token_type=Token.FREE, expires_at__gt=now,
    ).order_by("expires_at").first()
    if free:
        return free
    return user.tokens.filter(token_type=Token.TITHE).first()


def debit_my_sea_token(user, token):
    """Commit a my-sea-deposited token. Adapted from `apps.epic.models.
    debit_token` for solo my-sea per user spec 2026-05-20:

    - **CARTE → ValueError.** Defensive guard; caller should validate
      upstream via `_select_my_sea_token`.
    - **COIN.** No `current_room` (there is no room context); instead,
      set `next_ready_at = now + 24h` so the next-pickup gate kicks in.
      Unequip from the kit if equipped (parity w. room's COIN behavior).
    - **PASS.** No consumption — auto-admit. Stays equipped.
    - **FREE / TITHE.** Consumed (deleted).
    """
    from datetime import timedelta
    from apps.lyric.models import Token
    if token.token_type == Token.CARTE:
        raise ValueError("CARTE cannot pay for a my-sea draw")
    if token.token_type == Token.COIN:
        token.next_ready_at = timezone.now() + timedelta(hours=24)
        token.save(update_fields=["next_ready_at"])
        if user.equipped_trinket_id == token.pk:
            user.equipped_trinket = None
            user.save(update_fields=["equipped_trinket"])
    elif token.token_type == Token.PASS:
        pass
    else:
        token.delete()


def active_draw_for(user):
    """Return the user's most-recent draw within the quota window, or
    None. Single source of truth for "does the user have an active draw"
    (drives view branching: no-active → landing w. FREE DRAW; active w.
    empty hand → landing w. GATE VIEW; active w. non-empty hand → picker).

    Lazy stale-row cleanup: every call prunes the user's >24h rows, so
    the DB doesn't accumulate one row per user per day. The user-spec
    'auto-delete all draws after 24hrs, whether or not the user has
    deleted them' (2026-05-20) lands here w. no scheduler required."""
    cutoff = timezone.now() - timezone.timedelta(hours=FREE_DRAW_COOLDOWN_HOURS)
    MySeaDraw.objects.filter(user=user, created_at__lt=cutoff).delete()
    return MySeaDraw.objects.filter(
        user=user, created_at__gte=cutoff,
    ).order_by("-created_at").first()
