"""
Core client for communicating with SimpleX Chat CLI via WebSocket.

Mirrors the TypeScript SDK at packages/simplex-chat-client/typescript/src/client.ts
with Python-native async/await patterns.
"""

from __future__ import annotations

import json
import logging
from typing import Any

from .transport import ChatTransport, TransportError
from .types import (
    ChatType,
    ComposedMessage,
    Contact,
    GroupInfo,
    GroupMember,
    GroupMemberRole,
    MsgContent,
    Profile,
    User,
    ChatItem,
)

logger = logging.getLogger("dexgram.client")


class DexgramCommandError(Exception):
    def __init__(self, message: str, response: dict[str, Any]):
        super().__init__(message)
        self.response = response


class DexgramClient:
    """
    Async client for the SimpleX Chat CLI WebSocket API.

    Usage::

        client = await DexgramClient.create("ws://localhost:5225")
        user = await client.api_get_active_user()
        await client.api_send_text(ChatType.DIRECT, contact_id, "Hello!")
        await client.disconnect()
    """

    def __init__(self, transport: ChatTransport):
        self._transport = transport
        self._corr_id = 0

    @classmethod
    async def create(cls, url: str = "ws://localhost:5225", timeout: float = 10.0) -> DexgramClient:
        transport = await ChatTransport.connect(url=url, timeout=timeout)
        return cls(transport)

    def _next_corr_id(self) -> str:
        self._corr_id += 1
        return str(self._corr_id)

    async def send_command(self, cmd: str) -> dict[str, Any]:
        corr_id = self._next_corr_id()
        return await self._transport.send(corr_id, cmd)

    async def disconnect(self) -> None:
        await self._transport.close()

    @property
    def events(self):
        return self._transport.events()

    # ── User management ──

    async def api_get_active_user(self) -> User | None:
        r = await self.send_command("/u")
        if r.get("type") == "activeUser":
            return User.from_dict(r.get("user", {}))
        if r.get("type") == "chatCmdError":
            err = r.get("chatError", {})
            if err.get("type") == "error" and err.get("errorType", {}).get("type") == "noActiveUser":
                return None
        return None

    async def api_create_active_user(self, profile: Profile | None = None, past_timestamp: bool = False) -> User:
        new_user: dict[str, Any] = {"pastTimestamp": past_timestamp}
        if profile:
            new_user["profile"] = profile.to_dict()
        cmd = f"/_create user {json.dumps(new_user)}"
        r = await self.send_command(cmd)
        if r.get("type") == "activeUser":
            return User.from_dict(r.get("user", {}))
        raise DexgramCommandError("Failed to create user", r)

    async def api_list_users(self) -> list[dict[str, Any]]:
        r = await self.send_command("/users")
        if r.get("type") == "usersList":
            return r.get("users", [])
        raise DexgramCommandError("Failed to list users", r)

    async def api_set_active_user(self, user_id: int) -> User:
        r = await self.send_command(f"/_user {user_id}")
        if r.get("type") == "activeUser":
            return User.from_dict(r.get("user", {}))
        raise DexgramCommandError("Failed to set active user", r)

    # ── Profile ──

    async def api_update_profile(self, user_id: int, profile: Profile) -> Profile | None:
        cmd = f"/_profile {user_id} {json.dumps(profile.to_dict())}"
        r = await self.send_command(cmd)
        if r.get("type") == "userProfileNoChange":
            return None
        if r.get("type") == "userProfileUpdated":
            return Profile.from_dict(r.get("toProfile", {}))
        raise DexgramCommandError("Failed to update profile", r)

    # ── Address ──

    async def api_create_user_address(self, user_id: int) -> str:
        r = await self.send_command(f"/_address {user_id}")
        if r.get("type") == "userContactLinkCreated":
            link = r.get("connLinkContact", {})
            return link.get("connShortLink") or link.get("connFullLink", "")
        raise DexgramCommandError("Failed to create address", r)

    async def api_get_user_address(self, user_id: int) -> str | None:
        r = await self.send_command(f"/_show_address {user_id}")
        if r.get("type") == "userContactLink":
            link = r.get("contactLink", {}).get("connLinkContact", {})
            return link.get("connShortLink") or link.get("connFullLink")
        if r.get("type") == "chatCmdError":
            err = r.get("chatError", {})
            store_err = err.get("storeError", {})
            if store_err.get("type") == "userContactLinkNotFound":
                return None
        raise DexgramCommandError("Failed to get address", r)

    async def api_delete_user_address(self, user_id: int) -> None:
        r = await self.send_command(f"/_delete_address {user_id}")
        if r.get("type") == "userContactLinkDeleted":
            return
        raise DexgramCommandError("Failed to delete address", r)

    async def enable_address_auto_accept(
        self,
        user_id: int,
        auto_reply: MsgContent | None = None,
        business_address: bool = False,
    ) -> None:
        settings: dict[str, Any] = {
            "businessAddress": business_address,
            "autoAccept": {"acceptIncognito": False},
        }
        if auto_reply:
            settings["autoReply"] = auto_reply.to_dict()
        cmd = f"/_address_settings {user_id} {json.dumps(settings)}"
        r = await self.send_command(cmd)
        if r.get("type") != "userContactLinkUpdated":
            raise DexgramCommandError("Failed to enable auto accept", r)

    async def disable_address_auto_accept(self, user_id: int, business_address: bool = False) -> None:
        settings = {"businessAddress": business_address}
        cmd = f"/_address_settings {user_id} {json.dumps(settings)}"
        r = await self.send_command(cmd)
        if r.get("type") != "userContactLinkUpdated":
            raise DexgramCommandError("Failed to disable auto accept", r)

    # ── Contacts ──

    async def api_accept_contact(self, contact_req_id: int) -> Contact:
        r = await self.send_command(f"/_accept {contact_req_id}")
        if r.get("type") == "acceptingContactRequest":
            return Contact.from_dict(r.get("contact", {}))
        raise DexgramCommandError("Failed to accept contact", r)

    async def api_reject_contact(self, contact_req_id: int) -> None:
        r = await self.send_command(f"/_reject {contact_req_id}")
        if r.get("type") == "contactRequestRejected":
            return
        raise DexgramCommandError("Failed to reject contact", r)

    async def api_delete_chat(self, chat_type: ChatType, chat_id: int) -> None:
        cmd = f"/_delete {chat_type.value}{chat_id} full notify=on"
        r = await self.send_command(cmd)
        if chat_type == ChatType.DIRECT and r.get("type") == "contactDeleted":
            return
        if chat_type == ChatType.GROUP and r.get("type") == "groupDeletedUser":
            return
        raise DexgramCommandError("Failed to delete chat", r)

    # ── Sending messages ──

    async def api_send_messages(
        self,
        chat_type: ChatType,
        chat_id: int,
        messages: list[ComposedMessage],
    ) -> list[dict[str, Any]]:
        msgs_json = json.dumps([m.to_dict() for m in messages])
        cmd = f"/_send {chat_type.value}{chat_id} live=off ttl=default json {msgs_json}"
        r = await self.send_command(cmd)
        if r.get("type") == "newChatItems":
            return r.get("chatItems", [])
        raise DexgramCommandError("Failed to send messages", r)

    async def api_send_text(self, chat_type: ChatType, chat_id: int, text: str) -> list[dict[str, Any]]:
        msg = ComposedMessage(msg_content=MsgContent.text_content(text))
        return await self.api_send_messages(chat_type, chat_id, [msg])

    async def api_update_chat_item(
        self,
        chat_type: ChatType,
        chat_id: int,
        chat_item_id: int,
        msg_content: MsgContent,
    ) -> ChatItem:
        updated = json.dumps({"msgContent": msg_content.to_dict(), "mentions": {}})
        cmd = f"/_update item {chat_type.value}{chat_id} {chat_item_id} live=off {updated}"
        r = await self.send_command(cmd)
        if r.get("type") == "chatItemUpdated":
            return ChatItem.from_dict(r.get("chatItem", {}).get("chatItem", {}))
        raise DexgramCommandError("Failed to update chat item", r)

    async def api_delete_chat_items(
        self,
        chat_type: ChatType,
        chat_id: int,
        chat_item_ids: list[int],
        delete_mode: str = "broadcast",
    ) -> list[dict[str, Any]]:
        ids_str = ",".join(str(i) for i in chat_item_ids)
        cmd = f"/_delete item {chat_type.value}{chat_id} {ids_str} {delete_mode}"
        r = await self.send_command(cmd)
        if r.get("type") == "chatItemsDeleted":
            return r.get("chatItemDeletions", [])
        raise DexgramCommandError("Failed to delete chat items", r)

    # ── Connections ──

    async def api_create_link(self, user_id: int) -> str:
        r = await self.send_command(f"/_connect {user_id} incognito=off")
        if r.get("type") == "invitation":
            link = r.get("connLinkInvitation", {})
            return link.get("connShortLink") or link.get("connFullLink", "")
        raise DexgramCommandError("Failed to create link", r)

    async def api_connect(self, user_id: int, conn_link: str) -> str:
        cmd = f"/_connect {user_id} incognito=off {conn_link}"
        r = await self.send_command(cmd)
        if r.get("type") in ("sentConfirmation", "sentInvitation"):
            return r.get("type", "")
        raise DexgramCommandError("Connection error", r)

    # ── Groups ──

    async def api_new_group(self, user_id: int, group_profile: dict[str, Any]) -> GroupInfo:
        cmd = f"/_group {user_id} incognito=off {json.dumps(group_profile)}"
        r = await self.send_command(cmd)
        if r.get("type") == "groupCreated":
            return GroupInfo.from_dict(r.get("groupInfo", {}))
        raise DexgramCommandError("Failed to create group", r)

    async def api_add_member(self, group_id: int, contact_id: int, role: GroupMemberRole = GroupMemberRole.MEMBER) -> GroupMember:
        cmd = f"/_add #{group_id} {contact_id} {role.value}"
        r = await self.send_command(cmd)
        if r.get("type") == "sentGroupInvitation":
            return GroupMember.from_dict(r.get("member", {}))
        raise DexgramCommandError("Failed to add member", r)

    async def api_join_group(self, group_id: int) -> GroupInfo:
        r = await self.send_command(f"/_join #{group_id}")
        if r.get("type") == "userAcceptedGroupSent":
            return GroupInfo.from_dict(r.get("groupInfo", {}))
        raise DexgramCommandError("Failed to join group", r)

    async def api_remove_members(self, group_id: int, member_ids: list[int]) -> list[GroupMember]:
        ids_str = ",".join(str(i) for i in member_ids)
        cmd = f"/_remove #{group_id} {ids_str}"
        r = await self.send_command(cmd)
        if r.get("type") == "userDeletedMembers":
            return [GroupMember.from_dict(m) for m in r.get("members", [])]
        raise DexgramCommandError("Failed to remove members", r)

    async def api_leave_group(self, group_id: int) -> GroupInfo:
        r = await self.send_command(f"/_leave #{group_id}")
        if r.get("type") == "leftMemberUser":
            return GroupInfo.from_dict(r.get("groupInfo", {}))
        raise DexgramCommandError("Failed to leave group", r)

    async def api_list_members(self, group_id: int) -> list[GroupMember]:
        r = await self.send_command(f"/_members #{group_id}")
        if r.get("type") == "groupMembers":
            group_data = r.get("group", {})
            return [GroupMember.from_dict(m) for m in group_data.get("members", [])]
        raise DexgramCommandError("Failed to list members", r)

    async def api_update_group(self, group_id: int, group_profile: dict[str, Any]) -> GroupInfo:
        cmd = f"/_group_profile #{group_id} {json.dumps(group_profile)}"
        r = await self.send_command(cmd)
        if r.get("type") == "groupUpdated":
            return GroupInfo.from_dict(r.get("toGroup", {}))
        raise DexgramCommandError("Failed to update group", r)

    # ── Files ──

    async def api_receive_file(self, file_id: int) -> dict[str, Any]:
        r = await self.send_command(f"/freceive {file_id} approved_relays=on")
        if r.get("type") == "rcvFileAccepted":
            return r.get("chatItem", {})
        raise DexgramCommandError("Failed to receive file", r)

    # ── Bot-specific helpers ──

    async def set_bot_commands(self, commands_str: str) -> None:
        """
        Set bot commands using the CLI syntax.

        Example::
            await client.set_bot_commands(
                "'Browse':/catalog,'Orders':{'Track':/'track <id>','Cancel':/'cancel <id>'}"
            )
        """
        r = await self.send_command(f"/set bot commands {commands_str}")
        if r.get("type") not in ("userProfileUpdated", "userProfileNoChange"):
            raise DexgramCommandError("Failed to set bot commands", r)

    async def set_contact_commands(self, contact_id: int, commands: list[dict[str, Any]]) -> None:
        prefs = {"commands": commands}
        cmd = f"/_set prefs @{contact_id} {json.dumps(prefs)}"
        r = await self.send_command(cmd)
        if r.get("type") not in ("contactPrefsUpdated", "chatCmdError"):
            raise DexgramCommandError("Failed to set contact commands", r)
