"""
Wallet Activity Monitoring for Sardis.

Real-time monitoring and alerting for wallet activity with pattern detection,
anomaly identification, and automated responses.

Features:
- Real-time transaction monitoring
- Pattern detection and anomaly alerts
- Risk scoring for transactions
- Automated alert notifications
- Activity dashboards and reports
- Watchlist management
"""
from __future__ import annotations

import asyncio
import logging
import statistics
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Protocol, Set, Tuple, TYPE_CHECKING

if TYPE_CHECKING:
    from sardis_v2_core import Wallet, Transaction

logger = logging.getLogger(__name__)


class AlertSeverity(str, Enum):
    """Severity level of an alert."""
    INFO = "info"
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


class AlertType(str, Enum):
    """Type of alert."""
    LARGE_TRANSACTION = "large_transaction"
    UNUSUAL_TIME = "unusual_time"
    NEW_RECIPIENT = "new_recipient"
    HIGH_FREQUENCY = "high_frequency"
    VELOCITY_SPIKE = "velocity_spike"
    SUSPICIOUS_PATTERN = "suspicious_pattern"
    LIMIT_WARNING = "limit_warning"
    FAILED_TRANSACTION = "failed_transaction"
    BALANCE_LOW = "balance_low"
    WATCHLIST_HIT = "watchlist_hit"
    GEO_ANOMALY = "geo_anomaly"


class ActivityType(str, Enum):
    """Type of wallet activity."""
    SEND = "send"
    RECEIVE = "receive"
    SWAP = "swap"
    STAKE = "stake"
    UNSTAKE = "unstake"
    APPROVAL = "approval"
    CONTRACT_CALL = "contract_call"
    FAILED = "failed"


@dataclass
class WalletActivity:
    """A single wallet activity event."""
    activity_id: str
    wallet_id: str
    activity_type: ActivityType
    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    # Transaction details
    tx_hash: Optional[str] = None
    from_address: str = ""
    to_address: str = ""
    amount: Decimal = field(default_factory=lambda: Decimal("0"))
    token: str = "USDC"
    chain: str = "base"

    # Additional context
    merchant_id: Optional[str] = None
    merchant_category: Optional[str] = None
    ip_address: Optional[str] = None
    device_id: Optional[str] = None
    geo_location: Optional[str] = None

    # Risk assessment
    risk_score: float = 0.0
    risk_factors: List[str] = field(default_factory=list)

    # Metadata
    metadata: Dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "activity_id": self.activity_id,
            "wallet_id": self.wallet_id,
            "activity_type": self.activity_type.value,
            "timestamp": self.timestamp.isoformat(),
            "tx_hash": self.tx_hash,
            "from_address": self.from_address,
            "to_address": self.to_address,
            "amount": str(self.amount),
            "token": self.token,
            "chain": self.chain,
            "risk_score": self.risk_score,
            "risk_factors": self.risk_factors,
        }


@dataclass
class Alert:
    """An alert generated by the monitoring system."""
    alert_id: str
    wallet_id: str
    alert_type: AlertType
    severity: AlertSeverity
    title: str
    description: str
    created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))

    # Related data
    related_activity_id: Optional[str] = None
    related_tx_hash: Optional[str] = None

    # Alert state
    is_acknowledged: bool = False
    acknowledged_at: Optional[datetime] = None
    acknowledged_by: Optional[str] = None
    is_resolved: bool = False
    resolved_at: Optional[datetime] = None
    resolution_notes: Optional[str] = None

    # Alert metadata
    risk_score: float = 0.0
    recommended_actions: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)

    def acknowledge(self, by: str) -> None:
        """Acknowledge the alert."""
        self.is_acknowledged = True
        self.acknowledged_at = datetime.now(timezone.utc)
        self.acknowledged_by = by

    def resolve(self, notes: str = "") -> None:
        """Resolve the alert."""
        self.is_resolved = True
        self.resolved_at = datetime.now(timezone.utc)
        self.resolution_notes = notes

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary."""
        return {
            "alert_id": self.alert_id,
            "wallet_id": self.wallet_id,
            "alert_type": self.alert_type.value,
            "severity": self.severity.value,
            "title": self.title,
            "description": self.description,
            "created_at": self.created_at.isoformat(),
            "risk_score": self.risk_score,
            "is_acknowledged": self.is_acknowledged,
            "is_resolved": self.is_resolved,
            "recommended_actions": self.recommended_actions,
        }


@dataclass
class WatchlistEntry:
    """An entry in the watchlist."""
    entry_id: str
    entry_type: str  # "address", "merchant", "country", "category"
    value: str
    reason: str
    severity: AlertSeverity = AlertSeverity.HIGH
    added_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    added_by: str = "system"
    is_active: bool = True
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class ActivityPattern:
    """Detected activity pattern for a wallet."""
    pattern_id: str
    wallet_id: str
    pattern_type: str
    description: str
    confidence: float = 0.0
    first_detected: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    last_seen: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
    occurrence_count: int = 1
    is_normal: bool = True
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class MonitoringConfig:
    """Configuration for activity monitoring."""
    wallet_id: str

    # Alert thresholds
    large_transaction_threshold: Decimal = field(default_factory=lambda: Decimal("1000.00"))
    high_frequency_threshold: int = 10  # transactions per hour
    velocity_spike_multiplier: float = 3.0  # times normal velocity
    balance_low_threshold: Decimal = field(default_factory=lambda: Decimal("10.00"))

    # Time-based settings
    unusual_hours_start: int = 0  # 12 AM UTC
    unusual_hours_end: int = 6  # 6 AM UTC

    # Risk scoring weights
    risk_weight_amount: float = 0.3
    risk_weight_recipient: float = 0.2
    risk_weight_time: float = 0.15
    risk_weight_frequency: float = 0.2
    risk_weight_pattern: float = 0.15

    # Notification settings
    notify_on_severity: AlertSeverity = AlertSeverity.MEDIUM
    notification_cooldown_minutes: int = 15

    # Learning settings
    learning_period_days: int = 30
    pattern_detection_enabled: bool = True


class AlertNotificationService(Protocol):
    """Protocol for alert notifications."""

    async def send_alert(
        self,
        alert: Alert,
        channels: List[str],
    ) -> bool:
        """Send an alert notification."""
        ...


class ActivityMonitor:
    """
    Monitors wallet activity for security and compliance.

    Features:
    - Real-time transaction monitoring
    - Anomaly detection
    - Risk scoring
    - Alert generation
    - Pattern learning
    """

    def __init__(
        self,
        notification_service: Optional[AlertNotificationService] = None,
    ):
        self._notifications = notification_service

        # Storage (in production, use database)
        self._configs: Dict[str, MonitoringConfig] = {}
        self._activities: Dict[str, List[WalletActivity]] = {}  # wallet_id -> activities
        self._alerts: Dict[str, List[Alert]] = {}  # wallet_id -> alerts
        self._patterns: Dict[str, Dict[str, ActivityPattern]] = {}  # wallet_id -> pattern_id -> pattern
        self._watchlist: Dict[str, WatchlistEntry] = {}  # entry_id -> entry

        # Statistics for anomaly detection
        self._wallet_stats: Dict[str, Dict[str, Any]] = {}  # wallet_id -> stats
        self._notification_timestamps: Dict[str, datetime] = {}  # wallet_id -> last notification

        # Locks
        self._monitor_lock = asyncio.Lock()

    async def setup_monitoring(
        self,
        wallet_id: str,
        config: Optional[MonitoringConfig] = None,
    ) -> MonitoringConfig:
        """Set up monitoring for a wallet."""
        monitoring_config = config or MonitoringConfig(wallet_id=wallet_id)
        monitoring_config.wallet_id = wallet_id

        self._configs[wallet_id] = monitoring_config
        self._activities[wallet_id] = []
        self._alerts[wallet_id] = []
        self._patterns[wallet_id] = {}
        self._wallet_stats[wallet_id] = self._initialize_stats()

        logger.info(f"Set up activity monitoring for wallet {wallet_id}")
        return monitoring_config

    def _initialize_stats(self) -> Dict[str, Any]:
        """Initialize statistics tracking."""
        return {
            "total_transactions": 0,
            "total_volume": Decimal("0"),
            "avg_transaction_amount": Decimal("0"),
            "std_transaction_amount": Decimal("0"),
            "hourly_transaction_counts": defaultdict(int),
            "daily_transaction_counts": defaultdict(int),
            "known_recipients": set(),
            "transaction_amounts": [],
            "transaction_times": [],
            "last_activity_at": None,
        }

    async def record_activity(
        self,
        activity: WalletActivity,
    ) -> Tuple[WalletActivity, List[Alert]]:
        """
        Record and analyze a wallet activity.

        Args:
            activity: Activity to record

        Returns:
            Tuple of (activity with risk score, generated alerts)
        """
        async with self._monitor_lock:
            wallet_id = activity.wallet_id
            config = self._configs.get(wallet_id)

            if not config:
                # Auto-setup if not configured
                config = await self.setup_monitoring(wallet_id)

            # Calculate risk score
            activity.risk_score = await self._calculate_risk_score(activity, config)

            # Check for alerts
            alerts = await self._check_alert_conditions(activity, config)

            # Update statistics
            self._update_statistics(activity)

            # Detect patterns
            if config.pattern_detection_enabled:
                await self._detect_patterns(activity)

            # Store activity
            self._activities[wallet_id].append(activity)

            # Keep only last 1000 activities per wallet
            if len(self._activities[wallet_id]) > 1000:
                self._activities[wallet_id] = self._activities[wallet_id][-1000:]

            # Store and send alerts
            for alert in alerts:
                self._alerts[wallet_id].append(alert)

                if self._should_notify(wallet_id, alert, config):
                    await self._send_notification(alert)

            return activity, alerts

    async def _calculate_risk_score(
        self,
        activity: WalletActivity,
        config: MonitoringConfig,
    ) -> float:
        """Calculate risk score for an activity (0-100)."""
        risk_score = 0.0
        stats = self._wallet_stats.get(activity.wallet_id, {})

        # Amount risk
        if config.large_transaction_threshold > 0:
            amount_ratio = float(activity.amount / config.large_transaction_threshold)
            amount_risk = min(100, amount_ratio * 50)
            risk_score += amount_risk * config.risk_weight_amount

            # Compare to average
            avg_amount = stats.get("avg_transaction_amount", Decimal("0"))
            if avg_amount > 0 and activity.amount > avg_amount * 3:
                activity.risk_factors.append("amount_3x_average")
                risk_score += 20

        # New recipient risk
        known_recipients = stats.get("known_recipients", set())
        if activity.to_address and activity.to_address not in known_recipients:
            activity.risk_factors.append("new_recipient")
            risk_score += 30 * config.risk_weight_recipient

        # Time risk
        hour = activity.timestamp.hour
        if config.unusual_hours_start <= hour < config.unusual_hours_end:
            activity.risk_factors.append("unusual_time")
            risk_score += 40 * config.risk_weight_time

        # Frequency risk
        recent_count = self._count_recent_activities(activity.wallet_id, hours=1)
        if recent_count > config.high_frequency_threshold:
            activity.risk_factors.append("high_frequency")
            risk_score += 50 * config.risk_weight_frequency

        # Watchlist check
        watchlist_hit = await self._check_watchlist(activity)
        if watchlist_hit:
            activity.risk_factors.append(f"watchlist_hit:{watchlist_hit.entry_type}")
            risk_score += 70

        return min(100, risk_score)

    async def _check_alert_conditions(
        self,
        activity: WalletActivity,
        config: MonitoringConfig,
    ) -> List[Alert]:
        """Check for alert conditions."""
        import secrets

        alerts = []

        # Large transaction alert
        if activity.amount >= config.large_transaction_threshold:
            alerts.append(Alert(
                alert_id=f"alert_{secrets.token_hex(8)}",
                wallet_id=activity.wallet_id,
                alert_type=AlertType.LARGE_TRANSACTION,
                severity=AlertSeverity.MEDIUM if activity.amount < config.large_transaction_threshold * 5 else AlertSeverity.HIGH,
                title="Large Transaction Detected",
                description=f"Transaction of {activity.amount} {activity.token} detected",
                related_activity_id=activity.activity_id,
                related_tx_hash=activity.tx_hash,
                risk_score=activity.risk_score,
                recommended_actions=["Review transaction details", "Verify recipient"],
            ))

        # Unusual time alert
        hour = activity.timestamp.hour
        if config.unusual_hours_start <= hour < config.unusual_hours_end:
            alerts.append(Alert(
                alert_id=f"alert_{secrets.token_hex(8)}",
                wallet_id=activity.wallet_id,
                alert_type=AlertType.UNUSUAL_TIME,
                severity=AlertSeverity.LOW,
                title="Transaction at Unusual Time",
                description=f"Transaction at {hour}:00 UTC",
                related_activity_id=activity.activity_id,
                risk_score=activity.risk_score,
            ))

        # High frequency alert
        recent_count = self._count_recent_activities(activity.wallet_id, hours=1)
        if recent_count > config.high_frequency_threshold:
            alerts.append(Alert(
                alert_id=f"alert_{secrets.token_hex(8)}",
                wallet_id=activity.wallet_id,
                alert_type=AlertType.HIGH_FREQUENCY,
                severity=AlertSeverity.MEDIUM,
                title="High Transaction Frequency",
                description=f"{recent_count} transactions in the last hour",
                related_activity_id=activity.activity_id,
                risk_score=activity.risk_score,
                recommended_actions=["Check for automated activity", "Review recent transactions"],
            ))

        # New recipient alert
        known_recipients = self._wallet_stats.get(activity.wallet_id, {}).get("known_recipients", set())
        if activity.to_address and activity.to_address not in known_recipients:
            alerts.append(Alert(
                alert_id=f"alert_{secrets.token_hex(8)}",
                wallet_id=activity.wallet_id,
                alert_type=AlertType.NEW_RECIPIENT,
                severity=AlertSeverity.INFO,
                title="New Recipient Address",
                description=f"First transaction to {activity.to_address[:10]}...",
                related_activity_id=activity.activity_id,
                risk_score=activity.risk_score,
            ))

        # Watchlist alert
        watchlist_hit = await self._check_watchlist(activity)
        if watchlist_hit:
            alerts.append(Alert(
                alert_id=f"alert_{secrets.token_hex(8)}",
                wallet_id=activity.wallet_id,
                alert_type=AlertType.WATCHLIST_HIT,
                severity=watchlist_hit.severity,
                title="Watchlist Match Detected",
                description=f"Transaction matches watchlist entry: {watchlist_hit.reason}",
                related_activity_id=activity.activity_id,
                risk_score=100,
                recommended_actions=["Block transaction", "Review account", "Contact compliance"],
            ))

        # Failed transaction alert
        if activity.activity_type == ActivityType.FAILED:
            alerts.append(Alert(
                alert_id=f"alert_{secrets.token_hex(8)}",
                wallet_id=activity.wallet_id,
                alert_type=AlertType.FAILED_TRANSACTION,
                severity=AlertSeverity.LOW,
                title="Transaction Failed",
                description=f"Transaction to {activity.to_address[:10]}... failed",
                related_activity_id=activity.activity_id,
                risk_score=activity.risk_score,
            ))

        return alerts

    def _count_recent_activities(self, wallet_id: str, hours: int) -> int:
        """Count activities in the last N hours."""
        cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
        activities = self._activities.get(wallet_id, [])
        return sum(1 for a in activities if a.timestamp > cutoff)

    async def _check_watchlist(
        self,
        activity: WalletActivity,
    ) -> Optional[WatchlistEntry]:
        """Check if activity matches any watchlist entries."""
        for entry in self._watchlist.values():
            if not entry.is_active:
                continue

            if entry.entry_type == "address":
                if (activity.to_address.lower() == entry.value.lower() or
                    activity.from_address.lower() == entry.value.lower()):
                    return entry

            elif entry.entry_type == "merchant":
                if activity.merchant_id == entry.value:
                    return entry

            elif entry.entry_type == "category":
                if activity.merchant_category == entry.value:
                    return entry

            elif entry.entry_type == "country":
                if activity.geo_location and entry.value.lower() in activity.geo_location.lower():
                    return entry

        return None

    def _update_statistics(self, activity: WalletActivity) -> None:
        """Update wallet statistics with new activity."""
        wallet_id = activity.wallet_id

        if wallet_id not in self._wallet_stats:
            self._wallet_stats[wallet_id] = self._initialize_stats()

        stats = self._wallet_stats[wallet_id]

        # Update counters
        stats["total_transactions"] += 1
        stats["total_volume"] += activity.amount
        stats["last_activity_at"] = activity.timestamp

        # Update amounts list
        stats["transaction_amounts"].append(float(activity.amount))
        if len(stats["transaction_amounts"]) > 100:
            stats["transaction_amounts"] = stats["transaction_amounts"][-100:]

        # Update average and std
        if stats["transaction_amounts"]:
            stats["avg_transaction_amount"] = Decimal(str(
                statistics.mean(stats["transaction_amounts"])
            ))
            if len(stats["transaction_amounts"]) > 1:
                stats["std_transaction_amount"] = Decimal(str(
                    statistics.stdev(stats["transaction_amounts"])
                ))

        # Update known recipients
        if activity.to_address:
            stats["known_recipients"].add(activity.to_address)

        # Update hourly counts
        hour_key = activity.timestamp.strftime("%Y-%m-%d-%H")
        stats["hourly_transaction_counts"][hour_key] += 1

        # Update daily counts
        day_key = activity.timestamp.strftime("%Y-%m-%d")
        stats["daily_transaction_counts"][day_key] += 1

    async def _detect_patterns(self, activity: WalletActivity) -> None:
        """Detect activity patterns."""
        import secrets

        wallet_id = activity.wallet_id
        stats = self._wallet_stats.get(wallet_id, {})

        # Check for regular timing pattern
        if len(stats.get("transaction_times", [])) >= 10:
            # Pattern detection logic would go here
            pass

        # Check for repeated recipient pattern
        known_recipients = stats.get("known_recipients", set())
        if activity.to_address in known_recipients:
            pattern_id = f"pattern_repeat_{activity.to_address[:8]}"
            if pattern_id in self._patterns.get(wallet_id, {}):
                pattern = self._patterns[wallet_id][pattern_id]
                pattern.occurrence_count += 1
                pattern.last_seen = datetime.now(timezone.utc)
            else:
                pattern = ActivityPattern(
                    pattern_id=pattern_id,
                    wallet_id=wallet_id,
                    pattern_type="repeated_recipient",
                    description=f"Regular transactions to {activity.to_address[:10]}...",
                    confidence=0.5,
                )
                if wallet_id not in self._patterns:
                    self._patterns[wallet_id] = {}
                self._patterns[wallet_id][pattern_id] = pattern

    def _should_notify(
        self,
        wallet_id: str,
        alert: Alert,
        config: MonitoringConfig,
    ) -> bool:
        """Check if notification should be sent."""
        # Check severity threshold
        severity_order = [AlertSeverity.INFO, AlertSeverity.LOW, AlertSeverity.MEDIUM, AlertSeverity.HIGH, AlertSeverity.CRITICAL]
        if severity_order.index(alert.severity) < severity_order.index(config.notify_on_severity):
            return False

        # Check cooldown
        last_notification = self._notification_timestamps.get(wallet_id)
        if last_notification:
            cooldown = timedelta(minutes=config.notification_cooldown_minutes)
            if datetime.now(timezone.utc) - last_notification < cooldown:
                return False

        return True

    async def _send_notification(self, alert: Alert) -> None:
        """Send alert notification."""
        if self._notifications:
            await self._notifications.send_alert(
                alert,
                channels=["email", "push"],
            )

        self._notification_timestamps[alert.wallet_id] = datetime.now(timezone.utc)
        logger.info(f"Sent notification for alert {alert.alert_id}")

    async def add_to_watchlist(
        self,
        entry_type: str,
        value: str,
        reason: str,
        severity: AlertSeverity = AlertSeverity.HIGH,
        added_by: str = "system",
    ) -> WatchlistEntry:
        """Add an entry to the watchlist."""
        import secrets

        entry_id = f"watchlist_{secrets.token_hex(8)}"
        entry = WatchlistEntry(
            entry_id=entry_id,
            entry_type=entry_type,
            value=value,
            reason=reason,
            severity=severity,
            added_by=added_by,
        )

        self._watchlist[entry_id] = entry
        logger.info(f"Added watchlist entry: {entry_type}={value}")

        return entry

    def remove_from_watchlist(self, entry_id: str) -> bool:
        """Remove an entry from the watchlist."""
        entry = self._watchlist.get(entry_id)
        if entry:
            entry.is_active = False
            return True
        return False

    def get_alerts(
        self,
        wallet_id: str,
        include_resolved: bool = False,
        severity: Optional[AlertSeverity] = None,
        limit: int = 100,
    ) -> List[Dict[str, Any]]:
        """Get alerts for a wallet."""
        alerts = self._alerts.get(wallet_id, [])

        if not include_resolved:
            alerts = [a for a in alerts if not a.is_resolved]

        if severity:
            alerts = [a for a in alerts if a.severity == severity]

        return [a.to_dict() for a in sorted(alerts, key=lambda a: a.created_at, reverse=True)[:limit]]

    def get_activity_summary(
        self,
        wallet_id: str,
        days: int = 30,
    ) -> Dict[str, Any]:
        """Get activity summary for a wallet."""
        stats = self._wallet_stats.get(wallet_id, {})
        activities = self._activities.get(wallet_id, [])
        alerts = self._alerts.get(wallet_id, [])

        cutoff = datetime.now(timezone.utc) - timedelta(days=days)
        recent_activities = [a for a in activities if a.timestamp > cutoff]
        recent_alerts = [a for a in alerts if a.created_at > cutoff]

        return {
            "wallet_id": wallet_id,
            "period_days": days,
            "total_transactions": len(recent_activities),
            "total_volume": str(sum(a.amount for a in recent_activities)),
            "avg_transaction_amount": str(stats.get("avg_transaction_amount", 0)),
            "unique_recipients": len(stats.get("known_recipients", set())),
            "alert_count": len(recent_alerts),
            "unresolved_alerts": len([a for a in recent_alerts if not a.is_resolved]),
            "avg_risk_score": (
                statistics.mean(a.risk_score for a in recent_activities)
                if recent_activities else 0
            ),
            "last_activity_at": (
                stats.get("last_activity_at").isoformat()
                if stats.get("last_activity_at") else None
            ),
        }


# Singleton instance
_activity_monitor: Optional[ActivityMonitor] = None


def get_activity_monitor() -> ActivityMonitor:
    """Get the global activity monitor instance."""
    global _activity_monitor

    if _activity_monitor is None:
        _activity_monitor = ActivityMonitor()

    return _activity_monitor


__all__ = [
    "AlertSeverity",
    "AlertType",
    "ActivityType",
    "WalletActivity",
    "Alert",
    "WatchlistEntry",
    "ActivityPattern",
    "MonitoringConfig",
    "ActivityMonitor",
    "get_activity_monitor",
]
