"""
Centralized URL validation utilities for security.

This module provides secure URL validation to prevent XSS attacks,
data exfiltration, and other URL-based security vulnerabilities.
"""

import re
from typing import Optional, List
from urllib.parse import urlparse
from loguru import logger


class URLValidationError(ValueError):
    """Raised when URL construction or validation fails."""

    pass


class URLValidator:
    """Centralized URL validation for security."""

    # Unsafe URL schemes that could lead to XSS or data exfiltration
    UNSAFE_SCHEMES = (
        "javascript",
        "data",
        "vbscript",
        "about",
        "blob",
        "file",
    )

    # Safe schemes for external links
    SAFE_SCHEMES = ("http", "https", "ftp", "ftps")

    # Email scheme
    EMAIL_SCHEME = "mailto"

    # Common academic/research domains that should be allowed
    TRUSTED_ACADEMIC_DOMAINS = (
        "arxiv.org",
        "pubmed.ncbi.nlm.nih.gov",
        "ncbi.nlm.nih.gov",
        "biorxiv.org",
        "medrxiv.org",
        "doi.org",
        "nature.com",
        "science.org",
        "sciencedirect.com",
        "springer.com",
        "wiley.com",
        "plos.org",
        "pnas.org",
        "ieee.org",
        "acm.org",
    )

    @staticmethod
    def is_unsafe_scheme(url: str) -> bool:
        """
        Check if a URL uses an unsafe scheme.

        Args:
            url: The URL to check

        Returns:
            True if the URL uses an unsafe scheme, False otherwise
        """
        if not url:
            return False

        # Normalize the URL - trim whitespace and convert to lowercase
        normalized_url = url.strip().lower()

        # Check for unsafe schemes
        for scheme in URLValidator.UNSAFE_SCHEMES:
            if normalized_url.startswith(f"{scheme}:"):
                logger.warning(
                    f"Unsafe URL scheme detected: {scheme} in URL: {url[:100]}"
                )
                return True

        return False

    @staticmethod
    def is_safe_url(
        url: str,
        require_scheme: bool = True,
        allow_fragments: bool = True,
        allow_mailto: bool = False,
        trusted_domains: Optional[List[str]] = None,
    ) -> bool:
        """
        Validate if a URL is safe to use.

        Args:
            url: The URL to validate
            require_scheme: Whether to require an explicit scheme
            allow_fragments: Whether to allow fragment identifiers (#)
            allow_mailto: Whether to allow mailto: links
            trusted_domains: Optional list of trusted domains

        Returns:
            True if the URL is safe, False otherwise
        """
        if not url or not isinstance(url, str):
            return False

        # Check for unsafe schemes first
        if URLValidator.is_unsafe_scheme(url):
            return False

        # Handle fragment-only URLs
        if url.startswith("#"):
            return allow_fragments

        # Parse the URL
        try:
            parsed = urlparse(url)
        except Exception as e:
            logger.warning(f"Failed to parse URL '{url[:100]}': {e}")
            return False

        # Check scheme
        if not parsed.scheme:
            if require_scheme:
                return False
            # If no scheme is required, assume http/https for URL parsing
            parsed = urlparse(f"http://{url}")  # DevSkim: ignore DS137138

        scheme_lower = parsed.scheme.lower()

        # Check if it's a mailto link
        if scheme_lower == URLValidator.EMAIL_SCHEME:
            return allow_mailto

        # Check if it's a safe scheme
        if scheme_lower not in URLValidator.SAFE_SCHEMES:
            logger.warning(f"Unsafe URL scheme: {scheme_lower}")
            return False

        # Validate domain if trusted domains are specified
        if trusted_domains and parsed.hostname:
            hostname_lower = parsed.hostname.lower()
            if not any(
                hostname_lower == domain.lower()
                or hostname_lower.endswith(f".{domain.lower()}")
                for domain in trusted_domains
            ):
                logger.warning(
                    f"URL domain not in trusted list: {parsed.hostname}"
                )
                return False

        # Check for suspicious patterns in the URL
        if URLValidator._has_suspicious_patterns(url):
            return False

        return True

    @staticmethod
    def _has_suspicious_patterns(url: str) -> bool:
        """
        Check for suspicious patterns in URLs that might indicate attacks.

        Args:
            url: The URL to check

        Returns:
            True if suspicious patterns are found, False otherwise
        """
        suspicious_patterns = [
            # Double encoding
            r"%25[0-9a-fA-F]{2}",
            # Null bytes
            r"%00",
            # Unicode encoding bypass attempts
            r"\\u[0-9a-fA-F]{4}",
            # HTML entity encoding
            r"&(#x?[0-9a-fA-F]+|[a-zA-Z]+);",
        ]

        for pattern in suspicious_patterns:
            if re.search(pattern, url, re.IGNORECASE):
                logger.warning(f"Suspicious pattern found in URL: {pattern}")
                return True

        return False

    @staticmethod
    def sanitize_url(url: str, default_scheme: str = "https") -> Optional[str]:
        """
        Sanitize a URL by adding a scheme if missing and validating it.

        Args:
            url: The URL to sanitize
            default_scheme: The default scheme to add if missing

        Returns:
            Sanitized URL or None if the URL is unsafe
        """
        if not url:
            return None

        # Check for unsafe schemes
        if URLValidator.is_unsafe_scheme(url):
            return None

        # Strip whitespace
        url = url.strip()

        # Parse the URL
        try:
            parsed = urlparse(url)

            # Add scheme if missing
            if not parsed.scheme:
                url = f"{default_scheme}://{url}"
                parsed = urlparse(url)

            # Validate the final URL
            if URLValidator.is_safe_url(url, require_scheme=True):
                return url

        except Exception as e:
            logger.warning(f"Failed to sanitize URL '{url[:100]}': {e}")

        return None

    @staticmethod
    def is_academic_url(url: str) -> bool:
        """
        Check if a URL is from a known academic/research domain.

        Args:
            url: The URL to check

        Returns:
            True if the URL is from an academic domain, False otherwise
        """
        try:
            parsed = urlparse(url)
            if parsed.hostname:
                hostname_lower = parsed.hostname.lower()
                return any(
                    hostname_lower == domain
                    or hostname_lower.endswith(f".{domain}")
                    for domain in URLValidator.TRUSTED_ACADEMIC_DOMAINS
                )
        except Exception:
            pass

        return False

    @staticmethod
    def extract_doi(url: str) -> Optional[str]:
        """
        Extract DOI from a URL if present.

        Args:
            url: The URL to extract DOI from

        Returns:
            The DOI if found, None otherwise
        """
        # Common DOI patterns with explicit pattern identification
        doi_patterns = [
            (
                r"10\.\d{4,}(?:\.\d+)*\/[-._;()\/:a-zA-Z0-9]+",
                0,
            ),  # Direct DOI, group 0
            (r"doi\.org\/(10\.\d{4,}[^\s]*)", 1),  # doi.org URL, group 1
        ]

        for pattern, group_index in doi_patterns:
            match = re.search(pattern, url, re.IGNORECASE)
            if match:
                return match.group(group_index)

        return None

    @staticmethod
    def validate_http_url(url: str) -> bool:
        """
        Validate that a callback URL is well-formed and safe for HTTP/HTTPS use.

        This is stricter than is_safe_url() and specifically validates HTTP/HTTPS
        URLs for use as application callbacks (e.g., in notifications, redirects).
        It does NOT validate Apprise service URLs which use other protocols.

        Args:
            url: HTTP/HTTPS callback URL to validate

        Returns:
            True if valid

        Raises:
            URLValidationError: If URL is invalid
        """
        if not url or not isinstance(url, str):
            raise URLValidationError("URL must be a non-empty string")

        try:
            parsed = urlparse(url)

            # Must have a scheme
            if not parsed.scheme:
                raise URLValidationError(
                    "URL must have a scheme (http or https)"
                )

            # Must be http or https (callback URLs only)
            if parsed.scheme not in ("http", "https"):
                raise URLValidationError(
                    f"URL scheme must be http or https, got: {parsed.scheme}"
                )

            # Use the general security validator for additional safety
            if not URLValidator.is_safe_url(url, require_scheme=True):
                raise URLValidationError(
                    f"URL failed security validation: {url}"
                )

            # Must have a netloc (hostname)
            if not parsed.netloc:
                raise URLValidationError("URL must have a hostname")

            # Check for obvious hostname issues
            if parsed.netloc.startswith(".") or parsed.netloc.endswith("."):
                raise URLValidationError(f"Invalid hostname: {parsed.netloc}")

            # Path should be valid if present
            if parsed.path and not parsed.path.startswith("/"):
                raise URLValidationError(
                    f"URL path must start with /: {parsed.path}"
                )

            return True

        except Exception as e:
            if isinstance(e, URLValidationError):
                raise
            raise URLValidationError(f"Failed to validate URL: {e}")


def get_javascript_url_validator() -> str:
    """
    Get JavaScript code for URL validation that matches the Python implementation.

    Returns:
        JavaScript code as a string that can be embedded in web pages
    """
    return r"""
    // URL validation utilities matching Python URLValidator
    const URLValidator = {
        UNSAFE_SCHEMES: ['javascript', 'data', 'vbscript', 'about', 'blob', 'file'],
        SAFE_SCHEMES: ['http', 'https', 'ftp', 'ftps'],
        EMAIL_SCHEME: 'mailto',

        isUnsafeScheme: function(url) {
            if (!url) return false;

            const normalizedUrl = url.trim().toLowerCase();

            for (const scheme of this.UNSAFE_SCHEMES) {
                if (normalizedUrl.startsWith(scheme + ':')) {
                    console.warn(`Unsafe URL scheme detected: ${scheme}`);
                    return true;
                }
            }

            return false;
        },

        isSafeUrl: function(url, options = {}) {
            const {
                requireScheme = true,
                allowFragments = true,
                allowMailto = false,
                trustedDomains = []
            } = options;

            if (!url || typeof url !== 'string') {
                return false;
            }

            // Check for unsafe schemes first
            if (this.isUnsafeScheme(url)) {
                return false;
            }

            // Handle fragment-only URLs
            if (url.startsWith('#')) {
                return allowFragments;
            }

            // Parse the URL
            try {
                const parsed = new URL(url, window.location.href);
                const scheme = parsed.protocol.slice(0, -1).toLowerCase(); // Remove trailing ':'

                // Check if it's a mailto link
                if (scheme === this.EMAIL_SCHEME) {
                    return allowMailto;
                }

                // Check if it's a safe scheme
                if (!this.SAFE_SCHEMES.includes(scheme)) {
                    console.warn(`Unsafe URL scheme: ${scheme}`);
                    return false;
                }

                // Validate domain if trusted domains are specified
                if (trustedDomains.length > 0 && parsed.hostname) {
                    const hostname = parsed.hostname.toLowerCase();
                    const isTrusted = trustedDomains.some(domain =>
                        hostname === domain.toLowerCase() ||
                        hostname.endsWith('.' + domain.toLowerCase())
                    );

                    if (!isTrusted) {
                        console.warn(`URL domain not in trusted list: ${parsed.hostname}`);
                        return false;
                    }
                }

                return true;
            } catch (e) {
                console.warn(`Failed to parse URL: ${e.message}`);
                return false;
            }
        },

        sanitizeUrl: function(url, defaultScheme = 'https') {
            if (!url) return null;

            // Check for unsafe schemes
            if (this.isUnsafeScheme(url)) {
                return null;
            }

            // Strip whitespace
            url = url.trim();

            // Add scheme if missing
            if (!url.match(/^[a-zA-Z][a-zA-Z\d+\-.]*:/)) {
                url = `${defaultScheme}://${url}`;
            }

            // Validate the final URL
            if (this.isSafeUrl(url, { requireScheme: true })) {
                return url;
            }

            return null;
        }
    };
    """
