"""Sanctioned MCP daemon guards for imports and credential access (#558 / #695).

Direct ``import gitea_mcp_server`` from a shell bypasses native MCP transport.
#558 introduced a daemon marker; #695 hardens it so:

- Environment variables alone cannot reconstruct a native session
  (``GITEA_MCP_SANCTIONED_DAEMON=1`` / ``GITEA_ALLOW_DIRECT_MCP_IMPORT=1`` are
  insufficient for mutation gates).
- A process-local runtime record is established only by the resolved canonical
  entrypoint path (not basename) **and** the actual native MCP transport
  lifecycle (``bind_native_mcp_transport`` before ``mcp.run``). Merely
  importing or launching the entrypoint offline does not grant mutation
  authority.
- Public caller-controlled flags (including any former
  ``allow_test_bootstrap``) never establish trusted mutation provenance.
- Offline scripts that import internals fail closed on mutations.
- Pytest remains allowed for hermetic unit tests via ``is_pytest_runtime()``.
  A separate test-only seam may establish a **test-mode** native record for
  unit tests of transport gates; that record cannot authorize production
  Gitea mutation endpoints.

Manual deletion of session-state files is never a recovery path.
"""

from __future__ import annotations

import hashlib
import inspect
import os
import secrets
import time
from pathlib import Path
from typing import Any

SANCTIONED_DAEMON_ENV = "GITEA_MCP_SANCTIONED_DAEMON"
ALLOW_DIRECT_IMPORT_ENV = "GITEA_ALLOW_DIRECT_MCP_IMPORT"
ALLOW_KEYCHAIN_CLI_ENV = "GITEA_ALLOW_KEYCHAIN_CLI"
# Test-only: force provenance failure even under pytest (#695 regressions).
FORCE_PROVENANCE_FAIL_ENV = "GITEA_TEST_FORCE_UNSANCTIONED"

# Process-local native runtime (never persisted, never read from env alone).
_NATIVE_RUNTIME: dict[str, Any] | None = None

# Production transport identifiers accepted by bind_native_mcp_transport.
_PRODUCTION_TRANSPORTS = frozenset({"stdio"})
_RUNTIME_MODE_PRODUCTION = "production"
_RUNTIME_MODE_TEST = "test"
_PHASE_ENTRYPOINT_CLAIMED = "entrypoint_claimed"
_PHASE_TRANSPORT_BOUND = "transport_bound"

# Default session-state root (mirrors mcp_session_state; kept local to avoid
# import cycles). Used only to pin authority at transport bind (#695 AC2).
_DEFAULT_SESSION_STATE_DIR = os.path.expanduser("~/.cache/gitea-tools/session-state")
SESSION_STATE_DIR_ENV = "GITEA_MCP_SESSION_STATE_DIR"


class UnsanctionedRuntimeError(RuntimeError):
    """Raised when mutation/credential code runs outside a native MCP daemon."""


def is_pytest_runtime() -> bool:
    if (os.environ.get(FORCE_PROVENANCE_FAIL_ENV) or "").strip() in {
        "1",
        "true",
        "yes",
    }:
        return False
    import sys

    if "pytest" in sys.modules:
        return True
    return bool((os.environ.get("PYTEST_CURRENT_TEST") or "").strip())


def _package_root() -> Path:
    """Directory that contains the canonical MCP entrypoint modules."""
    return Path(__file__).resolve().parent


def canonical_entrypoint_paths() -> frozenset[str]:
    """Resolved absolute paths of official entrypoints (not basenames)."""
    root = _package_root()
    return frozenset(
        {
            str((root / "mcp_server.py").resolve()),
            str((root / "gitea_mcp_server.py").resolve()),
        }
    )


def _resolve_path(path: str | None) -> str | None:
    if not path:
        return None
    try:
        return str(Path(path).resolve())
    except (OSError, RuntimeError, ValueError):
        return None


def _caller_official_entrypoint_path() -> str | None:
    """Return the resolved canonical entrypoint path in the call stack, or None.

    Basename-only matches (e.g. an attacker file named ``mcp_server.py``
    elsewhere) are rejected. The path must equal one of
    :func:`canonical_entrypoint_paths`.
    """
    canonical = canonical_entrypoint_paths()
    for frame in inspect.stack()[1:20]:
        resolved = _resolve_path(frame.filename)
        if resolved and resolved in canonical:
            return resolved
    return None


def _caller_is_official_entrypoint() -> bool:
    """True when invoked from a resolved canonical entrypoint path (#695)."""
    return _caller_official_entrypoint_path() is not None


def _new_runtime_token() -> tuple[str, str]:
    token = secrets.token_hex(32)
    fingerprint = hashlib.sha256(token.encode()).hexdigest()[:16]
    return token, fingerprint


def mark_sanctioned_daemon() -> dict[str, Any]:
    """Claim the official entrypoint for this process (#695).

    This alone does **not** authorize mutations. Callers must subsequently
    bind the native MCP transport via :func:`bind_native_mcp_transport`.

    Only a stack frame whose **resolved absolute path** is the canonical
    ``mcp_server.py`` or ``gitea_mcp_server.py`` next to this module may
    claim the entrypoint. Basename spoofing is rejected.

    There is no public ``allow_test_bootstrap`` argument: caller-controlled
    flags must never establish trusted mutation provenance. Hermetic tests
    use :func:`install_test_native_runtime` (pytest-only, test mode).
    """
    global _NATIVE_RUNTIME
    if is_pytest_runtime():
        # Under pytest, production mark is a no-op for transport authority.
        # Tests that need a native-transport record use install_test_native_runtime.
        return native_runtime_status()

    entrypoint_path = _caller_official_entrypoint_path()
    if entrypoint_path is None:
        raise UnsanctionedRuntimeError(
            "mark_sanctioned_daemon rejected: not called from the resolved "
            "canonical MCP entrypoint path (#695). Basename-only names "
            "(e.g. a renamed runner called mcp_server.py) are insufficient. "
            "Offline import / standalone scripts cannot reconstruct native "
            "transport. Stop after native MCP failure; do not run offline "
            "mutation helpers."
        )

    token, fingerprint = _new_runtime_token()
    _NATIVE_RUNTIME = {
        "token": token,
        "token_fingerprint": fingerprint,
        "pid": os.getpid(),
        "started_at": time.time(),
        "entrypoint": "mcp_server",
        "entrypoint_path": entrypoint_path,
        "phase": _PHASE_ENTRYPOINT_CLAIMED,
        "transport": None,
        "mode": _RUNTIME_MODE_PRODUCTION,
    }
    # Legacy signal for older probes; alone does not authorize mutations.
    os.environ[SANCTIONED_DAEMON_ENV] = "1"
    return native_runtime_status()


def bind_native_mcp_transport(*, transport: str) -> dict[str, Any]:
    """Bind the live native MCP transport lifecycle (#695).

    Must be called from the resolved canonical entrypoint immediately before
    the real MCP server transport loop (e.g. ``mcp.run(transport=\"stdio\")``).
    Requires a prior successful :func:`mark_sanctioned_daemon` claim in this
    process. Import-only or offline launch without this bind leaves
    :func:`is_native_mcp_transport` false.
    """
    global _NATIVE_RUNTIME
    transport_name = (transport or "").strip().lower()
    if transport_name not in _PRODUCTION_TRANSPORTS:
        raise UnsanctionedRuntimeError(
            f"bind_native_mcp_transport rejected: transport {transport!r} is "
            f"not a production MCP transport (#695). Allowed: "
            f"{sorted(_PRODUCTION_TRANSPORTS)}."
        )

    entrypoint_path = _caller_official_entrypoint_path()
    if entrypoint_path is None:
        raise UnsanctionedRuntimeError(
            "bind_native_mcp_transport rejected: not called from the resolved "
            "canonical MCP entrypoint path (#695)."
        )

    if _NATIVE_RUNTIME is None or int(_NATIVE_RUNTIME.get("pid") or -1) != os.getpid():
        raise UnsanctionedRuntimeError(
            "bind_native_mcp_transport rejected: no entrypoint claim in this "
            "process (#695). Call mark_sanctioned_daemon() from the official "
            "entrypoint first."
        )

    if _NATIVE_RUNTIME.get("mode") != _RUNTIME_MODE_PRODUCTION:
        raise UnsanctionedRuntimeError(
            "bind_native_mcp_transport rejected: runtime mode is not "
            "production (#695)."
        )

    claimed = (_NATIVE_RUNTIME.get("entrypoint_path") or "").strip()
    if claimed and claimed != entrypoint_path:
        raise UnsanctionedRuntimeError(
            "bind_native_mcp_transport rejected: entrypoint path mismatch "
            "between mark and bind (#695)."
        )

    # Pin session-state root for this server lifetime (#695 AC2 / PR #701).
    # Changing GITEA_MCP_SESSION_STATE_DIR after bind must not manufacture a
    # second authority domain for decision locks / workflow proofs.
    raw_state = (os.environ.get(SESSION_STATE_DIR_ENV) or "").strip()
    if not raw_state:
        raw_state = _DEFAULT_SESSION_STATE_DIR
    try:
        pinned_state = str(Path(raw_state).resolve())
    except (OSError, RuntimeError, ValueError):
        pinned_state = raw_state

    _NATIVE_RUNTIME["phase"] = _PHASE_TRANSPORT_BOUND
    _NATIVE_RUNTIME["transport"] = transport_name
    _NATIVE_RUNTIME["entrypoint_path"] = entrypoint_path
    _NATIVE_RUNTIME["bound_at"] = time.time()
    _NATIVE_RUNTIME["session_state_dir"] = pinned_state
    os.environ[SANCTIONED_DAEMON_ENV] = "1"
    return native_runtime_status()


def install_test_native_runtime() -> dict[str, Any]:
    """Pytest-only seam for hermetic native-transport unit tests (#695).

    Establishes a **test-mode** process-local record so unit tests can exercise
    gates that require ``is_native_mcp_transport()``. This record:

    - is rejected outside pytest (including a fresh offline interpreter);
    - never uses production mode;
    - cannot authorize production Gitea mutation endpoints
      (:func:`assert_production_mutation_runtime` / production path of
      :func:`assert_sanctioned_mutation_runtime` when not under pytest).

    There is no public caller-controlled flag that forges production native
    transport.
    """
    global _NATIVE_RUNTIME
    if not is_pytest_runtime():
        raise UnsanctionedRuntimeError(
            "install_test_native_runtime rejected: test-mode native runtime "
            "is only available under pytest (#695). allow_test_bootstrap and "
            "similar caller-controlled flags do not exist and cannot authorize "
            "a fresh offline interpreter."
        )
    token, fingerprint = _new_runtime_token()
    _NATIVE_RUNTIME = {
        "token": token,
        "token_fingerprint": fingerprint,
        "pid": os.getpid(),
        "started_at": time.time(),
        "entrypoint": "test_bootstrap",
        "entrypoint_path": None,
        "phase": _PHASE_TRANSPORT_BOUND,
        "transport": "test",
        "mode": _RUNTIME_MODE_TEST,
        "bound_at": time.time(),
    }
    return native_runtime_status()


def clear_native_runtime_for_tests() -> None:
    """Test helper: drop native runtime (does not clear env)."""
    global _NATIVE_RUNTIME
    _NATIVE_RUNTIME = None


def pinned_session_state_dir() -> str | None:
    """Session-state root pinned for this production transport lifetime (#695 AC2).

    When production native transport is bound, durable session proofs must use
    this directory only. Env overrides of ``GITEA_MCP_SESSION_STATE_DIR`` after
    bind are ignored so redirected dirs (e.g. ``.mcp_session_701``) cannot
    manufacture independent decision-lock authority (PR #701 recurrence).
    """
    if not is_production_native_mcp_transport():
        return None
    pinned = (_NATIVE_RUNTIME or {}).get("session_state_dir")
    text = (str(pinned) if pinned is not None else "").strip()
    return text or None


def direct_import_env_enabled() -> bool:
    """True when the legacy direct-import opt-in env is set (never authorizes)."""
    return (os.environ.get(ALLOW_DIRECT_IMPORT_ENV) or "").strip().lower() in {
        "1",
        "true",
        "yes",
    }


def assert_no_direct_import_bypass(context: str = "mutation") -> None:
    """Fail closed when GITEA_ALLOW_DIRECT_MCP_IMPORT is used for mutations (#695 AC1).

    The env flag is never a sanctioned recovery path for LLM/agent sessions.
    Under pytest hermetic tests this is a no-op so unit tests can set the flag
    to prove it does not grant authority.
    """
    if is_pytest_runtime():
        return
    if not direct_import_env_enabled():
        return
    raise UnsanctionedRuntimeError(
        f"{ALLOW_DIRECT_IMPORT_ENV} does not authorize {context} (#695 AC1). "
        "Direct import of gitea_mcp_server mutation tools is forbidden. "
        "Stop after native MCP failure; reconnect the official MCP daemon. "
        "Do not set direct-import flags, offline runners, or redirected "
        f"{SESSION_STATE_DIR_ENV} directories to reconstruct gates."
    )


def is_native_mcp_transport() -> bool:
    """True when this process holds a transport-bound native runtime (#695)."""
    if (os.environ.get(FORCE_PROVENANCE_FAIL_ENV) or "").strip() in {
        "1",
        "true",
        "yes",
    }:
        return False
    if _NATIVE_RUNTIME is None:
        return False
    if int(_NATIVE_RUNTIME.get("pid") or -1) != os.getpid():
        return False
    if not (_NATIVE_RUNTIME.get("token") or "").strip():
        return False
    if _NATIVE_RUNTIME.get("phase") != _PHASE_TRANSPORT_BOUND:
        return False
    if not (_NATIVE_RUNTIME.get("transport") or "").strip():
        return False
    return True


def is_production_native_mcp_transport() -> bool:
    """True only for production-mode, transport-bound native runtime."""
    if not is_native_mcp_transport():
        return False
    return (_NATIVE_RUNTIME or {}).get("mode") == _RUNTIME_MODE_PRODUCTION


def is_sanctioned_mcp_daemon() -> bool:
    """Backward-compatible name; #695 requires native transport, not env alone."""
    if is_production_native_mcp_transport():
        return True
    if is_pytest_runtime():
        return True
    # Explicit direct-import override is for non-LLM operator/test tools only.
    # It is deliberately ignored when a native runtime is expected for mutations
    # under LLM sessions (tests use pytest path). Env alone never grants native.
    return False


def assert_production_mutation_runtime(context: str = "mutation") -> None:
    """Fail closed unless production native MCP transport is bound (#695).

    Test-mode bootstrap records and pytest-only hermetic allowances do **not**
    satisfy this gate. Use for production Gitea mutation endpoints that must
    never be reachable via test bootstrap.
    """
    if is_production_native_mcp_transport():
        return
    mode = (_NATIVE_RUNTIME or {}).get("mode")
    if mode == _RUNTIME_MODE_TEST:
        raise UnsanctionedRuntimeError(
            f"Test-mode native runtime cannot authorize production {context} "
            "(#695). install_test_native_runtime / former allow_test_bootstrap "
            "must never reach real Gitea mutation endpoints."
        )
    assert_sanctioned_mutation_runtime(context)


def assert_sanctioned_mutation_runtime(context: str = "mutation") -> None:
    """Fail closed when mutation code runs outside native MCP transport (#695).

    Under pytest, hermetic unit tests are allowed (profile/permission tests).
    Outside pytest, requires production-mode transport-bound native runtime.
    Test-mode records do not authorize non-pytest production mutations.
    ``GITEA_ALLOW_DIRECT_MCP_IMPORT`` never authorizes mutations (#695 AC1).
    """
    if is_pytest_runtime():
        return
    # AC1: direct-import env is never a mutation recovery path (PR #701).
    assert_no_direct_import_bypass(context)
    if is_production_native_mcp_transport():
        return
    mode = (_NATIVE_RUNTIME or {}).get("mode")
    if mode == _RUNTIME_MODE_TEST:
        raise UnsanctionedRuntimeError(
            f"Test-mode native runtime cannot authorize production {context} "
            "(#695). Test bootstrap cannot reach production mutation endpoints."
        )
    env_spoof = (os.environ.get(SANCTIONED_DAEMON_ENV) or "").strip() in {
        "1",
        "true",
        "yes",
    }
    extra = ""
    if env_spoof:
        extra = (
            f" Note: {SANCTIONED_DAEMON_ENV} alone is not sufficient (#695); "
            "native transport requires the official MCP entrypoint and a live "
            "transport bind."
        )
    phase = (_NATIVE_RUNTIME or {}).get("phase")
    if phase == _PHASE_ENTRYPOINT_CLAIMED:
        extra = (
            (extra + " ") if extra else " "
        ) + (
            "Entrypoint was claimed but native MCP transport was never bound "
            "(#695); offline launch/import of the real entrypoint does not "
            "grant mutation authority."
        )
    raise UnsanctionedRuntimeError(
        f"Unsanctioned / non-native runtime blocked {context} (#695). "
        "Do not import gitea_mcp_server or call mutation helpers from a raw "
        "shell, offline runner, or ad-hoc script after native MCP failure. "
        "Stop and reconnect the official MCP daemon (mcp_server.py) over "
        "native transport. "
        f"Do not set {ALLOW_DIRECT_IMPORT_ENV}, override "
        f"{SESSION_STATE_DIR_ENV}, or use raw token env vars in LLM "
        f"sessions.{extra}"
    )


def assert_keychain_access_allowed() -> None:
    """Fail closed for git-credential keychain fill outside sanctioned contexts."""
    if is_sanctioned_mcp_daemon():
        return
    # Operator-only keychain CLI remains available outside LLM mutation path.
    if (os.environ.get(ALLOW_KEYCHAIN_CLI_ENV) or "").strip() in {"1", "true", "yes"}:
        if not is_pytest_runtime():
            # Still block pure env spoof of SANCTIONED_DAEMON for keychain when
            # FORCE is set for tests.
            if (os.environ.get(FORCE_PROVENANCE_FAIL_ENV) or "").strip() in {
                "1",
                "true",
                "yes",
            }:
                pass
            else:
                return
    raise UnsanctionedRuntimeError(
        "Unsanctioned keychain/credential fill blocked (#558/#695). "
        "Token extraction via git-credential is only allowed inside the "
        f"official native MCP daemon or with explicit operator opt-in "
        f"{ALLOW_KEYCHAIN_CLI_ENV}=1 (never for offline mutation runners)."
    )


def native_runtime_status() -> dict[str, Any]:
    """LLM-safe native runtime status (no raw token)."""
    rt = _NATIVE_RUNTIME or {}
    return {
        "native_mcp_transport": is_native_mcp_transport(),
        "production_native_mcp_transport": is_production_native_mcp_transport(),
        "pytest": is_pytest_runtime(),
        "pid": rt.get("pid"),
        "token_fingerprint": rt.get("token_fingerprint"),
        "started_at": rt.get("started_at"),
        "entrypoint": rt.get("entrypoint"),
        "entrypoint_path": rt.get("entrypoint_path"),
        "phase": rt.get("phase"),
        "transport": rt.get("transport"),
        "mode": rt.get("mode"),
        "session_state_dir": pinned_session_state_dir() or rt.get("session_state_dir"),
        "session_state_dir_pinned": pinned_session_state_dir() is not None,
        "direct_import_env_set": direct_import_env_enabled(),
        "env_sanctioned_alone_insufficient": True,
        "sanctioned_env": SANCTIONED_DAEMON_ENV,
        "allow_direct_import_env": ALLOW_DIRECT_IMPORT_ENV,
        "session_state_dir_env": SESSION_STATE_DIR_ENV,
        "allow_keychain_cli_env": ALLOW_KEYCHAIN_CLI_ENV,
    }


def runtime_status() -> dict[str, Any]:
    """Backward-compatible status payload."""
    status = native_runtime_status()
    status["sanctioned_daemon"] = is_sanctioned_mcp_daemon()
    return status


def mutation_provenance_fields() -> dict[str, Any]:
    """Fields to attach to live mutation / review audit records (#695 AC6)."""
    st = native_runtime_status()
    transport = "native_mcp" if st["native_mcp_transport"] else "untrusted"
    if st.get("mode") == _RUNTIME_MODE_TEST and st["native_mcp_transport"]:
        transport = "test_native_mcp"
    return {
        "transport": transport,
        "native_mcp_transport": bool(st["native_mcp_transport"]),
        "production_native_mcp_transport": bool(
            st.get("production_native_mcp_transport")
        ),
        "native_runtime_pid": st.get("pid"),
        "native_token_fingerprint": st.get("token_fingerprint"),
        "entrypoint": st.get("entrypoint"),
        "phase": st.get("phase"),
        "mode": st.get("mode"),
        "session_state_dir": st.get("session_state_dir"),
        "session_state_dir_pinned": bool(st.get("session_state_dir_pinned")),
    }
