"""Bridge self-monitoring bot command handler.

Read-only commands plus a ``/reset`` operator action that clears in-memory
failure counters once the underlying issue has been fixed.

Counters are kept in module-level dicts inside
``services.bridge_self`` — see that module for the increment / reset
helpers used by the watcher / scheduler / dispatcher hot path. We only
read snapshots from those dicts here so we never block the emission
side or hold a lock across DB calls.
"""

from __future__ import annotations

import logging
from typing import Any

from ..database.models import (
    CommandConfig,
    CommandTracker,
    CommandTrackerListener,
    ServiceProvider,
    TelegramBot,
)
from ..services import bridge_self as bs
from .base import CommandResponse, ProviderCommandHandler
from .handler import _render_cmd_template

_LOGGER = logging.getLogger(__name__)

_BRIDGE_SELF_COMMANDS = {"status", "thresholds", "reset", "health"}


# ---------------------------------------------------------------------------
# Context builders — one per command. Each returns the dict passed to the
# template renderer.
# ---------------------------------------------------------------------------


async def _build_status_context(provider: ServiceProvider) -> dict[str, Any]:
    """Snapshot the calling user's counters + pending deferred backlog.

    User-scoped: ``get_user_*_failures`` filter the global counter dicts by
    ownership in a single batched query so one user cannot see another
    user's failing trackers/targets.
    """
    thresholds = await bs.get_user_thresholds(provider.user_id)

    poll_rows = await bs.get_user_poll_failures(provider.user_id)
    poll_failures = sorted(
        ({"tracker_id": r["id"], "tracker_name": r["name"], "count": r["count"]} for r in poll_rows),
        key=lambda r: r["tracker_id"],
    )

    target_rows = await bs.get_user_target_failures(provider.user_id)
    target_failures = sorted(
        ({"target_id": r["id"], "target_name": r["name"], "count": r["count"]} for r in target_rows),
        key=lambda r: r["target_id"],
    )

    deferred_pending = await bs.get_pending_deferred_count(provider.user_id)

    return {
        "poll_failures": poll_failures,
        "deferred_pending": deferred_pending,
        "deferred_threshold": thresholds["deferred_backlog_threshold"],
        "target_failures": target_failures,
    }


async def _build_thresholds_context(provider: ServiceProvider) -> dict[str, Any]:
    """Render the user's configured alert thresholds."""
    thresholds = await bs.get_user_thresholds(provider.user_id)
    return {
        "poll_failure_threshold": thresholds["poll_failure_threshold"],
        "deferred_backlog_threshold": thresholds["deferred_backlog_threshold"],
        "target_failure_threshold": thresholds["target_failure_threshold"],
    }


def _parse_reset_subject(args: str) -> tuple[str, int | None, str | None]:
    """Parse a ``/reset`` argument into ``(subject_type, subject_id, error)``.

    Accepted forms:
      * ``tracker:<id>`` — clear that tracker's poll-failure counter
      * ``target:<id>`` — clear that target's send-failure counter
      * ``all`` — clear every in-memory counter

    On a parse error we return ``("", None, "<message>")`` so the handler can
    render a templated error reply instead of propagating an exception.
    """
    raw = (args or "").strip()
    if not raw:
        return "", None, "Missing subject. Use tracker:<id>, target:<id>, or all."
    if raw.lower() == "all":
        return "all", None, None
    if ":" not in raw:
        return "", None, (
            f"Invalid subject '{raw}'. Use tracker:<id>, target:<id>, or all."
        )
    prefix, _, value = raw.partition(":")
    prefix = prefix.strip().lower()
    value = value.strip()
    if prefix not in ("tracker", "target"):
        return "", None, (
            f"Unknown subject type '{prefix}'. Use tracker, target, or all."
        )
    try:
        subject_id = int(value)
    except ValueError:
        return "", None, f"Invalid id '{value}'. Must be an integer."
    if subject_id <= 0:
        return "", None, f"Invalid id '{value}'. Must be a positive integer."
    return prefix, subject_id, None


async def _build_reset_context(args: str, provider: ServiceProvider) -> dict[str, Any]:
    """Reset a counter, enforcing that the subject belongs to ``provider.user_id``.

    Without this ownership check, any authenticated user could reset another
    user's counters (or wipe global state via ``all``). For ``all`` we only
    clear counters tied to trackers/targets owned by the calling user.
    """
    subject_type, subject_id, error = _parse_reset_subject(args)
    if error is not None:
        return {
            "subject_type": "",
            "subject_id": None,
            "subject_name": "",
            "previous_count": 0,
            "success": False,
            "error_message": error,
        }

    if subject_type == "all":
        cleared = await bs.reset_user_counters(provider.user_id)
        return {
            "subject_type": "all",
            "subject_id": None,
            "subject_name": "your counters",
            "previous_count": cleared,
            "success": True,
            "error_message": None,
        }

    # Verify the subject belongs to the calling user before touching the counter.
    owner_lookup = bs.find_tracker_owner if subject_type == "tracker" else bs.find_target_owner
    owner = await owner_lookup(subject_id) if subject_id is not None else None
    if owner is None or owner != provider.user_id:
        return {
            "subject_type": subject_type,
            "subject_id": subject_id,
            "subject_name": "",
            "previous_count": 0,
            "success": False,
            "error_message": f"{subject_type} {subject_id} not found",
        }

    failure_type = "poll_failures" if subject_type == "tracker" else "target_failures"
    name_lookup = bs.get_tracker_name if subject_type == "tracker" else bs.get_target_name
    subject_name = await name_lookup(subject_id) if subject_id is not None else ""
    previous = bs.reset_counter(failure_type, subject_id)
    return {
        "subject_type": subject_type,
        "subject_id": subject_id,
        "subject_name": subject_name,
        "previous_count": previous,
        "success": True,
        "error_message": None,
    }


async def _build_health_context(provider: ServiceProvider) -> dict[str, Any]:
    """Build the terse one-line health summary context."""
    from ..database.models import NotificationTracker
    from sqlmodel import select
    from sqlmodel.ext.asyncio.session import AsyncSession

    from ..database.engine import get_engine

    engine = get_engine()
    try:
        async with AsyncSession(engine) as session:
            result = await session.exec(
                select(NotificationTracker).where(
                    NotificationTracker.user_id == provider.user_id,
                    NotificationTracker.enabled == True,  # noqa: E712 — SQLModel needs ==
                )
            )
            tracker_count = len(list(result.all()))
    except Exception:  # noqa: BLE001
        _LOGGER.exception("health: failed to count trackers for user=%s", provider.user_id)
        tracker_count = 0

    # User-scoped: only count failures that belong to THIS user's trackers/targets.
    failing_tracker_count = len(await bs.get_user_poll_failures(provider.user_id))
    failing_target_count = len(await bs.get_user_target_failures(provider.user_id))
    deferred_pending = await bs.get_pending_deferred_count(provider.user_id)

    # Treat unknown deferred count (DB error) as not-healthy so operators
    # don't get a misleading "all clear" when the bridge can't introspect itself.
    healthy = (
        failing_tracker_count == 0
        and failing_target_count == 0
        and deferred_pending == 0
    )
    if healthy:
        summary = f"Bridge healthy: {tracker_count} trackers polling, 0 failures"
    else:
        parts: list[str] = []
        if failing_tracker_count:
            parts.append(f"{failing_tracker_count} trackers failing")
        if deferred_pending is None:
            parts.append("deferred backlog unknown (DB unavailable)")
        elif deferred_pending:
            parts.append(f"{deferred_pending} deferred")
        if failing_target_count:
            parts.append(f"{failing_target_count} targets failing")
        summary = ", ".join(parts) or "bridge state unknown"

    return {
        "healthy": healthy,
        "summary": summary,
        "tracker_count": tracker_count,
        "failing_tracker_count": failing_tracker_count,
        "deferred_pending": deferred_pending,
        "failing_target_count": failing_target_count,
    }


# ---------------------------------------------------------------------------
# Handler
# ---------------------------------------------------------------------------


class BridgeSelfCommandHandler(ProviderCommandHandler):
    """Read-only commands plus /reset for the bridge_self provider."""

    provider_type = "bridge_self"

    def get_provider_commands(self) -> set[str]:
        return _BRIDGE_SELF_COMMANDS

    async def handle(
        self,
        cmd: str,
        args: str,
        count: int,
        locale: str,
        response_mode: str,
        provider: ServiceProvider,
        cmd_templates: dict[str, dict[str, str]],
        bot: TelegramBot,
        tracker: CommandTracker,
        config: CommandConfig,
        *,
        listener: CommandTrackerListener | None = None,
        allowed_album_ids: set[str] | None = None,  # noqa: ARG002 — bridge_self has no album scope
        page: int = 1,
    ) -> CommandResponse | None:
        if cmd == "status":
            ctx = await _build_status_context(provider)
            return CommandResponse(text=_render_cmd_template(cmd_templates, "status", locale, ctx))
        if cmd == "thresholds":
            ctx = await _build_thresholds_context(provider)
            return CommandResponse(text=_render_cmd_template(cmd_templates, "thresholds", locale, ctx))
        if cmd == "reset":
            ctx = await _build_reset_context(args, provider)
            return CommandResponse(text=_render_cmd_template(cmd_templates, "reset", locale, ctx))
        if cmd == "health":
            ctx = await _build_health_context(provider)
            return CommandResponse(text=_render_cmd_template(cmd_templates, "health", locale, ctx))
        return None
