"""Activity feed service — the single, centralized event system.

Every activity row is created through :func:`publish_activity`; nothing else in
the codebase writes to ``activity_events``. Feature code just calls
``publish_activity(...)`` from where the action happens — no feed-specific logic
is scattered elsewhere. Reads (`get_user_activity`, `get_recent_activity`) apply
service-layer **grouping** (e.g. many saves → "3 people saved your listings
today") and resolve **deep links**, so templates contain no logic.

This is independent of the Notification system (which stays unchanged); some
actions publish both an activity and a notification.
"""

from __future__ import annotations

from flask import current_app, url_for


def _safe_url(endpoint, **values):
    """Build a URL, returning None if we're outside a request context or the
    target can't be built (a stale/removed entity)."""
    try:
        return url_for(endpoint, **values)
    except Exception:
        return None

from app.extensions import db
from app.models import ActivityEvent, Conversation, Listing, RelatedEntityType
from app.models.base import utcnow

# Registry: event_type -> display metadata. ``link`` names a deep-link target;
# ``groupable`` collapses same-type same-day events into one summarised entry.
# Adding a new event type is one entry here — no schema or template change.
ACTIVITY_TYPES: dict[str, dict] = {
    "listing_created": {"icon": "bi-bag-plus-fill", "link": "listing", "label": "View Listing"},
    "listing_sold": {"icon": "bi-cash-coin", "link": "listing", "label": "View Listing"},
    "listing_featured": {"icon": "bi-stars", "link": "listing", "label": "View Listing"},
    "listing_expired": {"icon": "bi-clock-history", "link": "listing", "label": "View Listing"},
    "favorite_received": {"icon": "bi-heart-fill", "link": "listing", "label": "View Listing",
                          "groupable": True, "group_title": "New saves",
                          "group_message": lambda n: f"{n} people saved your listings today."},
    "review_received": {"icon": "bi-star-fill", "link": "self_profile", "label": "Read Review"},
    "badge_earned": {"icon": "bi-patch-check-fill", "link": "self_profile", "label": "View Profile"},
    "new_negotiation": {"icon": "bi-chat-dots-fill", "link": "conversation", "label": "View Negotiation"},
    "offer_accepted": {"icon": "bi-check-circle-fill", "link": "conversation", "label": "View Negotiation"},
    "offer_rejected": {"icon": "bi-x-circle-fill", "link": "conversation", "label": "View Negotiation"},
    "premium_approved": {"icon": "bi-gem", "link": "premium", "label": "Explore Premium"},
    "premium_expired": {"icon": "bi-hourglass-bottom", "link": "premium", "label": "Explore Premium"},
    "report_resolved": {"icon": "bi-shield-check", "link": "reports", "label": "View Report"},
    "welcome": {"icon": "bi-emoji-smile", "link": "marketplace", "label": "Browse Marketplace"},
}

ACTIVITY_TYPE_FILTERS = [("all", "All activity")] + [
    (key, key.replace("_", " ").title()) for key in ACTIVITY_TYPES
]

# Presentation metadata for the redesigned Activity page (display only — no
# effect on how events are stored or queried):
#   category → which filter pill an event belongs to
#   tone     → colour of the left icon circle (maps to CSS tone-* classes)
#   status   → optional (label, tone) status badge shown on the card
_EVENT_CATEGORY = {
    "new_negotiation": "messages",
    "offer_accepted": "offers", "offer_rejected": "offers",
    "listing_created": "listings", "listing_sold": "listings",
    "listing_featured": "listings", "listing_expired": "listings",
    "favorite_received": "listings",
    "review_received": "system", "badge_earned": "system",
    "premium_approved": "system", "premium_expired": "system",
    "report_resolved": "system", "welcome": "system",
}
_EVENT_TONE = {
    "new_negotiation": "info", "offer_accepted": "success", "offer_rejected": "danger",
    "listing_created": "primary", "listing_sold": "success", "listing_featured": "warning",
    "listing_expired": "muted", "favorite_received": "danger", "review_received": "warning",
    "badge_earned": "success", "premium_approved": "primary", "premium_expired": "muted",
    "report_resolved": "info", "welcome": "primary",
}
_EVENT_STATUS = {  # event_type -> (badge_label, tone)
    "offer_accepted": ("Accepted", "success"),
    "offer_rejected": ("Rejected", "danger"),
    "listing_sold": ("Sold", "success"),
    "listing_featured": ("Featured", "warning"),
    "report_resolved": ("Resolved", "info"),
    "premium_approved": ("Premium", "primary"),
}

# Filter pills shown on the Activity page. Each maps to the event types above.
ACTIVITY_CATEGORIES = [
    ("all", "All", "bi-collection"),
    ("messages", "Messages", "bi-chat-dots"),
    ("offers", "Offers", "bi-tag"),
    ("listings", "Listings", "bi-bag"),
    ("system", "System", "bi-bell"),
]
_CATEGORY_EVENT_TYPES = {
    cat: [et for et, c in _EVENT_CATEGORY.items() if c == cat]
    for cat, _label, _icon in ACTIVITY_CATEGORIES if cat != "all"
}


def _presentation(event_type: str) -> dict:
    """Display-only metadata (tone/category/status) for a card."""
    status = _EVENT_STATUS.get(event_type)
    return {
        "tone": _EVENT_TONE.get(event_type, "primary"),
        "category": _EVENT_CATEGORY.get(event_type, "system"),
        "status_label": status[0] if status else None,
        "status_tone": status[1] if status else None,
    }


# --- Write path (the only place activity rows are created) ---------------------

def publish_activity(user, event_type, title, *, message=None,
                     related_entity_type=None, related_entity_id=None, icon=None):
    """Publish one activity-feed event for ``user``. The single write entry point.

    Never raises into the caller — a feed failure must not break the action that
    triggered it (mirrors the notification service). Icon defaults from the type
    registry. Returns the created :class:`ActivityEvent`, or ``None`` on failure.
    """
    if user is None or not getattr(user, "id", None):
        return None
    meta = ACTIVITY_TYPES.get(event_type, {})
    event = ActivityEvent(
        user_id=user.id,
        event_type=event_type,
        title=title,
        message=(message or None),
        related_entity_type=related_entity_type,
        related_entity_id=related_entity_id,
        icon=(icon or meta.get("icon")),
    )
    try:
        db.session.add(event)
        db.session.commit()
        return event
    except Exception:  # pragma: no cover - feed must never break the workflow
        db.session.rollback()
        current_app.logger.exception("Failed to publish activity %s for user %s", event_type, user.id)
        return None


def has_activity(user, event_type, related_entity_id=None) -> bool:
    """True when an event of this type (optionally for this entity) already
    exists for the user — lets callers keep milestone events (e.g. badge earned)
    idempotent."""
    query = db.select(ActivityEvent.id).where(
        ActivityEvent.user_id == user.id, ActivityEvent.event_type == event_type
    )
    if related_entity_id is not None:
        query = query.where(ActivityEvent.related_entity_id == related_entity_id)
    return db.session.scalar(query.limit(1)) is not None


# --- Read path -----------------------------------------------------------------

def unread_count(user) -> int:
    return db.session.scalar(
        db.select(db.func.count(ActivityEvent.id))
        .where(ActivityEvent.user_id == user.id, ActivityEvent.is_read.is_(False))
    ) or 0


def _entity_public_id_maps(events):
    """Batch-resolve the public_ids needed for deep links on this page (no N+1)."""
    listing_ids, convo_ids = set(), set()
    for e in events:
        if e.related_entity_type == RelatedEntityType.LISTING and e.related_entity_id:
            listing_ids.add(e.related_entity_id)
        elif e.related_entity_type == RelatedEntityType.CONVERSATION and e.related_entity_id:
            convo_ids.add(e.related_entity_id)
    listing_pubs = dict(db.session.execute(
        db.select(Listing.id, Listing.public_id).where(Listing.id.in_(listing_ids))
    ).all()) if listing_ids else {}
    convo_pubs = dict(db.session.execute(
        db.select(Conversation.id, Conversation.public_id).where(Conversation.id.in_(convo_ids))
    ).all()) if convo_ids else {}
    return listing_pubs, convo_pubs


def _action(event_type, entity_type, entity_id, viewer, listing_pubs, convo_pubs):
    """Resolve ``(url, label)`` for an event's deep link, or ``(None, None)``."""
    meta = ACTIVITY_TYPES.get(event_type, {})
    link, label = meta.get("link"), meta.get("label")
    url = None
    if link == "listing" and entity_id in listing_pubs:
        url = _safe_url("marketplace.listing_detail", public_id=listing_pubs[entity_id])
    elif link == "conversation" and entity_id in convo_pubs:
        url = _safe_url("negotiation.conversation_negotiation", public_id=convo_pubs[entity_id])
    elif link == "self_profile" and viewer is not None:
        url = _safe_url("auth.public_profile", public_id=viewer.public_id)
    elif link == "premium":
        url = _safe_url("premium.index")
    elif link == "reports":
        url = _safe_url("marketplace.my_reports")
    elif link == "marketplace":
        url = _safe_url("main.marketplace")
    return (url, label if url else None)


def _display(event, viewer, listing_pubs, convo_pubs) -> dict:
    url, label = _action(event.event_type, event.related_entity_type, event.related_entity_id,
                         viewer, listing_pubs, convo_pubs)
    return {
        "id": event.id, "event_type": event.event_type, "icon": event.icon,
        "title": event.title, "message": event.message, "created_at": event.created_at,
        "is_read": event.is_read, "count": 1, "action_url": url, "action_label": label,
        **_presentation(event.event_type),
    }


def _group(events, viewer):
    """Collapse consecutive same-type same-day *groupable* events into one entry.
    Events arrive newest-first, so same-day same-type rows are adjacent."""
    listing_pubs, convo_pubs = _entity_public_id_maps(events)
    items, i = [], 0
    while i < len(events):
        e = events[i]
        meta = ACTIVITY_TYPES.get(e.event_type, {})
        if meta.get("groupable"):
            day = e.created_at.date()
            j = i
            while j < len(events) and events[j].event_type == e.event_type and events[j].created_at.date() == day:
                j += 1
            group = events[i:j]
            if len(group) > 1:
                items.append({
                    "id": None, "event_type": e.event_type, "icon": meta.get("icon"),
                    "title": meta.get("group_title", "Activity"),
                    "message": meta["group_message"](len(group)) if callable(meta.get("group_message"))
                        else f"{len(group)} updates today.",
                    "created_at": group[0].created_at, "is_read": all(g.is_read for g in group),
                    "count": len(group),
                    "action_url": _safe_url("marketplace.my_listings") if e.event_type == "favorite_received" else None,
                    "action_label": "View my listings" if e.event_type == "favorite_received" else None,
                    "member_ids": [g.id for g in group],
                    **_presentation(e.event_type),
                })
                i = j
                continue
        items.append(_display(e, viewer, listing_pubs, convo_pubs))
        i += 1
    return items


def get_user_activity(user, page: int = 1, per_page: int = 20, *, unread_only: bool = False,
                      event_type: str | None = None, category: str | None = None):
    """Return ``(pagination, display_items)`` — the user's feed, newest first,
    with grouping and resolved deep links applied to the page.

    ``event_type`` filters to one exact type; ``category`` filters to a group of
    types (the Activity page's pills). Both filter in SQL — no extra queries."""
    query = db.select(ActivityEvent).where(ActivityEvent.user_id == user.id)
    if unread_only:
        query = query.where(ActivityEvent.is_read.is_(False))
    if event_type and event_type != "all" and event_type in ACTIVITY_TYPES:
        query = query.where(ActivityEvent.event_type == event_type)
    if category and category != "all" and category in _CATEGORY_EVENT_TYPES:
        query = query.where(ActivityEvent.event_type.in_(_CATEGORY_EVENT_TYPES[category]))
    query = query.order_by(ActivityEvent.created_at.desc(), ActivityEvent.id.desc())
    pagination = db.paginate(query, page=page, per_page=per_page, error_out=False)
    return pagination, _group(pagination.items, user)


def group_by_date(items: list[dict]) -> list[tuple[str, list[dict]]]:
    """Bucket already-ordered (newest-first) display items into date sections
    for the timeline: Today, Yesterday, Earlier This Week, Last Week, Earlier.
    Presentation only — no queries."""
    today = utcnow().date()
    sections: dict[str, list] = {}
    order: list[str] = []
    for it in items:
        delta = (today - it["created_at"].date()).days
        if delta <= 0:
            label = "Today"
        elif delta == 1:
            label = "Yesterday"
        elif delta < 7:
            label = "Earlier This Week"
        elif delta < 14:
            label = "Last Week"
        else:
            label = "Earlier"
        if label not in sections:
            sections[label] = []
            order.append(label)
        sections[label].append(it)
    return [(label, sections[label]) for label in order]


def get_recent_activity(user, limit: int = 5) -> list[dict]:
    """Latest grouped activity items for the dashboard preview card."""
    events = db.session.scalars(
        db.select(ActivityEvent).where(ActivityEvent.user_id == user.id)
        .order_by(ActivityEvent.created_at.desc(), ActivityEvent.id.desc())
        .limit(limit * 3)
    ).all()
    return _group(events, user)[:limit]


# --- Mutations -----------------------------------------------------------------

def mark_activity_read(activity_id: int, user) -> bool:
    event = db.session.get(ActivityEvent, activity_id)
    if event is None or event.user_id != user.id:
        return False
    if not event.is_read:
        event.is_read = True
        db.session.commit()
    return True


def mark_all_read(user) -> int:
    result = db.session.execute(
        db.update(ActivityEvent)
        .where(ActivityEvent.user_id == user.id, ActivityEvent.is_read.is_(False))
        .values(is_read=True)
    )
    db.session.commit()
    return result.rowcount or 0


def clear_read(user) -> int:
    result = db.session.execute(
        db.delete(ActivityEvent).where(ActivityEvent.user_id == user.id, ActivityEvent.is_read.is_(True))
    )
    db.session.commit()
    return result.rowcount or 0
