"""Offer models — MPS-21.

Make-an-offer mode lets a buyer propose a price for a Product. The seller
can accept, counter, or decline. State machine permits a finite number
of counter rounds before forcing terminal resolution. Auto-accept (when
buyer offer >= threshold) and auto-decline (when below threshold) skip
the seller's queue entirely.

State machine (offer.state):
    0 pending   — offer waiting on the other party (current_party)
    1 accepted  — terminal; cart line item created at agreed price
    2 countered — counter active (still pending to other party)
    3 declined  — terminal; rejected
    4 expired   — terminal; auto-expired past expires_timestamp
    5 withdrawn — terminal; buyer pulled the offer
    6 paid      — terminal; cart payment succeeded, transferred

current_party:
    0 buyer's turn (initial state, or seller just countered to buyer)
    1 seller's turn (buyer just countered or opened the offer)

See `lib/offer.py` for pure-function counter/accept/decline/expire logic.
"""

import uuid

from sqlalchemy import Column, BigInteger, Integer, UnicodeText
from sqlalchemy.orm import relationship, backref

from .meta import (
    Base,
    RBase,
    UUIDType,
    foreign_key,
    now_timestamp,
    get_object_by_id,
)

from ..lib.currency import cents_to_dollars


# State constants.
OFFER_STATE_PENDING = 0
OFFER_STATE_ACCEPTED = 1
OFFER_STATE_COUNTERED = 2
OFFER_STATE_DECLINED = 3
OFFER_STATE_EXPIRED = 4
OFFER_STATE_WITHDRAWN = 5
OFFER_STATE_PAID = 6
# Buyer-side back-out *after* the offer was accepted. Distinct from
# WITHDRAWN (which means the buyer pulled the offer mid-negotiation,
# before the seller responded). Surfaced separately in the seller
# inbox — they need to know the offer collapsed despite acceptance.
OFFER_STATE_BUYER_CANCELLED = 7

OFFER_STATE_INT_TO_HUMAN = {
    OFFER_STATE_PENDING: "Pending",
    OFFER_STATE_ACCEPTED: "Accepted",
    OFFER_STATE_COUNTERED: "Countered",
    OFFER_STATE_DECLINED: "Declined",
    OFFER_STATE_EXPIRED: "Expired",
    OFFER_STATE_WITHDRAWN: "Withdrawn",
    OFFER_STATE_PAID: "Paid",
    OFFER_STATE_BUYER_CANCELLED: "Cancelled by buyer",
}

OFFER_TERMINAL_STATES = (
    OFFER_STATE_ACCEPTED,
    OFFER_STATE_DECLINED,
    OFFER_STATE_EXPIRED,
    OFFER_STATE_WITHDRAWN,
    OFFER_STATE_PAID,
    OFFER_STATE_BUYER_CANCELLED,
)

OFFER_PARTY_BUYER = 0
OFFER_PARTY_SELLER = 1
OFFER_PARTY_OTHER = {
    OFFER_PARTY_BUYER: OFFER_PARTY_SELLER,
    OFFER_PARTY_SELLER: OFFER_PARTY_BUYER,
}


# Event log types.
OFFER_EVENT_OPEN = 0
OFFER_EVENT_COUNTER = 1
OFFER_EVENT_ACCEPT = 2
OFFER_EVENT_DECLINE = 3
OFFER_EVENT_WITHDRAW = 4
OFFER_EVENT_EXPIRE = 5
OFFER_EVENT_PAY = 6
OFFER_EVENT_BUYER_CANCEL = 7  # buyer-side back-out after acceptance

OFFER_EVENT_INT_TO_HUMAN = {
    OFFER_EVENT_OPEN: "Opened",
    OFFER_EVENT_COUNTER: "Countered",
    OFFER_EVENT_ACCEPT: "Accepted",
    OFFER_EVENT_DECLINE: "Declined",
    OFFER_EVENT_WITHDRAW: "Withdrew",
    OFFER_EVENT_EXPIRE: "Expired",
    OFFER_EVENT_PAY: "Paid",
    OFFER_EVENT_BUYER_CANCEL: "Cancelled by buyer",
}


# Defaults — shop settings override these per-shop.
DEFAULT_OFFER_EXPIRATION_HOURS = 168  # 7 days, pre-acceptance window
DEFAULT_OFFER_MAX_ROUNDS = 3
DEFAULT_OFFER_AUTO_ACCEPT_PCT = 95
DEFAULT_OFFER_AUTO_DECLINE_PCT = 50
DEFAULT_OFFER_ACCEPTANCE_PAYMENT_HOURS = 24  # post-acceptance pay window


class MpsOffer(RBase, Base):
    """One open negotiation between a buyer and seller for a single Product."""

    id = Column(UUIDType, primary_key=True, index=True)
    product_id = Column(
        UUIDType,
        foreign_key("Product", "id"),
        nullable=False,
        index=True,
    )
    shop_id = Column(UUIDType, foreign_key("Shop", "id"), nullable=False, index=True)
    buyer_user_id = Column(
        UUIDType,
        foreign_key("User", "id"),
        nullable=False,
        index=True,
    )

    state = Column(
        Integer,
        nullable=False,
        default=OFFER_STATE_PENDING,
        server_default=str(OFFER_STATE_PENDING),
    )
    current_amount_in_cents = Column(BigInteger, nullable=False)
    current_party = Column(
        Integer,
        nullable=False,
        default=OFFER_PARTY_SELLER,  # buyer just opened → seller's turn
        server_default=str(OFFER_PARTY_SELLER),
    )

    created_timestamp = Column(BigInteger, nullable=False)
    last_action_timestamp = Column(BigInteger, nullable=False)
    expires_timestamp = Column(BigInteger, nullable=False)
    paid_timestamp = Column(BigInteger, nullable=True)
    # When the seller (or auto-accept) flipped the offer to ACCEPTED.
    # Drives the buyer's pay-by deadline: accepted_timestamp +
    # shop.offer_acceptance_payment_hours.
    accepted_timestamp = Column(BigInteger, nullable=True)

    round_count = Column(
        Integer, nullable=False, default=0, server_default="0"
    )

    buyer_message = Column(UnicodeText, nullable=True)
    seller_message = Column(UnicodeText, nullable=True)

    product = relationship(argument="Product", uselist=False)
    shop = relationship(argument="Shop", uselist=False)
    buyer = relationship(argument="User", uselist=False)

    events = relationship(
        argument="MpsOfferEvent",
        lazy="dynamic",
        order_by="MpsOfferEvent.created_timestamp.asc()",
        back_populates="offer",
        cascade="all, delete-orphan",
    )

    def __init__(self, product, shop, buyer, amount_in_cents,
                 expires_timestamp, buyer_message=None):
        self.id = uuid.uuid1()
        self.product = product
        self.shop = shop
        self.buyer = buyer
        self.current_amount_in_cents = amount_in_cents
        self.current_party = OFFER_PARTY_SELLER  # seller responds to buyer's open
        self.state = OFFER_STATE_PENDING
        self.expires_timestamp = expires_timestamp
        self.buyer_message = buyer_message
        self.round_count = 0
        self.created_timestamp = now_timestamp()
        self.last_action_timestamp = self.created_timestamp

    @property
    def is_pending(self):
        return self.state == OFFER_STATE_PENDING

    @property
    def is_countered(self):
        return self.state == OFFER_STATE_COUNTERED

    @property
    def is_open(self):
        """Either party still has an action to take."""
        return self.state in (OFFER_STATE_PENDING, OFFER_STATE_COUNTERED)

    @property
    def is_accepted(self):
        return self.state == OFFER_STATE_ACCEPTED

    @property
    def is_paid(self):
        return self.state == OFFER_STATE_PAID

    @property
    def is_buyer_cancelled(self):
        return self.state == OFFER_STATE_BUYER_CANCELLED

    @property
    def is_terminal(self):
        return self.state in OFFER_TERMINAL_STATES

    @property
    def acceptance_pay_deadline_ms(self):
        """ACCEPTED offers must be paid before this absolute timestamp,
        else they auto-expire. Returns None for offers that never reached
        acceptance.
        """
        if self.accepted_timestamp is None:
            return None
        hours = (
            self.shop.offer_acceptance_payment_hours
            if self.shop is not None
            else DEFAULT_OFFER_ACCEPTANCE_PAYMENT_HOURS
        )
        return self.accepted_timestamp + (hours * 3600 * 1000)

    @property
    def pay_time_remaining_ms(self):
        """How long the buyer has left to pay. Zero if past the deadline
        or if the offer was never accepted."""
        deadline = self.acceptance_pay_deadline_ms
        if deadline is None:
            return 0
        return max(0, deadline - now_timestamp())

    @property
    def state_human(self):
        return OFFER_STATE_INT_TO_HUMAN.get(self.state, "Unknown")

    @property
    def time_remaining_ms(self):
        return max(0, self.expires_timestamp - now_timestamp())

    @property
    def is_expired(self):
        """True when expires_timestamp is past, regardless of state column.
        The tick job is responsible for flipping state to OFFER_STATE_EXPIRED;
        this property reports clock truth even before the tick runs."""
        return self.time_remaining_ms == 0 and not self.is_terminal

    @property
    def waiting_on_buyer(self):
        return self.is_open and self.current_party == OFFER_PARTY_BUYER

    @property
    def waiting_on_seller(self):
        return self.is_open and self.current_party == OFFER_PARTY_SELLER

    @property
    def current_amount(self):
        return cents_to_dollars(self.current_amount_in_cents)


class MpsOfferEvent(RBase, Base):
    """Audit log for an offer — every action gets a row."""

    id = Column(UUIDType, primary_key=True, index=True)
    offer_id = Column(
        UUIDType,
        foreign_key("MpsOffer", "id"),
        nullable=False,
        index=True,
    )
    actor_user_id = Column(UUIDType, foreign_key("User", "id"), nullable=True)
    event_type = Column(Integer, nullable=False)
    amount_in_cents = Column(BigInteger, nullable=True)
    message = Column(UnicodeText, nullable=True)
    created_timestamp = Column(BigInteger, nullable=False)

    offer = relationship(argument="MpsOffer", uselist=False, back_populates="events")
    actor = relationship(argument="User", uselist=False)

    def __init__(self, offer, event_type, actor=None, amount_in_cents=None,
                 message=None):
        self.id = uuid.uuid1()
        self.offer = offer
        self.event_type = event_type
        self.actor = actor
        self.amount_in_cents = amount_in_cents
        self.message = message
        self.created_timestamp = now_timestamp()

    @property
    def event_type_human(self):
        return OFFER_EVENT_INT_TO_HUMAN.get(self.event_type, "Unknown")

    @property
    def amount(self):
        if self.amount_in_cents is None:
            return None
        return cents_to_dollars(self.amount_in_cents)


def get_offer_by_id(dbsession, offer_id):
    return get_object_by_id(dbsession, offer_id, MpsOffer)
