import logging
from functools import wraps
from typing import Any, Dict, List, Optional

from .client import MCPClient

logger = logging.getLogger(__name__)


def _ensure_client(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        if self.client is None:
            logger.error("MCP client not connected")
            return {
                "ok": False,
                "error": "MCP client not connected. Call connect_mcp first.",
            }
        return func(self, *args, **kwargs)

    return wrapper


class MCPTools:
    """Synchronous tools wrapper for MCP HTTP server."""

    def __init__(self):
        self.client: Optional[MCPClient] = None
        logger.info("MCPTools initialized")

    def connect_mcp(
        self,
        mcp_url: Optional[str] = None,
        mcpUrl: Optional[str] = None,
        headers: Optional[Dict[str, str]] = None,
        timeout: int = 30,
        retries: int = 2,
        proxies: Optional[Dict[str, str]] = None,
    ) -> Dict[str, Any]:
        url = (mcp_url or mcpUrl or "").strip()
        if not url:
            return {"ok": False, "error": "mcp_url (or mcpUrl) is required"}

        self.client = MCPClient(
            base_url=url,
            headers=headers,
            timeout=timeout,
            retries=retries,
            proxies=proxies,
        )
        return {"ok": True, "base_url": url}

    @_ensure_client
    def close(self) -> Dict[str, Any]:
        self.client.close()
        return {"ok": True}

    @_ensure_client
    def _call_tool(self, tool: str, arguments: Optional[Dict[str, Any]] = None):
        return self.client.call_tool(tool, arguments or {})

    @_ensure_client
    def _structured(self, tool: str, arguments: Optional[Dict[str, Any]] = None):
        return self._call_tool(tool, arguments).get("structuredContent", {})

    # Session tools
    @_ensure_client
    def create_session(self, cdpUrl: str) -> Dict[str, Any]:
        sid = self.client.create_session(cdpUrl)
        return {"sessionId": sid}

    @_ensure_client
    def close_session(self, sessionId: str) -> Dict[str, Any]:
        ok = self.client.close_session(sessionId)
        return {"ok": bool(ok)}

    @_ensure_client
    def list_sessions(self) -> Dict[str, Any]:
        return {"sessions": self.client.list_local_sessions()}

    @_ensure_client
    def open_page(self, sessionId: str, url: str):
        return self._structured("openPage", {"sessionId": sessionId, "url": url})

    @_ensure_client
    def get_html(self, sessionId: str):
        return self._structured("getHTML", {"sessionId": sessionId})

    @_ensure_client
    def click(self, sessionId: str, selector: str):
        return self._structured("click", {"sessionId": sessionId, "selector": selector})

    @_ensure_client
    def get_bounding_box(self, sessionId: str, selector: str):
        return self._structured("getBoundingBox", {"sessionId": sessionId, "selector": selector})

    @_ensure_client
    def click_to_text(self, sessionId: str, text: str):
        return self._structured("clickToText", {"sessionId": sessionId, "text": text})

    @_ensure_client
    def click_by_node_id(self, sessionId: str, nodeId: int):
        return self._structured("clickByNodeId", {"sessionId": sessionId, "nodeId": nodeId})

    @_ensure_client
    def screenshot(self, sessionId: str):
        return self._call_tool("screenshot", {"sessionId": sessionId})

    @_ensure_client
    def import_cookies(self, sessionId: str, cookies: List[Dict[str, Any]]):
        return self._structured("importCookies", {"sessionId": sessionId, "cookies": cookies})

    @_ensure_client
    def click_bounding_box(self, sessionId: str, selector: str):
        return self._structured("clickBoundingBox", {"sessionId": sessionId, "selector": selector})

    @_ensure_client
    def upload_file(self, sessionId: str, selector: str, file_path: str):
        if not file_path:
            return {"ok": False, "error": "file_path is required"}

        try:
            with open(file_path, "rb") as f:
                resp = self.client._client.post("/upload", files={"file": f}, timeout=300)
            resp.raise_for_status()
            upload_id = resp.json().get("uploadId")
        except Exception as exc:
            logger.exception("upload_file failed")
            return {"ok": False, "error": str(exc)}

        if not upload_id:
            return {"ok": False, "error": "uploadId not returned"}

        return self._structured(
            "uploadFile",
            {"sessionId": sessionId, "selector": selector, "uploadId": upload_id},
        )

    @_ensure_client
    def perform(
        self,
        sessionId: str,
        action: str,
        target: Optional[str] = None,
        value: Optional[str] = None,
        x: Optional[float] = None,
        y: Optional[float] = None,
        from_point: Optional[Dict[str, float]] = None,
        to_point: Optional[Dict[str, float]] = None,
    ):
        args: Dict[str, Any] = {"sessionId": sessionId, "action": action}
        if target is not None:
            args["target"] = target
        if value is not None:
            args["value"] = value
        if x is not None:
            args["x"] = float(x)
        if y is not None:
            args["y"] = float(y)
        if from_point is not None:
            args["from"] = from_point
        if to_point is not None:
            args["to"] = to_point
        return self._structured("perform", args)

    @_ensure_client
    def wait_for_selector(
        self,
        sessionId: str,
        selector: str,
        timeout: Optional[int] = None,
        timeoutMs: Optional[int] = None,
    ):
        args: Dict[str, Any] = {"sessionId": sessionId, "selector": selector}
        resolved_timeout = timeout if timeout is not None else timeoutMs
        if resolved_timeout is not None:
            args["timeout"] = int(resolved_timeout)
        return self._structured("waitForSelector", args)

    @_ensure_client
    def scroll(
        self,
        sessionId: str,
        x: Optional[float] = None,
        y: Optional[float] = None,
        selector: Optional[str] = None,
        position: Optional[str] = None,
    ):
        args: Dict[str, Any] = {"sessionId": sessionId}
        if x is not None:
            args["x"] = float(x)
        if y is not None:
            args["y"] = float(y)
        if selector is not None:
            args["selector"] = selector
        if position is not None:
            args["position"] = position
        return self._structured("scroll", args)

    @_ensure_client
    def evaluate(self, sessionId: str, expression: str):
        return self._structured("evaluate", {"sessionId": sessionId, "expression": expression})

    @_ensure_client
    def evaluate_stream(self, sessionId: str, expression: str, chunkSize: int = 100):
        return self._structured(
            "evaluate.stream",
            {"sessionId": sessionId, "expression": expression, "chunkSize": chunkSize},
        )

    @_ensure_client
    def stream_pull(self, stream_id: str, offset: int = 0, limit: int = 100):
        return self._structured(
            "stream.pull", {"stream_id": stream_id, "offset": offset, "limit": limit}
        )

    @_ensure_client
    def get_cookies(self, sessionId: str):
        return self._structured("getCookies", {"sessionId": sessionId})

    @_ensure_client
    def parse_html_by_prompt(self, html: str, prompt: str):
        return self._structured("parseHTMLByPrompt", {"html": html, "prompt": prompt})

    @_ensure_client
    def viewport(self, sessionId: str, viewport: Optional[Dict[str, Any]] = None):
        args: Dict[str, Any] = {"sessionId": sessionId}
        if viewport is not None:
            args["viewport"] = viewport
        return self._structured("viewport", args)

    # Input tools
    @_ensure_client
    def send_keys(self, sessionId: str, text: str, interval: Optional[int] = None):
        args: Dict[str, Any] = {"sessionId": sessionId, "text": text}
        if interval is not None:
            args["interval"] = int(interval)
        return self._structured("sendKeys", args)

    @_ensure_client
    def type(self, sessionId: str, selector: str, text: str):
        return self._structured("type", {"sessionId": sessionId, "selector": selector, "text": text})

    @_ensure_client
    def send_text(self, sessionId: str, text: str, interval: Optional[int] = None):
        args: Dict[str, Any] = {"sessionId": sessionId, "text": text}
        if interval is not None:
            args["interval"] = int(interval)
        return self._structured("sendText", args)

    @_ensure_client
    def clear_all(self, sessionId: str):
        return self._structured("clearAll", {"sessionId": sessionId})

    @_ensure_client
    def send_combo(self, sessionId: str, text: str):
        return self._structured("sendCombo", {"sessionId": sessionId, "text": text})

    # Tab tools
    @_ensure_client
    def get_current_url(self, sessionId: str):
        return self._structured("getCurrentUrl", {"sessionId": sessionId})

    @_ensure_client
    def list_tabs(self, sessionId: str):
        return self._structured("listTabs", {"sessionId": sessionId})

    @_ensure_client
    def switch_tab(self, sessionId: str, tabId: Optional[str] = None, targetId: Optional[str] = None):
        resolved_tab_id = tabId or targetId
        if not resolved_tab_id:
            return {"ok": False, "error": "tabId (or targetId) is required"}
        return self._structured("switchTab", {"sessionId": sessionId, "tabId": resolved_tab_id})

    @_ensure_client
    def close_tab(self, sessionId: str, tabId: str):
        return self._structured("closeTab", {"sessionId": sessionId, "tabId": tabId})

    @_ensure_client
    def new_tab(self, sessionId: str, url: Optional[str] = None):
        args: Dict[str, Any] = {"sessionId": sessionId}
        if url:
            args["url"] = url
        return self._structured("newTab", args)

    @_ensure_client
    def switch_tab_by_title(self, sessionId: str, title: str):
        return self._structured("switchTabByTitle", {"sessionId": sessionId, "title": title})

    @_ensure_client
    def switch_tab_by_url(self, sessionId: str, url: str):
        return self._structured("switchTabByUrl", {"sessionId": sessionId, "url": url})

    @_ensure_client
    def current_tab(self, sessionId: str):
        return self._structured("currentTab", {"sessionId": sessionId})

    # DOM tools
    @_ensure_client
    def get_dom_tree(self, sessionId: str):
        return self._structured("getDomTree", {"sessionId": sessionId})

    @_ensure_client
    def get_clickable(self, sessionId: str):
        return self._structured("getClickable", {"sessionId": sessionId})

    @_ensure_client
    def selector_map(self, sessionId: str, selector: str):
        return self._structured("selectorMap", {"sessionId": sessionId, "selector": selector})

    @_ensure_client
    def get_clean_text(self, sessionId: str):
        return self._structured("getCleanText", {"sessionId": sessionId})

    # Element tools
    @_ensure_client
    def find_all(self, sessionId: str, selector: str):
        return self._structured("findAll", {"sessionId": sessionId, "selector": selector})

    @_ensure_client
    def find_element(self, sessionId: str, selector: str):
        return self._structured("findElement", {"sessionId": sessionId, "selector": selector})

    @_ensure_client
    def find_element_by_xpath(self, sessionId: str, xpath: str):
        return self._structured("findElementByXPath", {"sessionId": sessionId, "xpath": xpath})

    @_ensure_client
    def find_element_xpath(self, sessionId: str, xpath: str):
        return self.find_element_by_xpath(sessionId, xpath)

    @_ensure_client
    def find_element_by_text(self, sessionId: str, text: str):
        return self._structured("findElementByText", {"sessionId": sessionId, "text": text})

    @_ensure_client
    def find_element_by_prompt(self, sessionId: str, prompt: str):
        return self._structured("findElementByPrompt", {"sessionId": sessionId, "prompt": prompt})

    # Browser tools
    @_ensure_client
    def create_browser(self, payload: Optional[Dict[str, Any]] = None):
        return self._structured("createBrowser", payload or {})

    @_ensure_client
    def release_browser(self, pod_name: str):
        return self._structured("releaseBrowser", {"pod_name": pod_name})

    # Compatibility aliases
    @_ensure_client
    def cookies(self, sessionId: str):
        return self.get_cookies(sessionId)

    @_ensure_client
    def current_url(self, sessionId: str):
        return self.get_current_url(sessionId)

    @_ensure_client
    def set_viewport(
        self,
        sessionId: str,
        *,
        width: int,
        height: int,
        deviceScaleFactor: float = 1.0,
        mobile: bool = False,
    ):
        return self.viewport(
            sessionId,
            {
                "width": int(width),
                "height": int(height),
                "deviceScaleFactor": float(deviceScaleFactor),
                "mobile": bool(mobile),
            },
        )

    @_ensure_client
    def get_viewport(self, sessionId: str):
        return self.viewport(sessionId)

    @_ensure_client
    def drag_and_drop(self, sessionId: str, from_x: float, from_y: float, to_x: float, to_y: float):
        return self.perform(
            sessionId=sessionId,
            action="drag",
            from_point={"x": from_x, "y": from_y},
            to_point={"x": to_x, "y": to_y},
        )

    @_ensure_client
    def hover(self, sessionId: str, x: float, y: float):
        return self.perform(sessionId=sessionId, action="hover", x=x, y=y)

    @_ensure_client
    def evaluate_stream_all(
        self,
        sessionId: str,
        expression: str,
        chunkSize: int = 100,
        max_items: Optional[int] = None,
    ):
        init = self.evaluate_stream(sessionId, expression, chunkSize)
        stream_id = init.get("stream_id")
        if not stream_id:
            return []

        items = []
        offset = 0
        while True:
            chunk = self.stream_pull(stream_id, offset, chunkSize)
            items.extend(chunk.get("items", []))
            if max_items is not None and len(items) >= max_items:
                return items[:max_items]
            if not chunk.get("has_more"):
                break
            offset += chunkSize

        return items
