"""HTTP clients for dashboard aggregation and proxy calls."""
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import httpx

from dashboard.config import settings
from security.auth.verifier import build_internal_auth_headers


class DashboardClients:
    def __init__(self):
        self._headers = build_internal_auth_headers()

    async def _get(self, url: str, params: dict[str, Any] | None = None) -> Any:
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.get(url, params=params, headers=self._headers)
            resp.raise_for_status()
            return resp.json()

    async def _post(self, url: str, payload: dict[str, Any]) -> Any:
        async with httpx.AsyncClient(timeout=60.0) as client:
            resp = await client.post(url, json=payload, headers=self._headers)
            resp.raise_for_status()
            return resp.json()

    async def _put(self, url: str, payload: dict[str, Any]) -> Any:
        async with httpx.AsyncClient(timeout=60.0) as client:
            resp = await client.put(url, json=payload, headers=self._headers)
            resp.raise_for_status()
            return resp.json()

    async def _patch(self, url: str, payload: dict[str, Any]) -> Any:
        async with httpx.AsyncClient(timeout=60.0) as client:
            resp = await client.patch(url, json=payload, headers=self._headers)
            resp.raise_for_status()
            return resp.json()

    async def _delete(self, url: str) -> Any:
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.delete(url, headers=self._headers)
            if resp.status_code == 204:
                return {"ok": True}
            resp.raise_for_status()
            return resp.json() if resp.text else {"ok": True}

    async def normbase_health(self) -> Any:
        return await self._get(f"{settings.normbase_base_url}/health")

    async def eda_health(self) -> Any:
        return await self._get(f"{settings.eda_base_url}/health")

    async def mcp_health(self) -> dict[str, Any]:
        headers = {
            "Accept": "application/json, text/event-stream",
            "Content-Type": "application/json",
        }
        payload = {
            "jsonrpc": "2.0",
            "id": "dashboard-health-init",
            "method": "initialize",
            "params": {
                "protocolVersion": "2024-11-05",
                "capabilities": {},
                "clientInfo": {"name": "aif-dashboard-health", "version": "0.1.0"},
            },
        }
        async with httpx.AsyncClient(timeout=10.0) as client:
            # Probe MCP using a protocol-valid initialize request.
            resp = await client.post(
                f"{settings.mcp_base_url}/mcp",
                json=payload,
                headers=headers,
            )
            return {
                "status_code": resp.status_code,
                "reachable": resp.status_code in (200, 202),
            }

    async def mcp_rpc(self, payload: dict[str, Any]) -> Any:
        headers = {
            # Streamable HTTP MCP endpoints may require explicit content negotiation.
            "Accept": "application/json, text/event-stream",
            "Content-Type": "application/json",
        }
        async with httpx.AsyncClient(timeout=30.0) as client:
            resp = await client.post(
                f"{settings.mcp_base_url}/mcp",
                json=payload,
                headers=headers,
            )
            resp.raise_for_status()
            return self._decode_mcp_response(resp)

    async def list_mcp_tools(self) -> list[str]:
        """List MCP tools via streamable-http session handshake."""
        base_headers = {
            "Accept": "application/json, text/event-stream",
            "Content-Type": "application/json",
        }
        init_payload = {
            "jsonrpc": "2.0",
            "id": "dashboard-init",
            "method": "initialize",
            "params": {
                "protocolVersion": "2024-11-05",
                "capabilities": {},
                "clientInfo": {"name": "aif-dashboard", "version": "0.1.0"},
            },
        }
        async with httpx.AsyncClient(timeout=30.0) as client:
            init_resp = await client.post(
                f"{settings.mcp_base_url}/mcp",
                json=init_payload,
                headers=base_headers,
            )
            init_resp.raise_for_status()
            session_id = init_resp.headers.get("mcp-session-id", "").strip()
            if not session_id:
                raise RuntimeError("MCP initialize succeeded but missing mcp-session-id header.")

            list_payload = {
                "jsonrpc": "2.0",
                "id": "dashboard-tools-list",
                "method": "tools/list",
                "params": {},
            }
            list_headers = {**base_headers, "mcp-session-id": session_id}
            list_resp = await client.post(
                f"{settings.mcp_base_url}/mcp",
                json=list_payload,
                headers=list_headers,
            )
            list_resp.raise_for_status()
            data = self._decode_mcp_response(list_resp)

        result = data.get("result", data) if isinstance(data, dict) else {}
        raw_tools = result.get("tools", []) if isinstance(result, dict) else []
        names: list[str] = []
        for tool in raw_tools if isinstance(raw_tools, list) else []:
            if not isinstance(tool, dict):
                continue
            name = str(tool.get("name", "")).strip()
            if name and name not in names:
                names.append(name)
        return names

    def _decode_mcp_response(self, resp: httpx.Response) -> Any:
        content_type = (resp.headers.get("content-type") or "").lower()
        if content_type.startswith("text/event-stream"):
            # Streamable MCP returns SSE frames; extract JSON payload from data lines.
            for line in resp.text.splitlines():
                if not line.startswith("data:"):
                    continue
                payload = line[5:].strip()
                if not payload:
                    continue
                try:
                    return json.loads(payload)
                except Exception:
                    continue
            raise ValueError("No JSON payload found in MCP SSE response.")
        return resp.json()

    async def list_norms(
        self,
        page_size: int = 100,
        page: int = 1,
        status: str | None = None,
        field_id: str | None = None,
    ) -> Any:
        params: dict[str, Any] = {"page_size": page_size, "page": page}
        if status:
            params["status"] = status
        if field_id:
            params["field_id"] = field_id
        return await self._get(f"{settings.normbase_base_url}/api/v1/norms", params=params)

    async def get_norm(self, norm_id: str) -> Any:
        return await self._get(f"{settings.normbase_base_url}/api/v1/norms/{norm_id}")

    async def create_norm(self, payload: dict[str, Any]) -> Any:
        return await self._post(f"{settings.normbase_base_url}/api/v1/norms", payload)

    async def update_norm(self, norm_id: str, payload: dict[str, Any]) -> Any:
        return await self._put(f"{settings.normbase_base_url}/api/v1/norms/{norm_id}", payload)

    async def delete_norm(self, norm_id: str) -> Any:
        return await self._delete(f"{settings.normbase_base_url}/api/v1/norms/{norm_id}")

    async def list_fields(self, status: str | None = None) -> Any:
        params: dict[str, Any] = {}
        if status:
            params["status"] = status
        return await self._get(f"{settings.normbase_base_url}/api/v1/fields", params=params)

    async def create_field(self, payload: dict[str, Any]) -> Any:
        return await self._post(f"{settings.normbase_base_url}/api/v1/fields", payload)

    async def update_field(self, field_id: str, payload: dict[str, Any]) -> Any:
        return await self._patch(f"{settings.normbase_base_url}/api/v1/fields/{field_id}", payload)

    async def soft_delete_field(self, field_id: str) -> Any:
        return await self._delete(f"{settings.normbase_base_url}/api/v1/fields/{field_id}")

    async def retrieve_norms(self, payload: dict[str, Any]) -> Any:
        return await self._post(f"{settings.normbase_base_url}/api/v1/retrieve", payload)

    def _normbase_extraction_timeout(self) -> httpx.Timeout:
        r = float(settings.normbase_extraction_read_timeout_seconds)
        return httpx.Timeout(connect=30.0, read=r, write=120.0, pool=30.0)

    async def extraction_from_text(self, payload: dict[str, Any]) -> Any:
        async with httpx.AsyncClient(timeout=self._normbase_extraction_timeout()) as client:
            resp = await client.post(
                f"{settings.normbase_base_url}/api/v1/extraction/from-text",
                json=payload,
                headers={**self._headers, "Content-Type": "application/json"},
            )
            resp.raise_for_status()
            return resp.json()

    async def create_extraction_batch(self, payload: dict[str, Any]) -> Any:
        return await self._post(
            f"{settings.normbase_base_url}/api/v1/extraction/batches",
            payload,
        )

    async def get_extraction_batch(self, batch_id: str) -> Any:
        return await self._get(
            f"{settings.normbase_base_url}/api/v1/extraction/batches/{batch_id}"
        )

    async def upload_extraction_documents(
        self, batch_id: str, file_parts: list[tuple[str, bytes, str | None]]
    ) -> Any:
        """Upload files to a batch. Each tuple is (filename, content_bytes, content_type or None)."""
        multipart: list[tuple[str, tuple[str, bytes, str]]] = []
        for filename, content, ct in file_parts:
            ct_val = ct or "application/octet-stream"
            multipart.append(
                ("files", (filename, content, ct_val)),
            )
        async with httpx.AsyncClient(timeout=120.0) as client:
            resp = await client.post(
                f"{settings.normbase_base_url}/api/v1/extraction/batches/{batch_id}/documents",
                files=multipart,
                headers=self._headers,
            )
            resp.raise_for_status()
            return resp.json()

    async def extract_extraction_document(
        self,
        batch_id: str,
        document_id: str,
        *,
        default_lang: str = "zh-CN",
        max_norms: int = 30,
        submitter_user_id: str | None = None,
    ) -> Any:
        params: dict[str, Any] = {
            "default_lang": default_lang,
            "max_norms": max_norms,
        }
        if submitter_user_id:
            params["submitter_user_id"] = submitter_user_id
        async with httpx.AsyncClient(timeout=self._normbase_extraction_timeout()) as client:
            resp = await client.post(
                f"{settings.normbase_base_url}/api/v1/extraction/batches/{batch_id}/documents/{document_id}/extract",
                params=params,
                headers=self._headers,
            )
            resp.raise_for_status()
            return resp.json()

    async def check_action_compliance(self, payload: dict[str, Any]) -> Any:
        return await self._post(
            f"{settings.normbase_base_url}/api/v1/norms/check_action_compliance",
            payload,
        )

    async def report_norm_outcome(self, payload: dict[str, Any]) -> Any:
        return await self._post(f"{settings.normbase_base_url}/api/v1/norms/log-outcome", payload)

    async def list_agents(self, limit: int = 100) -> Any:
        return await self._get(f"{settings.eda_base_url}/api/v1/agents", params={"limit": limit})

    async def create_agent(self, payload: dict[str, Any]) -> Any:
        return await self._post(f"{settings.eda_base_url}/api/v1/agents", payload)

    async def get_agent(self, agent_id: str) -> Any:
        return await self._get(f"{settings.eda_base_url}/api/v1/agents/{agent_id}")

    async def update_agent(self, agent_id: str, payload: dict[str, Any]) -> Any:
        return await self._patch(f"{settings.eda_base_url}/api/v1/agents/{agent_id}", payload)

    async def delete_agent(self, agent_id: str) -> Any:
        return await self._delete(f"{settings.eda_base_url}/api/v1/agents/{agent_id}")

    async def agent_action(self, agent_id: str, action: str, payload: dict[str, Any]) -> Any:
        return await self._post(f"{settings.eda_base_url}/api/v1/agents/{agent_id}/{action}", payload)

    async def get_traces(self, agent_id: str, limit: int = 20) -> Any:
        return await self._get(
            f"{settings.eda_base_url}/api/v1/agents/{agent_id}/traces",
            params={"limit": limit},
        )

    async def issue_key(self, payload: dict[str, Any]) -> Any:
        return await self._post(f"{settings.normbase_base_url}/api/v1/keys/issue", payload)

    async def list_keys(self, agent_id: str | None = None, include_inactive: bool = False) -> Any:
        params: dict[str, Any] = {"include_inactive": include_inactive}
        if agent_id:
            params["agent_id"] = agent_id
        return await self._get(f"{settings.normbase_base_url}/api/v1/keys", params=params)

    async def revoke_key(self, key_id: str) -> Any:
        return await self._post(f"{settings.normbase_base_url}/api/v1/keys/{key_id}/revoke", {})

    async def rotate_key(self, key_id: str) -> Any:
        return await self._post(f"{settings.normbase_base_url}/api/v1/keys/{key_id}/rotate", {})

    def read_mcp_audit(self, limit: int = 100) -> list[dict[str, Any]]:
        if not settings.mcp_audit_log:
            return []
        path = Path(settings.mcp_audit_log)
        if not path.exists():
            return []
        lines = path.read_text(encoding="utf-8").splitlines()[-limit:]
        rows: list[dict[str, Any]] = []
        for line in lines:
            try:
                rows.append(json.loads(line))
            except Exception:
                continue
        return rows

