"""
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, TypeVar

import httpx
from pydantic import BaseModel, ValidationError

from .._constants import FUNCTION_RANGES, SERVER_ERROR_CODES, Actions, Presets
from .._models import CommandResponse
from ..exceptions import LovenseError, LovenseResponseParseError
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"

_ResponseModelT = TypeVar("_ResponseModelT", bound=BaseModel)


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]:
        """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 self.logging:
            print(self.decode_response(data))
        return data

    def _validate_response(
        self,
        data: dict[str, Any],
        model: type[_ResponseModelT],
    ) -> _ResponseModelT:
        """Validate a raw command response into a strict Pydantic model."""
        try:
            return model.model_validate(data)
        except ValidationError as e:
            raise LovenseResponseParseError(
                "Failed to validate Lovense API response",
                endpoint=self._transport.endpoint,
                payload=data,
            ) from e

    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,
    ) -> CommandResponse:
        """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._validate_response(self.send_command(payload), CommandResponse)

    def stop(self, toy_id: str | list[str] | None = None) -> CommandResponse:
        """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._validate_response(self.send_command(payload), CommandResponse)

    def pattern_request(
        self,
        rule: str,
        strength: str,
        time: float = 0,
        toy_id: str | list[str] | None = None,
    ) -> CommandResponse:
        """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._validate_response(self.send_command(payload), CommandResponse)

    def preset_request(
        self,
        name: str | Presets,
        time: float = 0,
        toy_id: str | list[str] | None = None,
    ) -> CommandResponse:
        """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._validate_response(self.send_command(payload), CommandResponse)

    def decode_response(self, response: dict[str, Any] | BaseModel | None) -> str:
        """Format response as human-readable string."""
        if response is None:
            return "No response received from the server."
        if isinstance(response, BaseModel):
            response = response.model_dump()
        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

    class _PlayContextManager:
        """Context manager: start a function command and auto-stop on exit."""

        def __init__(
            self,
            client: "ServerClient",
            actions: dict[str | Actions, int | float],
            *,
            time: float,
            loop_on_time: float | None,
            loop_off_time: float | None,
            toy_id: str | list[str] | None,
            stop_previous: bool | None,
        ) -> None:
            self._client = client
            self._actions = actions
            self._time = time
            self._loop_on_time = loop_on_time
            self._loop_off_time = loop_off_time
            self._toy_id = toy_id
            self._stop_previous = stop_previous
            self._response: CommandResponse | None = None

        def __enter__(self) -> CommandResponse:
            self._response = self._client.function_request(
                self._actions,
                time=self._time,
                loop_on_time=self._loop_on_time,
                loop_off_time=self._loop_off_time,
                toy_id=self._toy_id,
                stop_previous=self._stop_previous,
            )
            return self._response

        def __exit__(self, exc_type, exc, tb) -> bool:
            try:
                self._client.stop(self._toy_id)
            except LovenseError:
                if exc_type is None:
                    raise
            return False

    def play(
        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,
    ) -> "ServerClient._PlayContextManager":
        """Send a Function command on enter and stop the toy(s) on exit."""
        return ServerClient._PlayContextManager(
            self,
            actions,
            time=time,
            loop_on_time=loop_on_time,
            loop_off_time=loop_off_time,
            toy_id=toy_id,
            stop_previous=stop_previous,
        )
