"""Chat routes.

Thin request handlers that delegate to :mod:`app.chat.services`; all business
rules live in the service layer.
"""

from __future__ import annotations

from flask import flash, jsonify, redirect, render_template, request, session, url_for
from flask_login import current_user, login_required

from app.advertisements.rate_limit import check_rate_limit
from app.advertisements.reward_analytics import (
    CLIENT_EVENTS,
    EVENT_IMPRESSION,
    EVENT_REWARD_GRANTED,
    EVENT_REWARD_REJECTED,
    REASON_INACTIVE,
    REASON_RATE_LIMIT,
    record_rewarded_event,
)
from app.advertisements.reward_token import (
    RewardTokenError,
    issue_reward_token,
    verify_reward_token,
)
from app.advertisements.services import (
    get_active_rewarded_ad,
    get_active_rewarded_ad_by_public_id,
)
from app.chat import chat_bp
from app.chat.forms import MessageForm
from app.chat.services import (
    CONVERSATION_SORT_OPTIONS,
    DEFAULT_CONVERSATION_SORT,
    ChatError,
    SelfConversationError,
    find_conversation,
    get_conversation_for_user_or_404,
    get_messaging_state,
    get_unread_conversation_ids,
    get_user_conversations,
    mark_conversation_read,
    send_message,
    start_conversation_with_message,
    unlock_rewarded_messages,
)
from app.marketplace.services import get_visible_listing_or_404
from app.models import ConversationStatus
from app.negotiation.forms import OfferForm
from app.negotiation.services import get_conversation_timeline


def _flash_form_errors(form) -> None:
    for field_errors in form.errors.values():
        for error in field_errors:
            flash(error, "danger")


@chat_bp.route("/")
@login_required
def inbox():
    page = request.args.get("page", 1, type=int)

    # Sort preference: an explicit ?sort= wins and is remembered for the session;
    # otherwise fall back to the session's last choice, then the default.
    valid_sorts = {key for key, _ in CONVERSATION_SORT_OPTIONS}
    requested_sort = request.args.get("sort")
    if requested_sort in valid_sorts:
        sort = requested_sort
        session["inbox_sort"] = sort
    else:
        sort = session.get("inbox_sort", DEFAULT_CONVERSATION_SORT)
        if sort not in valid_sorts:
            sort = DEFAULT_CONVERSATION_SORT

    pagination = get_user_conversations(current_user, page, sort=sort)
    unread_ids = get_unread_conversation_ids(
        current_user, [c.id for c in pagination.items]
    )
    return render_template(
        "chat/inbox.html",
        pagination=pagination,
        unread_ids=unread_ids,
        sort=sort,
        sort_options=CONVERSATION_SORT_OPTIONS,
    )


@chat_bp.route("/<public_id>")
@login_required
def thread(public_id: str):
    conversation = get_conversation_for_user_or_404(public_id, current_user)
    mark_conversation_read(conversation, current_user)
    other = (
        conversation.seller
        if conversation.buyer_id == current_user.id
        else conversation.buyer
    )
    messaging_state = get_messaging_state(conversation, current_user)
    # The rewarded ad can be watched at any time (not only at the limit), so load
    # it whenever this participant is eligible.
    rewarded_ad = get_active_rewarded_ad() if messaging_state["can_watch_ad"] else None
    # Mint a signed, single-use reward token bound to this user and the served ad.
    # The modal refreshes it on open; it is verified at claim time to block forged
    # and replayed reward requests (see app.advertisements.reward_token).
    reward_token = (
        issue_reward_token(current_user.id, rewarded_ad.public_id if rewarded_ad else None)
        if messaging_state["can_watch_ad"]
        else None
    )
    return render_template(
        "chat/thread.html",
        conversation=conversation,
        listing=conversation.listing,
        timeline=get_conversation_timeline(conversation),
        other=other,
        form=MessageForm(),
        offer_form=OfferForm(),
        form_action=url_for("chat.send", public_id=conversation.public_id),
        is_closed=conversation.status == ConversationStatus.CLOSED,
        messaging_state=messaging_state,
        rewarded_ad=rewarded_ad,
        reward_token=reward_token,
    )


def _reward_limit(bucket: str, max_key: str, default: int) -> bool:
    """Apply the per-user reward rate limit for ``bucket``; True if within limit."""
    from flask import current_app

    return check_rate_limit(
        bucket, current_user.id,
        limit=current_app.config.get(max_key, default),
        window_seconds=current_app.config.get("REWARD_RATE_WINDOW_SECONDS", 60),
    )


@chat_bp.route("/<public_id>/reward-token")
@login_required
def reward_token(public_id: str):
    """Mint a fresh short-lived signed reward token when the ad modal opens.

    Bound to the authenticated participant and the advertisement currently shown
    (``ad`` query param). Returns an opaque signed token as JSON — never a secret.
    Opening the modal counts as one impression for rewarded-ad analytics.
    """
    conversation = get_conversation_for_user_or_404(public_id, current_user)
    if not get_messaging_state(conversation, current_user)["can_watch_ad"]:
        return jsonify({"error": "not_eligible"}), 403
    if not _reward_limit("reward_token", "REWARD_TOKEN_RATE_MAX", 30):
        return jsonify({"error": "rate_limited",
                        "message": "Too many requests. Please slow down."}), 429
    ad_public_id = (request.args.get("ad") or "").strip() or None
    ad = get_active_rewarded_ad_by_public_id(ad_public_id) if ad_public_id else None
    # The modal is now shown → record one impression (analytics only).
    record_rewarded_event(EVENT_IMPRESSION, user=current_user, ad=ad, request=request, audit=False)
    return jsonify({"token": issue_reward_token(current_user.id, ad.public_id if ad else None)})


@chat_bp.route("/<public_id>/ad-event")
@login_required
def ad_event(public_id: str):
    """Lightweight beacon for client-side rewarded-video events (analytics).

    Accepts ``type`` (video_start / video_success / video_fallback / video_error)
    and ``ad``. Fire-and-forget: always returns 204 and never affects the reward.
    """
    conversation = get_conversation_for_user_or_404(public_id, current_user)
    if not _reward_limit("reward_event", "REWARD_EVENT_RATE_MAX", 90):
        return ("", 204)
    event_type = (request.args.get("type") or "").strip()
    if event_type not in CLIENT_EVENTS:
        return ("", 204)
    ad_public_id = (request.args.get("ad") or "").strip() or None
    ad = get_active_rewarded_ad_by_public_id(ad_public_id) if ad_public_id else None
    record_rewarded_event(event_type, user=current_user, ad=ad, request=request)
    return ("", 204)


@chat_bp.route("/<public_id>/messages")
@login_required
def thread_messages(public_id: str):
    """Render only the message-list fragment (used by the auto-refresh poll)."""
    conversation = get_conversation_for_user_or_404(public_id, current_user)
    mark_conversation_read(conversation, current_user)
    other = (
        conversation.seller
        if conversation.buyer_id == current_user.id
        else conversation.buyer
    )
    return render_template(
        "chat/_conversation_timeline.html",
        timeline=get_conversation_timeline(conversation),
        other=other,
        conversation=conversation,
        offer_form=OfferForm(),
    )


@chat_bp.route("/<public_id>/send", methods=["POST"])
@login_required
def send(public_id: str):
    conversation = get_conversation_for_user_or_404(public_id, current_user)
    form = MessageForm()
    if form.validate_on_submit():
        try:
            send_message(conversation, current_user, form.message.data)
        except ChatError as exc:
            flash(str(exc), "warning")
    else:
        _flash_form_errors(form)
    return redirect(url_for("chat.thread", public_id=public_id))


@chat_bp.route("/<public_id>/watch-ad", methods=["POST"])
@login_required
def watch_ad(public_id: str):
    """Complete a rewarded advertisement to add more messages to this chat.

    The rewarded card posts here; on success this participant's message pool grows
    by :data:`MESSAGES_PER_REWARDED_AD` and the advertisement is recorded as
    viewed. Rewards stack indefinitely — a user can watch as many ads as they like.
    """
    conversation = get_conversation_for_user_or_404(public_id, current_user)

    # Rate limit before doing any work — blunts automated claim spam per user.
    if not _reward_limit("reward_claim", "REWARD_CLAIM_RATE_MAX", 15):
        record_rewarded_event(EVENT_REWARD_REJECTED, user=current_user, ad=None,
                              request=request, reason=REASON_RATE_LIMIT)
        flash("You're doing that a bit too fast. Please wait a moment and try again.", "warning")
        return redirect(url_for("chat.thread", public_id=public_id))

    # Verify the signed reward token before granting anything. This proves the
    # server issued this rewarded-ad session to THIS user and blocks forged and
    # replayed claims. The client countdown is a UX gate only — never trusted; the
    # reward amount always comes from server config (unlock_rewarded_messages).
    try:
        payload = verify_reward_token(request.form.get("reward_token"), user_id=current_user.id)
    except RewardTokenError as exc:
        record_rewarded_event(EVENT_REWARD_REJECTED, user=current_user, ad=None,
                              request=request, reason=getattr(exc, "reason", "invalid"))
        flash(str(exc), "warning")
        return redirect(url_for("chat.thread", public_id=public_id))

    # Resolve the exact advertisement the token was issued for and confirm it is
    # still live. A null ``aid`` is the built-in placeholder (reward still granted).
    ad = None
    ad_public_id = payload.get("aid")
    if ad_public_id:
        ad = get_active_rewarded_ad_by_public_id(ad_public_id)
        if ad is None:
            record_rewarded_event(EVENT_REWARD_REJECTED, user=current_user, ad=None,
                                  request=request, reason=REASON_INACTIVE)
            flash("This advertisement is no longer available. Please try again.", "warning")
            return redirect(url_for("chat.thread", public_id=public_id))

    try:
        unlocked = unlock_rewarded_messages(conversation, current_user, ad=ad)
        # Completed rewarded view: reward granted (analytics + audit trail).
        record_rewarded_event(EVENT_REWARD_GRANTED, user=current_user, ad=ad, request=request)
        flash(f"✅ +{unlocked} messages added!", "success")
    except ChatError as exc:
        flash(str(exc), "warning")
    return redirect(url_for("chat.thread", public_id=public_id))


@chat_bp.route("/start/<listing_public_id>")
@login_required
def start(listing_public_id: str):
    listing = get_visible_listing_or_404(listing_public_id)
    if listing.seller_id == current_user.id:
        flash("You cannot message yourself about your own listing.", "warning")
        return redirect(
            url_for("marketplace.listing_detail", public_id=listing_public_id)
        )

    existing = find_conversation(current_user, listing)
    if existing is not None:
        return redirect(url_for("chat.thread", public_id=existing.public_id))

    # Compose mode: the conversation is created lazily on the first message.
    return render_template(
        "chat/thread.html",
        conversation=None,
        listing=listing,
        timeline=[],
        other=listing.seller,
        form=MessageForm(),
        offer_form=OfferForm(),
        form_action=url_for("chat.start_send", listing_public_id=listing_public_id),
        is_closed=False,
    )


@chat_bp.route("/start/<listing_public_id>", methods=["POST"])
@login_required
def start_send(listing_public_id: str):
    listing = get_visible_listing_or_404(listing_public_id)
    form = MessageForm()
    if form.validate_on_submit():
        try:
            conversation, _ = start_conversation_with_message(
                current_user, listing, form.message.data
            )
            return redirect(url_for("chat.thread", public_id=conversation.public_id))
        except SelfConversationError as exc:
            flash(str(exc), "warning")
            return redirect(
                url_for("marketplace.listing_detail", public_id=listing_public_id)
            )
        except ChatError as exc:
            flash(str(exc), "warning")
    else:
        _flash_form_errors(form)

    # Validation failed: re-render compose mode (no conversation was created).
    return render_template(
        "chat/thread.html",
        conversation=None,
        listing=listing,
        timeline=[],
        other=listing.seller,
        form=form,
        offer_form=OfferForm(),
        form_action=url_for("chat.start_send", listing_public_id=listing_public_id),
        is_closed=False,
    )
