# coding=utf-8
"""
Synchronous Python 3 interface for swgoh-comlink.
"""

from __future__ import annotations

from json import loads
from typing import Any, cast

import httpx

from ._base import (
    DEFAULT_TIMEOUT,
    GAME_DATA_TIMEOUT,
    SwgohComlinkBase,
    param_alias,
)
from .exceptions import SwgohComlinkException, SwgohComlinkValueError
from .helpers import Constants

__all__ = ["SwgohComlink"]


class SwgohComlink(SwgohComlinkBase):
    """Synchronous interface for the swgoh-comlink service.

    Instances of this class are used to query the Star Wars Galaxy of Heroes
    game servers for exposed endpoints via the swgoh-comlink proxy library
    running on the same host.

    Args:
        url (str): The URL where swgoh-comlink is running. Defaults to 'http://localhost:3000'
        access_key (str): The HMAC public key. Default to None which indicates HMAC is not used.
        secret_key (str): The HMAC private key. Default to None which indicates HMAC is not used.
        stats_url (str): The url of the swgoh-stats service (if used), such as 'http://localhost:3223'
        host (str): IP address or DNS name of server where the swgoh-comlink service is running
        port (int): TCP port number where the swgoh-comlink service is running [Default: 3000]
        stats_port (int): TCP port number of where the comlink-stats service is running [Default: 3223]
        verify_ssl (bool): Whether to verify TLS certificates. [Default: True]

    Notes:
        If the 'host' and 'port' parameters are provided, the 'url' and 'stats_url' parameters are ignored.

    Examples:
        Basic usage::

            comlink = SwgohComlink()
            player = comlink.get_player(allycode=123456789)

        As a context manager::

            with SwgohComlink() as comlink:
                player = comlink.get_player(allycode=123456789)
    """

    __comlink_type__ = "SwgohComlink"

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.client = httpx.Client(
            base_url=self.url_base,
            headers={"Content-Type": "application/json"},
            verify=self.verify_ssl,
            timeout=DEFAULT_TIMEOUT,
        )
        self.stats_client = httpx.Client(
            base_url=self.stats_url_base,
            headers={"Content-Type": "application/json"},
            verify=self.verify_ssl,
            timeout=DEFAULT_TIMEOUT,
        )

    def __enter__(self) -> SwgohComlink:
        return self

    def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None:
        self.close()

    def close(self) -> None:
        """Close the underlying HTTP client connections."""
        self.client.close()
        self.stats_client.close()

    def _request(
        self,
        method: str = "POST",
        endpoint: str = "",
        payload: dict[str, Any] | list[Any] | None = None,
        stats: bool = False,
        timeout: float | None = None,
    ) -> dict[str, Any] | list[Any]:
        """Send an HTTP request to the comlink service.

        This is the single gateway for all HTTP communication.  Every public
        method should route through here so that HMAC authentication, TLS
        verification, and error handling are applied consistently.

        Args:
            method: HTTP method (``"GET"`` or ``"POST"``).
            endpoint: API endpoint path.
            payload: JSON body for the request (ignored for GET requests).
            stats: If True, use the stats service client.
            timeout: Per-request timeout override.

        Returns:
            Decoded JSON response (dict or list).

        Raises:
            SwgohComlinkException: On any network or decoding error.
        """
        req_headers = self._construct_request_headers(method, endpoint, payload)
        client = self.stats_client if stats else self.client

        request_kwargs: dict[str, Any] = {"headers": req_headers}
        if timeout is not None:
            request_kwargs["timeout"] = timeout
        if method.upper() != "GET":
            request_kwargs["json"] = payload

        try:
            r = client.request(method.upper(), f"/{endpoint}", **request_kwargs)
            return cast(dict[str, Any] | list[Any], loads(r.content.decode("utf-8")))
        except httpx.RequestError as e:
            raise SwgohComlinkException(e) from e

    def _post(
        self,
        endpoint: str = "",
        payload: dict[str, Any] | list[Any] | None = None,
        stats: bool = False,
        timeout: float | None = None,
    ) -> dict[str, Any] | list[Any]:
        """Send a POST request to the comlink service.

        Convenience wrapper around :meth:`_request`.
        """
        return self._request(method="POST", endpoint=endpoint, payload=payload, stats=stats, timeout=timeout)

    def _get_game_version(self) -> str:
        md = self.get_game_metadata()
        return str(md["latestGamedataVersion"])

    # ── Public API methods ───────────────────────────────────────────────

    def get_unit_stats(
        self, request_payload: dict[str, Any] | list[Any], flags: list[str] | None = None, language: str | None = None
    ) -> list[Any] | dict[str, Any]:
        """
        Calculate unit stats using the swgoh-stats service interface to swgoh-comlink.

        Args:
            request_payload: Dictionary or list of dictionaries containing units for which to calculate stats.
            flags: List of strings specifying which flags to include in the request URI.
            language: String indicating the desired localized language. Default "eng_us".

        Returns:
            Input object with the calculated stats for the specified units included.
        """
        endpoint_string = self._build_unit_stats_endpoint(flags, language)
        if isinstance(request_payload, dict):
            request_payload = [request_payload]
        return self._post(endpoint=endpoint_string, payload=request_payload, stats=True)

    def get_enums(self) -> dict[str, Any]:
        """
        Get an object containing the game data enums.

        Unlike most endpoints, ``/enums`` uses a GET request.

        Returns:
            A dictionary containing the game data enums.
        """
        return cast(dict[str, Any], self._request(method="GET", endpoint="enums"))

    # alias for non PEP usage of direct endpoint calls
    getEnums = get_enums

    def get_events(self, enums: bool = False) -> dict[str, list[Any]]:
        """
        Get an object containing the events game data.

        Args:
            enums: Boolean flag to indicate whether enum value should be converted in response. [Default is False]

        Returns:
            A single element dictionary containing a list of the events game data.
        """
        payload = {"payload": {}, "enums": enums}
        return cast(dict[str, list[Any]], self._post(endpoint="getEvents", payload=payload))

    # alias for non PEP usage of direct endpoint calls
    getEvents = get_events

    def get_game_data(
        self,
        version: str = "",
        include_pve_units: bool = True,
        request_segment: int = 0,
        enums: bool = False,
        items: str | None = None,
        device_platform: str = "Android",
    ) -> dict[str, Any]:
        """
        Get game data.

        Args:
            version: string (found in metadata key value 'latestGamedataVersion')
            include_pve_units: boolean [Defaults to True]
            request_segment: integer >=0 [Defaults to 0]
            enums: boolean [Defaults to False]
            items: string [Defaults to None] bitwise value indicating the collections to retrieve from game.
                NOTE: this parameter is mutually exclusive with request_segment.
            device_platform: string [Defaults to "Android"]

        Returns:
            A dictionary containing the game data.
        """
        if version == "":
            game_version = self._get_game_version()
        else:
            game_version = version

        payload = self._build_game_data_payload(
            game_version=game_version,
            include_pve_units=include_pve_units,
            request_segment=request_segment,
            enums=enums,
            items=items,
            device_platform=device_platform,
        )
        return cast(dict[str, Any], self._post(endpoint="data", payload=payload, timeout=GAME_DATA_TIMEOUT))

    # alias for non PEP usage of direct endpoint calls
    getGameData = get_game_data

    def get_localization(
        self, localization_id: str | None = None, locale: str | None = None, unzip: bool = False, enums: bool = False
    ) -> dict[str, Any]:
        """
        Get localization data from game.

        Args:
            localization_id: latestLocalizationBundleVersion found in game metadata.
            locale: string Specify only a specific locale to retrieve [for example "ENG_US"]
            unzip: boolean [Defaults to False]
            enums: boolean [Defaults to False]

        Returns:
            A dictionary containing the localization data.
        """
        if not localization_id:
            current_game_version = self.get_latest_game_data_version()
            localization_id = current_game_version["language"]

        if locale:
            assert localization_id is not None
            localization_id = localization_id + ":" + locale.upper()

        payload = {"unzip": unzip, "enums": enums, "payload": {"id": localization_id}}
        return cast(dict[str, Any], self._post(endpoint="localization", payload=payload))

    # aliases for non PEP usage of direct endpoint calls
    getLocalization = get_localization
    getLocalizationBundle = get_localization
    get_localization_bundle = get_localization

    def get_game_metadata(self, client_specs: dict[str, Any] | None = None, enums: bool = False) -> dict[str, Any]:
        """
        Get the game metadata. Game metadata contains the current game and localization versions.

        Args:
            client_specs: Optional dictionary containing client specifications.
            enums: Boolean signifying whether enums in response should be translated to text. [Default: False]

        Returns:
            A dictionary containing the game metadata.
        """
        if client_specs:
            payload: dict[str, Any] = {"payload": {"client_specs": client_specs}, "enums": enums}
        else:
            payload = {}
        return cast(dict[str, Any], self._post(endpoint="metadata", payload=payload))

    # alias for non PEP usage of direct endpoint calls
    getGameMetaData = get_game_metadata
    getMetaData = get_game_metadata
    get_metadata = get_game_metadata

    def get_player(self, allycode: str | int | None = None, player_id: str | None = None, enums: bool = False) -> dict[str, Any]:
        """
        Get player information from game. Either allycode or player_id must be provided.

        Args:
            allycode: integer or string representing player allycode
            player_id: string representing player game ID
            enums: boolean [Defaults to False]

        Returns:
            A dictionary containing the player information.
        """
        payload = self._get_player_payload(allycode=allycode, player_id=player_id, enums=enums)
        return cast(dict[str, Any], self._post(endpoint="player", payload=payload))

    # alias for non PEP usage of direct endpoint calls
    getPlayer = get_player

    @param_alias(param="player_details_only", alias="playerDetailsOnly")
    def get_player_arena(
        self, allycode: str | int | None = None, player_id: str | None = None, player_details_only: bool = False, enums: bool = False
    ) -> dict[str, Any]:
        """
        Get player arena information from game. Either allycode or player_id must be provided.

        Args:
            allycode: integer or string representing player allycode
            player_id: string representing player game ID
            player_details_only: filter results to only player details [Defaults to False]
            enums: boolean [Defaults to False]

        Returns:
            A dictionary containing the player arena information.
        """
        payload = self._get_player_payload(allycode=allycode, player_id=player_id, enums=enums)
        payload["payload"]["playerDetailsOnly"] = player_details_only
        return cast(dict[str, Any], self._post(endpoint="playerArena", payload=payload))

    # alias to allow for get_arena() calls as a shortcut for get_player_arena() and non PEP variations
    get_arena = get_player_arena
    get_player_arena_profile = get_player_arena
    getPlayerArena = get_player_arena
    getPlayerArenaProfile = get_player_arena

    @param_alias(param="include_recent_guild_activity_info", alias="includeRecent")
    def get_guild(self, guild_id: str, include_recent_guild_activity_info: bool = False, enums: bool = False) -> dict[str, Any]:
        """
        Get guild information for a specific Guild ID.

        Args:
            guild_id: String ID of guild to retrieve.
            include_recent_guild_activity_info: boolean [Default: False]
            enums: Should enums in response be translated to text. [Default: False]

        Returns:
            A dictionary containing the guild information.
        """
        if not guild_id:
            raise SwgohComlinkValueError("guild_id must not be empty.")
        payload = {
            "payload": {"guildId": guild_id, "includeRecentGuildActivityInfo": include_recent_guild_activity_info},
            "enums": enums,
        }
        guild: dict[str, Any] = cast(dict[str, Any], self._post(endpoint="guild", payload=payload))
        if "guild" in guild.keys():
            guild = guild["guild"]
        return guild

    # alias for non PEP usage of direct endpoint calls
    getGuild = get_guild

    def get_guilds_by_name(self, name: str, start_index: int = 0, count: int = 10, enums: bool = False) -> dict[str, Any]:
        """
        Search for guild by name and return match.

        Args:
            name: string for guild name search
            start_index: integer for where in the result list to begin
            count: integer for max matches to return [Default: 10]
            enums: Whether to translate enums in response to text [Default: False]

        Returns:
            A dictionary containing the guild search results.
        """
        payload = {
            "payload": {"name": name, "filterType": 4, "startIndex": start_index, "count": count},
            "enums": enums,
        }
        return cast(dict[str, Any], self._post(endpoint="getGuilds", payload=payload))

    # alias for non PEP usage of direct endpoint calls
    getGuildByName = get_guilds_by_name

    def get_guilds_by_criteria(
        self, search_criteria: dict[str, Any], start_index: int = 0, count: int = 10, enums: bool = False
    ) -> dict[str, Any]:
        """
        Search for guild by guild criteria and return matches.

        Args:
            search_criteria: Dictionary of search criteria
            start_index: integer for where in the result list to begin
            count: integer for max matches to return
            enums: Whether to translate enum values to text [Default: False]

        Returns:
            A dictionary containing the guild search results.
        """
        payload = {
            "payload": {"searchCriteria": search_criteria, "filterType": 5, "startIndex": start_index, "count": count},
            "enums": enums,
        }
        return cast(dict[str, Any], self._post(endpoint="getGuilds", payload=payload))

    # alias for non PEP usage of direct endpoint calls
    getGuildByCriteria = get_guilds_by_criteria

    def get_leaderboard(
        self,
        leaderboard_type: int,
        *,
        league: int | str | None = None,
        division: int | str | None = None,
        event_instance_id: str | None = None,
        group_id: str | None = None,
        enums: bool = False,
    ) -> dict[str, Any]:
        """
        Retrieve Grand Arena Championship leaderboard information.

        Args:
            leaderboard_type: Type 4 for GAC brackets, Type 6 for global leaderboards.
            league: Enum value or string name for league.
            division: Enum value or string name for division.
            event_instance_id: Event+instance ID (for type 4).
            group_id: Group ID (for type 4).
            enums: Whether to translate enum values to text [Default: False]

        Returns:
            A dictionary containing the leaderboard data.
        """
        leagues = Constants.LEAGUES
        divisions = Constants.DIVISIONS
        if isinstance(league, str):
            league = leagues[league.lower()]
        if isinstance(division, int) and len(str(division)) == 1:
            division = divisions[str(division).lower()]
        if isinstance(division, str):
            division = divisions[division.lower()]
        payload: dict[str, Any] = {
            "payload": {
                "leaderboardType": leaderboard_type,
            },
            "enums": enums,
        }
        if leaderboard_type == 4:
            payload["payload"]["eventInstanceId"] = event_instance_id
            payload["payload"]["groupId"] = group_id
        elif leaderboard_type == 6:
            payload["payload"]["league"] = league
            payload["payload"]["division"] = division
        return cast(dict[str, Any], self._post(endpoint="getLeaderboard", payload=payload))

    # alias for non PEP usage of direct endpoint calls
    getLeaderboard = get_leaderboard
    get_gac_leaderboard = get_leaderboard
    getGacLeaderboard = get_leaderboard

    def get_guild_leaderboard(self, leaderboard_id: list[dict[str, Any]], count: int = 200, enums: bool = False) -> dict[str, Any]:
        """
        Fetches the guild leaderboard data for given leaderboard ID.

        Parameters:
            leaderboard_id: A list containing one leaderboard ID dictionary.
            count: The number of leaderboard entries to retrieve. [Default: 200]
            enums: Whether or not to translate enum values. [Default: False]

        Returns:
            A dictionary containing the guild leaderboard data.
        """
        payload = dict(payload={"leaderboardId": leaderboard_id, "count": count}, enums=enums)
        return cast(dict[str, Any], self._post(endpoint="getGuildLeaderboard", payload=payload))

    # alias for non PEP usage of direct endpoint calls
    getGuildLeaderboard = get_guild_leaderboard

    def get_name_spaces(self, only_compatible: bool = False, enums: bool = False) -> dict[str, Any]:
        """Fetches namespaces based on the specified filter criteria."""
        payload = {"payload": {"onlyCompatible": only_compatible}, "enums": enums}
        return cast(dict[str, Any], self._post(endpoint="getNameSpaces", payload=payload))

    def get_segmented_content(
        self, content_name_space: str = "current", accept_language: str = "ENG_US", enums: bool = False
    ) -> dict[str, Any]:
        """Retrieves segmented content from a specified namespace."""
        payload = {
            "payload": {"contentNameSpace": content_name_space, "acceptLanguage": accept_language},
            "enums": enums,
        }
        return cast(dict[str, Any], self._post(endpoint="getSegmentedContent", payload=payload))

    # ── Helper methods ───────────────────────────────────────────────────

    def get_latest_game_data_version(self) -> dict[str, Any]:
        """Get the latest game data and language bundle versions.

        Returns:
            Dictionary with 'game' and 'language' version strings.
        """
        current_metadata = self.get_metadata()
        return {
            "game": current_metadata["latestGamedataVersion"],
            "language": current_metadata["latestLocalizationBundleVersion"],
        }

    # alias for shorthand call
    getVersion = get_latest_game_data_version
