"""Human-facing guidance for egress-policy denials.

The PDP returns terse machine reason codes (``scope_mismatch_private_only``,
``provider_cloud_only``, …) — great for logs, useless for a user staring at a
blocked action. This module maps each reason to a CLEAR sentence that says
WHAT was blocked, WHY, and — crucially — HOW TO ALLOW IT (the exact setting to
change), so a block is never a dead end.

Use :func:`denial_guidance` at any USER-FACING surface (HTTP error responses,
the research form, tool errors). Keep the raw ``reason`` code in audit logs.

Implementation note: messages are plain strings with ``{target}`` and
``{scope_setting}`` placeholders, resolved by a single ``str.format`` in
:func:`denial_guidance` — NOT f-strings — so every entry uses the same
``{target}`` syntax (no ``{{target}}`` escaping traps). Don't put a literal
``{``/``}`` in a message, or ``.format`` will choke.
"""

from __future__ import annotations

from typing import Optional

# Where the user changes the egress scope, named once so the wording stays
# consistent everywhere. Injected as the ``{scope_setting}`` placeholder.
_SCOPE_SETTING = (
    "Settings → Privacy & Egress → Egress Scope (or the Privacy & Egress panel "
    "on the research form, for a one-off run)"
)

# reason code -> (what happened, how to allow it). ``{target}`` is filled with
# the blocked engine/provider/host; ``{scope_setting}`` with _SCOPE_SETTING.
_GUIDANCE: dict[str, tuple[str, str]] = {
    "scope_mismatch_private_only": (
        "{target} was blocked because your Egress Scope is set to "
        "Private only — only local sources (your collections, local engines) "
        "may run, and nothing leaves the machine.",
        "To use it, change your Egress Scope to Both or Public only in "
        "{scope_setting}.",
    ),
    "scope_mismatch_public_only": (
        "{target} was blocked because your Egress Scope is set to "
        "Public only — local/private sources are excluded from this run.",
        "To use it, change your Egress Scope to Both or Private only in "
        "{scope_setting}; or, if this is a collection whose contents are "
        "non-sensitive, mark it Public on the collection page.",
    ),
    "strict_public_host": (
        "{target} was blocked because your Egress Scope is Strict — only your "
        "single primary engine may run, with no expansion to other hosts.",
        "To allow it, change your Egress Scope away from Strict in "
        "{scope_setting}.",
    ),
    "strict_not_primary": (
        "{target} was blocked because your Egress Scope is Strict — only your "
        "chosen primary search engine may run.",
        "To use {target}, either make it your primary engine, or change "
        "your Egress Scope away from Strict in {scope_setting}.",
    ),
    "blocked_metadata_ip": (
        "{target} was blocked because it targets a cloud-metadata endpoint "
        "(e.g. 169.254.169.254). These are NEVER permitted under any scope — "
        "they are a common credential-theft (SSRF) target.",
        "This is a hard safety rule and cannot be overridden in settings. If "
        "you believe this is a mistake, the host genuinely resolves to a "
        "cloud-metadata address.",
    ),
    "provider_cloud": (
        "Cloud embeddings ({target}) were blocked because local embeddings are "
        "required for this run (Private only scope, or the “Require local "
        "embeddings” toggle).",
        "To use cloud embeddings, turn off “Require local embeddings”, or "
        "change your Egress Scope away from Private only in {scope_setting}; "
        "or configure a local embeddings endpoint (e.g. sentence_transformers "
        "or a local Ollama URL).",
    ),
    "provider_cloud_only": (
        "The cloud LLM provider “{target}” was blocked because a local LLM is "
        "required for this run (Private only scope, or the “Require local LLM "
        "endpoint” toggle).",
        "To use this provider, turn off “Require local LLM endpoint”, or change "
        "your Egress Scope away from Private only in {scope_setting}; or "
        "switch to a local provider (Ollama, LM Studio, LlamaCpp).",
    ),
    "provider_remote": (
        "“{target}” was blocked because its configured endpoint resolves to a "
        "remote (non-local) host, and a local endpoint is required for this "
        "run.",
        "To use it, point its URL at a local address (localhost / your LAN), "
        "turn off the “Require local” toggle, or change your Egress Scope in "
        "{scope_setting}.",
    ),
    "provider_url_unset": (
        "“{target}” was blocked because no local endpoint URL is configured for "
        "it, so it can't be certified as local while a local endpoint is "
        "required.",
        "Configure a local URL for the provider, or use a local-default "
        "provider (Ollama, LM Studio, LlamaCpp), or turn off the “Require "
        "local” toggle.",
    ),
    "elasticsearch_cloud_id_public_egress": (
        "The Elasticsearch Cloud ID was blocked because your Egress Scope is "
        "Private only / Strict — a Cloud ID points at hosted Elastic Cloud "
        "(public egress).",
        "To use Elasticsearch locally, configure a local hosts= URL instead of "
        "a Cloud ID; or change your Egress Scope away from Private only / "
        "Strict in {scope_setting}.",
    ),
    "denial_quota_exceeded": (
        "This run hit its limit of blocked URL fetches — typically a document "
        "that loops the agent through many forbidden links.",
        "The run continues, but further blocked fetches are refused to protect "
        "performance. Widen your Egress Scope if these URLs should be allowed, "
        "or ignore this if the blocked links are junk.",
    ),
    "unknown_egress_scope": (
        "Your saved Egress Scope value is unrecognised (corrupted or set to an "
        "invalid value), so the run was refused rather than guessing.",
        "Re-select a valid Egress Scope in {scope_setting}.",
    ),
    "engine_unknown": (
        "The search engine “{target}” isn't recognised, so it was refused "
        "(fail-closed).",
        "Check the engine name, or pick a different search engine.",
    ),
    "unclassified": (
        "“{target}” couldn't be classified as public or local, so it was "
        "refused (fail-closed).",
        "Pick a recognised engine, or check the engine's configuration.",
    ),
}

# Reasons that are NOT really a policy block the user can act on — they are
# parse/format failures. Given a short, honest explanation instead of "change
# a setting".
_NON_POLICY = {
    "url_malformed": "The URL is malformed and could not be fetched.",
    "no_hostname": "The URL has no host and could not be fetched.",
    "unsupported_scheme": (
        "The link uses a scheme that can't be fetched (only http/https URLs "
        "are retrieved)."
    ),
    "dangerous_scheme": (
        "The link uses a non-web scheme (javascript:/data:/file:/…) and was "
        "skipped — it isn't fetchable content."
    ),
    "host_unclassified": "The host could not be resolved or classified.",
    "internal_error": (
        "An internal error occurred while evaluating the egress policy."
    ),
}


def denial_guidance(reason: str, *, target: Optional[str] = None) -> str:
    """Return a clear, user-facing explanation + instructions for a denial.

    ``reason`` is the PDP machine code; ``target`` is the blocked thing
    (engine / provider / host), inserted into the message. Always returns a
    non-empty string, even for unknown reasons.
    """
    label = target or "This action"
    if reason in _GUIDANCE:
        what, how = _GUIDANCE[reason]
        # Two-step: the f-string only CONCATENATES the two plain templates
        # (no placeholder interpolation — {target}/{scope_setting} are literal
        # here), then .format() fills them. Don't collapse this into a single
        # f-string or the placeholders would need escaping again.
        return f"{what} {how}".format(
            target=label, scope_setting=_SCOPE_SETTING
        )
    if reason in _NON_POLICY:
        return _NON_POLICY[reason]
    # Unknown reason — be honest, don't invent an instruction.
    return (
        f"{label} was blocked by the egress policy (reason: {reason}). Check "
        f"your Egress Scope and the “Require local” toggles in {_SCOPE_SETTING}."
    )
