"""Status/dashboard API route."""

from datetime import datetime, timedelta, timezone

from fastapi import APIRouter, Depends, Query
from sqlalchemy import delete as sa_delete
from sqlmodel import func, select
from sqlmodel.ext.asyncio.session import AsyncSession

from ..auth.dependencies import get_current_user
from ..database.engine import get_session
from ..database.models import (
    Action,
    CommandConfig,
    CommandTemplateConfig,
    CommandTracker,
    EmailBot,
    EventLog,
    MatrixBot,
    NotificationTarget,
    NotificationTracker,
    ServiceProvider,
    TelegramBot,
    TemplateConfig,
    TrackingConfig,
    User,
)

router = APIRouter(prefix="/api/status", tags=["status"])


@router.get("")
async def get_status(
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
    # Event filtering
    event_type: str | None = Query(None),
    provider_id: int | None = Query(None),
    search: str | None = Query(None),
    sort: str = Query("newest"),
    limit: int = Query(20, ge=1, le=100),
    offset: int = Query(0, ge=0),
):
    """Get dashboard status data with enriched events."""
    providers_count = (await session.exec(
        select(func.count()).select_from(ServiceProvider).where(ServiceProvider.user_id == user.id)
    )).one()

    trackers_result = await session.exec(
        select(NotificationTracker).where(NotificationTracker.user_id == user.id)
    )
    trackers = trackers_result.all()
    active_count = sum(1 for t in trackers if t.enabled)

    targets_count = (await session.exec(
        select(func.count()).select_from(NotificationTarget).where(NotificationTarget.user_id == user.id)
    )).one()

    # Build events query with filters.  EventLog.user_id is the owner column;
    # action events (event_type starts with "action_") have tracker_id NULL but
    # user_id set, so we filter by user_id directly.
    events_query = select(EventLog).where(EventLog.user_id == user.id)

    if event_type:
        events_query = events_query.where(EventLog.event_type == event_type)
    if provider_id is not None:
        events_query = events_query.where(EventLog.provider_id == provider_id)
    if search:
        events_query = events_query.where(
            EventLog.collection_name.contains(search)
            | EventLog.tracker_name.contains(search)
            | EventLog.action_name.contains(search)
            | EventLog.provider_name.contains(search)
        )

    # Count total matching events (for pagination)
    count_query = select(func.count()).select_from(events_query.subquery())
    total_events = (await session.exec(count_query)).one()

    # Aggregate per-provider event counts across ALL matching events (ignoring
    # offset/limit) so the "On watch" deck can show full-corpus stats when the
    # user opts out of page-scoped stats. Sums ``assets_count`` (falling back to
    # 1 per event) to mirror the frontend's per-page derivation.
    provider_counts_query = (
        select(
            EventLog.provider_id,
            EventLog.provider_name,
            func.sum(func.coalesce(EventLog.assets_count, 1)).label("total"),
        )
        .where(EventLog.user_id == user.id)
        .group_by(EventLog.provider_id, EventLog.provider_name)
    )
    if event_type:
        provider_counts_query = provider_counts_query.where(EventLog.event_type == event_type)
    if provider_id is not None:
        provider_counts_query = provider_counts_query.where(EventLog.provider_id == provider_id)
    if search:
        provider_counts_query = provider_counts_query.where(
            EventLog.collection_name.contains(search)
            | EventLog.tracker_name.contains(search)
            | EventLog.action_name.contains(search)
            | EventLog.provider_name.contains(search)
        )
    provider_counts_rows = (await session.exec(provider_counts_query)).all()

    # Sort
    if sort == "oldest":
        events_query = events_query.order_by(EventLog.created_at.asc())
    else:
        events_query = events_query.order_by(EventLog.created_at.desc())

    events_query = events_query.offset(offset).limit(limit)
    recent_events = await session.exec(events_query)
    event_rows = recent_events.all()

    # Resolve live tracker names from FK (fall back to stored snapshot when deleted)
    tracker_ids = {e.tracker_id for e in event_rows if e.tracker_id is not None}
    tracker_name_map: dict[int, str] = {}
    if tracker_ids:
        tracker_rows = (await session.exec(
            select(NotificationTracker.id, NotificationTracker.name).where(
                NotificationTracker.id.in_(tracker_ids)
            )
        )).all()
        tracker_name_map = {tid: tname for tid, tname in tracker_rows}

    # Resolve live provider names similarly. Includes IDs from the aggregated
    # provider counts so the "all events" deck shows up-to-date names even for
    # providers that don't appear on the current page.
    provider_ids = {e.provider_id for e in event_rows if e.provider_id is not None}
    provider_ids.update(pid for pid, _pname, _total in provider_counts_rows if pid is not None)
    provider_name_map: dict[int, str] = {}
    if provider_ids:
        provider_rows = (await session.exec(
            select(ServiceProvider.id, ServiceProvider.name).where(
                ServiceProvider.id.in_(provider_ids)
            )
        )).all()
        provider_name_map = {pid: pname for pid, pname in provider_rows}

    # Resolve live action names so renames are reflected; fall back to snapshot.
    action_ids = {e.action_id for e in event_rows if e.action_id is not None}
    action_name_map: dict[int, str] = {}
    if action_ids:
        action_rows = (await session.exec(
            select(Action.id, Action.name).where(Action.id.in_(action_ids))
        )).all()
        action_name_map = {aid: aname for aid, aname in action_rows}

    # Live-resolve command tracker and bot names for command_* events
    # (mirrors the action/tracker pattern above). Falls back to the
    # snapshot stored on the EventLog when the entity has been deleted.
    cmd_tracker_ids = {
        e.command_tracker_id for e in event_rows if e.command_tracker_id is not None
    }
    cmd_tracker_name_map: dict[int, str] = {}
    if cmd_tracker_ids:
        cmd_tracker_rows = (await session.exec(
            select(CommandTracker.id, CommandTracker.name).where(
                CommandTracker.id.in_(cmd_tracker_ids)
            )
        )).all()
        cmd_tracker_name_map = {tid: tname for tid, tname in cmd_tracker_rows}

    bot_ids = {
        e.telegram_bot_id for e in event_rows if e.telegram_bot_id is not None
    }
    bot_name_map: dict[int, str] = {}
    if bot_ids:
        bot_rows = (await session.exec(
            select(TelegramBot.id, TelegramBot.name).where(TelegramBot.id.in_(bot_ids))
        )).all()
        bot_name_map = {bid: bname for bid, bname in bot_rows}

    def _display_tracker_name(e: EventLog) -> str:
        if e.tracker_id is not None and e.tracker_id in tracker_name_map:
            return tracker_name_map[e.tracker_id]
        return f"(deleted) {e.tracker_name}" if e.tracker_name else "(deleted)"

    def _display_provider_name(e: EventLog) -> str:
        if e.provider_id is not None and e.provider_id in provider_name_map:
            return provider_name_map[e.provider_id]
        return e.provider_name or ""

    def _display_action_name(e: EventLog) -> str:
        if e.action_id is not None and e.action_id in action_name_map:
            return action_name_map[e.action_id]
        if e.action_name:
            return f"(deleted) {e.action_name}"
        return ""

    def _display_command_tracker_name(e: EventLog) -> str:
        if (
            e.command_tracker_id is not None
            and e.command_tracker_id in cmd_tracker_name_map
        ):
            return cmd_tracker_name_map[e.command_tracker_id]
        if e.command_tracker_name:
            return f"(deleted) {e.command_tracker_name}"
        return ""

    def _display_bot_name(e: EventLog) -> str:
        if e.telegram_bot_id is not None and e.telegram_bot_id in bot_name_map:
            return bot_name_map[e.telegram_bot_id]
        if e.bot_name:
            return f"(deleted) {e.bot_name}"
        return ""

    def _display_subject(e: EventLog) -> str:
        """The primary label shown on the event row.

        For action events the ``collection_name`` stores the action name;
        use the live-resolved action name when available so renames show.
        For command events the ``collection_name`` already stores the
        rendered ``/cmd args`` string so we just pass it through.
        """
        if e.action_id is not None or (e.event_type or "").startswith("action_"):
            return _display_action_name(e) or e.collection_name
        return e.collection_name

    # Build the provider event count map keyed by live provider name (matches
    # the frontend's keying scheme). Falls back to the stored snapshot name
    # when the provider has been deleted.
    provider_event_counts: dict[str, int] = {}
    for pid, pname, total in provider_counts_rows:
        display_name = (
            provider_name_map.get(pid) if pid is not None else None
        ) or pname or ""
        if not display_name:
            continue
        provider_event_counts[display_name] = (
            provider_event_counts.get(display_name, 0) + int(total or 0)
        )

    return {
        "providers": providers_count,
        "trackers": {"total": len(trackers), "active": active_count},
        "targets": targets_count,
        "total_events": total_events,
        "provider_event_counts": provider_event_counts,
        "recent_events": [
            {
                "id": e.id,
                "event_type": e.event_type,
                "collection_name": _display_subject(e),
                "tracker_id": e.tracker_id,
                "tracker_name": _display_tracker_name(e),
                "action_id": e.action_id,
                "action_name": _display_action_name(e),
                "command_tracker_id": e.command_tracker_id,
                "command_tracker_name": _display_command_tracker_name(e),
                "telegram_bot_id": e.telegram_bot_id,
                "bot_name": _display_bot_name(e),
                "provider_name": _display_provider_name(e),
                "provider_id": e.provider_id,
                "assets_count": e.assets_count or 0,
                "created_at": e.created_at.isoformat() + ("Z" if not e.created_at.tzinfo else ""),
                "details": e.details or {},
            }
            for e in event_rows
        ],
    }


@router.delete("/events")
async def clear_events(
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
    older_than_days: int | None = Query(None, ge=0),
):
    """Delete all event log entries for the current user.

    Optionally keep events newer than `older_than_days` days.
    """
    stmt = sa_delete(EventLog).where(EventLog.user_id == user.id)
    if older_than_days is not None:
        cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
        stmt = stmt.where(EventLog.created_at < cutoff)

    # Use session.execute() for DELETE (consistent with other endpoints and
    # avoids sqlmodel wrapping a CursorResult that may drop rowcount).
    result = await session.execute(stmt)
    await session.commit()
    return {"deleted": result.rowcount or 0}


@router.get("/counts")
async def get_nav_counts(
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    """Return entity counts for sidebar navigation badges.

    Combines user-owned counts, system-owned shared counts, and per-type
    target counts into a single round-trip via a UNION ALL of label + count
    rows. SQLAlchemy AsyncSession is single-threaded so we cannot
    asyncio.gather; collapsing 16 SELECTs into one is the optimisation.
    """
    from sqlalchemy import literal, union_all

    counts: dict[str, int] = {}

    user_id = user.id

    # User-owned counts: one (label, count) per model.
    user_models = [
        (ServiceProvider, "providers"),
        (NotificationTracker, "notification_trackers"),
        (TrackingConfig, "tracking_configs"),
        (TemplateConfig, "template_configs"),
        (NotificationTarget, "targets"),
        (TelegramBot, "telegram_bots"),
        (EmailBot, "email_bots"),
        (MatrixBot, "matrix_bots"),
        (CommandTracker, "command_trackers"),
        (CommandConfig, "command_configs"),
        (CommandTemplateConfig, "command_template_configs"),
    ]
    # System-owned shared counts (user_id=0) folded back into the same key.
    system_models = [
        (TemplateConfig, "template_configs"),
        (CommandTemplateConfig, "command_template_configs"),
        (TrackingConfig, "tracking_configs"),
        (CommandConfig, "command_configs"),
    ]
    target_types = ("telegram", "webhook", "email", "discord", "slack", "ntfy", "matrix")

    # Initialise counts to 0 so missing UNION rows surface as zeroes
    # instead of KeyErrors when a category has no rows.
    for _model, key in user_models:
        counts[key] = 0
    for ttype in target_types:
        counts[f"targets_{ttype}"] = 0

    queries = []
    for model, key in user_models:
        queries.append(
            select(literal(key).label("k"), func.count().label("c"))
            .select_from(model).where(model.user_id == user_id)
        )
    for model, key in system_models:
        queries.append(
            select(literal(f"__sys__:{key}").label("k"), func.count().label("c"))
            .select_from(model).where(model.user_id == 0)
        )
    for ttype in target_types:
        queries.append(
            select(literal(f"target:{ttype}").label("k"), func.count().label("c"))
            .select_from(NotificationTarget).where(
                NotificationTarget.user_id == user_id,
                NotificationTarget.type == ttype,
            )
        )

    union_q = union_all(*queries)
    rows = (await session.execute(union_q)).all()
    for label, value in rows:
        if label.startswith("__sys__:"):
            counts[label.removeprefix("__sys__:")] += int(value or 0)
        elif label.startswith("target:"):
            counts[f"targets_{label.removeprefix('target:')}"] = int(value or 0)
        else:
            counts[label] = int(value or 0)

    return counts


@router.get("/chart")
async def get_event_chart(
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
    days: int = Query(14, ge=1, le=90),
    event_type: str | None = Query(None),
    provider_id: int | None = Query(None),
    search: str | None = Query(None),
):
    """Return daily event counts by type for the last N days, with optional filters."""
    cutoff = datetime.now(timezone.utc) - timedelta(days=days)

    day_col = func.date(EventLog.created_at)

    query = (
        select(
            day_col.label("day"),
            EventLog.event_type,
            func.count().label("total"),
        )
        .where(EventLog.user_id == user.id, EventLog.created_at >= cutoff)
    )

    if event_type:
        query = query.where(EventLog.event_type == event_type)
    if provider_id is not None:
        query = query.where(EventLog.provider_id == provider_id)
    if search:
        query = query.where(
            EventLog.collection_name.contains(search)
            | EventLog.tracker_name.contains(search)
            | EventLog.action_name.contains(search)
            | EventLog.provider_name.contains(search)
        )

    query = query.group_by(day_col, EventLog.event_type).order_by(day_col)

    rows = (await session.exec(query)).all()

    # Build a dict: { "2026-03-15": { "assets_added": 18, ... }, ... }
    by_day: dict[str, dict[str, int]] = {}
    for row in rows:
        day_str = str(row.day)
        if day_str not in by_day:
            by_day[day_str] = {}
        by_day[day_str][row.event_type] = row.total

    # Fill in missing days so the frontend gets a continuous series
    result = []
    for i in range(days):
        d = (datetime.now(timezone.utc) - timedelta(days=days - 1 - i)).strftime("%Y-%m-%d")
        counts = by_day.get(d, {})
        result.append({"date": d, **counts})

    return {"days": result}
