"""
High-level notification manager with database integration.
"""

from datetime import datetime, timezone, timedelta
from typing import Dict, Optional, Any
from collections import deque
import threading

from loguru import logger

from .service import NotificationService
from .templates import EventType
from .exceptions import RateLimitError


class NotificationManager:
    """
    High-level notification manager that uses settings snapshots for
    thread-safe access to user settings.

    This manager is designed to be used from background threads (e.g., queue
    processors) by passing a settings_snapshot dictionary captured from the
    main thread.

    **Per-User Rate Limiting:**
    The rate limiter is shared across ALL NotificationManager instances as a
    singleton, but supports per-user rate limit configuration. Each user has
    their own rate limits based on their settings, which are configured when
    the NotificationManager is initialized with the required user_id parameter.

    **How It Works:**
    - The first NotificationManager instance creates the shared RateLimiter
      with default limits
    - Each instance configures user-specific limits by passing user_id to __init__
    - The rate limiter maintains separate counters and limits for each user
    - Users are completely isolated - one user's limit doesn't affect others

    Example:
        >>> # User A with conservative limits
        >>> snapshot_a = {"notifications.rate_limit_per_hour": 5}
        >>> manager_a = NotificationManager(snapshot_a, user_id="user_a")
        >>> # ✅ user_a gets 5/hour
        >>>
        >>> # User B with generous limits (doesn't affect User A!)
        >>> snapshot_b = {"notifications.rate_limit_per_hour": 20}
        >>> manager_b = NotificationManager(snapshot_b, user_id="user_b")
        >>> # ✅ user_b gets 20/hour, user_a still has 5/hour
    """

    # Shared rate limiter instance across all NotificationManager instances
    # This ensures rate limits are enforced correctly even when multiple
    # NotificationManager instances are created
    _shared_rate_limiter: Optional["RateLimiter"] = None
    _rate_limiter_lock = threading.Lock()

    def __init__(self, settings_snapshot: Dict[str, Any], user_id: str):
        """
        Initialize the notification manager.

        Args:
            settings_snapshot: Dictionary of settings key-value pairs captured
                             from SettingsManager.get_settings_snapshot().
                             This allows thread-safe access to user settings
                             from background threads.
            user_id: User identifier for per-user rate limiting. The rate limits
                    from settings_snapshot will be configured for this user.

        Example:
            >>> # In main thread with database session
            >>> settings_manager = SettingsManager(session)
            >>> snapshot = settings_manager.get_settings_snapshot()
            >>>
            >>> # In background thread (thread-safe)
            >>> notification_manager = NotificationManager(
            ...     settings_snapshot=snapshot,
            ...     user_id="user123"
            ... )
            >>> notification_manager.send_notification(...)
        """
        # Store settings snapshot for thread-safe access
        self._settings_snapshot = settings_snapshot
        self._user_id = user_id

        # Get security settings for notification service
        # Default to False for security - only enable for development/testing
        allow_private_ips = self._get_setting(
            "notifications.allow_private_ips", default=False
        )

        self.service = NotificationService(allow_private_ips=allow_private_ips)

        # Initialize shared rate limiter on first use
        # The shared rate limiter now supports per-user limits, so each user's
        # settings are respected regardless of initialization order.
        with NotificationManager._rate_limiter_lock:
            if NotificationManager._shared_rate_limiter is None:
                # Create shared rate limiter with default limits
                # (individual users can have different limits)
                default_max_per_hour = self._get_setting(
                    "notifications.rate_limit_per_hour", default=10
                )
                default_max_per_day = self._get_setting(
                    "notifications.rate_limit_per_day", default=50
                )

                logger.info(
                    f"Initializing shared rate limiter with defaults: "
                    f"{default_max_per_hour}/hour, {default_max_per_day}/day"
                )

                NotificationManager._shared_rate_limiter = RateLimiter(
                    max_per_hour=default_max_per_hour,
                    max_per_day=default_max_per_day,
                )

            # Use the shared instance
            self._rate_limiter = NotificationManager._shared_rate_limiter

            # Configure per-user rate limits
            max_per_hour = self._get_setting(
                "notifications.rate_limit_per_hour", default=10
            )
            max_per_day = self._get_setting(
                "notifications.rate_limit_per_day", default=50
            )

            self._rate_limiter.set_user_limits(
                user_id, max_per_hour, max_per_day
            )
            logger.debug(
                f"Configured rate limits for user {user_id}: "
                f"{max_per_hour}/hour, {max_per_day}/day"
            )

    def _get_setting(self, key: str, default: Any = None) -> Any:
        """
        Get a setting value from snapshot.

        Args:
            key: Setting key
            default: Default value if not found

        Returns:
            Setting value or default
        """
        return self._settings_snapshot.get(key, default)

    def send_notification(
        self,
        event_type: EventType,
        context: Dict[str, Any],
        force: bool = False,
    ) -> bool:
        """
        Send a notification for an event.

        Uses the user_id that was provided during initialization for
        rate limiting and user preferences.

        Args:
            event_type: Type of event
            context: Context data for the notification
            force: If True, bypass rate limiting

        Returns:
            True if notification was sent successfully

        Raises:
            RateLimitError: If rate limit is exceeded and force=False
        """
        try:
            logger.debug(
                f"Sending notification: event_type={event_type.value}, "
                f"user_id={self._user_id}, force={force}"
            )
            logger.debug(f"Context keys: {list(context.keys())}")

            # Check if notifications are enabled for this event type
            should_notify = self._should_notify(event_type)
            logger.debug(
                f"Notification enabled check for {event_type.value}: "
                f"{should_notify}"
            )

            if not force and not should_notify:
                logger.debug(
                    f"Notifications disabled for event type: "
                    f"{event_type.value} (user: {self._user_id})"
                )
                return False

            # Check rate limit using the manager's user_id
            rate_limit_ok = self._rate_limiter.is_allowed(self._user_id)
            logger.debug(
                f"Rate limit check for {self._user_id}: {rate_limit_ok}"
            )

            if not force and not rate_limit_ok:
                logger.warning(f"Rate limit exceeded for user {self._user_id}")
                raise RateLimitError(
                    "Notification rate limit exceeded. "
                    "Please wait before sending more notifications."
                )

            # Get service URLs from settings (snapshot or database)
            service_urls = self._get_setting(
                "notifications.service_url", default=""
            )

            if not service_urls or not service_urls.strip():
                logger.debug(
                    f"No notification service URLs configured for user "
                    f"{self._user_id}"
                )
                return False

            # Send notification with service URLs
            logger.debug(f"Calling service.send_event for {event_type.value}")
            result = self.service.send_event(
                event_type, context, service_urls=service_urls
            )

            # Log to database if enabled
            if result:
                self._log_notification(event_type, context)
                logger.info(
                    f"Notification sent: {event_type.value} to user "
                    f"{self._user_id}"
                )
            else:
                logger.warning(
                    f"Notification failed: {event_type.value} to user "
                    f"{self._user_id}"
                )

            return result

        except RateLimitError:
            logger.warning(f"Rate limit error for user {self._user_id}")
            raise
        except Exception:
            logger.exception(
                f"Error sending notification for {event_type.value} to user "
                f"{self._user_id}"
            )
            return False

    def test_service(self, url: str) -> Dict[str, Any]:
        """
        Test a notification service.

        Args:
            url: Service URL to test

        Returns:
            Dict with test results
        """
        return self.service.test_service(url)

    def _should_notify(self, event_type: EventType) -> bool:
        """
        Check if notifications should be sent for this event type.

        Uses the manager's settings snapshot to determine if the event type
        is enabled for the user.

        Args:
            event_type: Event type to check

        Returns:
            True if notifications should be sent
        """
        try:
            # Check event-specific setting (from snapshot or database)
            setting_key = f"notifications.on_{event_type.value}"
            enabled = self._get_setting(setting_key, default=False)

            return enabled

        except Exception as e:
            logger.debug(f"Error checking notification preferences: {e}")
            # Default to disabled on error to avoid infinite loops during login
            return False

    def _log_notification(
        self, event_type: EventType, context: Dict[str, Any]
    ) -> None:
        """
        Log a sent notification (simplified logging to application logs only).

        Uses the manager's user_id for logging.

        Args:
            event_type: Event type
            context: Notification context
        """
        try:
            title = (
                context.get("query")
                or context.get("subscription_name")
                or "Unknown"
            )
            logger.info(
                f"Notification sent: {event_type.value} - {title} "
                f"(user: {self._user_id})"
            )
        except Exception as e:
            logger.debug(f"Failed to log notification: {e}")


class RateLimiter:
    """
    Simple in-memory rate limiter for notifications with per-user limit support.

    This rate limiter tracks notification counts per user and enforces
    configurable rate limits. Each user can have their own rate limits,
    which are stored separately from the notification counts.

    **Per-User Limits:**
    Rate limits can be configured per-user using `set_user_limits()`.
    If no user-specific limits are set, the default limits (passed to
    __init__) are used.

    **Memory Storage:**
    This implementation stores rate limits in memory only, which means
    limits are reset when the server restarts. This is acceptable for normal
    users since they cannot restart the server. If an admin restarts the server,
    rate limits reset which is reasonable behavior.

    **Thread Safety:**
    This implementation is thread-safe using threading.Lock() for concurrent
    requests from the same user.

    **Multi-Worker Limitation:**
    In multi-worker deployments, each worker process maintains its own rate
    limit counters. Users could potentially bypass rate limits by distributing
    requests across different workers, getting up to N × max_per_hour
    notifications (where N = number of workers). For single-worker deployments
    (the default for LDR), this is not a concern. For production multi-worker
    deployments, consider implementing Redis-based rate limiting.

    Example:
        >>> limiter = RateLimiter(max_per_hour=10, max_per_day=50)
        >>> # Set custom limits for specific user
        >>> limiter.set_user_limits("user_a", max_per_hour=5, max_per_day=25)
        >>> limiter.set_user_limits("user_b", max_per_hour=20, max_per_day=100)
        >>> # Users get their configured limits
        >>> limiter.is_allowed("user_a")  # Limited to 5/hour
        >>> limiter.is_allowed("user_b")  # Limited to 20/hour
        >>> limiter.is_allowed("user_c")  # Uses defaults: 10/hour
    """

    def __init__(
        self,
        max_per_hour: int = 10,
        max_per_day: int = 50,
        cleanup_interval_hours: int = 24,
    ):
        """
        Initialize rate limiter with default limits.

        Args:
            max_per_hour: Default maximum notifications per hour per user
            max_per_day: Default maximum notifications per day per user
            cleanup_interval_hours: How often to run cleanup of inactive users (hours)
        """
        # Default limits used when no user-specific limits are set
        self.max_per_hour = max_per_hour
        self.max_per_day = max_per_day
        self.cleanup_interval_hours = cleanup_interval_hours

        # Per-user rate limit configuration (user_id -> (max_per_hour, max_per_day))
        self._user_limits: Dict[str, tuple[int, int]] = {}

        # Per-user notification counts
        self._hourly_counts: Dict[str, deque] = {}
        self._daily_counts: Dict[str, deque] = {}

        self._last_cleanup = datetime.now(timezone.utc)
        self._lock = threading.Lock()  # Thread safety for all operations

    def set_user_limits(
        self, user_id: str, max_per_hour: int, max_per_day: int
    ) -> None:
        """
        Set rate limits for a specific user.

        This allows each user to have their own rate limit configuration.
        If not set, the user will use the default limits passed to __init__.

        Args:
            user_id: User identifier
            max_per_hour: Maximum notifications per hour for this user
            max_per_day: Maximum notifications per day for this user

        Example:
            >>> limiter = RateLimiter(max_per_hour=10, max_per_day=50)
            >>> limiter.set_user_limits("power_user", max_per_hour=20, max_per_day=100)
            >>> limiter.set_user_limits("limited_user", max_per_hour=5, max_per_day=25)
        """
        with self._lock:
            self._user_limits[user_id] = (max_per_hour, max_per_day)
            logger.debug(
                f"Set rate limits for user {user_id}: "
                f"{max_per_hour}/hour, {max_per_day}/day"
            )

    def get_user_limits(self, user_id: str) -> tuple[int, int]:
        """
        Get the effective rate limits for a user.

        Returns the user-specific limits if set, otherwise returns defaults.

        Args:
            user_id: User identifier

        Returns:
            Tuple of (max_per_hour, max_per_day)
        """
        with self._lock:
            return self._user_limits.get(
                user_id, (self.max_per_hour, self.max_per_day)
            )

    def is_allowed(self, user_id: str) -> bool:
        """
        Check if a notification is allowed for a user.

        Uses per-user rate limits if configured via set_user_limits(),
        otherwise uses the default limits from __init__.

        Args:
            user_id: User identifier

        Returns:
            True if notification is allowed, False if rate limit exceeded
        """
        with self._lock:
            now = datetime.now(timezone.utc)

            # Periodic cleanup of inactive users
            self._cleanup_inactive_users_if_needed(now)

            # Initialize queues for user if needed
            if user_id not in self._hourly_counts:
                self._hourly_counts[user_id] = deque()
                self._daily_counts[user_id] = deque()

            # Clean old entries
            self._clean_old_entries(user_id, now)

            # Get user-specific limits or defaults
            max_per_hour, max_per_day = self._user_limits.get(
                user_id, (self.max_per_hour, self.max_per_day)
            )

            # Check limits
            hourly_count = len(self._hourly_counts[user_id])
            daily_count = len(self._daily_counts[user_id])

            if hourly_count >= max_per_hour:
                logger.warning(
                    f"Hourly rate limit exceeded for user {user_id}: "
                    f"{hourly_count}/{max_per_hour}"
                )
                return False

            if daily_count >= max_per_day:
                logger.warning(
                    f"Daily rate limit exceeded for user {user_id}: "
                    f"{daily_count}/{max_per_day}"
                )
                return False

            # Record this notification
            self._hourly_counts[user_id].append(now)
            self._daily_counts[user_id].append(now)

            return True

    def _clean_old_entries(self, user_id: str, now: datetime) -> None:
        """
        Remove old entries from rate limit counters.

        Args:
            user_id: User identifier
            now: Current time
        """
        hour_ago = now - timedelta(hours=1)
        day_ago = now - timedelta(days=1)

        # Clean hourly queue
        while (
            self._hourly_counts[user_id]
            and self._hourly_counts[user_id][0] < hour_ago
        ):
            self._hourly_counts[user_id].popleft()

        # Clean daily queue
        while (
            self._daily_counts[user_id]
            and self._daily_counts[user_id][0] < day_ago
        ):
            self._daily_counts[user_id].popleft()

    def reset(self, user_id: Optional[str] = None) -> None:
        """
        Reset rate limits for a user or all users.

        Args:
            user_id: User to reset, or None for all users
        """
        if user_id:
            self._hourly_counts.pop(user_id, None)
            self._daily_counts.pop(user_id, None)
        else:
            self._hourly_counts.clear()
            self._daily_counts.clear()

    def _cleanup_inactive_users_if_needed(self, now: datetime) -> None:
        """
        Periodically clean up data for inactive users to prevent memory leaks.

        Args:
            now: Current timestamp
        """
        # Check if cleanup is needed
        if now - self._last_cleanup < timedelta(
            hours=self.cleanup_interval_hours
        ):
            return

        logger.debug("Running periodic cleanup of inactive notification users")

        # Define inactive threshold (users with no activity for 7 days)
        inactive_threshold = now - timedelta(days=7)

        inactive_users = []

        # Find users with no recent activity
        # Convert to list to avoid "dictionary changed size during iteration" error
        for user_id in list(self._hourly_counts.keys()):
            # Check if user has any recent entries
            hourly_entries = self._hourly_counts.get(user_id, [])
            daily_entries = self._daily_counts.get(user_id, [])

            # If no entries or all entries are old, mark as inactive
            has_recent_activity = False
            for entry in hourly_entries + daily_entries:
                if entry > inactive_threshold:
                    has_recent_activity = True
                    break

            if not has_recent_activity:
                inactive_users.append(user_id)

        # Remove inactive users
        for user_id in inactive_users:
            self._hourly_counts.pop(user_id, None)
            self._daily_counts.pop(user_id, None)
            logger.debug(
                f"Cleaned up inactive user {user_id} from rate limiter"
            )

        if inactive_users:
            logger.info(
                f"Cleaned up {len(inactive_users)} inactive users from rate limiter"
            )

        self._last_cleanup = now
