from __future__ import annotations

from dataclasses import dataclass
import json
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urljoin
from urllib.request import Request, urlopen


class BackendAuthRequiredError(Exception):
    """Returned when backend rejects unauthenticated bootstrap."""


@dataclass
class PassiveApiClient:
    """Passive-only API contract scaffold for IuVe Connect endpoints."""

    base_url: str

    def _post_json(
        self,
        path: str,
        body: dict[str, object],
        headers: dict[str, str] | None = None,
        *,
        timeout: int = 15,
    ) -> dict[str, object]:
        return self._request_json("POST", path, body, headers=headers, timeout=timeout)

    def _patch_json(
        self,
        path: str,
        body: dict[str, object],
        headers: dict[str, str] | None = None,
        *,
        timeout: int = 15,
    ) -> dict[str, object]:
        return self._request_json("PATCH", path, body, headers=headers, timeout=timeout)

    def _put_json(
        self,
        path: str,
        body: dict[str, object],
        headers: dict[str, str] | None = None,
        *,
        timeout: int = 15,
    ) -> dict[str, object]:
        return self._request_json("PUT", path, body, headers=headers, timeout=timeout)

    def _request_json(
        self,
        method: str,
        path: str,
        body: dict[str, object],
        headers: dict[str, str] | None = None,
        *,
        timeout: int = 15,
    ) -> dict[str, object]:
        url = urljoin(self.base_url.rstrip("/") + "/", path.lstrip("/"))
        payload = json.dumps(body, ensure_ascii=True, separators=(",", ":")).encode("utf-8")
        req_headers = {"Content-Type": "application/json"}
        if headers:
            req_headers.update(headers)
        request = Request(url=url, data=payload, headers=req_headers, method=method)
        try:
            with urlopen(request, timeout=timeout) as response:
                raw = response.read().decode("utf-8")
                return json.loads(raw) if raw else {}
        except HTTPError as exc:
            raw = exc.read().decode("utf-8", errors="replace")
            try:
                parsed = json.loads(raw)
                detail = parsed.get("detail", "backend_http_error")
            except Exception:
                detail = "backend_http_error"
            if exc.code in {401, 403}:
                raise BackendAuthRequiredError(str(detail)) from exc
            raise RuntimeError(f"http_{exc.code}:{detail}") from exc
        except URLError as exc:
            raise RuntimeError("backend_unreachable") from exc

    def _get_json(
        self,
        path: str,
        headers: dict[str, str] | None = None,
        *,
        timeout: int = 20,
    ) -> dict[str, object]:
        url = urljoin(self.base_url.rstrip("/") + "/", path.lstrip("/"))
        req_headers = {"Accept": "application/json"}
        if headers:
            req_headers.update(headers)
        request = Request(url=url, headers=req_headers, method="GET")
        try:
            with urlopen(request, timeout=timeout) as response:
                raw = response.read().decode("utf-8")
                return json.loads(raw) if raw else {}
        except HTTPError as exc:
            raw = exc.read().decode("utf-8", errors="replace")
            try:
                parsed = json.loads(raw)
                detail = parsed.get("detail") or parsed.get("error") or "backend_http_error"
            except Exception:
                detail = "backend_http_error"
            if exc.code in {401, 403}:
                raise BackendAuthRequiredError(str(detail)) from exc
            raise RuntimeError(f"http_{exc.code}:{detail}") from exc
        except URLError as exc:
            raise RuntimeError("backend_unreachable") from exc

    def get_desktop_release_latest(self, *, platform: str, auth_token: str) -> dict[str, object]:
        return self._get_json(
            f"/api/desktop-agent/releases/{platform}/latest",
            headers={"Authorization": f"Bearer {auth_token}"},
        )

    def download_desktop_release(
        self,
        *,
        platform: str,
        auth_token: str,
        destination_path: str | Path,
    ) -> dict[str, str]:
        url = urljoin(self.base_url.rstrip("/") + "/", f"api/desktop-agent/download/{platform}")
        request = Request(
            url=url,
            headers={"Authorization": f"Bearer {auth_token}"},
            method="GET",
        )
        target = Path(destination_path)
        target.parent.mkdir(parents=True, exist_ok=True)

        try:
            with urlopen(request, timeout=120) as response:
                target.write_bytes(response.read())
                return {
                    "checksum": str(response.headers.get("X-Checksum-SHA256") or "").strip().lower(),
                    "release_state": str(response.headers.get("X-Release-State") or "").strip(),
                    "content_type": str(response.headers.get("Content-Type") or "").strip(),
                }
        except HTTPError as exc:
            raw = exc.read().decode("utf-8", errors="replace")
            try:
                parsed = json.loads(raw)
                detail = parsed.get("detail") or parsed.get("error") or "backend_http_error"
            except Exception:
                detail = "backend_http_error"
            if exc.code in {401, 403}:
                raise BackendAuthRequiredError(str(detail)) from exc
            raise RuntimeError(f"http_{exc.code}:{detail}") from exc
        except URLError as exc:
            raise RuntimeError("backend_unreachable") from exc

    def authenticate_pair_token(
        self,
        pair_token: str,
        *,
        device_name: str = "IuVe Agent",
        device_type: str = "desktop",
        platform: str = "unknown",
        device_identifier: str = "",
    ) -> dict[str, str]:
        response = self._post_json(
            "/api/devices/pair-token/authenticate",
            {
                "token": pair_token,
            },
        )
        return {
            "pair_token_id": str(response.get("token_id", "")),
            "bootstrap_access_token": str(response.get("bootstrap_access_token", "")),
            "bootstrap_expires_in": str(response.get("expires_in", "")),
        }

    def register_device(self, *, auth_token: str, pair_token_id: str, metadata: dict[str, str]) -> dict[str, str]:
        device_name = str(metadata.get("device_name") or metadata.get("hostname") or "IuVe Agent")
        platform_value = str(metadata.get("platform") or metadata.get("os_name") or "unknown")
        device_identifier = str(metadata.get("device_identifier") or metadata.get("hostname") or "")
        device_type = str(metadata.get("device_type", "desktop"))
        body: dict[str, object] = {
            "token_id": pair_token_id,
            "device_name": device_name,
            "device_type": device_type,
            "platform": platform_value,
            "device_identifier": device_identifier,
        }
        avatar_base64 = str(metadata.get("avatar_base64") or "").strip()
        if avatar_base64:
            body["avatar_base64"] = avatar_base64
        response = self._post_json(
            "/api/devices/register",
            body,
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return {
            "device_id": str(response.get("id", response.get("device_id", ""))),
            "device_auth_token": str(response.get("device_auth_token", "")),
            "heartbeat_secret": str(response.get("heartbeat_secret", "")),
            "heartbeat_url": str(response.get("heartbeat_url", "")),
            "server_time": str(response.get("server_time", "")),
        }

    def upload_device_avatar(
        self,
        *,
        auth_token: str,
        avatar_base64: str,
        avatar_sha256: str | None = None,
    ) -> dict[str, object]:
        body: dict[str, object] = {"avatar_base64": avatar_base64}
        if avatar_sha256:
            body["avatar_sha256"] = avatar_sha256
        response = self._post_json(
            "/api/devices/agent/avatar",
            body,
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        if not isinstance(response, dict):
            return {}
        return response

    def submit_heartbeat(self, *, auth_token: str, signed_payload: dict[str, object]) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/heartbeat",
            signed_payload,
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        if not isinstance(response, dict):
            return {}
        return response

    def fetch_next_command(self, *, auth_token: str) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/commands/next",
            {},
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def get_agent_profile(self, *, auth_token: str) -> dict[str, object]:
        response = self._get_json(
            "/api/devices/agent/me",
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def probe_backend_health(self, *, timeout: int = 5) -> bool:
        """Fast liveness probe for iuve.eu (no auth)."""
        try:
            payload = self._get_json("/api/health", timeout=timeout)
        except Exception:
            return False
        return str((payload or {}).get("status") or "").strip().lower() == "ok"

    def probe_orchestra_endpoint(self, *, auth_token: str, timeout: int = 8) -> tuple[bool, str]:
        """
        Lightweight orchestra liveness check via the llm-intent route.

        Returns (available, reason). Uses an empty body so the server rejects
        before invoking LLM classification while still proving auth + routing.
        """
        try:
            self._post_json(
                "/api/devices/agent/actions/llm-intent",
                {"text": "", "trust_state": ""},
                headers={"Authorization": f"Bearer {auth_token}"},
                timeout=timeout,
            )
            return True, "ok"
        except BackendAuthRequiredError as exc:
            return False, str(exc or "auth_failed")
        except RuntimeError as exc:
            message = str(exc or "")
            lowered = message.lower()
            if "backend_unreachable" in lowered:
                return False, "backend_unreachable"
            if message.startswith("http_400") or message.startswith("http_422"):
                return True, "endpoint_reachable"
            if message.startswith("http_503"):
                if "orchestra" in lowered or "avatar_provider_orchestra_disabled" in lowered:
                    return False, "orchestra_disabled"
                return False, "orchestra_unavailable"
            if message.startswith("http_402"):
                return True, "endpoint_reachable_entitlement"
            if message.startswith("http_403"):
                if any(
                    token in lowered
                    for token in (
                        "capability_denied",
                        "cloud_providers",
                        "cloud_llm_denied",
                        "usage_limit",
                    )
                ):
                    return True, "endpoint_reachable_entitlement"
                return False, message or "auth_failed"
            return False, message or "probe_failed"

    def get_saas_context(self, *, auth_token: str) -> dict[str, object]:
        response = self._get_json(
            "/api/devices/agent/saas-context",
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def get_owner_ui_grants(self, *, auth_token: str) -> dict[str, object]:
        response = self._get_json(
            "/api/devices/agent/capabilities/owner-grants",
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def sync_owner_ui_grants(
        self,
        *,
        auth_token: str,
        grants: list[dict[str, object]],
    ) -> dict[str, object]:
        response = self._put_json(
            "/api/devices/agent/capabilities/owner-grants",
            {"grants": grants},
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def update_interface_language(self, *, auth_token: str, interface_language: str) -> dict[str, object]:
        response = self._patch_json(
            "/api/devices/agent/interface-language",
            {"interface_language": str(interface_language)},
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def submit_command_result(
        self,
        *,
        auth_token: str,
        command_id: str,
        success: bool,
        result: dict[str, object] | None = None,
        error_message: str | None = None,
    ) -> dict[str, object]:
        response = self._post_json(
            f"/api/devices/agent/commands/{command_id}/result",
            {
                "success": bool(success),
                "result": result if isinstance(result, dict) else {},
                "error_message": None if success else str(error_message or "command_failed"),
            },
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def post_orchestra_decision(
        self,
        *,
        auth_token: str,
        body: dict[str, object],
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/orchestra/decision",
            body,
            headers={"Authorization": f"Bearer {auth_token}"},
            timeout=30,
        )
        return response if isinstance(response, dict) else {}

    def request_execution_plan_v2(
        self,
        *,
        auth_token: str,
        request_text: str,
        is_compound: bool | None = None,
        context: dict[str, object] | None = None,
    ) -> dict[str, object]:
        body: dict[str, object] = {
            "request_text": request_text,
            "include_memory": True,
        }
        if is_compound is not None:
            body["is_compound"] = bool(is_compound)
        if context:
            body["context"] = dict(context)
        response = self._post_json(
            "/api/devices/agent/actions/plan-v2",
            body,
            headers={"Authorization": f"Bearer {auth_token}"},
            timeout=30,
        )
        return response if isinstance(response, dict) else {}

    def request_web_fetch_bounded(self, *, auth_token: str, url: str) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/actions/web-fetch",
            {"url": url},
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def request_summarize_content(
        self,
        *,
        auth_token: str,
        content: str,
        goal_summary: str = "",
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/actions/summarize-content",
            {
                "content": content,
                "goal_summary": goal_summary,
            },
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def request_generate_content(
        self,
        *,
        auth_token: str,
        content_type: str,
        topic: str = "",
        goal_summary: str = "",
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/actions/generate-content",
            {
                "content_type": content_type,
                "topic": topic,
                "goal_summary": goal_summary,
            },
            headers={"Authorization": f"Bearer {auth_token}"},
            timeout=30,
        )
        return response if isinstance(response, dict) else {}

    def list_avatar_providers(self, *, auth_token: str) -> dict[str, object]:
        return self._get_json(
            "/api/devices/agent/avatar/providers",
            headers={"Authorization": f"Bearer {auth_token}"},
        )

    def connect_avatar_provider(
        self,
        *,
        auth_token: str,
        provider_id: str,
        consent: bool = True,
        complete: bool = False,
    ) -> dict[str, object]:
        return self._post_json(
            f"/api/devices/agent/avatar/providers/{provider_id}/connect",
            {"consent": bool(consent), "complete": bool(complete)},
            headers={"Authorization": f"Bearer {auth_token}"},
        )

    def disconnect_avatar_provider(
        self,
        *,
        auth_token: str,
        provider_id: str,
        consent: bool = True,
    ) -> dict[str, object]:
        return self._post_json(
            f"/api/devices/agent/avatar/providers/{provider_id}/disconnect",
            {"consent": bool(consent)},
            headers={"Authorization": f"Bearer {auth_token}"},
        )

    def patch_avatar_provider(
        self,
        *,
        auth_token: str,
        provider_id: str,
        enabled: bool | None = None,
        priority: int | None = None,
    ) -> dict[str, object]:
        body: dict[str, object] = {}
        if enabled is not None:
            body["enabled"] = bool(enabled)
        if priority is not None:
            body["priority"] = int(priority)
        return self._patch_json(
            f"/api/devices/agent/avatar/providers/{provider_id}",
            body,
            headers={"Authorization": f"Bearer {auth_token}"},
        )

    def get_avatar_provider_health(self, *, auth_token: str, provider_id: str) -> dict[str, object]:
        return self._get_json(
            f"/api/devices/agent/avatar/providers/{provider_id}/health",
            headers={"Authorization": f"Bearer {auth_token}"},
        )

    def request_avatar_llm_intent(
        self,
        *,
        auth_token: str,
        text: str,
        trust_state: str = "",
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/actions/llm-intent",
            {
                "text": text,
                "trust_state": trust_state,
            },
            headers={"Authorization": f"Bearer {auth_token}"},
            timeout=45,
        )
        return response if isinstance(response, dict) else {}

    def request_avatar_llm_advisory(
        self,
        *,
        auth_token: str,
        text: str,
        trust_state: str = "",
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/actions/llm-advisory",
            {
                "text": text,
                "trust_state": trust_state,
            },
            headers={"Authorization": f"Bearer {auth_token}"},
            timeout=60,
        )
        return response if isinstance(response, dict) else {}

    def request_avatar_verify_vision(
        self,
        *,
        auth_token: str,
        verify_prompt: str,
        image_base64: str,
        expected_text: str = "",
        plan_id: str = "",
        step_id: str = "",
        evidence_sha256: str = "",
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/actions/verify-vision",
            {
                "verify_prompt": verify_prompt,
                "image_base64": image_base64,
                "expected_text": expected_text,
                "plan_id": plan_id,
                "step_id": step_id,
                "evidence_sha256": evidence_sha256,
            },
            headers={"Authorization": f"Bearer {auth_token}"},
            timeout=60,
        )
        return response if isinstance(response, dict) else {}

    def post_capability_snapshot(
        self,
        *,
        auth_token: str,
        snapshot: dict[str, object],
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/capability-snapshot",
            {"snapshot": snapshot},
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def get_capability_graph(self, *, auth_token: str) -> dict[str, object]:
        response = self._get_json(
            "/api/devices/agent/capability-graph",
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def post_runtime_context(
        self,
        *,
        auth_token: str,
        context: dict[str, object],
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/runtime-context",
            {"context": context},
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def post_action_journal(
        self,
        *,
        auth_token: str,
        event: dict[str, object],
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/action-journal",
            event,
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def query_goal_memory(
        self,
        *,
        auth_token: str,
        query: str,
        topic: str = "",
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/goal-memory/query",
            {"query": query, "topic": topic},
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def upsert_goal_memory(
        self,
        *,
        auth_token: str,
        entry: dict[str, object],
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/goal-memory",
            entry,
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}

    def post_outcome_memory(
        self,
        *,
        auth_token: str,
        outcome: dict[str, object],
    ) -> dict[str, object]:
        response = self._post_json(
            "/api/devices/agent/outcome-memory",
            outcome,
            headers={"Authorization": f"Bearer {auth_token}"},
        )
        return response if isinstance(response, dict) else {}
