"""Simplified alert system.

Send alerts to Telegram or webhooks. No classes. No inheritance. No plugin architecture.

Usage:
    payload = AlertPayload(
        job_id="my-job",
        job_name="My Job",
        event_type=AlertEvent.CONFIRMED,
        message="Transaction confirmed!",
    )
    config = AlertConfig(
        telegram_token="...",
        telegram_chat_ids=["123456"],
    )
    await send_alert(payload, config)
"""

from __future__ import annotations

import asyncio
import threading
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Coroutine

import httpx

from brawny.logging import get_logger

logger = get_logger(__name__)


class AlertEvent(str, Enum):
    """Alert event types. Aligned with OE2 hook reduction."""

    TRIGGERED = "triggered"
    CONFIRMED = "confirmed"
    FAILED = "failed"


@dataclass
class AlertPayload:
    """Data object for alert content."""

    job_id: str
    job_name: str
    event_type: AlertEvent
    message: str
    parse_mode: str = "Markdown"
    chain_id: int = 1
    timestamp: datetime = field(default_factory=datetime.utcnow)


@dataclass
class AlertConfig:
    """Transport configuration. Passed once, not spread across callsites."""

    telegram_token: str | None = None
    telegram_chat_ids: list[str] = field(default_factory=list)
    webhook_url: str | None = None
    rate_limit_seconds: float = 3.0


# Module-level state for rate limiting only
# NOTE: No module-level httpx.AsyncClient - asyncio objects are not safe to share
# across multiple event loops / loop lifetimes. For low-volume alerts, we create
# a fresh client per request (httpx context manager handles cleanup).
_last_sent: dict[str, datetime] = {}
# Use threading.Lock, not asyncio.Lock - avoids event loop binding issues
_last_sent_lock = threading.Lock()


async def send_alert(payload: AlertPayload, config: AlertConfig) -> None:
    """Send alert to configured destinations. Fire-and-forget."""
    tasks: list[Coroutine[Any, Any, None]] = []

    if config.telegram_token and config.telegram_chat_ids:
        for chat_id in config.telegram_chat_ids:
            if _should_send(payload, "telegram", chat_id, config.rate_limit_seconds):
                tasks.append(_send_telegram(config.telegram_token, chat_id, payload))

    if config.webhook_url:
        if _should_send(payload, "webhook", config.webhook_url, config.rate_limit_seconds):
            tasks.append(_send_webhook(config.webhook_url, payload))

    if tasks:
        results = await asyncio.gather(*tasks, return_exceptions=True)
        for i, result in enumerate(results):
            if isinstance(result, Exception):
                _log_failure(payload, tasks[i], result)


def _should_send(
    payload: AlertPayload,
    dest_type: str,
    destination_id: str,
    limit_seconds: float,
) -> bool:
    """Check rate limit. Key includes dest_type to avoid collisions.

    Key format: job_id:event_type:dest_type:destination_id
    - Multiple chat IDs rate-limited independently
    - Telegram + webhook don't suppress each other
    - dest_type prevents test collisions

    Uses threading.Lock (not asyncio.Lock) to avoid event loop binding issues.
    """
    key = f"{payload.job_id}:{payload.event_type.value}:{dest_type}:{destination_id}"

    with _last_sent_lock:
        now = datetime.utcnow()
        if key in _last_sent:
            if (now - _last_sent[key]).total_seconds() < limit_seconds:
                return False
        _last_sent[key] = now
        return True


async def _send_telegram(token: str, chat_id: str, payload: AlertPayload) -> None:
    """Send message to Telegram. Pure function, no state."""
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    data = {
        "chat_id": chat_id,
        "text": payload.message,
        "parse_mode": payload.parse_mode,
        "disable_web_page_preview": True,
    }
    async with httpx.AsyncClient(timeout=10.0) as client:
        resp = await client.post(url, json=data)
        resp.raise_for_status()


async def _send_webhook(url: str, payload: AlertPayload) -> None:
    """Send payload to webhook. Pure function, no state.

    Schema (frozen):
    - job_id: str
    - job_name: str
    - event_type: str (enum value)
    - message: str
    - chain_id: int
    - timestamp: str (ISO8601 UTC)

    Do not add fields without versioning discussion.
    """
    async with httpx.AsyncClient(timeout=10.0) as client:
        resp = await client.post(
            url,
            json={
                "job_id": payload.job_id,
                "job_name": payload.job_name,
                "event_type": payload.event_type.value,
                "message": payload.message,
                "chain_id": payload.chain_id,
                "timestamp": payload.timestamp.isoformat() + "Z",
            },
        )
        resp.raise_for_status()


def _log_failure(payload: AlertPayload, task: Coroutine[Any, Any, None], error: Exception) -> None:
    """Log alert failure with enough context to debug."""
    task_name = task.__qualname__ if hasattr(task, "__qualname__") else str(task)

    if "telegram" in task_name.lower():
        logger.warning(
            "alert_delivery_failed",
            job_id=payload.job_id,
            event_type=payload.event_type.value,
            destination="telegram",
            error=str(error),
        )
    elif "webhook" in task_name.lower():
        logger.warning(
            "alert_delivery_failed",
            job_id=payload.job_id,
            event_type=payload.event_type.value,
            destination="webhook",
            error=str(error),
        )
    else:
        logger.warning(
            "alert_delivery_failed",
            job_id=payload.job_id,
            event_type=payload.event_type.value,
            error=str(error),
        )


# =============================================================================
# Public alert() Function for Job Hooks
# =============================================================================


def alert(message: str, *, parse_mode: str | None = None) -> None:
    """Send alert from within a job hook.

    Behavior:
    - In sync context: blocking (asyncio.run)
    - In async context: fire-and-forget (create_task, exceptions logged)

    In async context, delivery is best-effort; failures are logged and do not
    fail the hook.

    Must be called from on_trigger, on_success, or on_failure.
    Uses current job context for routing (chat IDs, etc.)

    Args:
        message: Alert text
        parse_mode: "Markdown", "MarkdownV2", "HTML", or None (default: "Markdown")

    Raises:
        RuntimeError: If called outside a job hook

    Example:
        def on_success(self, ctx):
            alert(f"Harvested {ctx.intent.metadata['profit']} rewards")
    """
    from brawny._context import get_alert_context

    ctx = get_alert_context()
    if ctx is None:
        raise RuntimeError("alert() must be called from within a job hook")

    # Build payload from context
    payload = AlertPayload(
        job_id=ctx.job_id,
        job_name=ctx.job_name,
        event_type=AlertEvent.CONFIRMED,  # Generic info event
        message=message,
        parse_mode=parse_mode or "Markdown",
        chain_id=ctx.chain_id,
    )

    # Dispatch based on event loop state
    try:
        asyncio.get_running_loop()
    except RuntimeError:
        # No event loop - run synchronously (blocking)
        asyncio.run(send_alert(payload, ctx.alert_config))
    else:
        # Inside event loop - fire-and-forget with exception logging
        asyncio.create_task(_send_alert_logged(payload, ctx.alert_config))


async def _send_alert_logged(payload: AlertPayload, config: AlertConfig) -> None:
    """Fire-and-forget alert with exception logging."""
    try:
        await send_alert(payload, config)
    except Exception:
        logger.exception("Failed to send alert", job_id=payload.job_id)


