"""URL utility functions for the local deep research application."""

from functools import lru_cache
from typing import Optional
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

from loguru import logger

from ..security import redact_url_for_log, validate_url
from ..security.network_utils import is_private_ip

# Re-export for backwards compatibility
__all__ = [
    "normalize_url",
    "is_private_ip",
    "canonical_url_key",
    "is_safe_custom_llm_endpoint",
]

# Tracking query parameter keys (matched lowercased).
_TRACKING_PARAMS = frozenset(
    {
        "fbclid",
        "gclid",
        "msclkid",
        "yclid",
        "dclid",
        "gad_source",
        "mc_eid",
        "mc_cid",
        "ref_src",
        "igshid",
        "_ga",
        "_gl",
    }
)
# Tracking param name prefixes (matched lowercased).
_TRACKING_PREFIXES = ("utm_",)


def normalize_url(raw_url: str) -> str:
    """
    Normalize a URL to ensure it has a proper scheme and format.

    Args:
        raw_url: The raw URL string to normalize

    Returns:
        A properly formatted URL string

    Examples:
        >>> normalize_url("localhost:11434")
        'http://localhost:11434'
        >>> normalize_url("https://example.com:11434")
        'https://example.com:11434'
        >>> normalize_url("http:example.com")
        'http://example.com'
    """
    if not raw_url:
        raise ValueError("URL cannot be empty")

    # Clean up the URL
    raw_url = raw_url.strip()

    # First check if the URL already has a proper scheme
    if raw_url.startswith(("http://", "https://")):
        return raw_url

    # Handle case where URL is malformed like "http:hostname" (missing //)
    if raw_url.startswith(("http:", "https:")) and not raw_url.startswith(
        ("http://", "https://")
    ):
        scheme = raw_url.split(":", 1)[0]
        rest = raw_url.split(":", 1)[1]
        return f"{scheme}://{rest}"

    # Handle URLs that start with //
    if raw_url.startswith("//"):
        # Remove the // and process
        raw_url = raw_url[2:]

    # At this point, we should have hostname:port or just hostname
    # Determine if this is localhost or an external host
    hostname = raw_url.split(":")[0].split("/")[0]

    # Handle IPv6 addresses in brackets
    if hostname.startswith("[") and "]" in raw_url:
        # Extract the IPv6 address including brackets
        hostname = raw_url.split("]")[0] + "]"

    # Use http for local/private addresses, https for external hosts
    scheme = "http" if is_private_ip(hostname) else "https"

    return f"{scheme}://{raw_url}"


@lru_cache(maxsize=1024)
def canonical_url_key(url: str) -> str:
    """Return a canonical form of ``url`` suitable for deduplication and
    display in a Sources / citations listing.

    The canonical form:
    - lowercases scheme and host (paths stay case-sensitive),
    - strips userinfo (``user:pass@`` — never leak creds),
    - strips default ports (80/http, 443/https),
    - strips fragments,
    - drops tracking query params (``utm_*``, ``fbclid``, ``gclid``,
      ``msclkid``, ``yclid``, ``dclid``, ``gad_source``, ``mc_eid``,
      ``mc_cid``, ``ref_src``, ``igshid``, ``_ga``, ``_gl``),
    - trims a trailing ``/`` from non-root paths.

    Click-through behavior is preserved — tracking params carry no
    content, and mainstream browsers already strip them automatically.
    Percent-encoding is not normalized; query param order is preserved
    as-is.

    Falls back to ``url.strip()`` when the input is not a recognizable
    absolute URL (e.g. ``mailto:``, ``data:``, or protocol-relative
    ``//host/p``), since canonicalization would be ambiguous.
    """
    if not url:
        return ""
    try:
        parsed = urlsplit(url)
    except Exception:
        return url.strip()
    # Require both a scheme and a netloc; otherwise canonicalization is
    # ambiguous (mailto:, data:, protocol-relative, etc.).
    if not parsed.scheme or not parsed.netloc:
        return url.strip()

    scheme = parsed.scheme.lower()

    # Strip userinfo (user:pass@host) from netloc.
    netloc = parsed.netloc.rsplit("@", 1)[-1]

    # Split host/port carefully so IPv6 literals survive.
    if netloc.startswith("["):
        end = netloc.find("]")
        host = netloc[: end + 1]
        rest = netloc[end + 1 :]
        port = rest[1:] if rest.startswith(":") else ""
    elif ":" in netloc:
        host, _, port = netloc.rpartition(":")
        host = host.lower()
    else:
        host, port = netloc.lower(), ""

    if (scheme == "https" and port == "443") or (
        scheme == "http" and port == "80"
    ):
        port = ""
    netloc = f"{host}:{port}" if port else host

    # Filter query params case-insensitively on key; preserve order/values.
    if parsed.query:
        pairs = parse_qsl(parsed.query, keep_blank_values=True)
        kept = [
            (k, v)
            for k, v in pairs
            if not (
                k.lower() in _TRACKING_PARAMS
                or any(k.lower().startswith(p) for p in _TRACKING_PREFIXES)
            )
        ]
        query_str = urlencode(kept, doseq=True) if kept else ""
    else:
        query_str = ""

    path = parsed.path
    if path and path != "/" and path.endswith("/"):
        path = path.rstrip("/")

    return urlunsplit((scheme, netloc, path, query_str, ""))


def is_safe_custom_llm_endpoint(custom_endpoint: Optional[str]) -> bool:
    """SSRF guard for a user-supplied custom LLM endpoint, applied at the
    request boundary as fail-fast defense-in-depth.

    The endpoint is normalized exactly as the OpenAI-compatible provider
    normalizes it (:func:`normalize_url`), so scheme-less local endpoints
    such as ``localhost:11434`` or ``192.168.1.10:8000`` are handled the
    same way the provider handles them, then validated with
    :func:`validate_url` allowing private IPs / localhost. That accepts
    local LLM backends (Ollama / LM Studio / vLLM) while still blocking
    cloud-metadata and link-local targets. An empty / unset endpoint is
    safe (there is nothing to send to). On rejection a redacted warning
    is logged (the raw URL may carry credentials).

    This is not the sole protection: the OpenAI-compatible provider's
    ``assert_base_url_safe`` re-validates the same URL before the
    LangChain client is constructed. This guard simply rejects early —
    before any DB row is written or research thread is spawned — and
    keeps the endpoint out of the logs.
    """
    endpoint = (custom_endpoint or "").strip()
    if not endpoint:
        return True
    candidate = normalize_url(endpoint)
    if validate_url(candidate, allow_private_ips=True):
        return True
    logger.warning(
        "SSRF protection: rejected custom_endpoint URL: {}",
        redact_url_for_log(candidate),
    )
    return False
