from __future__ import annotations

import httpx

from .models import (
    BanCategoryCounts,
    BanItem,
    BanListResponse,
    BanScope,
    BanSummary,
    BanType,
    CreateBanRequest,
    CreateBanResponse,
    DeviceCheckRequest,
    DeviceCheckResponse,
    PublisherPolicy,
    Reputation,
    RevokeBanResponse,
    UpdatePolicyRequest,
)


class SentinelApiError(Exception):
    def __init__(self, status_code: int, error_code: str):
        self.status_code = status_code
        self.error_code = error_code
        super().__init__(f"SentinelID API error {status_code}: {error_code}")


class SentinelServerClient:
    """Synchronous client for SentinelID Server API."""

    def __init__(
        self,
        api_key: str,
        game_id: str,
        base_url: str = "https://api.sentineltrustplay.io",
    ):
        self._client = httpx.Client(
            base_url=base_url.rstrip("/"),
            headers={
                "Authorization": f"Bearer {api_key}",
                "X-Game-Id": game_id,
                "Content-Type": "application/json",
            },
        )

    def _request(
        self,
        method: str,
        path: str,
        *,
        json: dict | None = None,
        params: dict | None = None,
    ) -> dict:
        resp = self._client.request(method, path, json=json, params=params)
        if resp.status_code >= 400:
            try:
                body = resp.json()
                error_code = body.get("error", "unknown")
            except Exception:
                error_code = f"http_{resp.status_code}"
            raise SentinelApiError(resp.status_code, error_code)
        return resp.json()

    def device_check(self, req: DeviceCheckRequest) -> DeviceCheckResponse:
        raw = self._request(
            "POST",
            "/v1/device/check",
            json={
                "device_id": req.device_id,
                "publisher_id": req.publisher_id,
                "game_id": req.game_id,
                "alg": req.alg,
                "payload_b64": req.payload_b64,
                "sig_b64": req.sig_b64,
            },
        )
        return _parse_device_check_response(raw)

    def create_ban(self, req: CreateBanRequest) -> CreateBanResponse:
        body: dict = {
            "device_id": req.device_id,
            "ban_type": req.ban_type.value
            if isinstance(req.ban_type, BanType)
            else req.ban_type,
            "scope": req.scope.value
            if isinstance(req.scope, BanScope)
            else req.scope,
            "reason_code": req.reason_code,
        }
        if req.expires_at is not None:
            body["expires_at"] = req.expires_at
        if req.details is not None:
            body["details"] = req.details
        if req.idempotency_key is not None:
            body["idempotency_key"] = req.idempotency_key
        raw = self._request("POST", "/v1/bans", json=body)
        return CreateBanResponse(
            **{k: raw[k] for k in CreateBanResponse.__dataclass_fields__ if k in raw}
        )

    def revoke_ban(self, ban_id: str) -> RevokeBanResponse:
        raw = self._request("POST", f"/v1/bans/{ban_id}/revoke")
        return RevokeBanResponse(
            ban_id=raw["ban_id"], device_id=raw["device_id"], status=raw["status"]
        )

    def list_cheat_bans(
        self,
        device_id: str,
        status: str | None = None,
        limit: int | None = None,
        cursor: str | None = None,
    ) -> BanListResponse:
        params = _ban_list_params(status, limit, cursor)
        raw = self._request(
            "GET", f"/v1/device/{device_id}/bans/cheat", params=params
        )
        return _parse_ban_list(raw)

    def list_social_bans(
        self,
        device_id: str,
        status: str | None = None,
        limit: int | None = None,
        cursor: str | None = None,
    ) -> BanListResponse:
        params = _ban_list_params(status, limit, cursor)
        raw = self._request(
            "GET", f"/v1/device/{device_id}/bans/social", params=params
        )
        return _parse_ban_list(raw)

    def get_policy(self) -> PublisherPolicy:
        raw = self._request("GET", "/v1/policy")
        return PublisherPolicy(**raw)

    def update_policy(self, req: UpdatePolicyRequest) -> PublisherPolicy:
        body = {
            k: v
            for k, v in {
                "enforce_game": req.enforce_game,
                "enforce_publisher": req.enforce_publisher,
                "enforce_global": req.enforce_global,
                "enforce_cheat_global": req.enforce_cheat_global,
                "enforce_social_global": req.enforce_social_global,
                "rep_include_game": req.rep_include_game,
                "rep_include_publisher": req.rep_include_publisher,
                "rep_include_global": req.rep_include_global,
            }.items()
            if v is not None
        }
        raw = self._request("PUT", "/v1/policy", json=body)
        return PublisherPolicy(**raw)

    def close(self):
        self._client.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()


class AsyncSentinelServerClient:
    """Async client for SentinelID Server API."""

    def __init__(
        self,
        api_key: str,
        game_id: str,
        base_url: str = "https://api.sentineltrustplay.io",
    ):
        self._client = httpx.AsyncClient(
            base_url=base_url.rstrip("/"),
            headers={
                "Authorization": f"Bearer {api_key}",
                "X-Game-Id": game_id,
                "Content-Type": "application/json",
            },
        )

    async def _request(
        self,
        method: str,
        path: str,
        *,
        json: dict | None = None,
        params: dict | None = None,
    ) -> dict:
        resp = await self._client.request(method, path, json=json, params=params)
        if resp.status_code >= 400:
            try:
                body = resp.json()
                error_code = body.get("error", "unknown")
            except Exception:
                error_code = f"http_{resp.status_code}"
            raise SentinelApiError(resp.status_code, error_code)
        return resp.json()

    async def device_check(self, req: DeviceCheckRequest) -> DeviceCheckResponse:
        raw = await self._request(
            "POST",
            "/v1/device/check",
            json={
                "device_id": req.device_id,
                "publisher_id": req.publisher_id,
                "game_id": req.game_id,
                "alg": req.alg,
                "payload_b64": req.payload_b64,
                "sig_b64": req.sig_b64,
            },
        )
        return _parse_device_check_response(raw)

    async def create_ban(self, req: CreateBanRequest) -> CreateBanResponse:
        body: dict = {
            "device_id": req.device_id,
            "ban_type": req.ban_type.value
            if isinstance(req.ban_type, BanType)
            else req.ban_type,
            "scope": req.scope.value
            if isinstance(req.scope, BanScope)
            else req.scope,
            "reason_code": req.reason_code,
        }
        if req.expires_at is not None:
            body["expires_at"] = req.expires_at
        if req.details is not None:
            body["details"] = req.details
        if req.idempotency_key is not None:
            body["idempotency_key"] = req.idempotency_key
        raw = await self._request("POST", "/v1/bans", json=body)
        return CreateBanResponse(
            **{k: raw[k] for k in CreateBanResponse.__dataclass_fields__ if k in raw}
        )

    async def revoke_ban(self, ban_id: int) -> RevokeBanResponse:
        raw = await self._request("POST", f"/v1/bans/{ban_id}/revoke")
        return RevokeBanResponse(
            ban_id=raw["ban_id"], device_id=raw["device_id"], status=raw["status"]
        )

    async def list_cheat_bans(
        self,
        device_id: str,
        status: str | None = None,
        limit: int | None = None,
        cursor: str | None = None,
    ) -> BanListResponse:
        params = _ban_list_params(status, limit, cursor)
        raw = await self._request(
            "GET", f"/v1/device/{device_id}/bans/cheat", params=params
        )
        return _parse_ban_list(raw)

    async def list_social_bans(
        self,
        device_id: str,
        status: str | None = None,
        limit: int | None = None,
        cursor: str | None = None,
    ) -> BanListResponse:
        params = _ban_list_params(status, limit, cursor)
        raw = await self._request(
            "GET", f"/v1/device/{device_id}/bans/social", params=params
        )
        return _parse_ban_list(raw)

    async def get_policy(self) -> PublisherPolicy:
        raw = await self._request("GET", "/v1/policy")
        return PublisherPolicy(**raw)

    async def update_policy(self, req: UpdatePolicyRequest) -> PublisherPolicy:
        body = {
            k: v
            for k, v in {
                "enforce_game": req.enforce_game,
                "enforce_publisher": req.enforce_publisher,
                "enforce_global": req.enforce_global,
                "enforce_cheat_global": req.enforce_cheat_global,
                "enforce_social_global": req.enforce_social_global,
                "rep_include_game": req.rep_include_game,
                "rep_include_publisher": req.rep_include_publisher,
                "rep_include_global": req.rep_include_global,
            }.items()
            if v is not None
        }
        raw = await self._request("PUT", "/v1/policy", json=body)
        return PublisherPolicy(**raw)

    async def close(self):
        await self._client.aclose()

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.close()


# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------


def _ban_list_params(
    status: str | None, limit: int | None, cursor: str | None
) -> dict | None:
    params: dict = {}
    if status is not None:
        params["status"] = status
    if limit is not None:
        params["limit"] = limit
    if cursor is not None:
        params["cursor"] = cursor
    return params or None


def _parse_device_check_response(raw: dict) -> DeviceCheckResponse:
    return DeviceCheckResponse(
        device_id=raw["device_id"],
        publisher_id=raw["publisher_id"],
        game_id=raw["game_id"],
        banned=raw["banned"],
        bans=BanSummary(
            cheat=BanCategoryCounts(**raw["bans"]["cheat"]),
            social=BanCategoryCounts(**raw["bans"]["social"]),
        ),
        reputation=Reputation(
            cheat_score=raw["reputation"]["cheat_score"],
            social_score=raw["reputation"]["social_score"],
        ),
        status=raw["status"],
    )


def _parse_ban_list(raw: dict) -> BanListResponse:
    return BanListResponse(
        device_id=raw["device_id"],
        status=raw["status"],
        counts=BanCategoryCounts(**raw["counts"]),
        items=[
            BanItem(**{k: item.get(k) for k in BanItem.__dataclass_fields__})
            for item in raw["items"]
        ],
    )
