"""Fetch a remote agent so it can be scanned like a local path.

Today this supports **git URLs** — ``abom scan --url https://github.com/org/agent``
shallow-clones the repo into a temporary directory, which the scanner then reads
exactly like a local path. The temp dir is always cleaned up.

A future ``--mcp <url>`` mode will live alongside this (enumerating a live agent's
declared tools/resources/prompts over the Model Context Protocol).
"""
from __future__ import annotations

import logging
import re
import shutil
import subprocess
import tempfile
from contextlib import contextmanager
from pathlib import Path

log = logging.getLogger("abom.remote")

# Explicit git URL schemes.
_SCHEME_RE = re.compile(r"^(https?://|git://|git@|ssh://)")
# Bare "org/repo" GitHub shorthand: each part starts alphanumeric (so local
# paths like "./rel", "../x", "/abs/path" are NOT treated as repos).
_SHORTHAND_RE = re.compile(r"^[A-Za-z0-9][\w.-]*/[A-Za-z0-9][\w.-]*$")


def is_git_url(s: str) -> bool:
    s = s.strip()
    return bool(_SCHEME_RE.match(s) or _SHORTHAND_RE.match(s))


def _normalize(url: str) -> str:
    """Expand a bare ``org/repo`` to a full GitHub https URL."""
    url = url.strip()
    if not _SCHEME_RE.match(url) and _SHORTHAND_RE.match(url):
        return f"https://github.com/{url}"
    return url


@contextmanager
def fetch_git(url: str, *, ref: str | None = None, depth: int = 1):
    """Shallow-clone ``url`` into a temp dir; yield the path. Cleans up on exit.

    Raises ``RuntimeError`` on clone failure (network, auth, bad URL) with a
    readable message. Only clones — never runs any code from the repo.
    """
    if shutil.which("git") is None:
        raise RuntimeError("git is not installed; cannot fetch a remote repo URL")

    target = _normalize(url)
    tmp = Path(tempfile.mkdtemp(prefix="abom-remote-"))
    dest = tmp / "repo"
    cmd = ["git", "clone", "--depth", str(depth), "--quiet"]
    if ref:
        cmd += ["--branch", ref]
    cmd += [target, str(dest)]

    log.info("cloning %s (depth=%d%s)", target, depth, f", ref={ref}" if ref else "")
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
        if proc.returncode != 0:
            err = (proc.stderr or "").strip().splitlines()
            detail = err[-1] if err else f"exit {proc.returncode}"
            raise RuntimeError(f"git clone failed for {target}: {detail}")
        yield dest
    except subprocess.TimeoutExpired:
        raise RuntimeError(f"git clone timed out for {target}")
    finally:
        shutil.rmtree(tmp, ignore_errors=True)
        log.debug("cleaned up %s", tmp)


# ---- MCP endpoint scanning ----------------------------------------------
# We speak the MCP "Streamable HTTP" transport directly (JSON-RPC 2.0 over HTTP
# POST) so the core CLI stays dependency-light — no full mcp SDK required, just
# httpx. This enumerates what a *live* agent declares: its tools, resources, and
# prompts. That is the server's own attestation, not inference.
_MCP_PROTOCOL_VERSION = "2025-06-18"


def is_mcp_url(s: str) -> bool:
    return s.strip().lower().startswith(("http://", "https://"))


def _parse_jsonrpc_body(resp) -> dict:
    """Parse an MCP response that may be application/json OR an SSE stream."""
    import json as _json

    ctype = resp.headers.get("content-type", "")
    if "text/event-stream" in ctype:
        # take the JSON from the last `data:` line of the SSE stream
        data = None
        for line in resp.text.splitlines():
            if line.startswith("data:"):
                data = line[5:].strip()
        if data is None:
            raise RuntimeError("MCP server returned an empty SSE stream")
        return _json.loads(data)
    return resp.json()


def fetch_mcp(url: str, *, timeout: float = 30.0, headers: dict | None = None) -> dict:
    """Connect to an MCP endpoint and enumerate its declared capabilities.

    ``headers`` adds extra HTTP headers (e.g. ``{"Authorization": "Bearer …"}``
    for auth-gated servers like GitHub's). Returns ``{"server": {...},
    "tools": [...], "resources": [...], "prompts": [...]}``. Raises
    ``RuntimeError`` with a readable message on any transport/protocol failure.
    Read-only: only `*/list` methods are called; no tool is ever invoked.
    """
    try:
        import httpx
    except ModuleNotFoundError:
        raise RuntimeError(
            "MCP scanning needs httpx — install it with: pip install httpx "
            "(or pip install 'abom-cli[mcp]')"
        )

    base_headers = {
        "Accept": "application/json, text/event-stream",
        "Content-Type": "application/json",
        "MCP-Protocol-Version": _MCP_PROTOCOL_VERSION,
    }
    if headers:
        base_headers.update(headers)
    rpc_id = [0]

    def rpc(client, method, params=None, session_id=None):
        rpc_id[0] += 1
        headers = dict(base_headers)
        if session_id:
            headers["Mcp-Session-Id"] = session_id
        body = {"jsonrpc": "2.0", "id": rpc_id[0], "method": method}
        if params is not None:
            body["params"] = params
        return client.post(url, json=body, headers=headers)

    out: dict = {"server": {}, "tools": [], "resources": [], "prompts": []}
    try:
        with httpx.Client(timeout=timeout, follow_redirects=True) as client:
            # 1. initialize
            init = rpc(client, "initialize", {
                "protocolVersion": _MCP_PROTOCOL_VERSION,
                "capabilities": {},
                "clientInfo": {"name": "abom-scan", "version": "0.1"},
            })
            if init.status_code >= 400:
                raise RuntimeError(f"MCP initialize failed: HTTP {init.status_code}")
            session_id = init.headers.get("mcp-session-id") or init.headers.get("Mcp-Session-Id")
            init_doc = _parse_jsonrpc_body(init)
            if "error" in init_doc:
                raise RuntimeError(f"MCP initialize error: {init_doc['error'].get('message')}")
            out["server"] = (init_doc.get("result") or {}).get("serverInfo", {})

            # 2. initialized notification (best-effort)
            try:
                rpc(client, "notifications/initialized", {}, session_id)
            except Exception:
                pass

            # 3. enumerate capabilities (read-only)
            for method, key in (("tools/list", "tools"),
                                 ("resources/list", "resources"),
                                 ("prompts/list", "prompts")):
                try:
                    r = rpc(client, method, {}, session_id)
                    doc = _parse_jsonrpc_body(r)
                    out[key] = (doc.get("result") or {}).get(key, []) if "error" not in doc else []
                except Exception as exc:
                    log.debug("%s failed: %s", method, exc)
        return out
    except RuntimeError:
        raise
    except Exception as exc:
        raise RuntimeError(f"could not reach MCP endpoint {url}: {exc}")


def mcp_to_scan_result(url: str, caps: dict) -> dict:
    """Turn MCP capabilities into the scanner's result shape (agent + components).

    Each declared tool/resource/prompt becomes a component whose detected_from is
    the live endpoint — honest provenance: the server attested to it over MCP.
    """
    server = caps.get("server") or {}
    src = f"mcp:{url}"
    components: list[dict] = []

    # the MCP server itself
    components.append({
        "type": "mcpServer",
        "name": server.get("name") or url,
        **({"version": server["version"]} if server.get("version") else {}),
        "endpoint": url, "detected_from": src,
    })
    for t in caps.get("tools", []):
        components.append({
            "type": "tool", "name": t.get("name", "?"),
            **({"description": t["description"]} if t.get("description") else {}),
            "detected_from": src,
        })
    for r in caps.get("resources", []):
        components.append({
            "type": "dataSource", "kind": "mcp-resource",
            "name": r.get("name") or r.get("uri", "?"),
            **({"uri": r["uri"]} if r.get("uri") else {}),
            "detected_from": src,
        })
    for pr in caps.get("prompts", []):
        components.append({
            "type": "prompt", "name": pr.get("name", "?"), "role": "mcp-prompt",
            "detected_from": src,
        })

    name = server.get("name") or url.rstrip("/").split("/")[-1] or "mcp-agent"
    return {
        "agent": {"name": name, "version": server.get("version", "0.0.0")},
        "components": components,
        "controls": {"scan": "mcp-endpoint", "endpoint": url,
                     "note": "declared capabilities over MCP (server attestation)"},
    }
