"""Egress-policy warning checks.

Pure functions: take primitive values, return a warning dict or ``None``.
These surface on the research-form banner so users can see at a glance
when their current policy lets data leave the machine.

The audit identified three independent vectors that each deserve their
own banner:

1. **Public search egress enabled** — the active scope permits any public
   engine to fire. Loudest on a fresh install where the default scope is
   ``both``; suppressed via ``ui.egress_warnings_acknowledged`` so first-
   launch isn't a wall of red.
2. **Cloud LLM enabled** — the user hasn't opted into
   ``llm.require_local_endpoint`` and the configured provider is one of
   the unambiguously-cloud providers. Critical-severity because the
   user's full prompt content leaks on every research run.
3. **Cloud embeddings enabled** — even worse: indexing a corpus with
   OpenAI embeddings POSTs every chunk to OpenAI. Critical-severity.
"""

from typing import Optional

# Single source of truth — these warning checks now live in the same
# egress package as the PDP, so import the cloud-provider set directly
# instead of maintaining a hand-synced copy (which previously risked
# drift: a provider added to one list but not the other).
from .policy import _CLOUD_LLM_PROVIDERS


def check_public_egress_enabled(
    egress_scope: str,
    acknowledged: bool,
) -> Optional[dict]:
    """Banner when the active scope permits public-internet search engines.

    Fires for ``both`` and ``public_only`` (the two scopes that allow
    public engines). Suppressed when the user has acknowledged the
    egress-policy warnings via the fresh-install flag — otherwise every
    new install would face three loud banners before doing anything.
    """
    if acknowledged:
        return None
    # "adaptive" can resolve to a public-allowing scope (public or
    # unclassifiable primary), so warn conservatively — better a dismissible
    # banner than a silent public-egress path the user didn't expect.
    if egress_scope not in ("both", "public_only", "adaptive"):
        return None

    return {
        "type": "public_egress_enabled",
        "icon": "🌐",
        "title": "Public search egress enabled",
        "message": (
            "This run can reach external search engines. Set the Egress "
            "Scope to 'Private only' below if you want to keep all "
            "research traffic on-machine."
        ),
        "dismissKey": "app.warnings.dismiss_egress_policy",
        "actionUrl": "#policy_egress_scope",
        "actionLabel": "Adjust scope",
    }


def check_effective_scope(
    egress_scope: str,
    effective_scope: str,
    primary_engine: str,
    acknowledged: bool,
) -> Optional[dict]:
    """Informational banner stating what the ADAPTIVE scope actually resolves
    to for the current primary engine.

    Adaptive is opaque on its own ("follows your primary"); this makes the
    effective posture explicit so the user knows whether THIS config means
    public searches, private/local-only, or both. Only fires for ``adaptive``
    (the explicit scopes are self-describing in the dropdown). Has its own
    dismiss flag so dismissing it doesn't hide the risk banners.
    """
    if acknowledged:
        return None
    if (egress_scope or "").lower() != "adaptive":
        return None
    eff = (effective_scope or "").lower()
    primary = primary_engine or "your primary engine"
    base = {
        "type": "egress_effective_scope",
        "dismissKey": "app.warnings.dismiss_adaptive_scope_info",
        "actionUrl": "#policy_egress_scope",
        "actionLabel": "Change mode",
    }
    if eff == "private_only":
        return {
            **base,
            "icon": "🔒",
            "title": "Adaptive → Private only (stays local)",
            "message": (
                f"Your primary engine ('{primary}') is private, so this run "
                "stays on your machine: only local engines run, and LLM + "
                "embeddings are forced local — nothing leaves the box."
            ),
        }
    if eff == "public_only":
        return {
            **base,
            "icon": "🌐",
            "title": "Adaptive → Public searches enabled",
            "message": (
                f"Your primary engine ('{primary}') is public, so this run "
                "uses public web/academic engines. Your local collections are "
                "not queried."
            ),
        }
    # both (unclassifiable primary)
    return {
        **base,
        "icon": "🔀",
        "title": "Adaptive → Public + private searches enabled",
        "message": (
            f"Your primary ('{primary}') could not be classified as public "
            "or private, so this run can use both public engines and your "
            "local collections."
        ),
    }


def check_cloud_llm_enabled(
    provider: str,
    require_local_endpoint: bool,
    acknowledged: bool,
) -> Optional[dict]:
    """Banner when the configured LLM provider is cloud-only and the
    require-local-endpoint toggle is off.

    Critical-severity: the user's full prompt content (including the
    research query and all retrieved context) is sent to the provider on
    every call.
    """
    if acknowledged:
        return None
    if require_local_endpoint:
        return None
    if not provider or provider.lower() not in _CLOUD_LLM_PROVIDERS:
        return None

    return {
        "type": "cloud_llm_enabled",
        "icon": "☁️",
        "title": "LLM provider is cloud-hosted",
        "message": (
            f"Your LLM provider ({provider}) is cloud-hosted. Query "
            "content will be sent off-machine on every research run, "
            "independent of the Egress Scope setting. Tick 'Require local "
            "LLM endpoint' below if you want fully local inference."
        ),
        "dismissKey": "app.warnings.dismiss_cloud_llm",
        "actionUrl": "#llm_require_local_endpoint",
        "actionLabel": "Require local LLM",
    }


def check_cloud_embeddings_enabled(
    embeddings_provider: str,
    embeddings_base_url: str,
    require_local_embeddings: bool,
    acknowledged: bool,
) -> Optional[dict]:
    """Banner when the embeddings provider sends data off-machine on
    indexing.

    Highest-severity of the three: indexing a private corpus with OpenAI
    embeddings POSTs every chunk to OpenAI's API. A user who is unaware
    of this loses their corpus.

    Suppressed when ``base_url`` is set to a local URL (LM Studio,
    vLLM, llama.cpp), since the OpenAI provider type is then pointed at
    a local endpoint.
    """
    if acknowledged:
        return None
    if require_local_embeddings:
        return None
    if (embeddings_provider or "").lower() != "openai":
        return None

    # If base_url is set and points to a local hostname, suppress the
    # warning — the user has configured OpenAI-compatible-but-local
    # (LM Studio, vLLM, etc.). Uses is_private_ip so RFC1918 ranges
    # (10.x, 172.16-31.x, 192.168.x), CGNAT, link-local, IPv6 private,
    # and .local mDNS hosts are all recognised. Substring-matching
    # against a small list missed legitimate private-network endpoints.
    #
    # NOTE: this is a LITERAL-IP / .local check only — it does NOT resolve
    # DNS, unlike the enforcing evaluate_embeddings (which DNS-resolves via
    # _classify_host). So a private-DNS base_url (e.g. my-llm.internal → 10.x)
    # is treated as LOCAL by enforcement (nothing leaves) yet still shows this
    # banner. That divergence is intentional: this advisory runs on every
    # settings-page render and must stay synchronous/cheap, so it deliberately
    # over-warns (the safe direction) rather than add a blocking DNS lookup to
    # page load. The banner never blocks anything; the PDP is the source of
    # truth for what actually egresses.
    if embeddings_base_url:
        try:
            from urllib.parse import urlsplit

            from ..network_utils import is_private_ip

            parsed = urlsplit(embeddings_base_url)
            hostname = parsed.hostname
            if hostname and is_private_ip(hostname):
                return None
        except Exception:  # noqa: silent-exception
            # Defensive: if URL parsing fails, fall through to issuing
            # the banner — the failure itself is a sign of misconfig
            # the user should see, not a reason to mask the warning.
            pass

    return {
        "type": "cloud_embeddings_enabled",
        "icon": "📤",
        "title": "Document chunks will be sent to OpenAI",
        "message": (
            "Your embeddings provider is OpenAI. Indexing a collection "
            "will POST every chunk to OpenAI's API — the entire corpus "
            "leaves the machine. Tick 'Require local embeddings' below "
            "to switch to sentence-transformers or a local OpenAI-"
            "compatible endpoint."
        ),
        "dismissKey": "app.warnings.dismiss_cloud_embeddings",
        "actionUrl": "#embeddings_require_local",
        "actionLabel": "Require local embeddings",
    }
