"""
Standard API — By Server client.

Business logic: command building, payloads (token+uid), response handling.
Transport: HttpTransport (lovensepy.transport).

QR pairing: get_qr_code() for Step 2 of the flow. After user scans,
Lovense POSTs to your Callback URL (configured in dashboard) with uid.
"""

import hashlib
import json
from typing import Any

import httpx

from .._constants import FUNCTION_RANGES, SERVER_ERROR_CODES, Actions, Presets
from ..transport import HttpTransport

__all__ = ["ServerClient", "get_qr_code"]

SERVER_COMMAND_URL = "https://api.lovense-api.com/api/lan/v2/command"
GET_QR_CODE_URL = "https://api.lovense-api.com/api/lan/getQrCode"


def get_qr_code(
    developer_token: str,
    uid: str,
    uname: str | None = None,
    utoken: str | None = None,
    timeout: float = 10.0,
) -> dict[str, Any]:
    """
    Get QR code for Standard API Server pairing (Step 2).

    User scans QR with Lovense Remote; Lovense POSTs to your Callback URL
    (configured in developer dashboard) with uid and toy info.

    Args:
        developer_token: From Lovense developer dashboard
        uid: User ID on your application
        uname: User nickname (optional)
        utoken: MD5(uid + salt) for verification (optional). If omitted, uses MD5(uid).
        timeout: Request timeout

    Returns:
        dict with "qr" (image URL), "code" (6-char code for PC Remote)

    Raises:
        ValueError: If API rejects the request
        httpx.HTTPError: On network errors

    Security note:
        When utoken is omitted, the library uses MD5(uid) as required by the API.
        MD5 is cryptographically weak. For applications where verification matters,
        pass a proper utoken computed as MD5(uid + salt) with your own secret salt.
    """
    payload: dict[str, Any] = {
        "token": developer_token,
        "uid": uid,
        "v": 2,
    }
    if uname is not None:
        payload["uname"] = uname
    if utoken is not None:
        payload["utoken"] = utoken
    else:
        payload["utoken"] = hashlib.md5(uid.encode(), usedforsecurity=False).hexdigest()

    form = {k: str(v) for k, v in payload.items()}
    with httpx.Client(timeout=timeout) as client:
        resp = client.post(GET_QR_CODE_URL, data=form)
        resp.raise_for_status()
        data = resp.json()
    if data.get("code") != 0 and not data.get("result"):
        raise ValueError(data.get("message", "Failed to get QR code"))
    if not data.get("data"):
        raise ValueError("No QR data in response")
    return data["data"]


class ServerClient:
    """
    Standard API Server client.

    Sends commands via Lovense cloud (HttpTransport). Requires developer token
    and uid from QR code pairing flow.
    """

    def __init__(
        self,
        developer_token: str,
        uid: str,
        timeout: float = 10.0,
        logging: bool = False,
    ) -> None:
        self.developer_token = developer_token
        self.uid = uid
        self.timeout = timeout
        self.logging = logging
        self.last_command: dict[str, Any] | None = None
        self.error_codes = SERVER_ERROR_CODES
        self.actions = Actions
        self.presets = Presets

        self._transport = HttpTransport(
            endpoint=SERVER_COMMAND_URL,
            headers={},
            timeout=timeout,
            verify=True,
            logging=logging,
        )

    @property
    def api_endpoint(self) -> str:
        """Endpoint URL for consistency with LANClient."""
        return self._transport.endpoint

    def _base_payload(self) -> dict[str, Any]:
        return {"token": self.developer_token, "uid": self.uid}

    def send_command(
        self,
        command_data: dict[str, Any],
        timeout: float | None = None,
    ) -> dict[str, Any] | None:
        """Send command to Lovense server API."""
        timeout = timeout or self.timeout
        cmd = dict(command_data)

        # Clamp timeSec for non-zero values (API constraint, same as LAN)
        if (ts := cmd.get("timeSec")) is not None and ts != 0:
            cmd["timeSec"] = max(1.0, min(float(ts), 6000.0))

        payload = {**self._base_payload(), **cmd}
        self.last_command = payload

        data = self._transport.post(payload, timeout=timeout)
        if data is None:
            return None

        if self.logging:
            print(self.decode_response(data))
        return data

    def _clamp_actions(self, actions: dict[str | Actions, int | float]) -> dict[str, int | float]:
        result: dict[str, int | float] = {}
        for action, value in actions.items():
            key = str(action)
            if key in FUNCTION_RANGES:
                lo, hi = FUNCTION_RANGES[key]
                result[key] = int(max(lo, min(hi, value)))
            else:
                result[key] = value
        return result

    def function_request(
        self,
        actions: dict[str | Actions, int | float],
        time: float = 0,
        loop_on_time: float | None = None,
        loop_off_time: float | None = None,
        toy_id: str | list[str] | None = None,
        stop_previous: bool | None = None,
    ) -> dict[str, Any] | None:
        """Send Function command."""
        clamped = self._clamp_actions(actions)
        action_str = ",".join(f"{k}:{v}" for k, v in clamped.items())
        payload: dict[str, Any] = {
            "command": "Function",
            "action": action_str,
            "timeSec": time,
            "apiVer": 1,
        }
        if loop_on_time is not None:
            payload["loopRunningSec"] = max(loop_on_time, 1)
        if loop_off_time is not None:
            payload["loopPauseSec"] = max(loop_off_time, 1)
        if toy_id is not None:
            payload["toy"] = toy_id
        if stop_previous is not None:
            payload["stopPrevious"] = 1 if stop_previous else 0
        return self.send_command(payload)

    def stop(self, toy_id: str | list[str] | None = None) -> dict[str, Any] | None:
        """Stop all toys."""
        payload: dict[str, Any] = {
            "command": "Function",
            "action": "Stop",
            "timeSec": 0,
            "apiVer": 1,
        }
        if toy_id is not None:
            payload["toy"] = toy_id
        return self.send_command(payload)

    def pattern_request(
        self,
        rule: str,
        strength: str,
        time: float = 0,
        toy_id: str | list[str] | None = None,
    ) -> dict[str, Any] | None:
        """Send Pattern command."""
        payload: dict[str, Any] = {
            "command": "Pattern",
            "rule": rule,
            "strength": strength,
            "timeSec": time,
            "apiVer": 2,
        }
        if toy_id is not None:
            payload["toy"] = toy_id
        return self.send_command(payload)

    def preset_request(
        self,
        name: str | Presets,
        time: float = 0,
        toy_id: str | list[str] | None = None,
    ) -> dict[str, Any] | None:
        """Send Preset command."""
        payload: dict[str, Any] = {
            "command": "Preset",
            "name": str(name),
            "timeSec": time,
            "apiVer": 1,
        }
        if toy_id is not None:
            payload["toy"] = toy_id
        return self.send_command(payload)

    def decode_response(self, response: dict[str, Any] | None) -> str:
        """Format response as human-readable string."""
        if response is None:
            return "No response received from the server."
        code = response.get("code")
        msg = self.error_codes.get(code, "Unknown") if isinstance(code, int) else str(code)
        out = f"Server response: code={code}, {msg}\n"
        if (data := response.get("data")) is not None:
            out += f"Data: {json.dumps(data, indent=2)}"
        return out
