"""Review service layer (Trust System — Phase 2).

All review eligibility, validation, persistence and notification logic lives here
so routes stay thin. A review may only exist for a :class:`CompletedTransaction`
the user took part in, one per reviewer per transaction, with a 1–5 rating.
Business rules are enforced here and backed by database constraints.
"""

from __future__ import annotations

from flask import current_app
from sqlalchemy.exc import IntegrityError

from app.extensions import db
from app.models import (
    CompletedTransaction,
    NotificationType,
    RelatedEntityType,
    Review,
    User,
)
from app.models.review import MAX_COMMENT_LENGTH, MAX_RATING, MIN_RATING
from app.notifications.services import create_notification


class ReviewError(Exception):
    """Base error for the review workflow."""


class ReviewPermissionError(ReviewError):
    """Raised when a user may not review this transaction (non-participant/self)."""


class DuplicateReviewError(ReviewError):
    """Raised when the reviewer already reviewed this transaction."""


class InvalidReviewError(ReviewError):
    """Raised when review content (rating/comment) fails validation."""


def get_transaction_or_404(public_id: str) -> CompletedTransaction:
    """Fetch a completed transaction by public id (or abort 404).

    Reviewing anything that isn't a completed transaction is impossible by
    construction — an incomplete sale has no ``CompletedTransaction`` row.
    """
    return db.first_or_404(
        db.select(CompletedTransaction).where(CompletedTransaction.public_id == public_id)
    )


def _reviewee_id_for(transaction: CompletedTransaction, reviewer: User) -> int | None:
    """The counterparty the reviewer would review, or ``None`` if not a participant."""
    if reviewer.id == transaction.buyer_id:
        return transaction.seller_id
    if reviewer.id == transaction.seller_id:
        return transaction.buyer_id
    return None


def is_participant(transaction: CompletedTransaction, user: User) -> bool:
    return user is not None and user.id in (transaction.buyer_id, transaction.seller_id)


def has_reviewed(transaction: CompletedTransaction, reviewer: User) -> bool:
    return db.session.scalar(
        db.select(Review.id).where(
            Review.transaction_id == transaction.id,
            Review.reviewer_id == reviewer.id,
        ).limit(1)
    ) is not None


def can_review(transaction: CompletedTransaction, reviewer: User) -> bool:
    """True when ``reviewer`` is a participant who hasn't reviewed this transaction."""
    return _reviewee_id_for(transaction, reviewer) is not None and not has_reviewed(transaction, reviewer)


def submit_review(transaction: CompletedTransaction, reviewer: User, rating, comment=None) -> Review:
    """Validate eligibility + content, persist the review, notify the recipient.

    Raises a :class:`ReviewError` subclass for any rule violation. The unique
    constraint on ``(transaction_id, reviewer_id)`` is the final guard against a
    duplicate created by a concurrent submit. The buyer's and seller's reviews are
    fully independent — neither blocks nor requires the other.
    """
    reviewee_id = _reviewee_id_for(transaction, reviewer)
    if reviewee_id is None:
        raise ReviewPermissionError("You can only review a transaction you took part in.")
    if reviewee_id == reviewer.id:  # defensive; the model also forbids it
        raise ReviewPermissionError("You cannot review yourself.")

    try:
        rating = int(rating)
    except (TypeError, ValueError):
        raise InvalidReviewError("Please choose a rating from 1 to 5.")
    if rating < MIN_RATING or rating > MAX_RATING:
        raise InvalidReviewError(f"Rating must be between {MIN_RATING} and {MAX_RATING}.")

    comment = (comment or "").strip() or None
    if comment and len(comment) > MAX_COMMENT_LENGTH:
        raise InvalidReviewError(f"Comment cannot exceed {MAX_COMMENT_LENGTH} characters.")

    if has_reviewed(transaction, reviewer):
        raise DuplicateReviewError("You have already reviewed this transaction.")

    review = Review(
        transaction_id=transaction.id,
        reviewer_id=reviewer.id,
        reviewee_id=reviewee_id,
        rating=rating,
        comment=comment,
    )
    db.session.add(review)
    try:
        db.session.commit()
    except IntegrityError as exc:  # unique(transaction_id, reviewer_id) — concurrent double submit
        db.session.rollback()
        raise DuplicateReviewError("You have already reviewed this transaction.") from exc

    # Refresh the reviewee's cached reputation (rating aggregates) synchronously.
    # (Also publishes a "badge earned" activity if a new badge was crossed.)
    from app.reviews.reputation import update_reputation_after_review
    update_reputation_after_review(review)

    _notify_review_recipient(review, transaction)

    # Publish a "review received" activity to the reviewee (feed only).
    from app.activity.services import publish_activity
    reviewee = db.session.get(User, reviewee_id)
    reviewer_name = _reviewer_first_name(reviewer)
    publish_activity(reviewee, "review_received", "New review received",
                     message=f"{reviewer_name} left you a {rating}-star review.")
    return review


def _reviewer_first_name(user: User) -> str:
    profile = getattr(user, "profile", None)
    if profile and profile.full_name:
        return profile.full_name.strip().split(" ")[0]
    return "A buyer" if user is None or not user.email else user.email.split("@")[0]


def _notify_review_recipient(review: Review, transaction: CompletedTransaction) -> None:
    """Tell the reviewee they received a review — never revealing its text.
    A delivery failure never rolls back the committed review."""
    reviewee = db.session.get(User, review.reviewee_id)
    if reviewee is None:
        return
    try:
        create_notification(
            user=reviewee,
            notification_type=NotificationType.SYSTEM,
            title="New Review",
            message="You received a new review from a recent transaction.",
            related_entity_type=RelatedEntityType.LISTING,
            related_entity_id=transaction.listing_id,
        )
    except Exception:  # pragma: no cover - notifications must never break the review
        db.session.rollback()
        current_app.logger.exception("Failed to send review notification to user %s", review.reviewee_id)


def get_review_context(listing, user) -> dict | None:
    """Review-CTA context for a listing's completed transaction, for ``user``.

    Returns ``None`` when there is no completed transaction or ``user`` isn't a
    participant. Reused by the listing detail page for both buyer and seller so
    the "Leave Review" / "Review Submitted" affordance is defined once.
    """
    from app.marketplace.services import get_transaction_for_listing

    if user is None or not getattr(user, "is_authenticated", False):
        return None
    transaction = get_transaction_for_listing(listing)
    if transaction is None:
        return None
    if user.id == transaction.buyer_id:
        review_for, counterpart = "seller", transaction.seller
    elif user.id == transaction.seller_id:
        review_for, counterpart = "buyer", transaction.buyer
    else:
        return None
    profile = getattr(counterpart, "profile", None)
    name = (profile.full_name if profile and profile.full_name
            else (counterpart.email if counterpart else "the other party"))
    return {
        "transaction": transaction,
        "review_for": review_for,            # role of the person being reviewed
        "counterpart_name": name,
        "already_reviewed": has_reviewed(transaction, user),
    }
