"""offer_tick — auto-expiration for offers past their current deadline.

Two windows expire:
- PENDING / COUNTERED past expires_timestamp (negotiation window).
- ACCEPTED past acceptance_pay_deadline_ms (buyer's pay window).

Idempotent: running twice on an already-expired offer is a no-op.
"""

from ..models.offer import (
    MpsOffer,
    OFFER_STATE_PENDING,
    OFFER_STATE_COUNTERED,
    OFFER_STATE_ACCEPTED,
    now_timestamp,
)
from .offer import expire_offer


def tick(dbsession, now_ms=None):
    """Expire any pending/countered offer past expires_timestamp and any
    accepted-but-unpaid offer past its acceptance pay deadline.
    Returns {expired: int}. Caller manages its txn boundary.
    """
    if now_ms is None:
        now_ms = now_timestamp()

    # Negotiation-window expiry (pre-acceptance).
    pre_accept = (
        dbsession.query(MpsOffer)
        .filter(MpsOffer.state.in_([OFFER_STATE_PENDING, OFFER_STATE_COUNTERED]))
        .filter(MpsOffer.expires_timestamp <= now_ms)
        .all()
    )

    # Pay-window expiry (post-acceptance). expire_offer computes the
    # deadline from accepted_timestamp + shop.offer_acceptance_payment_hours
    # rather than a stored absolute, so we filter in-Python via the helper.
    post_accept_candidates = (
        dbsession.query(MpsOffer)
        .filter(MpsOffer.state == OFFER_STATE_ACCEPTED)
        .filter(MpsOffer.accepted_timestamp.isnot(None))
        .all()
    )

    expired = 0
    for offer in pre_accept:
        expire_offer(offer, now_ms=now_ms)
        expired += 1
    for offer in post_accept_candidates:
        deadline = offer.acceptance_pay_deadline_ms
        if deadline is None or deadline > now_ms:
            continue
        expire_offer(offer, now_ms=now_ms)
        expired += 1
    return {"expired": expired}
