"""
Core notification service using Apprise.
"""

import re
from typing import Dict, List, Optional, Any
from urllib.parse import urlparse

import apprise
from loguru import logger
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type,
)

from .exceptions import ServiceError, SendError
from .templates import EventType, NotificationTemplate
from ..security.url_builder import mask_sensitive_url
from ..security.notification_validator import (
    NotificationURLValidator,
)


# Prefix emitted by NotificationURLValidator when an http(s) URL targets a
# private/internal IP. Pinned as a constant so the call site and the
# validator stay in lockstep — see NotificationURLValidator.validate_service_url
# docstring. The lockstep integration test
# ``test_test_service_ip_rejection_matrix`` in
# tests/web/services/test_notification_coverage.py guards the wording.
PRIVATE_IP_REJECTION_PREFIX = "Blocked private/internal IP address:"


def _admin_hint_would_help(url: str) -> bool:
    """True iff some operator escape hatch would actually unblock ``url``.

    The admin hint names two env vars, and each unblocks a *disjoint* set
    of destinations, so both must be probed:

    - ``LDR_NOTIFICATIONS_ALLOW_PRIVATE_IPS=true`` unblocks RFC1918 / CGNAT
      / loopback / link-local / IPv6 ULA — probed via ``allow_private_ips``.
    - ``LDR_SECURITY_ALLOW_NAT64=true`` unblocks the two NAT64 prefixes
      (``64:ff9b::/96``, ``64:ff9b:1::/48``) wrapping a non-metadata IPv4 —
      probed via ``allow_nat64``. The NAT64 prefixes live in the blocked
      set but NOT in the private/loopback ranges, so the private-IPs probe
      alone can NEVER unblock them; without this second probe the
      NAT64 half of the hint would be dead for the very URLs it documents.

    If NEITHER probe unblocks ``url`` it targets an always-blocked category
    (cloud-metadata IPs, 6to4, Teredo, discard prefix, IPv4-mapped IPv6 of
    metadata, NAT64-wrapped metadata) and the hint would mislead, so it is
    suppressed.

    Delegating to the validator (which re-resolves DNS) keeps a single
    source of truth — any future always-blocked category is handled
    automatically — and covers NAT64 reached via DNS64, not just literal
    NAT64 addresses.
    """
    private_would_unblock, _ = NotificationURLValidator.validate_service_url(
        url, allow_private_ips=True
    )
    if private_would_unblock:
        return True
    nat64_would_unblock, _ = NotificationURLValidator.validate_service_url(
        url, allow_nat64=True
    )
    return nat64_would_unblock


# Backward compatibility constants - now handled by Tenacity internally
MAX_RETRY_ATTEMPTS = 3
INITIAL_RETRY_DELAY = 0.5
RETRY_BACKOFF_MULTIPLIER = 2


class NotificationService:
    """
    Low-level notification service that wraps Apprise.
    """

    # Regex patterns for common service types (for validation)
    SERVICE_PATTERNS = {
        "email": r"^mailto://",
        "discord": r"^discord://",
        "slack": r"^slack://",
        "telegram": r"^tgram://",
        "smtp": r"^(smtp|smtps)://",
    }

    def __init__(
        self,
        allow_private_ips: bool = False,
        outbound_allowed: bool = False,
    ):
        """
        Initialize the notification service.

        Args:
            allow_private_ips: Whether to allow notifications to private/local IPs
                              (default: False for security). Set to True for
                              development/testing environments only.
            outbound_allowed: Server-level master switch
                    (env-only via LDR_NOTIFICATIONS_ALLOW_OUTBOUND).
                    Default False — outbound notifications are off until
                    the operator opts in. See SECURITY.md "Notification
                    Webhook SSRF" for the rationale.
        """
        self.apprise = apprise.Apprise()
        self.allow_private_ips = allow_private_ips
        self.outbound_allowed = outbound_allowed

    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=0.5, max=10),
        retry=retry_if_exception_type((Exception,)),
        reraise=True,
    )
    def _send_with_retry(
        self,
        title: str,
        body: str,
        apprise_instance: apprise.Apprise,
        tag: Optional[str] = None,
        attach: Optional[List[str]] = None,
    ) -> bool:
        """
        Send a notification using the provided Apprise instance with retry logic.

        This method is decorated with Tenacity to handle retries automatically.

        Args:
            title: Notification title
            body: Notification body text
            apprise_instance: Apprise instance to use for sending
            tag: Optional tag to target specific services
            attach: Optional list of file paths to attach

        Returns:
            True if notification was sent successfully

        Raises:
            SendError: If sending fails after all retry attempts
        """
        logger.debug(
            f"Sending notification: title='{title[:50]}...', tag={tag}"
        )
        logger.debug(f"Body preview: {body[:200]}...")

        # Send notification
        notify_result = apprise_instance.notify(
            title=title,
            body=body,
            tag=tag,
            attach=attach,
        )

        if notify_result:
            logger.debug(f"Notification sent successfully: '{title[:50]}...'")
            return True
        error_msg = "Failed to send notification to any service"
        logger.warning(error_msg)
        raise SendError(error_msg)

    def send(
        self,
        title: str,
        body: str,
        service_urls: Optional[str] = None,
        tag: Optional[str] = None,
        attach: Optional[List[str]] = None,
    ) -> bool:
        """
        Send a notification to service URLs with automatic retry.

        Args:
            title: Notification title
            body: Notification body text
            service_urls: Comma-separated list of service URLs to override configured ones
            tag: Optional tag to target specific services
            attach: Optional list of file paths to attach

        Returns:
            True if notification was sent successfully to at least one service

        Raises:
            SendError: If sending fails after all retry attempts

        Note:
            Temporary Apprise instances are created for each send operation
            and are automatically garbage collected by Python when they go
            out of scope. This simple approach is ideal for small deployments
            (~5 users) and avoids memory management complexity.
        """
        # Defense-in-depth: enforce the operator-level master switch at
        # the service layer too, not just at NotificationManager.
        # Today the manager always wraps this method, but keeping the
        # gate here means a future direct caller cannot accidentally
        # bypass it. See SECURITY.md "Notification Webhook SSRF".
        if not self.outbound_allowed:
            logger.warning(
                "Notification not sent: outbound notifications are disabled "
                "at the server level. Set "
                "LDR_NOTIFICATIONS_ALLOW_OUTBOUND=true to enable. See "
                "SECURITY.md 'Notification Webhook SSRF'."
            )
            return False

        # If service_urls are provided, validate before trying to send
        if service_urls:
            logger.debug("Creating Apprise instance for provided service URLs")

            # Validate service URLs for security (SSRF prevention)
            is_valid, error_msg = (
                NotificationURLValidator.validate_multiple_urls(
                    service_urls, allow_private_ips=self.allow_private_ips
                )
            )

            if not is_valid:
                logger.error(
                    f"Service URL validation failed: {error_msg}. "
                    f"URL: {mask_sensitive_url(service_urls)}"
                )
                raise ServiceError(f"Invalid service URL: {error_msg}")

        try:
            # If service_urls are provided, create a new Apprise instance
            if service_urls:
                temp_apprise = apprise.Apprise()
                result = temp_apprise.add(service_urls, tag=tag)

                if not result:
                    logger.error(
                        f"Failed to add service URLs to Apprise: "
                        f"{mask_sensitive_url(service_urls)}"
                    )
                    return False

                # Send notification with the temp instance (with retry)
                return self._send_with_retry(
                    title, body, temp_apprise, tag, attach
                )
            # Use the configured apprise instance
            if len(self.apprise) == 0:
                logger.debug("No notification services configured in Apprise")
                return False

            # Send notification (with retry)
            return self._send_with_retry(title, body, self.apprise, tag, attach)

        except Exception as e:
            # Tenacity will retry, but if all retries fail, raise SendError
            logger.exception(
                f"Failed to send notification after retries: '{title[:50]}...'"
            )
            raise SendError(f"Failed to send notification: {str(e)}")

    def send_event(
        self,
        event_type: EventType,
        context: Dict[str, Any],
        service_urls: Optional[str] = None,
        tag: Optional[str] = None,
        custom_template: Optional[Dict[str, str]] = None,
    ) -> bool:
        """
        Send a notification for a specific event type.

        Args:
            event_type: Type of event
            context: Context data for template formatting
            service_urls: Comma-separated list of service URLs
            tag: Optional tag to target specific services
            custom_template: Optional custom template override

        Returns:
            True if notification was sent successfully
        """
        logger.debug(f"send_event: event_type={event_type.value}, tag={tag}")
        logger.debug(f"Context: {context}")

        # Format notification using template
        message = NotificationTemplate.format(
            event_type, context, custom_template
        )
        logger.debug(
            f"Template formatted - title: '{message['title'][:50]}...'"
        )

        # Send notification
        return self.send(
            title=message["title"],
            body=message["body"],
            service_urls=service_urls,
            tag=tag,
        )

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

        Args:
            url: Apprise-compatible service URL

        Returns:
            Dict with 'success' boolean and optional 'error' message
        """
        # Server-level master switch (env-only). Mirrors
        # NotificationManager's gate so the "Send Test Notification"
        # button cannot bypass it. WARNING level so the operator sees
        # the actionable signal naming the env var.
        if not self.outbound_allowed:
            logger.warning(
                "Notification test refused: outbound notifications are "
                "disabled at the server level. Set "
                "LDR_NOTIFICATIONS_ALLOW_OUTBOUND=true to enable. See "
                "SECURITY.md 'Notification Webhook SSRF'."
            )
            return {
                "success": False,
                "error": (
                    "Outbound notifications are disabled. The server "
                    "administrator must set "
                    "LDR_NOTIFICATIONS_ALLOW_OUTBOUND=true to enable "
                    "notification webhooks. See SECURITY.md "
                    "'Notification Webhook SSRF' for details."
                ),
            }

        try:
            # Validate service URL for security (SSRF prevention)
            is_valid, error_msg = NotificationURLValidator.validate_service_url(
                url, allow_private_ips=self.allow_private_ips
            )

            if not is_valid:
                logger.warning(
                    f"Test service URL validation failed: {error_msg}. "
                    f"URL: {mask_sensitive_url(url)}"
                )
                # Surface the validator's reason so users know what to fix.
                # The hostname/scheme echoed here was supplied by the user
                # in the same request, so this is not a server-side leak.
                # When the rejection is a recoverable private/internal IP —
                # one that LDR_NOTIFICATIONS_ALLOW_PRIVATE_IPS=true OR
                # LDR_SECURITY_ALLOW_NAT64=true would actually unblock —
                # append the admin escape hatches; the user cannot fix this
                # themselves. Suppress the hint for always-blocked categories
                # (metadata, 6to4, Teredo, discard, IPv4-mapped IPv6 of
                # metadata, NAT64-wrapped metadata): no env var can help, so
                # naming one would mislead.
                user_error = error_msg or "Invalid notification service URL."
                if error_msg and error_msg.startswith(
                    PRIVATE_IP_REJECTION_PREFIX
                ):
                    if _admin_hint_would_help(url):
                        user_error += (
                            ". To unblock this destination, ask the server "
                            "administrator to set "
                            "LDR_NOTIFICATIONS_ALLOW_PRIVATE_IPS=true "
                            "(IPv6-only NAT64 deployments also need "
                            "LDR_SECURITY_ALLOW_NAT64=true). "
                            "See SECURITY.md 'Notification Webhook SSRF'."
                        )
                return {
                    "success": False,
                    "error": user_error,
                }

            # Create temporary Apprise instance
            temp_apprise = apprise.Apprise()
            add_result = temp_apprise.add(url)

            if not add_result:
                return {
                    "success": False,
                    "error": "Failed to add service URL",
                }

            # Send test notification
            result = temp_apprise.notify(
                title="Test Notification",
                body=(
                    "This is a test notification from Local Deep Research. "
                    "If you see this, your service is configured correctly!"
                ),
            )

            if result:
                return {
                    "success": True,
                    "message": "Test notification sent successfully",
                }
            return {
                "success": False,
                "error": "Failed to send test notification",
            }

        except Exception:
            logger.exception("Error testing notification service")
            return {
                "success": False,
                "error": "Failed to test notification service.",
            }

    @staticmethod
    def _validate_url(url: str) -> None:
        """
        Validate a notification service URL.

        Args:
            url: URL to validate

        Raises:
            ServiceError: If URL is invalid

        Note:
            URL scheme validation is handled by Apprise itself, which maintains
            a comprehensive whitelist of supported notification services.
            Apprise will reject unsupported schemes like 'file://' or 'javascript://'.
            See: https://github.com/caronc/apprise/wiki
        """
        if not url or not isinstance(url, str):
            raise ServiceError("URL must be a non-empty string")

        # Check if it looks like a URL
        parsed = urlparse(url)
        if not parsed.scheme:
            raise ServiceError(
                "Invalid URL format. Must be an Apprise-compatible "
                "service URL (e.g., discord://webhook_id/token)"
            )

    def get_service_type(self, url: str) -> Optional[str]:
        """
        Detect service type from URL.

        Args:
            url: Service URL

        Returns:
            Service type name or None if unknown
        """
        for service_name, pattern in self.SERVICE_PATTERNS.items():
            if re.match(pattern, url, re.IGNORECASE):
                return service_name
        return "unknown"
