"""
SDK bootstrap verification -- fail-closed distribution authorization.

At GovernanceClient init time, this module:
  1. Hashes the API key (SHA-256, never sends the raw key).
  2. Computes a SHA-512 artifact hash of the installed package files.
  3. Collects a runtime fingerprint (IP hash, cloud region, OS, etc.).
  4. Calls GET /v1/dist/verify on the gateway.
  5. On ALLOW: stores session_id and starts a heartbeat daemon thread.
  6. On DENY or any error: locks the client (fail-closed).

Env vars:
  KEVROS_SKIP_BOOTSTRAP=1  -- skip bootstrap verification (dev/test only).
                              Logs a warning when skipped. Default is ON.

All network I/O uses stdlib urllib.request (no third-party deps).
All errors result in fail-closed behavior. Error messages are deliberately
uninformative to avoid leaking internal state.
"""

from __future__ import annotations

import hashlib
import json
import logging
import os
import platform
import socket
import threading
import urllib.error
import urllib.request
from typing import Any, Callable, Dict, Optional

logger = logging.getLogger("kevros.bootstrap")

_BOOTSTRAP_TIMEOUT = 10  # seconds
_DEFAULT_HEARTBEAT_INTERVAL = 300  # seconds


# ---------------------------------------------------------------------------
# Fingerprint helpers
# ---------------------------------------------------------------------------


def _sha256(data: str) -> str:
    """Return hex SHA-256 of a UTF-8 string."""
    return hashlib.sha256(data.encode("utf-8")).hexdigest()


def _get_outbound_ip_hash() -> str:
    """Hash of the host's outbound IP (best-effort, empty on failure)."""
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        try:
            s.settimeout(2)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
        finally:
            s.close()
        return _sha256(ip)
    except Exception:
        return ""


def _detect_azure_region() -> str:
    """Best-effort Azure region from environment."""
    region = os.environ.get("AZURE_REGION", "")
    if region:
        return region
    # Azure App Service / Functions sets WEBSITE_SITE_NAME but not a direct
    # region var; the region is encoded in REGION_NAME on some hosts.
    return os.environ.get("REGION_NAME", "")


def _detect_cloud_provider() -> str:
    """Best-effort cloud provider detection via IMDS (instance metadata)."""
    # Azure IMDS
    try:
        req = urllib.request.Request(
            "http://169.254.169.254/metadata/instance?api-version=2021-02-01",
            headers={"Metadata": "true"},
        )
        with urllib.request.urlopen(req, timeout=2) as resp:
            data = json.loads(resp.read())
            if data.get("compute", {}).get("azEnvironment"):
                return "azure"
    except Exception:
        pass
    # AWS IMDS
    try:
        req = urllib.request.Request("http://169.254.169.254/latest/meta-data/ami-id")
        with urllib.request.urlopen(req, timeout=2) as resp:
            if resp.status == 200:
                return "aws"
    except Exception:
        pass
    # GCP IMDS
    try:
        req = urllib.request.Request(
            "http://169.254.169.254/computeMetadata/v1/project/project-id",
            headers={"Metadata-Flavor": "Google"},
        )
        with urllib.request.urlopen(req, timeout=2) as resp:
            if resp.status == 200:
                return "gcp"
    except Exception:
        pass
    return "unknown"


def _get_runtime_fingerprint() -> Dict[str, str]:
    """Collect non-sensitive environment signals for distribution verification."""
    return {
        "source_ip_hash": _get_outbound_ip_hash(),
        "azure_region": _detect_azure_region(),
        "cloud_provider": _detect_cloud_provider(),
        "python_version": platform.python_version(),
        "os_type": platform.system(),
        "hostname_hash": _sha256(socket.gethostname()),
        "installed_path_hash": _sha256(os.path.dirname(__file__)),
    }


def _get_artifact_hash() -> str:
    """SHA-512 of all .py files in the installed package directory."""
    pkg_dir = os.path.dirname(__file__)
    h = hashlib.sha512()
    try:
        py_files = sorted(
            f for f in os.listdir(pkg_dir) if f.endswith(".py")
        )
        for fname in py_files:
            fpath = os.path.join(pkg_dir, fname)
            try:
                with open(fpath, "rb") as fh:
                    h.update(fh.read())
            except OSError:
                continue
    except OSError:
        pass
    return h.hexdigest()


# ---------------------------------------------------------------------------
# Lock helper
# ---------------------------------------------------------------------------


def _locked_method(*_args: Any, **_kwargs: Any) -> None:
    """Stub that replaces all public methods when the client is locked."""
    raise RuntimeError("Client not initialized")


# List of public method names on GovernanceClient that must be replaced when
# the client is locked.  Async variants are included.
_PUBLIC_METHODS = [
    "verify",
    "averify",
    "attest",
    "aattest",
    "bind",
    "abind",
    "verify_outcome",
    "averify_outcome",
    "bundle",
    "abundle",
    "verify_peer",
    "averify_peer",
    "verify_peer_token",
    "averify_peer_token",
    "verify_certificate",
    "averify_certificate",
    "build_trust_headers",
    "trusted_request",
    "health",
    "ahealth",
]


def _lock_client(client: Any) -> None:
    """Replace every public method on *client* with a fail-closed stub."""
    client._locked = True
    for name in _PUBLIC_METHODS:
        if hasattr(client, name):
            setattr(client, name, _locked_method)


# ---------------------------------------------------------------------------
# Heartbeat
# ---------------------------------------------------------------------------


class DistributionHeartbeat:
    """Daemon thread that periodically re-verifies the SDK distribution."""

    def __init__(
        self,
        *,
        gateway_url: str,
        api_key_hash: str,
        session_id: str,
        artifact_hash: str,
        runtime_fingerprint: Dict[str, str],
        interval: float,
        lock_callback: Callable[[], None],
    ) -> None:
        self._gateway_url = gateway_url.rstrip("/")
        self._api_key_hash = api_key_hash
        self._session_id = session_id
        self._artifact_hash = artifact_hash
        self._runtime_fingerprint = runtime_fingerprint
        self._interval = max(interval, 60)  # floor at 60s
        self._lock_callback = lock_callback
        self._stop_event = threading.Event()
        self._thread: Optional[threading.Thread] = None

    def start(self) -> None:
        """Start the heartbeat daemon thread."""
        self._thread = threading.Thread(
            target=self._run,
            name="kevros-heartbeat",
            daemon=True,
        )
        self._thread.start()

    def stop(self) -> None:
        """Signal the heartbeat thread to stop."""
        self._stop_event.set()

    def _run(self) -> None:
        """Main loop: sleep, then call heartbeat endpoint."""
        while not self._stop_event.wait(timeout=self._interval):
            try:
                self._heartbeat()
            except Exception:
                # Any failure locks the client (fail-closed).
                logger.debug("Heartbeat failed, locking client")
                self._lock_callback()
                break

    def _heartbeat(self) -> None:
        """Call /v1/dist/heartbeat and lock on DENY or error."""
        payload = json.dumps({
            "session_id": self._session_id,
            "runtime_fingerprint": self._runtime_fingerprint,
        }).encode("utf-8")

        req = urllib.request.Request(
            f"{self._gateway_url}/v1/dist/heartbeat",
            data=payload,
            headers={"Content-Type": "application/json"},
            method="POST",
        )

        try:
            with urllib.request.urlopen(req, timeout=_BOOTSTRAP_TIMEOUT) as resp:
                body = json.loads(resp.read())
        except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError):
            raise RuntimeError("Heartbeat request failed")

        decision = body.get("decision", "").upper()
        if decision != "ALLOW":
            raise RuntimeError("Heartbeat denied")

        # Update interval if the server requests a different one.
        next_int = body.get("next_interval")
        if isinstance(next_int, (int, float)) and next_int >= 60:
            self._interval = float(next_int)


# ---------------------------------------------------------------------------
# Bootstrap verification
# ---------------------------------------------------------------------------


class BootstrapResult:
    """Outcome of bootstrap_verify()."""

    __slots__ = ("allowed", "session_id", "heartbeat_interval", "detail")

    def __init__(
        self,
        allowed: bool,
        session_id: str = "",
        heartbeat_interval: float = _DEFAULT_HEARTBEAT_INTERVAL,
        detail: str = "",
    ) -> None:
        self.allowed = allowed
        self.session_id = session_id
        self.heartbeat_interval = heartbeat_interval
        self.detail = detail


def bootstrap_verify(
    api_key: str,
    gateway_url: str,
) -> BootstrapResult:
    """
    Call the gateway's /v1/dist/verify endpoint to authorize this SDK instance.

    On success returns a BootstrapResult with allowed=True.
    On any failure (network error, DENY, malformed response) returns
    allowed=False (fail-closed).

    The raw API key is never transmitted -- only its SHA-256 hash is sent.

    Args:
        api_key: The resolved Kevros API key.
        gateway_url: Base URL of the governance gateway.

    Returns:
        BootstrapResult indicating whether the SDK is authorized.
    """
    try:
        api_key_hash = _sha256(api_key) if api_key else ""
        artifact_hash = _get_artifact_hash()
        fingerprint = _get_runtime_fingerprint()

        payload = json.dumps({
            "api_key_hash": api_key_hash,
            "artifact_hash": artifact_hash,
            "sdk_version": _get_sdk_version(),
            "runtime_fingerprint": fingerprint,
        }).encode("utf-8")

        req = urllib.request.Request(
            f"{gateway_url.rstrip('/')}/v1/dist/verify",
            data=payload,
            headers={"Content-Type": "application/json"},
            method="POST",
        )

        with urllib.request.urlopen(req, timeout=_BOOTSTRAP_TIMEOUT) as resp:
            body = json.loads(resp.read())

        decision = body.get("decision", "").upper()
        if decision != "ALLOW":
            return BootstrapResult(allowed=False, detail="Initialization failed")

        session_id = body.get("session_id", "")
        heartbeat_interval = body.get("next_interval", _DEFAULT_HEARTBEAT_INTERVAL)
        if not isinstance(heartbeat_interval, (int, float)):
            heartbeat_interval = _DEFAULT_HEARTBEAT_INTERVAL

        return BootstrapResult(
            allowed=True,
            session_id=session_id,
            heartbeat_interval=float(heartbeat_interval),
        )

    except Exception:
        # Fail-closed: any exception means the SDK is not authorized.
        return BootstrapResult(allowed=False, detail="Initialization failed")


def _get_sdk_version() -> str:
    """Return the installed SDK version string."""
    try:
        from . import __version__
        return __version__
    except Exception:
        return "unknown"
