"""Negotiation service layer.

All offer business logic lives here so routes remain thin.
"""

from __future__ import annotations

from decimal import Decimal

from app.extensions import db
from app.models import (
    Conversation,
    ListingStatus,
    Notification,
    NotificationType,
    Offer,
    OfferStatus,
    RelatedEntityType,
    User,
)
from app.marketplace.services import reserve_listing_for_offer
from app.notifications.services import create_notification


class NegotiationError(Exception):
    """Base error for negotiation operations."""


class InvalidOfferError(NegotiationError):
    """Raised when an offer fails validation before persistence."""


class OfferCreationError(NegotiationError):
    """Raised when an offer cannot be created for the current context."""


def get_conversation_timeline(conversation: Conversation):
    """Return a unified timeline of visible messages and offers sorted by creation time."""
    items = []
    for message in conversation.messages:
        if message.deleted_at is None:
            items.append((message.created_at, "message", message))
    for offer in conversation.offers:
        items.append((offer.created_at, "offer", offer))
    items.sort(key=lambda item: item[0])
    return [item[2] for item in items]


def get_offer_for_conversation_or_404(public_id: str, conversation: Conversation) -> Offer:
    """Return an offer for the given conversation, or raise 404."""
    return db.first_or_404(
        db.select(Offer).where(
            Offer.public_id == public_id,
            Offer.conversation_id == conversation.id,
        )
    )


def _ensure_offer_still_pending(offer: Offer) -> None:
    if offer.status != OfferStatus.PENDING:
        raise InvalidOfferError("This offer is no longer pending.")


def _send_offer_notification(
    recipient: User,
    offer: Offer,
    notification_type: NotificationType,
    title: str,
    message: str,
) -> None:
    existing_notification = db.session.scalar(
        db.select(Notification)
        .where(
            Notification.user_id == recipient.id,
            Notification.notification_type == notification_type,
            Notification.related_entity_type == RelatedEntityType.OFFER,
            Notification.related_entity_id == offer.id,
            Notification.read_at.is_(None),
        )
        .limit(1)
    )
    if existing_notification is None:
        create_notification(
            user=recipient,
            notification_type=notification_type,
            title=title,
            message=message,
            related_entity_type=RelatedEntityType.OFFER,
            related_entity_id=offer.id,
        )


def create_offer(conversation: Conversation, buyer: User, amount, optional_message: str | None = None) -> Offer:
    """Create a pending offer for the given conversation and buyer."""
    if conversation.buyer_id != buyer.id:
        raise OfferCreationError("Only the buyer can create an offer for this conversation.")

    listing = conversation.listing
    if listing is None:
        raise OfferCreationError("This listing is no longer available.")
    if listing.status != ListingStatus.AVAILABLE:
        raise OfferCreationError("This listing is not available for offers.")

    amount_value = Decimal(str(amount)) if amount is not None else Decimal("0")
    if amount_value <= 0:
        raise InvalidOfferError("Offer amount must be greater than zero.")

    pending_count = db.session.scalar(
        db.select(db.func.count())
        .select_from(Offer)
        .where(
            Offer.conversation_id == conversation.id,
            Offer.status == OfferStatus.PENDING,
        )
    )
    if pending_count:
        raise InvalidOfferError("You already have a pending offer for this conversation.")

    clean_message = (optional_message or "").strip() or None
    if clean_message and len(clean_message) > 300:
        raise InvalidOfferError("Offer message cannot exceed 300 characters.")

    offer = Offer(
        listing_id=conversation.listing_id,
        conversation_id=conversation.id,
        buyer_id=buyer.id,
        seller_id=conversation.seller_id,
        amount=amount_value,
        optional_message=clean_message,
        status=OfferStatus.PENDING,
    )
    db.session.add(offer)
    db.session.commit()

    _send_offer_notification(
        conversation.seller,
        offer,
        NotificationType.OFFER,
        "New offer",
        f"{buyer.email} sent you an offer for {conversation.listing.title}.",
    )
    _publish_offer_activity(conversation.seller, "new_negotiation", "New negotiation",
                            f"You received an offer on “{conversation.listing.title}”.", conversation.id)

    return offer


def _publish_offer_activity(user, event_type, title, message, conversation_id):
    """Route negotiation events through the single activity API (feed only)."""
    from app.activity.services import publish_activity
    publish_activity(user, event_type, title, message=message,
                     related_entity_type=RelatedEntityType.CONVERSATION, related_entity_id=conversation_id)


def accept_offer(offer: Offer, acting_user: User) -> Offer:
    """Accept a pending offer when the acting user is authorized to do so."""
    _ensure_offer_still_pending(offer)

    conversation = offer.conversation
    if conversation is None:
        raise OfferCreationError("This offer is no longer available.")

    is_buyer_offer = (
        offer.buyer_id == conversation.buyer_id and offer.seller_id == conversation.seller_id
    )
    is_seller_counter = (
        offer.buyer_id == conversation.seller_id and offer.seller_id == conversation.buyer_id
    )
    can_accept = (
        acting_user.id == conversation.seller_id and is_buyer_offer
    ) or (
        acting_user.id == conversation.buyer_id and is_seller_counter
    )
    if not can_accept:
        raise OfferCreationError("You are not allowed to accept this offer.")

    offer.status = OfferStatus.ACCEPTED
    db.session.commit()
    reserve_listing_for_offer(offer)

    recipient = offer.buyer if acting_user.id == offer.seller_id else offer.seller
    _send_offer_notification(
        recipient,
        offer,
        NotificationType.OFFER_ACCEPTED,
        "Offer accepted",
        f"{acting_user.email} accepted your offer for {offer.listing.title}.",
    )
    _publish_offer_activity(recipient, "offer_accepted", "Offer accepted",
                            f"Your offer on “{offer.listing.title}” was accepted.", conversation.id)
    return offer


def reject_offer(offer: Offer, acting_user: User) -> Offer:
    """Reject a pending offer when the acting user is authorized to do so."""
    _ensure_offer_still_pending(offer)
    if acting_user.id != offer.seller_id:
        raise OfferCreationError("You are not allowed to reject this offer.")

    offer.status = OfferStatus.REJECTED
    db.session.commit()

    _send_offer_notification(
        offer.buyer,
        offer,
        NotificationType.OFFER_REJECTED,
        "Offer rejected",
        f"{acting_user.email} rejected your offer for {offer.listing.title}.",
    )
    _publish_offer_activity(offer.buyer, "offer_rejected", "Offer declined",
                            f"Your offer on “{offer.listing.title}” was declined.", offer.conversation_id)
    return offer


def counter_offer(offer: Offer, acting_user: User, amount, optional_message: str | None = None) -> Offer:
    """Counter a pending offer by creating a new pending offer and closing the old one."""
    _ensure_offer_still_pending(offer)
    if acting_user.id != offer.seller_id:
        raise OfferCreationError("You are not allowed to counter this offer.")

    pending_count = db.session.scalar(
        db.select(db.func.count())
        .select_from(Offer)
        .where(
            Offer.conversation_id == offer.conversation_id,
            Offer.status == OfferStatus.PENDING,
        )
    )
    if pending_count != 1:
        raise InvalidOfferError("Only one pending offer can exist in a conversation.")

    amount_value = Decimal(str(amount)) if amount is not None else Decimal("0")
    if amount_value <= 0:
        raise InvalidOfferError("Offer amount must be greater than zero.")

    clean_message = (optional_message or "").strip() or None
    if clean_message and len(clean_message) > 300:
        raise InvalidOfferError("Offer message cannot exceed 300 characters.")

    offer.status = OfferStatus.COUNTERED

    counter_offer_record = Offer(
        listing_id=offer.listing_id,
        conversation_id=offer.conversation_id,
        buyer_id=acting_user.id,
        seller_id=offer.buyer_id,
        amount=amount_value,
        optional_message=clean_message,
        status=OfferStatus.PENDING,
    )
    db.session.add(counter_offer_record)
    db.session.commit()

    _send_offer_notification(
        offer.buyer,
        counter_offer_record,
        NotificationType.OFFER,
        "Counter offer",
        f"{acting_user.email} sent you a counter offer for {offer.listing.title}.",
    )
    return counter_offer_record
