"""Admin audit-log service.

Centralises the append-only audit trail: one :func:`record_admin_action` write
path used by every admin action, and one :func:`get_audit_logs` read path (SQL
filtering + pagination, no N+1) used by the audit page. Routes never build audit
rows or queries themselves.

Adding a new auditable action is a one-line change to :data:`ACTION_CATALOG` —
no schema change and no edits here.
"""

from __future__ import annotations

import json
from datetime import datetime, time

from flask import current_app
from sqlalchemy import case, func, or_
from sqlalchemy.orm import selectinload

from app.extensions import db
# Reuse the exact salted-SHA256 helpers used for ListingView — raw IPs / user
# agents are never stored anywhere.
from app.marketplace.services import _client_ip, _hash_value
from app.models import AdminAuditLog, User, UserProfile
from app.models.base import utcnow

# Action catalog: key -> (human label, category, default entity type). The
# category drives the summary cards; the label drives display. Extend this to add
# a new auditable action — nothing else needs to change.
ACTION_CATALOG: dict[str, tuple[str, str, str]] = {
    "premium_granted": ("Premium Granted", "premium", "user"),
    "premium_removed": ("Premium Removed", "premium", "user"),
    "user_suspended": ("User Suspended", "user", "user"),
    "user_reactivated": ("User Reactivated", "user", "user"),
    "listing_featured": ("Listing Featured", "listing", "listing"),
    "listing_unfeatured": ("Listing Unfeatured", "listing", "listing"),
    "listing_deleted": ("Listing Deleted", "listing", "listing"),
    "listing_restored": ("Listing Restored", "listing", "listing"),
    "listing_marked_sold": ("Listing Marked Sold", "listing", "listing"),
    "listing_marked_reserved": ("Listing Marked Reserved", "listing", "listing"),
    "ad_created": ("Advertisement Created", "advertisement", "advertisement"),
    "ad_updated": ("Advertisement Updated", "advertisement", "advertisement"),
    "ad_deleted": ("Advertisement Deleted", "advertisement", "advertisement"),
    "ad_activated": ("Advertisement Activated", "advertisement", "advertisement"),
    "ad_deactivated": ("Advertisement Deactivated", "advertisement", "advertisement"),
    "bulk_listing": ("Bulk Listing Action", "bulk", "listing"),
    "bulk_advertisement": ("Bulk Advertisement Action", "bulk", "advertisement"),
    "report_assigned": ("Report Assigned", "report", "report"),
    "report_under_review": ("Report Marked Under Review", "report", "report"),
    "report_resolved": ("Report Resolved", "report", "report"),
    "report_dismissed": ("Report Dismissed", "report", "report"),
    "report_listing_removed": ("Reported Listing Removed", "report", "report"),
    "report_seller_suspended": ("Reported Seller Suspended", "report", "report"),
    "seller_warned": ("Seller Warned", "report", "report"),
    "reward_claimed": ("Reward Claimed", "reward", "advertisement"),
    "reward_rejected": ("Reward Claim Rejected", "reward", "advertisement"),
    "reward_video_failed": ("Reward Video Failed", "reward", "advertisement"),
}

# category -> [action keys], derived once for SQL summary aggregation.
_CATEGORY_ACTIONS: dict[str, list[str]] = {}
for _key, (_label, _category, _etype) in ACTION_CATALOG.items():
    _CATEGORY_ACTIONS.setdefault(_category, []).append(_key)

_ENTITY_TYPE_BY_CLASS = {
    "User": "user", "Listing": "listing", "Advertisement": "advertisement", "Report": "report",
}

# Filter / sort option lists — exposed so the template and query stay in sync.
AUDIT_SORT_OPTIONS = [("newest", "Newest"), ("oldest", "Oldest")]
ACTION_FILTER_OPTIONS = [("all", "All actions")] + [(k, v[0]) for k, v in ACTION_CATALOG.items()]
ENTITY_TYPE_FILTER_OPTIONS = [
    ("all", "All types"), ("user", "User"), ("listing", "Listing"), ("advertisement", "Advertisement"),
]


def action_label(action: str) -> str:
    """Human label for an action key (falls back to a title-cased key)."""
    entry = ACTION_CATALOG.get(action)
    return entry[0] if entry else action.replace("_", " ").title()


def action_category(action: str) -> str:
    entry = ACTION_CATALOG.get(action)
    return entry[1] if entry else "other"


# --- Write path ------------------------------------------------------------

def _entity_name(entity) -> str | None:
    cls = type(entity).__name__
    if cls == "User":
        profile = getattr(entity, "profile", None)
        return (profile.full_name if profile and profile.full_name else entity.email)
    if cls in ("Listing", "Advertisement"):
        return getattr(entity, "title", None)
    if cls == "Report":
        return f"Report {entity.public_id[:8]}"
    return str(entity)


def _describe_entity(entity, action: str) -> tuple[str | None, str | None, str | None]:
    """Resolve ``(entity_type, entity_public_id, entity_name)`` from the entity.

    ``entity`` may be a model instance, a plain string (a synthetic name for bulk
    actions with no single entity), or ``None``. The entity type falls back to
    the action catalog default so bulk rows are still typed.
    """
    default_type = ACTION_CATALOG.get(action, (None, None, None))[2]
    if entity is None:
        return default_type, None, None
    if isinstance(entity, str):
        return default_type, None, entity
    etype = _ENTITY_TYPE_BY_CLASS.get(type(entity).__name__, default_type)
    return etype, getattr(entity, "public_id", None), _entity_name(entity)


def record_admin_action(admin, action, entity, request=None, details=None) -> AdminAuditLog | None:
    """Append one audit-log entry (the only way audit rows are created).

    ``entity`` may be a model instance, a string (synthetic name for bulk
    actions), or ``None``. ``request`` supplies the IP / user-agent, which are
    stored **only** as salted SHA-256 hashes. ``details`` is any JSON-serialisable
    context. Never raises into the caller: an audit failure must not undo or block
    the admin action it records.
    """
    entity_type, entity_public_id, entity_name = _describe_entity(entity, action)
    ip_hash = user_agent_hash = None
    if request is not None:
        ip_hash = _hash_value(_client_ip(request))
        user_agent_hash = _hash_value(request.headers.get("User-Agent"))

    log = AdminAuditLog(
        admin_id=getattr(admin, "id", None),
        action=action,
        entity_type=entity_type,
        entity_public_id=entity_public_id,
        entity_name=entity_name,
        details=json.dumps(details, default=str) if details else None,
        ip_hash=ip_hash,
        user_agent_hash=user_agent_hash,
        created_at=utcnow(),
    )
    try:
        db.session.add(log)
        db.session.commit()
    except Exception:  # pragma: no cover - defensive; audit must never break actions
        db.session.rollback()
        current_app.logger.exception("Failed to record admin audit action %s", action)
        return None
    return log


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

def get_audit_summary() -> dict:
    """Summary cards via a single conditional-aggregation pass (+ today count)."""
    today_start = datetime.combine(utcnow().date(), time.min)

    def _cat(category):
        keys = _CATEGORY_ACTIONS.get(category, [])
        return func.coalesce(func.sum(case((AdminAuditLog.action.in_(keys), 1), else_=0)), 0)

    row = db.session.execute(
        db.select(
            func.count(AdminAuditLog.id),
            func.coalesce(func.sum(case((AdminAuditLog.created_at >= today_start, 1), else_=0)), 0),
            _cat("premium"),
            _cat("listing"),
            _cat("advertisement"),
            _cat("bulk"),
        )
    ).one()
    total, today, premium, listing, advertisement, bulk = (int(x) for x in row)
    return {
        "total_events": total,
        "today_events": today,
        "premium_actions": premium,
        "listing_actions": listing,
        "advertisement_actions": advertisement,
        "bulk_actions": bulk,
    }


def get_audit_logs(page=1, search=None, admin=None, action=None, entity_type=None,
                   date_from=None, date_to=None, sort="newest"):
    """Return ``(pagination, rows)`` for the audit table.

    All filtering / sorting happens in SQL (so pagination is correct across
    pages); the admin (and their profile) are eager-loaded with ``selectinload``
    so rendering the administrator column never triggers a per-row query.
    """
    per_page = current_app.config.get("DEFAULT_PAGE_SIZE", 20)
    query = db.select(AdminAuditLog).options(
        selectinload(AdminAuditLog.admin).selectinload(User.profile)
    )

    if search and search.strip():
        esc = search.strip().replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
        like = f"%{esc}%"
        query = query.where(or_(
            AdminAuditLog.entity_name.ilike(like, escape="\\"),
            AdminAuditLog.entity_public_id.ilike(like, escape="\\"),
            AdminAuditLog.admin_id.in_(db.select(User.id).where(User.email.ilike(like, escape="\\"))),
        ))

    if admin and admin != "all":
        query = query.where(
            AdminAuditLog.admin_id.in_(db.select(User.id).where(User.public_id == admin))
        )
    if action and action != "all" and action in ACTION_CATALOG:
        query = query.where(AdminAuditLog.action == action)
    if entity_type and entity_type != "all":
        query = query.where(AdminAuditLog.entity_type == entity_type)
    if date_from is not None:
        query = query.where(AdminAuditLog.created_at >= date_from)
    if date_to is not None:
        query = query.where(AdminAuditLog.created_at <= date_to)

    if sort == "oldest":
        query = query.order_by(AdminAuditLog.created_at.asc(), AdminAuditLog.id.asc())
    else:  # newest
        query = query.order_by(AdminAuditLog.created_at.desc(), AdminAuditLog.id.desc())

    pagination = db.paginate(query, page=page, per_page=per_page, error_out=False)
    return pagination, [_row(log) for log in pagination.items]


def _row(log: AdminAuditLog) -> dict:
    admin = log.admin
    profile = admin.profile if admin else None
    return {
        "id": log.id,
        "admin_name": (profile.full_name if profile and profile.full_name else (admin.email if admin else "—")),
        "admin_email": admin.email if admin else "—",
        "action": log.action,
        "label": action_label(log.action),
        "category": action_category(log.action),
        "entity_type": log.entity_type,
        "entity_name": log.entity_name,
        "entity_public_id": log.entity_public_id,
        "details": log.details_data,
        "ip_hash": log.ip_hash,
        "user_agent_hash": log.user_agent_hash,
        "created_at": log.created_at,
    }


def get_admin_filter_options() -> list[tuple[str, str]]:
    """``(public_id, label)`` options of admins who have audit entries, ``all`` first.

    One grouped query joined to the user, so the dropdown scales to the set of
    admins that actually acted rather than every user.
    """
    rows = db.session.execute(
        db.select(User.public_id, User.email, UserProfile.full_name)
        .select_from(AdminAuditLog)
        .join(User, AdminAuditLog.admin_id == User.id)
        .outerjoin(UserProfile, UserProfile.user_id == User.id)
        .group_by(User.public_id, User.email, UserProfile.full_name)
        .order_by(User.email)
    ).all()
    options = [("all", "All administrators")]
    options += [(pid, name or email) for pid, email, name in rows]
    return options


def get_recent_admin_activity(limit: int = 5) -> list[dict]:
    """Latest audit events for the dashboard 'Recent Admin Activity' card."""
    logs = db.session.scalars(
        db.select(AdminAuditLog)
        .options(selectinload(AdminAuditLog.admin).selectinload(User.profile))
        .order_by(AdminAuditLog.created_at.desc(), AdminAuditLog.id.desc())
        .limit(limit)
    ).all()
    return [_row(log) for log in logs]
