import asyncio
import time
import typing as t
from enum import Enum

from ton_core import NetworkGlobalID

from .exceptions import (
    RequestTimeoutError,
    TonConnectError,
    TonConnectErrors,
    WalletAlreadyConnectedError,
    WalletNotConnectedError,
)
from .models import (
    Account,
    ActiveConnection,
    AppWallet,
    AppWallets,
    ConnectEventError,
    ConnectEventSuccess,
    ConnectItem,
    ConnectRequest,
    DisconnectEventError,
    DisconnectEventSuccess,
    RpcRequestBase,
    RpcResponseSuccessBase,
    SendTransactionPayload,
    SendTransactionResult,
    SendTransactionRpcRequest,
    SignDataPayload,
    SignDataResult,
    SignDataRpcRequest,
    TonAddressItem,
    TonProofItem,
    Wallet,
    WalletMessage,
    WalletResponseError,
    WalletResponseSuccess,
)
from .provider import Provider
from .provider.storage import ProviderStorage
from .utils import (
    STANDARD_UNIVERSAL_LINK,
    generate_universal_link,
    verify_send_transaction_support,
    verify_sign_data_support,
    verify_wallet_features,
    verify_wallet_network,
)

_ConnectResult = tuple[
    Wallet | None,
    TonConnectError | None,
]
_SendTransactionResult = tuple[
    SendTransactionResult | None,
    TonConnectError | None,
]
_SignDataResult = tuple[
    SignDataResult | None,
    TonConnectError | None,
]
_RequestResult = _SendTransactionResult | _SignDataResult


class Event(str, Enum):
    """Events dispatched by ``Connector``."""

    CONNECT = "connect"
    """Wallet connection result."""
    DISCONNECT = "disconnect"
    """Wallet disconnection result."""
    TRANSACTION = "transaction"
    """Send transaction result."""
    SIGN_DATA = "sign_data"
    """Sign data result."""
    MESSAGE = "message"
    """Raw wallet message before processing."""
    ERROR = "error"
    """Provider/bridge-level or unhandled handler error."""


class Connector:
    """TonConnect session connector managing wallet interactions."""

    DEFAULT_CONNECT_TIMEOUT: float = 15 * 60
    DEFAULT_REQUEST_TIMEOUT: float = 5 * 60

    def __init__(
        self,
        storage: ProviderStorage,
        session_key: str,
        manifest_url: str,
        app_wallets: AppWallets,
        handlers: dict[Event, t.Callable[..., t.Awaitable[None]]],
        headers: dict[str, str] | None = None,
        context: dict[str, t.Any] | None = None,
    ) -> None:
        """Initialize the connector.

        :param storage: Provider storage.
        :param session_key: Unique session key.
        :param manifest_url: URL to ``tonconnect-manifest.json``.
        :param app_wallets: Wallet descriptors passed to the provider as bridge connection sources.
        :param handlers: Event handler mapping.
        :param headers: Extra HTTP headers, or ``None``.
        :param context: User context dict, or ``None``.
        """
        self._session_key = session_key
        self._manifest_url = manifest_url
        self._app_wallets = app_wallets
        self._handlers = handlers
        self._context: dict[str, t.Any] = context or {}

        self._wallet: Wallet | None = None
        self._network: NetworkGlobalID | None = None

        self._connect_future: asyncio.Future[_ConnectResult] | None = None
        self._connect_timeout_task: asyncio.Task[None] | None = None

        self._request_futures: dict[int, asyncio.Future[_RequestResult]] = {}
        self._request_timeout_tasks: dict[int, asyncio.Task[None]] = {}
        self._request_events: dict[int, Event] = {}

        self._storage = storage
        self._provider = Provider(
            storage=self._storage,
            app_wallets=app_wallets,
            on_message=self._on_message,
            on_error=self._on_provider_error,
            headers=headers,
        )

    def __getitem__(self, key: str) -> t.Any:
        """Get a context value by key."""
        return self._context[key]

    def __setitem__(self, key: str, value: t.Any) -> None:
        """Set a context value by key."""
        self._context[key] = value

    @property
    def session_key(self) -> str:
        """Unique session key."""
        return self._session_key

    @property
    def connected(self) -> bool:
        """Whether a wallet is currently connected."""
        return self._wallet is not None

    @property
    def storage(self) -> ProviderStorage:
        """Provider storage instance."""
        return self._storage

    @property
    def wallet(self) -> Wallet | None:
        """Connected wallet, or ``None``."""
        return self._wallet

    @property
    def account(self) -> Account | None:
        """Connected wallet account, or ``None``."""
        return self._wallet.account if self._wallet else None

    @property
    def app_wallet(self) -> AppWallet | None:
        """Matched wallet app descriptor, or ``None``."""
        bridge_url = self._provider.bridge_url
        if bridge_url is None:
            return None
        for w in self._app_wallets:
            if w.bridge_url == bridge_url:
                return w
        return None

    def make_connect_url(
        self,
        request: ConnectRequest,
        app_wallet: AppWallet | None = None,
        redirect_url: str | None = None,
    ) -> str:
        """Generate a universal link for a connect request.

        :param request: Connect request.
        :param app_wallet: Wallet descriptor for custom universal link, or ``None``.
        :param redirect_url: Post-connect redirect URL, or ``None``.
        :return: Link for connecting a wallet.
        """
        universal_link = STANDARD_UNIVERSAL_LINK
        if app_wallet is not None and app_wallet.universal_url:
            universal_link = app_wallet.universal_url

        return generate_universal_link(
            universal_link,
            message=request,
            session_id=self._provider.session_id,
            redirect_url=redirect_url,
        )

    def make_connect_request(
        self,
        ton_proof_payload: str | None = None,
    ) -> ConnectRequest:
        """Build a ``ConnectRequest`` with standard items.

        :param ton_proof_payload: TON Proof challenge payload, or ``None``.
        :return: Connect request.
        """
        items: list[ConnectItem] = [TonAddressItem()]
        if ton_proof_payload is not None:
            items.append(TonProofItem(payload=ton_proof_payload))
        return ConnectRequest(
            manifest_url=self._manifest_url,
            items=items,
        )

    async def connect(
        self,
        request: ConnectRequest,
        network: NetworkGlobalID | None = None,
        app_wallet: AppWallet | None = None,
        redirect_url: str | None = None,
        timeout: float | None = DEFAULT_CONNECT_TIMEOUT,
    ) -> str:
        """Initiate a wallet connection.

        :param request: Connect request.
        :param network: Expected network, or ``None``.
        :param app_wallet: Wallet descriptor for custom universal link, or ``None``.
        :param redirect_url: Post-connect redirect URL, or ``None``.
        :param timeout: Connection timeout in seconds, or ``None``.
        :return: Universal link for connecting a wallet.
        :raises WalletAlreadyConnectedError: If already connected.
        """
        if self.connected:
            raise WalletAlreadyConnectedError()
        self._cancel_connect()
        await self._provider.close_connection()
        await self._provider.connect(request)

        self._network = network
        loop = asyncio.get_running_loop()
        self._connect_future = loop.create_future()

        if timeout is not None:
            self._connect_timeout_task = asyncio.create_task(
                self._run_connect_timeout(timeout),
            )

        return self.make_connect_url(request, app_wallet, redirect_url=redirect_url)

    async def restore(self) -> bool:
        """Restore a connection from storage.

        :return: ``True`` if an active connection was restored.
        """
        connect_event = await self._provider.restore_connection()
        if connect_event is None:
            return False

        wallet = Wallet.from_payload(connect_event.payload)
        self._network = wallet.account.network
        self._wallet = wallet
        return True

    async def disconnect(self) -> None:
        """Disconnect the wallet.

        :raises WalletNotConnectedError: If no wallet is connected.
        """
        if not self.connected:
            raise WalletNotConnectedError()

        await self._provider.disconnect()
        self._wallet = None
        self._network = None
        self._cancel_all_requests()

    async def send_transaction(
        self,
        payload: SendTransactionPayload,
        timeout: float | None = None,
    ) -> int:
        """Request the wallet to sign and send a transaction.

        :param payload: Transaction payload.
        :param timeout: Request timeout in seconds, or ``None``.
        :return: Request ID for ``wait_transaction``.
        """
        has_extra = any(m.extra_currency is not None for m in payload.messages)

        if self._wallet is not None:
            verify_send_transaction_support(
                self._wallet,
                len(payload.messages),
                app_wallet=self.app_wallet,
                has_extra_currency=has_extra,
            )

        if self._network is not None and payload.network is None:
            payload = payload.model_copy(update={"network": self._network})
        if self.account is not None and payload.from_address is None:
            payload = payload.model_copy(update={"from_address": self.account.address})

        if timeout is None and payload.valid_until is not None:
            timeout = max(payload.valid_until - int(time.time()), 0)
        if timeout is None:
            timeout = self.DEFAULT_REQUEST_TIMEOUT  # type: ignore[unreachable]

        return await self._send_request(
            SendTransactionRpcRequest(params=[payload]),
            Event.TRANSACTION,
            timeout,
        )

    async def sign_data(
        self,
        payload: SignDataPayload,
        timeout: float = DEFAULT_REQUEST_TIMEOUT,
    ) -> int:
        """Request the wallet to sign arbitrary data.

        :param payload: Sign data payload.
        :param timeout: Request timeout in seconds, or ``None``.
        :return: Request ID for ``wait_sign_data``.
        """
        if self._wallet is not None:
            verify_sign_data_support(
                self._wallet,
                payload.type,
                app_wallet=self.app_wallet,
            )

        if self._network is not None and payload.network is None:
            payload = payload.model_copy(update={"network": self._network})
        if self.account is not None and payload.from_address is None:
            payload = payload.model_copy(update={"from_address": self.account.address})

        return await self._send_request(
            SignDataRpcRequest(params=[payload]),
            Event.SIGN_DATA,
            timeout,
        )

    async def wait_connect(self) -> _ConnectResult:
        """Await the pending connect result.

        :return: `(wallet, None)` on success, `(None, error)` on failure.
        :raises TonConnectError: If no connect request is pending.
        """
        if self._connect_future is None:
            raise TonConnectError("No pending connect request")
        try:
            return await self._connect_future
        finally:
            self._connect_future = None

    async def wait_transaction(self, request_id: int) -> _SendTransactionResult:
        """Await a pending send transaction result.

        :param request_id: Request ID from ``send_transaction``.
        :return: `(result, None)` on success, `(None, error)` on failure.
        :raises TonConnectError: If request ID is unknown or not a transaction.
        """
        event = self._request_events.get(request_id)
        if event is not Event.TRANSACTION:
            raise TonConnectError(f"Request {request_id} is not a pending transaction")
        return await self._wait_request(request_id)  # type: ignore[return-value]

    async def wait_sign_data(self, request_id: int) -> _SignDataResult:
        """Await a pending sign data result.

        :param request_id: Request ID from ``sign_data``.
        :return: `(result, None)` on success, `(None, error)` on failure.
        :raises TonConnectError: If request ID is unknown or not sign_data.
        """
        event = self._request_events.get(request_id)
        if event is not Event.SIGN_DATA:
            raise TonConnectError(f"Request {request_id} is not a pending sign_data")
        return await self._wait_request(request_id)  # type: ignore[return-value]

    def drop_connect(self) -> None:
        """Cancel the pending connect request."""
        future = self._connect_future
        if future is None:
            return
        self._connect_future = None
        self._cancel_connect_timeout()
        if not future.done():
            future.cancel()

    def drop_request(self, request_id: int) -> None:
        """Cancel a pending request by ID.

        :param request_id: Request ID to cancel.
        """
        self._cancel_request_timeout(request_id)
        self._request_events.pop(request_id, None)
        future = self._request_futures.pop(request_id, None)
        if future is not None and not future.done():
            future.cancel()

    async def close(self) -> None:
        """Close all bridge connections without notifying the wallet."""
        self._cancel_connect()
        self._cancel_all_requests()
        await self._provider.close_connection()

    async def pause(self) -> None:
        """Pause SSE listening."""
        await self._provider.pause()

    async def unpause(self) -> None:
        """Resume SSE listening."""
        await self._provider.unpause()

    async def to_connection(self) -> ActiveConnection | None:
        """Return the active connection from storage, or ``None``."""
        try:
            conn = await self._storage.get_connection()
        except TonConnectError:
            return None
        return conn if isinstance(conn, ActiveConnection) else None

    async def _wait_request(self, request_id: int) -> _RequestResult:
        """Await a pending request result."""
        future = self._request_futures.get(request_id)
        if future is None:
            raise TonConnectError(f"Unknown request id: {request_id}")
        try:
            return await future
        finally:
            if request_id in self._request_futures:
                del self._request_futures[request_id]

    async def _emit_connect(self, error: TonConnectError | None) -> None:
        """Resolve the connect future and dispatch the event."""
        self._cancel_connect_timeout()
        if self._connect_future is not None and not self._connect_future.done():
            self._connect_future.set_result((self._wallet, error))
        await self._dispatch(Event.CONNECT, error)

    async def _emit_request(
        self,
        request_id: int,
        result: t.Any,
        error: TonConnectError | None,
    ) -> None:
        """Resolve a request future and dispatch the event."""
        self._cancel_request_timeout(request_id)
        event = self._request_events.pop(request_id, None)
        future = self._request_futures.pop(request_id, None)
        if future is not None and not future.done():
            future.set_result((result, error))
        if event is not None:
            await self._dispatch(event, request_id, result, error)

    async def _dispatch(self, event: Event, *args: t.Any) -> None:
        """Dispatch an event to its registered handler."""
        handler = self._handlers.get(event)
        if handler is None:
            return
        try:
            await handler(self, *args)
        except Exception as error:
            if event is not Event.ERROR:
                if not isinstance(error, TonConnectError):
                    error = TonConnectError(str(error))
                await self._dispatch(Event.ERROR, error)

    async def _send_request(
        self,
        request: RpcRequestBase,
        event: Event,
        timeout: float | None,
    ) -> int:
        """Encrypt, send, and register a pending request."""
        if not self.connected:
            raise WalletNotConnectedError()

        request_id = await self._provider.request(request)
        loop = asyncio.get_running_loop()

        self._request_futures[request_id] = loop.create_future()
        self._request_events[request_id] = event

        if timeout is not None:
            self._request_timeout_tasks[request_id] = asyncio.create_task(
                self._run_request_timeout(request_id, timeout),
            )

        return request_id

    async def _run_connect_timeout(self, timeout: float) -> None:
        """Fire connect timeout after delay."""
        try:
            await asyncio.sleep(timeout)
        except asyncio.CancelledError:
            return
        self._connect_timeout_task = None
        await self._provider.close_connection()
        await self._emit_connect(RequestTimeoutError())

    async def _run_request_timeout(self, request_id: int, timeout: float) -> None:
        """Fire request timeout after delay."""
        try:
            await asyncio.sleep(timeout)
        except asyncio.CancelledError:
            return
        if request_id in self._request_timeout_tasks:
            del self._request_timeout_tasks[request_id]
        await self._emit_request(request_id, None, RequestTimeoutError())

    def _cancel_connect_timeout(self) -> None:
        """Cancel the connect timeout task."""
        if self._connect_timeout_task is not None:
            self._connect_timeout_task.cancel()
            self._connect_timeout_task = None

    def _cancel_connect(self) -> None:
        """Cancel the pending connect future and timeout."""
        self._cancel_connect_timeout()
        if self._connect_future is not None and not self._connect_future.done():
            self._connect_future.cancel()
        self._connect_future = None

    def _cancel_request_timeout(self, request_id: int) -> None:
        """Cancel the timeout task for a request."""
        task = self._request_timeout_tasks.pop(request_id, None)
        if task is not None:
            task.cancel()

    def _cancel_all_requests(self) -> None:
        """Cancel all pending request futures and timeouts."""
        for future in self._request_futures.values():
            if not future.done():
                future.cancel()
        self._request_futures.clear()
        for task in self._request_timeout_tasks.values():
            task.cancel()
        self._request_timeout_tasks.clear()
        self._request_events.clear()

    def resume_connect(self, timeout: float) -> None:
        """Resume waiting for a pending connect after restart.

        :param timeout: Remaining timeout in seconds.
        """
        self._cancel_connect()
        loop = asyncio.get_running_loop()
        self._connect_future = loop.create_future()
        self._connect_timeout_task = asyncio.create_task(
            self._run_connect_timeout(timeout),
        )

    def resume_request(self, request_id: int, event: Event, timeout: float) -> None:
        """Resume waiting for a pending RPC request after restart.

        :param request_id: The original RPC request ID.
        :param event: Event type (``Event.TRANSACTION`` or ``Event.SIGN_DATA``).
        :param timeout: Remaining timeout in seconds.
        """
        loop = asyncio.get_running_loop()
        self._request_futures[request_id] = loop.create_future()
        self._request_events[request_id] = event
        self._request_timeout_tasks[request_id] = asyncio.create_task(
            self._run_request_timeout(request_id, timeout),
        )

    async def _on_provider_error(self, error: Exception) -> None:
        """Handle provider-level errors."""
        if not isinstance(error, TonConnectError):
            error = TonConnectError(str(error))
        await self._dispatch(Event.ERROR, error)

    async def _on_message(self, message: WalletMessage) -> None:
        """Route an incoming wallet message to the appropriate handler."""
        await self._dispatch(Event.MESSAGE, message)

        if isinstance(message, ConnectEventSuccess):
            await self._handle_connect_success(message)
        elif isinstance(message, ConnectEventError):
            await self._handle_connect_error(message)
        elif isinstance(message, DisconnectEventSuccess):
            await self._handle_disconnect_success(message)
        elif isinstance(message, DisconnectEventError):
            await self._handle_disconnect_error(message)
        elif isinstance(message, RpcResponseSuccessBase):
            await self._handle_response_success(message)
        elif isinstance(message, WalletResponseError):
            await self._handle_response_error(message)
        else:
            error = TonConnectError(f"Unsupported message: {message}")  # type: ignore[unreachable]
            await self._dispatch(Event.ERROR, error)

    async def _handle_connect_success(self, message: ConnectEventSuccess) -> None:
        """Process a successful connect event."""
        wallet = Wallet.from_payload(message.payload)
        try:
            verify_wallet_features(wallet)
            if self._network is not None:
                verify_wallet_network(wallet, self._network)
        except TonConnectError as err:
            await self._storage.remove_connection()
            await self._provider.close_connection()
            await self._emit_connect(err)
            return

        self._wallet = wallet
        await self._emit_connect(None)

    async def _handle_connect_error(self, message: ConnectEventError) -> None:
        """Process a failed connect event."""
        error = TonConnectErrors.from_code(
            message.payload.code,
            message.payload.message,
        )
        await self._emit_connect(error)

    async def _handle_disconnect_success(self, _: DisconnectEventSuccess) -> None:
        """Process a successful disconnect event."""
        self._wallet = None
        self._cancel_all_requests()
        await self._storage.remove_connection()
        await self._provider.close_connection()
        self._cancel_connect_timeout()
        await self._dispatch(Event.DISCONNECT, None)

    async def _handle_disconnect_error(self, message: DisconnectEventError) -> None:
        """Process a failed disconnect event."""
        error = TonConnectErrors.from_code(
            message.payload.code,
            message.payload.message,
        )
        await self._dispatch(Event.DISCONNECT, error)

    async def _handle_response_success(self, message: WalletResponseSuccess) -> None:
        """Process a successful RPC response."""
        await self._emit_request(int(message.id), message.result, None)

    async def _handle_response_error(self, message: WalletResponseError) -> None:
        """Process a failed RPC response."""
        error = TonConnectErrors.from_code(
            message.error.code,
            message.error.message,
        )
        await self._emit_request(int(message.id), None, error)
