"""
SeevoMap SDK — HTTP client for the SeevoMap HuggingFace Space API.

Usage:
    from seevomap import SeevoMap
    svm = SeevoMap()
    results = svm.search("my task", top_k=5)
"""

import json
import time

import requests

DEFAULT_ENDPOINT = "https://akiwatanabe-seevomap.hf.space"
GRADIO_API_PREFIX = "/gradio_api/call"


class SeevoMapError(Exception):
    """SeevoMap API error."""


class SeevoMap:
    """Client for the SeevoMap knowledge graph."""

    def __init__(self, endpoint: str = None):
        self.endpoint = (endpoint or DEFAULT_ENDPOINT).rstrip("/")
        self._session = requests.Session()
        self._session.headers["Content-Type"] = "application/json"

    def _call_gradio(self, api_name: str, data: list, max_retries: int = 2):
        """Call a Gradio API endpoint using the /call/{api_name} protocol."""
        url = f"{self.endpoint}{GRADIO_API_PREFIX}/{api_name}"

        for attempt in range(max_retries + 1):
            try:
                # Step 1: Submit the call
                resp = self._session.post(url, json={"data": data}, timeout=30)
                resp.raise_for_status()
                event_id = resp.json().get("event_id")
                if not event_id:
                    raise SeevoMapError(f"No event_id returned from {api_name}")

                # Step 2: Poll for result
                result_url = f"{self.endpoint}{GRADIO_API_PREFIX}/{api_name}/{event_id}"
                for _ in range(60):  # max 60 seconds
                    r = self._session.get(result_url, timeout=10, stream=True)
                    for line in r.iter_lines(decode_unicode=True):
                        if not line:
                            continue
                        if line.startswith("data: "):
                            payload = json.loads(line[6:])
                            return payload
                        if line.startswith("event: error"):
                            raise SeevoMapError(f"API error from {api_name}")
                    time.sleep(0.5)

                raise SeevoMapError(f"Timeout waiting for {api_name} result")

            except requests.RequestException as e:
                if attempt < max_retries:
                    time.sleep(1)
                    continue
                raise SeevoMapError(f"Request failed: {e}") from e

    def search(self, query: str, top_k: int = 10) -> list[dict]:
        """Search for related research experiences.

        Returns list of dicts with keys: id, score, domain, idea, metric_name, metric_value, success, status
        """
        # ui_search returns [markdown, prompt_text, plotly_figure]
        # We call it and parse the prompt text, but it's better to have a direct API
        # For now, use the Gradio API
        result = self._call_gradio("ui_search", [query, top_k])
        # result is [markdown_str, prompt_str, plotly_json]
        # Parse the prompt text to extract structured results
        if isinstance(result, list) and len(result) >= 2:
            # Return the prompt text which has structured info
            return self._parse_search_results(result[0], result[1])
        return []

    def _parse_search_results(self, markdown: str, prompt: str) -> list[dict]:
        """Parse search results from the markdown output."""
        results = []
        if not markdown:
            return results

        for block in markdown.split("\n\n---\n\n"):
            block = block.strip()
            if not block:
                continue
            # Extract fields from markdown format
            entry = {"raw": block}
            results.append(entry)
        return results

    def inject(self, query: str, top_k: int = 10) -> str:
        """Get formatted prompt context for agent injection.

        Returns a string ready to paste into your agent's system prompt.
        """
        result = self._call_gradio("ui_search", [query, top_k])
        if isinstance(result, list) and len(result) >= 2:
            return result[1]  # The prompt text
        return ""

    def get_node(self, node_id: str) -> dict:
        """Get full details of a single node."""
        result = self._call_gradio("ui_node_detail", [node_id])
        if isinstance(result, list) and len(result) >= 1:
            try:
                return json.loads(result[0])
            except (json.JSONDecodeError, TypeError):
                return {"raw": result[0]}
        return {"error": "No result"}

    def stats(self) -> dict:
        """Get graph statistics. Returns from cached stats on server."""
        # Stats are rendered as static markdown, so we scrape the Space
        # Alternative: call a custom API if available
        try:
            resp = self._session.get(f"{self.endpoint}", timeout=15)
            # For now, return a basic ping
            return {"endpoint": self.endpoint, "reachable": resp.status_code == 200}
        except requests.RequestException:
            return {"endpoint": self.endpoint, "reachable": False}

    def submit(self, node: dict) -> dict:
        """Submit a new experiment node to the graph.

        Args:
            node: dict with at minimum:
                - task.domain (str)
                - idea.text (str)
                - result.metric_name (str)
                - result.metric_value (number)

        Returns dict with status and node_id on success.
        """
        node_json = json.dumps(node, ensure_ascii=False)
        result = self._call_gradio("submit_node", [node_json])
        if isinstance(result, list) and len(result) >= 1:
            try:
                return json.loads(result[0])
            except (json.JSONDecodeError, TypeError):
                return {"raw": result[0]}
        return {"error": "No result"}

    def review(self, node_id: str, action: str) -> dict:
        """Review a pending node (maintainer only).

        Args:
            node_id: ID of the node to review
            action: 'approve' or 'reject'
        """
        result = self._call_gradio("review_node", [node_id, action])
        if isinstance(result, list) and len(result) >= 1:
            try:
                return json.loads(result[0])
            except (json.JSONDecodeError, TypeError):
                return {"raw": result[0]}
        return {"error": "No result"}

    def list_pending(self) -> list[dict]:
        """List all pending review nodes."""
        result = self._call_gradio("list_pending", [])
        if isinstance(result, list) and len(result) >= 1:
            try:
                return json.loads(result[0])
            except (json.JSONDecodeError, TypeError):
                return []
        return []
