"""
Budget Alert System - GitHub-style budget tracking with alerts.

Provides:
- Configurable budget limits (daily, monthly)
- Threshold-based alerting (75%, 90%, 100%)
- Product-specific and bundled budgets
- Blocking on budget exhaustion
"""

import asyncio
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Callable, Optional

logger = logging.getLogger(__name__)


class AlertSeverity(Enum):
    """Alert severity levels."""

    INFO = "info"
    WARNING = "warning"
    CRITICAL = "critical"


class BudgetType(Enum):
    """Types of budgets."""

    DAILY = "daily"
    WEEKLY = "weekly"
    MONTHLY = "monthly"
    PER_TASK = "per_task"
    PER_AGENT = "per_agent"


@dataclass
class BudgetConfig:
    """Budget configuration with limits and thresholds."""

    # Spending limits
    daily_limit_usd: float = 50.0
    weekly_limit_usd: float = 200.0
    monthly_limit_usd: float = 500.0

    # Per-task and per-agent limits
    per_task_limit_usd: Optional[float] = None
    per_agent_limit_usd: Optional[float] = None

    # Alert thresholds (percentages)
    alert_thresholds: list[int] = field(default_factory=lambda: [50, 75, 90, 100])

    # Behavior
    block_on_exhaustion: bool = True
    soft_limit_percentage: float = 110.0  # Allow slight overage before hard block

    # Notification settings
    notification_channels: list[str] = field(default_factory=list)
    alert_cooldown_minutes: int = 60  # Don't repeat same alert within cooldown

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "daily_limit_usd": self.daily_limit_usd,
            "weekly_limit_usd": self.weekly_limit_usd,
            "monthly_limit_usd": self.monthly_limit_usd,
            "per_task_limit_usd": self.per_task_limit_usd,
            "per_agent_limit_usd": self.per_agent_limit_usd,
            "alert_thresholds": self.alert_thresholds,
            "block_on_exhaustion": self.block_on_exhaustion,
            "soft_limit_percentage": self.soft_limit_percentage,
        }


@dataclass
class BudgetAlert:
    """A budget alert."""

    alert_id: str
    budget_type: BudgetType
    threshold_percent: int
    current_spend_usd: float
    limit_usd: float
    severity: AlertSeverity
    message: str
    created_at: datetime = field(default_factory=datetime.now)
    agent_id: Optional[str] = None
    task_id: Optional[str] = None

    @property
    def percentage_used(self) -> float:
        """Get percentage of budget used."""
        if self.limit_usd <= 0:
            return 0.0
        return (self.current_spend_usd / self.limit_usd) * 100

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "alert_id": self.alert_id,
            "budget_type": self.budget_type.value,
            "threshold_percent": self.threshold_percent,
            "current_spend_usd": self.current_spend_usd,
            "limit_usd": self.limit_usd,
            "severity": self.severity.value,
            "message": self.message,
            "created_at": self.created_at.isoformat(),
            "percentage_used": self.percentage_used,
            "agent_id": self.agent_id,
            "task_id": self.task_id,
        }


class BudgetExhaustedError(Exception):
    """Raised when budget is exhausted and blocking is enabled."""

    def __init__(self, message: str, budget_type: BudgetType, current_spend: float, limit: float):
        super().__init__(message)
        self.budget_type = budget_type
        self.current_spend = current_spend
        self.limit = limit


class BudgetAlertSystem:
    """
    GitHub-style budget tracking with alerts.

    Monitors spending against configured limits and sends alerts
    at configurable thresholds (default: 50%, 75%, 90%, 100%).

    Example:
        config = BudgetConfig(
            daily_limit_usd=50.0,
            alert_thresholds=[75, 90, 100],
        )
        alerts = BudgetAlertSystem(config)

        # Register callback
        alerts.on_alert(send_to_slack)

        # Check spending
        alerts.check_and_alert(current_spend=35.0, budget_type=BudgetType.DAILY)
    """

    def __init__(self, config: Optional[BudgetConfig] = None):
        """
        Initialize the alert system.

        Args:
            config: Budget configuration
        """
        self._config = config or BudgetConfig()
        self._alerts_sent: dict[str, datetime] = {}  # alert_key -> last_sent
        self._alert_history: list[BudgetAlert] = []
        self._callbacks: list[Callable[[BudgetAlert], None]] = []
        self._alert_counter = 0

    def set_config(self, config: BudgetConfig) -> None:
        """Update configuration."""
        self._config = config

    def get_config(self) -> BudgetConfig:
        """Get current configuration."""
        return self._config

    def on_alert(self, callback: Callable[[BudgetAlert], None]) -> None:
        """Register alert callback."""
        self._callbacks.append(callback)

    def check_and_alert(
        self,
        current_spend: float,
        budget_type: BudgetType,
        agent_id: Optional[str] = None,
        task_id: Optional[str] = None,
    ) -> Optional[BudgetAlert]:
        """
        Check spending against budget and send alerts.

        Args:
            current_spend: Current spending amount
            budget_type: Type of budget to check
            agent_id: Optional agent ID for per-agent budgets
            task_id: Optional task ID for per-task budgets

        Returns:
            BudgetAlert if alert was triggered, None otherwise

        Raises:
            BudgetExhaustedError: If budget exhausted and blocking enabled
        """
        limit = self._get_limit(budget_type, agent_id)
        if limit <= 0:
            return None

        percentage = (current_spend / limit) * 100
        triggered_alert = None

        # Check thresholds in descending order
        for threshold in sorted(self._config.alert_thresholds, reverse=True):
            if percentage >= threshold:
                alert = self._create_alert_if_needed(
                    budget_type=budget_type,
                    threshold=threshold,
                    current_spend=current_spend,
                    limit=limit,
                    agent_id=agent_id,
                    task_id=task_id,
                )
                if alert:
                    triggered_alert = alert
                break

        # Check for budget exhaustion
        if percentage >= 100 and self._config.block_on_exhaustion:
            if percentage >= self._config.soft_limit_percentage:
                raise BudgetExhaustedError(
                    f"{budget_type.value.title()} budget exhausted: "
                    f"${current_spend:.2f} / ${limit:.2f}",
                    budget_type=budget_type,
                    current_spend=current_spend,
                    limit=limit,
                )

        return triggered_alert

    async def check_and_alert_async(
        self,
        current_spend: float,
        budget_type: BudgetType,
        agent_id: Optional[str] = None,
        task_id: Optional[str] = None,
    ) -> Optional[BudgetAlert]:
        """Async version of check_and_alert."""
        return self.check_and_alert(current_spend, budget_type, agent_id, task_id)

    def _get_limit(self, budget_type: BudgetType, agent_id: Optional[str] = None) -> float:
        """Get limit for budget type."""
        limits = {
            BudgetType.DAILY: self._config.daily_limit_usd,
            BudgetType.WEEKLY: self._config.weekly_limit_usd,
            BudgetType.MONTHLY: self._config.monthly_limit_usd,
            BudgetType.PER_TASK: self._config.per_task_limit_usd or 0,
            BudgetType.PER_AGENT: self._config.per_agent_limit_usd or 0,
        }
        return limits.get(budget_type, 0)

    def _create_alert_if_needed(
        self,
        budget_type: BudgetType,
        threshold: int,
        current_spend: float,
        limit: float,
        agent_id: Optional[str] = None,
        task_id: Optional[str] = None,
    ) -> Optional[BudgetAlert]:
        """Create alert if not in cooldown."""
        alert_key = f"{budget_type.value}_{threshold}_{agent_id or ''}"

        # Check cooldown
        if alert_key in self._alerts_sent:
            last_sent = self._alerts_sent[alert_key]
            cooldown = timedelta(minutes=self._config.alert_cooldown_minutes)
            if datetime.now() - last_sent < cooldown:
                return None

        # Determine severity
        if threshold >= 100:
            severity = AlertSeverity.CRITICAL
        elif threshold >= 90:
            severity = AlertSeverity.CRITICAL
        elif threshold >= 75:
            severity = AlertSeverity.WARNING
        else:
            severity = AlertSeverity.INFO

        # Create alert
        self._alert_counter += 1
        alert = BudgetAlert(
            alert_id=f"alert-{self._alert_counter}",
            budget_type=budget_type,
            threshold_percent=threshold,
            current_spend_usd=current_spend,
            limit_usd=limit,
            severity=severity,
            message=self._format_alert_message(budget_type, threshold, current_spend, limit),
            agent_id=agent_id,
            task_id=task_id,
        )

        # Record and notify
        self._alerts_sent[alert_key] = datetime.now()
        self._alert_history.append(alert)
        self._notify_callbacks(alert)

        logger.warning(f"Budget alert: {alert.message}")
        return alert

    def _format_alert_message(
        self,
        budget_type: BudgetType,
        threshold: int,
        current_spend: float,
        limit: float,
    ) -> str:
        """Format alert message."""
        type_name = budget_type.value.replace("_", " ").title()

        if threshold >= 100:
            return f"{type_name} budget EXHAUSTED: ${current_spend:.2f} / ${limit:.2f}"
        else:
            return f"{type_name} budget at {threshold}%: ${current_spend:.2f} / ${limit:.2f}"

    def _notify_callbacks(self, alert: BudgetAlert) -> None:
        """Notify all registered callbacks."""
        for callback in self._callbacks:
            try:
                callback(alert)
            except Exception as e:
                logger.error(f"Error in alert callback: {e}")

    def get_alert_history(
        self,
        budget_type: Optional[BudgetType] = None,
        severity: Optional[AlertSeverity] = None,
        limit: int = 100,
    ) -> list[BudgetAlert]:
        """
        Get alert history.

        Args:
            budget_type: Filter by budget type
            severity: Filter by severity
            limit: Maximum results

        Returns:
            List of alerts
        """
        results = self._alert_history

        if budget_type:
            results = [a for a in results if a.budget_type == budget_type]
        if severity:
            results = [a for a in results if a.severity == severity]

        return results[-limit:]

    def clear_alert_cooldowns(self) -> None:
        """Clear all alert cooldowns (useful for testing)."""
        self._alerts_sent.clear()

    def get_status(self) -> dict[str, Any]:
        """Get current budget status."""
        return {
            "config": self._config.to_dict(),
            "alerts_sent_count": len(self._alert_history),
            "active_cooldowns": len(self._alerts_sent),
        }


# Module-level instance
_alert_system: Optional[BudgetAlertSystem] = None


def get_budget_alert_system() -> BudgetAlertSystem:
    """Get or create the global budget alert system."""
    global _alert_system
    if _alert_system is None:
        _alert_system = BudgetAlertSystem()
    return _alert_system


def set_budget_alert_system(system: Optional[BudgetAlertSystem]) -> None:
    """Set the global budget alert system."""
    global _alert_system
    _alert_system = system
