# coding=utf-8
import asyncio
import contextlib
import aiohttp
import psutil
import logging

from discord import Client as DiscordClient
from typing import Any, Optional, Union, List, Dict, Iterable, Callable, Awaitable
from discord.ext.commands import Context

# this could be relative, but apparently Python doesn't like it
from statcord import exceptions


class Client:
    """Client for using the statcord API"""

    def __init__(self, bot, token, **kwargs):
        self.logger = logging.getLogger("statcord")
        self.logging_level = kwargs.get("logging_level", logging.WARNING)
        self.logger.setLevel(self.logging_level)

        if not isinstance(bot, DiscordClient):
            raise TypeError(f"Expected class deriving from discord.Client for arg bot not {bot.__class__.__qualname__}")
        if not isinstance(token, str):
            raise TypeError(f"Expected str for arg token not {token.__class__.__qualname__}")

        self.bot: DiscordClient = bot
        self.key: str = token
        self.base: str = "https://api.statcord.com/v3/"
        self.session: aiohttp.ClientSession = aiohttp.ClientSession(loop=bot.loop)

        self.mem: Optional[bool] = None
        if kwargs.get("mem"):
            if isinstance(kwargs["mem"], bool):
                self.mem = kwargs["mem"]
            else:
                raise TypeError(f"Memory config: expected type bool not {kwargs['mem'].__class__.__qualname__}.")
        else:
            self.mem = True

        self.cpu: Optional[bool] = None
        if kwargs.get("cpu"):
            if isinstance(kwargs["cpu"], bool):
                self.cpu = kwargs["cpu"]
            else:
                raise TypeError(f"CPU config: expected type bool not {kwargs['cpu'].__class__.__qualname__}")
        else:
            self.cpu = True

        self.bandwidth: Optional[bool] = None
        if kwargs.get("bandwidth"):
            if isinstance(kwargs["bandwidth"], bool):
                self.bandwidth = kwargs["bandwidth"]
            else:
                raise TypeError("Bandwidth config: expected type bool")
        else:
            self.bandwidth = True

        self.debug: Optional[bool] = None
        if kwargs.get("debug"):
            if isinstance(kwargs["debug"], bool):
                self.debug = kwargs["debug"]
            else:
                raise TypeError(f"Debug config: expected type bool not {kwargs['debug'].__class__.__qualname__}")
        else:
            self.debug = False

        self.custom1: Optional[Callable[[], Awaitable[str]]] = kwargs.get("custom1") or None
        self.custom2: Optional[Callable[[], Awaitable[str]]] = kwargs.get("custom2") or None
        self.active: List[int] = []
        self.commands: int = 0
        self.popular: List[Dict[str, Union[str, int]]] = []
        self.previous_bandwidth: int = psutil.net_io_counters().bytes_sent + psutil.net_io_counters().bytes_recv

        self.logger.debug("Statcord debug mode enabled")

    @staticmethod
    def __headers() -> Dict[str, str]:
        return {'Content-Type': 'application/json'}

    def _trace(self) -> Dict[str,Any]:
        return {}

    # noinspection SpellCheckingInspection
    async def __handle_response(self, res: aiohttp.ClientResponse) -> dict:
        try:
            msg = await res.json() or {}
        except aiohttp.ContentTypeError:
            msg = await res.text()
        self.logger.debug(f"Handling response: {msg!s}")
        status = res.status
        if status == 200:
            self.logger.debug("Code 200 OK")
            return msg
        elif status == 429:
            raise exceptions.TooManyRequests(status, msg, int(msg.get("timeleft") or '600'))
        else:
            self.logger.debug(f"Code {status}")
            raise exceptions.RequestFailure(status=status, response=msg)

    @property
    def servers(self) -> str:
        return str(len(self.bot.guilds))

    @property
    def _user_counter(self) -> Iterable[int]:
        for g in self.bot.guilds:
            with contextlib.suppress(AttributeError):
                yield g.member_count

    @property
    def users(self) -> str:
        return str(sum(self._user_counter))

    async def post_data(self) -> None:
        bot_id = str(self.bot.user.id)
        commands = str(self.commands)

        if self.mem:
            mem = psutil.virtual_memory()
            mem_used = str(mem.used)
            mem_load = str(mem.percent)
        else:
            mem_used = "0"
            mem_load = "0"

        if self.cpu:
            cpu_load = str(psutil.cpu_percent())
        else:
            cpu_load = "0"

        if self.bandwidth:
            current_bandwidth = psutil.net_io_counters().bytes_sent + psutil.net_io_counters().bytes_recv
            bandwidth = str(current_bandwidth - self.previous_bandwidth)
            self.previous_bandwidth = current_bandwidth
        else:
            bandwidth = "0"

        if self.custom1:
            # who knows why PyCharm gets annoyed there /shrug
            # noinspection PyCallingNonCallable
            custom1 = str(await self.custom1())
        else:
            custom1 = "0"

        if self.custom2:
            # who knows why PyCharm gets annoyed there /shrug
            # noinspection PyCallingNonCallable
            custom2 = str(await self.custom2())
        else:
            custom2 = "0"

        # noinspection SpellCheckingInspection
        data = {
            "id": bot_id,
            "key": self.key,
            "servers": self.servers,
            "users": self.users,
            "commands": commands,
            "active": self.active,
            "popular": self.popular,
            "memactive": mem_used,
            "memload": mem_load,
            "cpuload": cpu_load,
            "bandwidth": bandwidth,
            "custom1": custom1,
            "custom2": custom2,
        }

        data.update(self._trace())

        self.logger.debug(f"Posting stats: {data!s}")
        self.active = []
        self.commands = 0
        self.popular = []

        async with self.session.post(url=self.base + "stats", json=data, headers=self.__headers()) as resp:
            await self.__handle_response(resp)

    def start_loop(self) -> None:
        self.bot.loop.create_task(self.__loop())

    def command_run(self, ctx: Context) -> None:
        self.commands += 1
        if ctx.author.id not in self.active:
            self.active.append(ctx.author.id)

        command = ctx.command.name
        self.logger.debug(f"Command {command} has been run by {ctx.author.id}")
        for cmd in filter(lambda x: x["name"] == command, self.popular):
            cmd["count"] = str(int(cmd["count"]) + 1)
            break
        else:
            self.popular.append({"name": command, "count": "1"})

    async def __loop(self) -> None:
        """
        The internal loop used for automatically posting server/guild count stats
        """
        await self.bot.wait_until_ready()
        if self.debug:
            self.logger.debug("Statcord Auto Post has started!")
        while not self.bot.is_closed():
            self.logger.debug("Posting stats...")
            try:
                await self.post_data()
            except Exception as e:
                if isinstance(e,exceptions.TooManyRequests):
                    self.logger.debug(f"Code 429 Ratelimited. Waiting {e.wait} seconds before retrying.")
                    await asyncio.sleep(e.wait)
                    continue
                if isinstance(e,exceptions.RequestFailure):
                    self.logger.debug(f"HTTP {e.status} Server Error. Waiting 10 minutes before retrying.")
                    await asyncio.sleep(600)
                    continue
                self.logger.debug("Got error, dispatching error handlers.")
                await self.on_error(e)
            else:
                self.logger.debug("Posted stats successfully.")
            await asyncio.sleep(60)

    async def on_error(self, error: BaseException) -> None:
        self.logger.exception("Statcord posting exception occurred.", exc_info=error)
