"""``Pillar`` — the main SDK class.

Manages tool registration, inbound request handling, signature verification,
and framework adapter wiring.
"""
from __future__ import annotations

import asyncio
import inspect
import json
import logging
import traceback
from typing import Any, Callable

from pillar._context import ToolContext
from pillar._decorator import ToolRegistration, make_tool_decorator
from pillar._errors import SignatureVerificationError, ToolNotFoundError
from pillar._registration import register_tools
from pillar._signature import verify_webhook_signature

logger = logging.getLogger("pillar")


class Pillar:
    """Customer-facing entry point for the Pillar backend SDK.

    Usage::

        from pillar import Pillar, ToolContext

        pillar = Pillar(secret="plr_...")

        @pillar.tool(description="Look up a customer by email")
        async def lookup_customer(email: str, ctx: ToolContext) -> dict:
            ...
    """

    def __init__(
        self,
        secret: str | None = None,
        *,
        endpoint_url: str | None = None,
        auto_register: bool = True,
        base_url: str = "https://api.trypillar.com",
        _logger: logging.Logger | None = None,
        api_key: str | None = None,
        signing_secret: str | None = None,
    ) -> None:
        resolved = secret or api_key
        if not resolved:
            raise ValueError("Pillar requires a `secret` (or deprecated `api_key`).")
        self._secret = resolved
        self._endpoint_url = endpoint_url
        self._auto_register = auto_register
        self._base_url = base_url.rstrip("/")
        self._logger = _logger or logger
        self._tools: dict[str, ToolRegistration] = {}
        self._registered = False

    # -- decorator -----------------------------------------------------------

    def tool(
        self,
        description: str,
        *,
        name: str | None = None,
        guidance: str | None = None,
        input_schema: dict[str, Any] | None = None,
        output_schema: dict[str, Any] | None = None,
        timeout_ms: int = 30_000,
        channel_compatibility: list[str] | None = None,
        tool_type: str = "server_side",
    ) -> Callable[..., Any]:
        """Decorator to register a function as a Pillar tool."""
        return make_tool_decorator(
            self._tools,
            description,
            name=name,
            guidance=guidance,
            input_schema=input_schema,
            output_schema=output_schema,
            timeout_ms=timeout_ms,
            channel_compatibility=channel_compatibility,
            tool_type=tool_type,
        )

    # -- registration --------------------------------------------------------

    def register(self) -> dict[str, Any] | None:
        """Register all tools with Pillar Cloud.

        Called automatically on the first ``handle()`` call when
        ``auto_register=True``, or call manually for deferred registration.

        Returns ``None`` and logs a warning on failure instead of raising,
        so the server can continue to operate. Tools are loaded locally and
        registration will be retried on the next ``handle()`` call.
        """
        if not self._endpoint_url:
            self._logger.warning(
                "endpoint_url is required for registration — skipping. "
                "Pass it to Pillar(...) or set pillar._endpoint_url before calling register()."
            )
            return None
        try:
            result = register_tools(
                secret=self._secret,
                base_url=self._base_url,
                endpoint_url=self._endpoint_url,
                tools=self._tools,
            )
            self._registered = True
            return result
        except Exception as exc:
            self._logger.warning(
                "Cloud registration failed — tools are loaded locally and "
                "will retry on next request: %s",
                exc,
            )
            return None

    # -- core dispatch -------------------------------------------------------

    async def handle(
        self,
        body: bytes | str,
        headers: dict[str, str] | None = None,
    ) -> tuple[dict[str, Any], int]:
        """Core dispatch: verify signature, find tool, call handler, return result.

        Framework adapters extract *body* and *headers* from the framework's
        request object and call this method.

        Returns ``(response_dict, http_status_code)``.
        """
        if isinstance(body, str):
            body = body.encode()

        try:
            payload = json.loads(body)
        except (json.JSONDecodeError, ValueError):
            return {"success": False, "error": "Invalid JSON"}, 400

        action = payload.get("action")

        # Ping — no signature check required
        if action == "ping":
            return {"status": "ok"}, 200

        # Signature verification
        if headers:
            sig_header = headers.get("X-Pillar-Signature") or headers.get(
                "x-pillar-signature", ""
            )
            if not sig_header or not verify_webhook_signature(
                sig_header, body, self._secret
            ):
                self._logger.warning("Signature verification failed")
                return {"success": False, "error": "Invalid signature"}, 401

        if action not in ("tool_call", "tool_confirm"):
            return {"success": False, "error": f"Unknown action: {action}"}, 400

        is_confirm = action == "tool_confirm"

        # Auto-register on first real call
        if self._auto_register and not self._registered and self._endpoint_url:
            try:
                self.register()
            except Exception:
                self._logger.exception("Auto-registration failed")

        call_id = payload.get("call_id", "")
        tool_name = payload.get("tool_name", "")

        if is_confirm:
            arguments = payload.get("confirm_payload", {})
            payload["confirmed"] = True
        else:
            arguments = payload.get("arguments", {})

        reg = self._tools.get(tool_name)
        if reg is None:
            self._logger.warning("Tool not found: %s", tool_name)
            return {
                "call_id": call_id,
                "success": False,
                "error": f"Tool '{tool_name}' not found",
            }, 404

        ctx = ToolContext.from_payload(payload)

        try:
            result = await self._invoke(reg.handler, arguments, ctx)
        except (ValueError, KeyError, TypeError) as exc:
            self._logger.info("Tool %s raised %s: %s", tool_name, type(exc).__name__, exc)
            return {"call_id": call_id, "success": False, "error": str(exc)}, 200
        except PermissionError as exc:
            self._logger.warning("Permission denied in tool %s: %s", tool_name, exc)
            return {"call_id": call_id, "success": False, "error": str(exc)}, 200
        except Exception:
            self._logger.exception("Unhandled exception in tool %s", tool_name)
            return {"call_id": call_id, "success": False, "error": "Internal error"}, 500

        return {"call_id": call_id, "success": True, "result": result}, 200

    # -- framework adapters (convenience methods) ----------------------------

    def django_view(self) -> Any:
        """Return a Django view function for use in ``urlpatterns``."""
        from pillar.adapters._django import make_django_view

        return make_django_view(self)

    def flask_handler(self) -> Any:
        """Return a Flask handler for use with ``app.route()``."""
        from pillar.adapters._flask import make_flask_handler

        return make_flask_handler(self)

    def serve(self, port: int = 8787, host: str = "0.0.0.0") -> None:
        """Run a standalone HTTP server for customers without an existing web app."""
        from pillar.adapters._standalone import run_server

        run_server(self, host=host, port=port)

    # -- internals -----------------------------------------------------------

    async def _invoke(
        self,
        handler: Callable[..., Any],
        arguments: dict[str, Any],
        ctx: ToolContext,
    ) -> Any:
        """Call *handler* with the right arguments, handling sync/async."""
        sig = inspect.signature(handler)
        kwargs: dict[str, Any] = {}

        for name, param in sig.parameters.items():
            if name in ("ctx", "context") or (
                param.annotation is not inspect.Parameter.empty
                and (
                    param.annotation is ToolContext
                    or (isinstance(param.annotation, type) and issubclass(param.annotation, ToolContext))
                )
            ):
                kwargs[name] = ctx
            elif name in arguments:
                kwargs[name] = arguments[name]
            elif param.default is not inspect.Parameter.empty:
                pass  # let the function use its default
            # else: missing required arg — Python will raise TypeError

        if inspect.iscoroutinefunction(handler):
            return await handler(**kwargs)
        else:
            return await asyncio.to_thread(handler, **kwargs)
