"""Sitemule Blueprint MCP Server.

Provides IBM i program analysis tools via the Model Context Protocol (MCP).
Supports both STDIO and Streamable HTTP transports.
"""

import json
import logging
import os
import re
import time
from html.parser import HTMLParser
from urllib.parse import urlencode

import httpx
from fastmcp import FastMCP

from .config import settings

# Tracing hook — development.py is NOT committed to git and NOT included in
# release builds.  Activate by setting BLUEPRINT_TRACE=1 in mcp.json env.
try:
    if os.getenv("BLUEPRINT_TRACE") == "1":
        from .development import record_har as _record_har  # type: ignore[import]
    else:
        _record_har = None
except ImportError:
    _record_har = None

# --- Logging ---

# All log output goes to stderr so it never interferes with the MCP
# protocol on stdout.  VS Code surfaces stderr in the MCP output channel.
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(message)s",
    datefmt="%H:%M:%S",
)
logger = logging.getLogger("blueprint")

# --- Server Setup ---

mcp = FastMCP(
    name="blueprint",
    instructions=(
        "Sitemule Blueprint MCP Server for IBM i program analysis. "
        "Use the available tools to explore program call chains, "
        "view source code, and search the source repository."
    ),
)

# --- Compact response helpers ---

class _TextExtractor(HTMLParser):
    """Minimal HTML → plain-text converter using stdlib only."""

    _BLOCK = {"p", "div", "br", "li", "tr", "td", "th",
               "h1", "h2", "h3", "h4", "h5", "h6", "pre", "blockquote"}
    _SKIP  = {"script", "style", "head"}

    def __init__(self) -> None:
        super().__init__(convert_charrefs=True)
        self._parts: list[str] = []
        self._depth = 0

    def handle_starttag(self, tag: str, attrs: list) -> None:
        if tag in self._SKIP:
            self._depth += 1
        elif tag in self._BLOCK and self._parts and self._parts[-1] != "\n":
            self._parts.append("\n")

    def handle_endtag(self, tag: str) -> None:
        if tag in self._SKIP:
            self._depth = max(0, self._depth - 1)
        elif tag in self._BLOCK and self._parts and self._parts[-1] != "\n":
            self._parts.append("\n")

    def handle_data(self, data: str) -> None:
        if self._depth == 0:
            self._parts.append(data)

    def get_text(self) -> str:
        text = "".join(self._parts)
        lines = [ln.rstrip() for ln in text.splitlines()]
        compacted: list[str] = []
        blank_run = 0
        for ln in lines:
            if ln == "":
                blank_run += 1
                if blank_run <= 1:
                    compacted.append("")
            else:
                blank_run = 0
                compacted.append(ln)
        return "\n".join(compacted).strip()


def _html_to_text(html: str) -> str:
    ex = _TextExtractor()
    ex.feed(html)
    return ex.get_text()


def _strip_nulls(obj: object) -> object:
    """Recursively drop None and empty-string values from dicts/lists."""
    if isinstance(obj, dict):
        return {k: _strip_nulls(v) for k, v in obj.items()
                if v is not None and v != ""}
    if isinstance(obj, list):
        return [_strip_nulls(item) for item in obj]
    return obj


def _strip_envelope(data: object) -> object:
    """Unwrap Blueprint API envelope {success, root, metaData, rows[]} → row(s)."""
    if not isinstance(data, dict):
        return data
    rows = data.get("rows")
    if isinstance(rows, list):
        return rows[0] if len(rows) == 1 else rows
    return data


def _compact_graph(data: dict) -> str:
    """Render a nodes/edges graph response as a compact markdown table + edge list."""
    nodes: list[dict] = data.get("nodes") or []
    edges: list[dict] = data.get("edges") or []
    layout: str = data.get("layout") or ""

    node_by_id: dict[int, dict] = {n["id"]: n for n in nodes if isinstance(n, dict) and "id" in n}

    lines = [f"## Graph ({layout}) — {len(nodes)} nodes, {len(edges)} edges\n",
             "| depth | name | type | attr | description | domain |",
             "|-------|------|------|------|-------------|--------|"]

    for n in nodes:
        depth   = str(n.get("row", ""))
        name    = n.get("text", "")
        ntype   = n.get("type", "")
        attr    = n.get("attribute", "")
        desc    = (n.get("description") or "")[:60]
        domain  = n.get("domainName") or ""
        lines.append(f"| {depth} | {name} | {ntype} | {attr} | {desc} | {domain} |")

    if edges:
        lines.append("\n### Edges")
        for e in edges:
            frm   = node_by_id.get(e.get("from", 0), {}).get("text") or str(e.get("from", ""))
            to    = node_by_id.get(e.get("to", 0),   {}).get("text") or str(e.get("to", ""))
            etype = e.get("edgeType", "")
            io    = e.get("ioType")
            io_str = f" io={io}" if io is not None else ""
            lines.append(f"{frm} → {to} ({etype}{io_str})")

    return "\n".join(lines)


def _compact_sources(sources: list[dict]) -> dict:
    """Return the most-recent source member in full + a list of other library names."""
    if not sources:
        return {}
    ordered = sorted(sources,
                     key=lambda s: s.get("sourceLastUpdateTimestamp") or "",
                     reverse=True)
    primary = _strip_nulls(ordered[0])
    others  = [s["sourceLibraryName"] for s in ordered[1:] if s.get("sourceLibraryName")]
    if others:
        primary["alsoIn"] = others  # type: ignore[index]
    return primary  # type: ignore[return-value]


def _compact_value(v: object) -> object:
    """Recursively compact a single value for low-token mode."""
    if isinstance(v, str):
        return _html_to_text(v) if v.lstrip().startswith("<") else v
    if isinstance(v, dict):
        return {k: _compact_value(val) for k, val in v.items() if val is not None and val != ""}
    if isinstance(v, list):
        return [_compact_value(i) for i in v]
    return v


def _compact_serialize(data: object) -> str:
    """Serialize *data* in compact form for low-token-usage mode."""
    if isinstance(data, str):
        return _html_to_text(data) if data.lstrip().startswith("<") else data

    # Graph structure → markdown table (much more token-efficient than raw JSON)
    if (isinstance(data, dict)
            and isinstance(data.get("nodes"), list)
            and isinstance(data.get("edges"), list)):
        return _compact_graph(data)

    # Recursively strip nulls and convert inline HTML strings
    cleaned = _compact_value(data)
    return json.dumps(cleaned, separators=(",", ":"))


# --- Token tracking ---

class _TokenStats:
    """Session-level token-usage tracker (rough estimate: 4 chars ≈ 1 token)."""

    def __init__(self) -> None:
        self.session_raw:  int = 0
        self.session_sent: int = 0

    @staticmethod
    def _tok(n: int) -> int:
        return max(1, n // 4)

    def record(self, label: str, raw_len: int, sent_len: int) -> None:
        self.session_raw  += raw_len
        self.session_sent += sent_len
        saved     = raw_len - sent_len
        pct       = int(100 * saved / raw_len) if raw_len else 0
        sess_saved = self._tok(self.session_raw) - self._tok(self.session_sent)
        logger.info(
            "📊 %-42s raw=%d→%d chars (~%d→%d tok) saved %d%% | session saved ~%d tok",
            label + ":",
            raw_len, sent_len,
            self._tok(raw_len), self._tok(sent_len),
            pct,
            sess_saved,
        )


_stats = _TokenStats()


# --- API Client ---


async def fetch_data(func: str, _label: str = "", **kwargs: str | int | None) -> str:
    """Fetch data from the Blueprint API.

    Sends ``func`` plus any additional keyword arguments as query parameters.
    The API key is injected as a header if configured.
    This is internal plumbing; the API key is never exposed to the LLM.

    Returns the (optionally compacted, envelope-stripped) response as a string.
    Token usage is logged to stderr after every call.
    """
    params: dict[str, str | int] = {"func": func}
    for key, value in kwargs.items():
        if value is not None and value != "":
            params[key] = value

    headers: dict[str, str] = {}
    if settings.api_key:
        headers["Authorization"] = f"Bearer {settings.api_key}"

    label = _label or func
    logger.info("API %-12s %s", func, {k: v for k, v in kwargs.items() if v is not None})
    t0 = time.monotonic()

    try:
        async with httpx.AsyncClient(timeout=60.0) as client:
            response = await client.get(
                settings.api_base_url,
                params=params,
                headers=headers,
            )
            response.raise_for_status()
            elapsed = time.monotonic() - t0
            logger.info("API %-12s → %s (%.1fs)", func, response.status_code, elapsed)

            raw_json = response.json()
            raw_len = len(response.content)
            data = _strip_envelope(raw_json)

            if _record_har is not None:
                _record_har(func, params, settings.api_base_url, elapsed * 1000, response.content, raw_json)

            if settings.compact_responses:
                result = _compact_serialize(data)
            else:
                result = json.dumps(data, indent=2)

            _stats.record(label, raw_len, len(result))
            return result

    except httpx.HTTPStatusError as exc:
        elapsed = time.monotonic() - t0
        logger.error("API %-12s → HTTP %s (%.1fs)", func, exc.response.status_code, elapsed)
        return json.dumps(
            {
                "error": True,
                "message": f"HTTP {exc.response.status_code} from {settings.api_base_url}",
                "details": exc.response.text[:500],
            },
            indent=2,
        )
    except Exception as exc:
        elapsed = time.monotonic() - t0
        logger.error("API %-12s → %s (%.1fs)", func, exc, elapsed)
        return json.dumps(
            {
                "error": True,
                "message": f"Failed to fetch data from {settings.api_base_url}?func={func}",
                "details": str(exc),
            },
            indent=2,
        )


async def fetch_field(
    func: str,
    *,
    include: set[str] | None = None,
    exclude: set[str] | None = None,
    _label: str = "",
    **kwargs: str | int | None,
) -> str:
    """Like fetch_data but filters specific fields from the unwrapped row(s).

    Args:
        include: If set, only return these keys from the top-level row dict.
        exclude: If set, drop these keys (ignored when *include* is given).
    """
    params: dict[str, str | int] = {"func": func}
    for key, value in kwargs.items():
        if value is not None and value != "":
            params[key] = value

    headers: dict[str, str] = {}
    if settings.api_key:
        headers["Authorization"] = f"Bearer {settings.api_key}"

    label = _label or func
    logger.info("API %-12s %s", func, {k: v for k, v in kwargs.items() if v is not None})
    t0 = time.monotonic()

    try:
        async with httpx.AsyncClient(timeout=60.0) as client:
            response = await client.get(
                settings.api_base_url,
                params=params,
                headers=headers,
            )
            response.raise_for_status()
            elapsed = time.monotonic() - t0
            logger.info("API %-12s → %s (%.1fs)", func, response.status_code, elapsed)

            raw_json = response.json()
            raw_len = len(response.content)
            data = _strip_envelope(raw_json)

            # Field filtering (applies to single-row dict or each row in a list)
            def _filter(row: object) -> object:
                if not isinstance(row, dict):
                    return row
                if include:
                    return {k: row[k] for k in include if k in row}
                if exclude:
                    return {k: v for k, v in row.items() if k not in exclude}
                return row

            if isinstance(data, list):
                data = [_filter(r) for r in data]
            else:
                data = _filter(data)

            if _record_har is not None:
                _record_har(func, params, settings.api_base_url, elapsed * 1000, response.content, raw_json)

            if settings.compact_responses:
                result = json.dumps(_compact_value(data), separators=(",", ":"))
            else:
                result = json.dumps(data, indent=2)

            _stats.record(label, raw_len, len(result))
            return result

    except httpx.HTTPStatusError as exc:
        elapsed = time.monotonic() - t0
        logger.error("API %-12s → HTTP %s (%.1fs)", func, exc.response.status_code, elapsed)
        return json.dumps({"error": True, "message": f"HTTP {exc.response.status_code}"}, indent=2)
    except Exception as exc:
        elapsed = time.monotonic() - t0
        logger.error("API %-12s → %s (%.1fs)", func, exc, elapsed)
        return json.dumps({"error": True, "message": str(exc)}, indent=2)


def graph_view_uri(func: str, **params: str | int | None) -> str:
    """Build a vscode:// deep-link URI that opens the Blueprint graph view in VS Code.

    The VS Code extension's URI handler intercepts this URI and opens the
    backend's web UI (inspect.html) in VS Code's Simple Browser panel.
    """
    query: dict[str, str] = {"func": func}
    for k, v in params.items():
        if v is not None and v != "":
            query[k] = str(v)
    return f"vscode://sitemule.blueprint-mcp-settings/graph?{urlencode(query)}"
