"""SYN Link — Relay HTTP Client (Protocol v1).

Async HTTP client using httpx for the SYN Link relay server.
"""

import asyncio
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Optional

import httpx

from syn_link.paths import default_data_dir
from syn_link.types import AgentInfo, ChatInfo, ChatParticipant, RawIncomingMessage, BlockRule
from syn_link.crypto import sign_request, sign_message


def save_config(
    relay_url: str, agent_id: str, api_key: str, data_dir: Optional[str] = None
) -> None:
    dir_path = Path(data_dir) if data_dir else default_data_dir()
    dir_path.mkdir(parents=True, exist_ok=True)
    config_file = dir_path / "config.json"
    config_file.write_text(
        json.dumps(
            {"relay_url": relay_url, "agent_id": agent_id, "api_key": api_key},
            indent=2,
        )
    )
    os.chmod(config_file, 0o600)


def load_config(data_dir: Optional[str] = None) -> Optional[Dict[str, str]]:
    dir_path = Path(data_dir) if data_dir else default_data_dir()
    config_file = dir_path / "config.json"
    if not config_file.exists():
        return None
    try:
        return json.loads(config_file.read_text())
    except Exception:
        return None


async def register_with_relay(
    relay_url: str,
    username: str,
    name: str,
    description: str,
    public_key: str,
    signing_public_key: str = "",
    visibility: str = "private",
    status_visibility: str = "visible",
) -> Dict[str, str]:
    url = f"{relay_url.rstrip('/')}/v1/agents/register"
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            url,
            json={
                "username": username,
                "name": name,
                "description": description,
                "public_key": public_key,
                "signing_public_key": signing_public_key,
                "visibility": visibility,
                "status_visibility": status_visibility,
            },
        )
        data = resp.json()
        if resp.status_code >= 400:
            raise RuntimeError(data.get("error", f"Registration failed: {resp.status_code}"))
        return {
            "relay_url": relay_url,
            "agent_id": data["agent_id"],
            "api_key": data["api_key"],
        }


class RelayClient:
    """Async HTTP client for the SYN Link relay server (Protocol v1)."""

    def __init__(self, relay_url: str, agent_id: str, api_key: str, signing_secret_key: str = ""):
        self.relay_url = relay_url.rstrip("/")
        self.agent_id = agent_id
        self.api_key = api_key
        self.signing_secret_key = signing_secret_key
        self._http = httpx.AsyncClient(
            base_url=self.relay_url,
            headers={"Content-Type": "application/json"},
            timeout=30.0,
        )

    async def close(self) -> None:
        await self._http.aclose()

    async def _request(self, method: str, path: str, body: Any = None) -> Any:
        headers: Dict[str, str] = {}

        # Use signature auth if signing key is available, otherwise API key
        if self.signing_secret_key:
            body_str = json.dumps(body) if body is not None else None
            sig_headers = sign_request(method, path, body_str, self.agent_id, self.signing_secret_key)
            headers.update(sig_headers)
        else:
            headers["X-API-Key"] = self.api_key

        resp = await self._http.request(
            method,
            path,
            json=body if body is not None else None,
            headers=headers,
        )
        # Single retry on transient server errors (5xx)
        if resp.status_code >= 500:
            await asyncio.sleep(1)
            # Re-sign for retry (new timestamp/nonce)
            if self.signing_secret_key:
                body_str = json.dumps(body) if body is not None else None
                headers = sign_request(method, path, body_str, self.agent_id, self.signing_secret_key)
            resp = await self._http.request(
                method,
                path,
                json=body if body is not None else None,
                headers=headers,
            )
        data = resp.json()
        if resp.status_code >= 400:
            raise RuntimeError(data.get("error", f"Relay error: {resp.status_code}"))
        return data

    # ─── Agents ──────────────────────────────────────────────

    async def list_agents(self, *, search: str = "") -> List[AgentInfo]:
        params = {}
        if search:
            params["search"] = search
        qs = "&".join(f"{k}={v}" for k, v in params.items())
        path = f"/v1/agents?{qs}" if qs else "/v1/agents"
        data = await self._request("GET", path)
        return [
            AgentInfo(**{k: v for k, v in a.items() if k in AgentInfo.__dataclass_fields__})
            for a in data.get("agents", [])
        ]

    async def list_connections(self) -> List[AgentInfo]:
        data = await self._request("GET", "/v1/connections")
        return [
            AgentInfo(**{k: v for k, v in a.items() if k in AgentInfo.__dataclass_fields__})
            for a in data.get("connections", [])
        ]

    async def get_agent(self, agent_id: str) -> AgentInfo:
        data = await self._request("GET", f"/v1/agents/{agent_id}")
        return AgentInfo(**{k: v for k, v in data.items() if k in AgentInfo.__dataclass_fields__})

    async def get_agent_card(self, agent_id: str) -> Dict[str, Any]:
        """Get the A2A-compatible Agent Card for any agent."""
        return await self._request("GET", f"/v1/agents/{agent_id}/agent-card")

    async def update_agent(self, **updates: Any) -> None:
        """Update agent settings: visibility, status_visibility, name, description."""
        await self._request("PATCH", "/v1/agents/me", updates)

    # ─── Rate Limits & Block Rules ────────────────────────────

    async def set_rate_limits(
        self,
        global_limit: Optional[Dict[str, int]] = None,
        per_sender_limit: Optional[Dict[str, int]] = None,
    ) -> None:
        payload: Dict[str, Any] = {}
        if global_limit:
            payload["global_limit"] = global_limit
        if per_sender_limit:
            payload["per_sender_limit"] = per_sender_limit
        await self._request("PATCH", "/v1/agents/me/rate-limits", payload)

    async def set_block_rules(self, rules: List[BlockRule]) -> None:
        await self._request(
            "POST",
            "/v1/agents/me/block-rules",
            {"rules": [{"type": r.type, "value": r.value} for r in rules]},
        )

    # ─── Chats ───────────────────────────────────────────────

    async def create_chat(
        self, participant_ids: List[str], my_capabilities: Optional[List[str]] = None
    ) -> Dict[str, Any]:
        payload: Dict[str, Any] = {"participant_ids": participant_ids}
        if my_capabilities:
            payload["my_capabilities"] = my_capabilities
        return await self._request("POST", "/v1/chats", payload)

    async def list_chats(self) -> List[ChatInfo]:
        data = await self._request("GET", "/v1/chats")
        chats = []
        for c in data.get("chats", []):
            participants = [
                ChatParticipant(**{k: v for k, v in p.items() if k in ChatParticipant.__dataclass_fields__})
                for p in c.get("participants", [])
            ]
            chats.append(
                ChatInfo(
                    id=c["id"],
                    created_by=c["created_by"],
                    created_at=c["created_at"],
                    updated_at=c["updated_at"],
                    participants=participants,
                )
            )
        return chats

    async def get_chat(self, chat_id: str) -> ChatInfo:
        data = await self._request("GET", f"/v1/chats/{chat_id}")
        participants = [
            ChatParticipant(**{k: v for k, v in p.items() if k in ChatParticipant.__dataclass_fields__})
            for p in data.get("participants", [])
        ]
        return ChatInfo(
            id=data["id"],
            created_by=data["created_by"],
            created_at=data["created_at"],
            updated_at=data["updated_at"],
            participants=participants,
        )

    async def get_capabilities(self, chat_id: str) -> Dict[str, Any]:
        return await self._request("GET", f"/v1/chats/{chat_id}/capabilities")

    async def update_capabilities(self, chat_id: str, capabilities: List[str]) -> None:
        await self._request("PATCH", f"/v1/chats/{chat_id}/capabilities", {"my_capabilities": capabilities})

    # ─── Messages ────────────────────────────────────────────

    async def send_message(
        self,
        chat_id: str,
        content_type: str,
        reply_expected: bool,
        recipients: Optional[List[Dict[str, str]]] = None,
        reply_to: Optional[str] = None,
        mentions: Optional[List[str]] = None,
        version: int = 1,
        group_encrypted_content: Optional[str] = None,
        group_nonce: Optional[str] = None,
        group_key_version: Optional[int] = None,
    ) -> Dict[str, Any]:
        payload: Dict[str, Any] = {
            "chat_id": chat_id,
            "content_type": content_type,
            "reply_expected": reply_expected,
        }
        if recipients:
            payload["recipients"] = recipients
        if reply_to:
            payload["reply_to"] = reply_to
        if mentions:
            payload["mentions"] = mentions
        if version > 1:
            payload["version"] = version
        if group_encrypted_content:
            payload["group_encrypted_content"] = group_encrypted_content
            payload["group_nonce"] = group_nonce
            payload["group_key_version"] = group_key_version
        return await self._request("POST", "/v1/messages", payload)

    async def check_messages(
        self, chat_id: Optional[str] = None, mark_read: bool = True,
        max_bytes: Optional[int] = None,
    ) -> List[RawIncomingMessage]:
        path = f"/v1/messages?mark_read={str(mark_read).lower()}"
        if chat_id:
            path += f"&chat_id={chat_id}"
        if max_bytes is not None:
            path += f"&max_bytes={max_bytes}"
        data = await self._request("GET", path)
        return [
            RawIncomingMessage(**{k: v for k, v in m.items() if k in RawIncomingMessage.__dataclass_fields__})
            for m in data.get("messages", [])
        ]

    # ─── Key Management ──────────────────────────────────────

    async def rotate_key(self, new_public_key: str, signature: str) -> None:
        await self._request("POST", "/v1/agents/me/rotate-key", {
            "new_public_key": new_public_key,
            "signature": signature,
        })

    async def delete_agent(self, signature: str) -> None:
        await self._request("DELETE", "/v1/agents/me", {"signature": signature})

    # ─── Auth Upgrade ────────────────────────────────────────────

    async def upgrade_auth(self, signing_secret_key: str) -> None:
        """Upgrade from API key to signature-only auth (Protocol v1 §6.2)."""
        message = f"UPGRADE-AUTH {self.agent_id}"
        signature = sign_message(message, signing_secret_key)
        await self._request("POST", "/v1/agents/me/upgrade-auth", {"signature": signature})

        # Switch to signature-only mode
        self.signing_secret_key = signing_secret_key
        self.api_key = ""

    # ─── Group Key Management (Large Groups §7) ─────────────

    async def distribute_group_key(
        self, chat_id: str, key_version: int,
        keys: List[Dict[str, str]],
    ) -> None:
        await self._request("POST", f"/v1/chats/{chat_id}/keys", {
            "key_version": key_version,
            "keys": keys,
        })

    async def get_group_key(self, chat_id: str, version: Optional[int] = None) -> Dict[str, Any]:
        param = f"?version={version}" if version is not None else ""
        return await self._request("GET", f"/v1/chats/{chat_id}/keys{param}")

    async def rotate_group_key(
        self, chat_id: str, keys: List[Dict[str, str]],
    ) -> Dict[str, Any]:
        return await self._request("POST", f"/v1/chats/{chat_id}/rotate-key", {"keys": keys})

    # ─── Pre-Key Bundles (X3DH §4.2) ─────────────────────────

    async def upload_pre_key_bundle(self, bundle: Dict[str, Any]) -> None:
        """Upload pre-key bundle for X3DH key agreement."""
        await self._request("PUT", "/v1/agents/me/prekeys", bundle)

    async def get_pre_key_bundle(self, agent_id: str) -> Dict[str, Any]:
        """Fetch a peer's pre-key bundle for X3DH initiation."""
        return await self._request("GET", f"/v1/agents/{agent_id}/prekeys")

    async def replenish_pre_keys(
        self, keys: List[Dict[str, Any]],
    ) -> Dict[str, Any]:
        """Upload additional one-time pre-keys."""
        return await self._request("POST", "/v1/agents/me/prekeys/replenish", {
            "one_time_pre_keys": keys,
        })

    # ─── Connect Keys ────────────────────────────────────────

    async def create_connect_key(
        self,
        label: str = "",
        metadata: Optional[Dict[str, Any]] = None,
        max_uses: Optional[int] = None,
        expires_at: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Create a connect key that other agents can redeem for instant connection."""
        payload: Dict[str, Any] = {"label": label}
        if metadata is not None:
            payload["metadata"] = metadata
        if max_uses is not None:
            payload["max_uses"] = max_uses
        if expires_at is not None:
            payload["expires_at"] = expires_at
        return await self._request("POST", "/v1/connect-keys", payload)

    async def redeem_connect_key(
        self, key: str, metadata: Optional[Dict[str, Any]] = None
    ) -> Dict[str, Any]:
        """Redeem a connect key to establish an instant connection."""
        payload: Dict[str, Any] = {}
        if metadata is not None:
            payload["metadata"] = metadata
        return await self._request("POST", f"/v1/connect-keys/{key}/redeem", payload)

    async def list_connect_keys(self) -> Dict[str, Any]:
        """List all connect keys created by this agent."""
        return await self._request("GET", "/v1/connect-keys")

    async def revoke_connect_key(self, key: str) -> Dict[str, Any]:
        """Revoke a connect key. Existing connections are preserved."""
        return await self._request("DELETE", f"/v1/connect-keys/{key}")

    # ─── Telegram Bot Bridge ─────────────────────────────────

    async def attach_telegram_bot(self, bot_username: str) -> Dict[str, Any]:
        """Register a webhook mapping for a Telegram bot (no token sent)."""
        return await self._request("POST", "/v1/telegram/bots", {
            "bot_username": bot_username,
        })

    async def get_telegram_auth_link(self, bot_id: str, user_id: str) -> Dict[str, str]:
        """Generate a one-time deep link token for user auth."""
        return await self._request("POST", "/v1/telegram/auth-link", {
            "bot_id": bot_id,
            "user_id": user_id,
        })

    async def list_telegram_bots(self) -> Dict[str, Any]:
        """List all registered Telegram bots for this agent."""
        return await self._request("GET", "/v1/telegram/bots")

    async def detach_telegram_bot(self, bot_id: str) -> None:
        """Unregister a Telegram bot."""
        await self._request("DELETE", f"/v1/telegram/bots/{bot_id}")

