import asyncio
import datetime
import json
import mimetypes
import time
from enum import Enum
from http import HTTPStatus
from typing import List, Dict, Any, Union, Tuple, BinaryIO

from httpx._types import RequestFiles
from pydantic import PrivateAttr

from sirius import common
from sirius.common import DataClass
from sirius.communication.discord import constants
from sirius.communication.discord.exceptions import ServerNotFoundException, DuplicateServersFoundException, \
    RoleNotFoundException
from sirius.constants import EnvironmentSecret
from sirius.exceptions import OperationNotSupportedException
from sirius.http_requests import AsyncHTTPSession, HTTPResponse, ClientSideException

default_bot: Union["Bot", None] = None


class ServerName(Enum):
    VITA = "Vita"
    AURUM = "Aurum"
    AORTA = "Aorta"


class AortaTextChannels(Enum):
    DEBUG = "logs"
    NOTIFICATION = "notification"
    WISE = "wise"


class RoleType(Enum):
    EVERYONE = "@everyone"
    BOT = "Bot"
    OTHER = ""


class DiscordMedia(DataClass):
    media: bytes
    file_extension: str


class DiscordHTTPSession(AsyncHTTPSession):

    @staticmethod
    def _get_headers(default_headers: Dict[str, Any] | None = None) -> Dict[str, Any]:
        if not default_headers:
            default_headers = {}

        default_headers.setdefault("Authorization", f"Bot {common.get_environmental_secret(EnvironmentSecret.DISCORD_BOT_TOKEN)}")
        return default_headers

    async def get(self, url: str, query_params: Dict[str, Any] | None = None, headers: Dict[str, Any] | None = None) -> HTTPResponse:
        headers = DiscordHTTPSession._get_headers(headers)

        try:
            return await super().get(url, query_params, headers)
        except ClientSideException as e:
            http_response: HTTPResponse = e.data["http_response"]

            if http_response.response_code == HTTPStatus.TOO_MANY_REQUESTS:
                await asyncio.sleep(http_response.data["retry_after"] + 0.1)
                return await self.get(url, query_params, headers)

            raise e

    async def put(self, url: str, data: Dict[str, Any], headers: Dict[str, Any] | None = None, file: RequestFiles | None = None) -> HTTPResponse:
        headers = DiscordHTTPSession._get_headers(headers)

        try:
            return await super().put(url, data, headers)
        except ClientSideException as e:
            http_response: HTTPResponse = e.data["http_response"]

            if http_response.response_code == HTTPStatus.TOO_MANY_REQUESTS:
                await asyncio.sleep(http_response.data["retry_after"] + 0.1)
                return await self.put(url, data, headers)

            raise e

    async def post(self, url: str, data: Dict[str, Any] | None = None, headers: Dict[str, Any] | None = None, files: RequestFiles | None = None) -> HTTPResponse:
        headers = DiscordHTTPSession._get_headers(headers)

        try:
            return await super().post(url, data, headers, files)
        except ClientSideException as e:
            http_response: HTTPResponse = e.data["http_response"]

            if http_response.response_code == HTTPStatus.TOO_MANY_REQUESTS:
                await asyncio.sleep(http_response.data["retry_after"] + 0.1)
                return await self.post(url, data, headers)

            raise e

    async def delete(self, url: str, headers: Dict[str, Any] | None = None) -> HTTPResponse:
        headers = DiscordHTTPSession._get_headers(headers)

        try:
            return await super().delete(url, headers)
        except ClientSideException as e:
            http_response: HTTPResponse = e.data["http_response"]

            if http_response.response_code == HTTPStatus.TOO_MANY_REQUESTS:
                await asyncio.sleep(http_response.data["retry_after"] + 0.1)
                return await self.delete(url, headers)

            raise e


class DiscordDefaults(DataClass):

    @staticmethod
    async def send_message(text_channel_name: str, message: str) -> None:
        global default_bot
        default_bot = await Bot.get() if default_bot is None else default_bot
        server: Server = await default_bot.get_server()
        text_channel: TextChannel = await server.get_text_channel(text_channel_name)
        text_channel.send_message(message)

    @classmethod
    async def notify(cls, message: str) -> None:
        await DiscordDefaults.send_message(AortaTextChannels.NOTIFICATION.value, message)


class Bot(DataClass):
    id: int
    username: str
    name: str
    server_list: List["Server"] = []
    _http_session: DiscordHTTPSession = PrivateAttr()

    @property
    def http_session(self) -> DiscordHTTPSession:
        return self._http_session

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    async def _initialize(self) -> None:
        for server in await Server.get_all_servers(self):
            if server in self.server_list:
                existing_server: Server = next(filter(lambda s: s.id == server.id, self.server_list))
                existing_server.__dict__.update(**server.model_dump())
            else:
                self.server_list.append(server)

    async def get_server(self, server_name: str | None = None) -> "Server":
        if server_name is None:
            server_name = Server.get_default_server_name()

        server_list: List[Server] = list(filter(lambda s: s.name == server_name, self.server_list))
        if len(server_list) == 1:
            return server_list[0]
        elif len(server_list) == 0:
            raise ServerNotFoundException(f"Server not found\n"
                                          f"Bot Name: {self.username}\n"
                                          f"Server Name: {server_name}\n")
        else:
            raise DuplicateServersFoundException(f"Duplicate servers found\n"
                                                 f"Bot Name: {self.username}\n"
                                                 f"Server Name: {server_name}\n"
                                                 f"Number of servers: {len(server_list)}\n")

    @staticmethod
    async def get() -> "Bot":
        url: str = constants.ENDPOINT__BOT__GET_BOT
        http_session: DiscordHTTPSession = DiscordHTTPSession(url)
        response: HTTPResponse = await http_session.get(url)

        bot: Bot = Bot.model_construct(id=response.data["id"],
                                       username=response.data["username"],
                                       name=response.data["global_name"])
        bot._http_session = http_session
        await bot._initialize()

        return bot


class Server(DataClass):
    id: int
    name: str
    text_channel_list: List["TextChannel"] = []
    user_list: List["User"] = []
    role_list: List["Role"] = []
    bot: Bot

    @property
    def http_session(self) -> DiscordHTTPSession:
        return self.bot.http_session

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    async def _initialize(self) -> None:
        for text_channel in await TextChannel.get_all(self):
            if text_channel in self.text_channel_list:
                existing_text_channel: TextChannel = next(filter(lambda t: t.id == text_channel.id, self.text_channel_list))
                existing_text_channel.__dict__.update(**text_channel.model_dump())
            else:
                self.text_channel_list.append(text_channel)

        for user in await User.get_all(self):
            if user in self.user_list:
                existing_user: User = next(filter(lambda u: u.id == user.id, self.user_list))
                existing_user.__dict__.update(user.model_dump())
            else:
                self.user_list.append(user)

        for role in await Role.get_all(self):
            if role in self.role_list:
                existing_role: Role = next(filter(lambda r: r.id == role.id, self.role_list))
                existing_role.__dict__.update(role.model_dump())
            else:
                self.role_list.append(role)

    async def get_text_channel(self, text_channel_name: str, is_public_channel: bool = False) -> "TextChannel":
        text_channel_list: List[TextChannel] = list(filter(lambda t: t.name == text_channel_name, self.text_channel_list))

        if len(text_channel_list) == 1:
            return text_channel_list[0]
        elif len(text_channel_list) == 0:
            text_channel: TextChannel = await TextChannel.create(text_channel_name, self, is_public_channel=is_public_channel)
            self.text_channel_list.append(text_channel)
            return text_channel
        else:
            raise DuplicateServersFoundException(f"Duplicate channels found\n"
                                                 f"Server Name: {self.name}\n"
                                                 f"Channel Name: {text_channel_name}\n"
                                                 f"Number of channels: {len(text_channel_list)}\n")

    async def get_user(self, username: str) -> "User":
        username = username.lower()
        self.user_list = await User.get_all(self) if len(self.user_list) == 0 else self.user_list

        try:
            return next(filter(lambda u: u.username == username, self.user_list))
        except StopIteration:
            raise RoleNotFoundException(f"User not found\n"
                                        f"Server Name: {self.name}\n"
                                        f"Username: {username}")

    async def get_role(self, role_type: RoleType) -> "Role":
        if role_type == RoleType.OTHER:
            raise OperationNotSupportedException("OTHER Role Type searches are not allowed")

        self.role_list = await Role.get_all(self) if len(self.role_list) == 0 else self.role_list

        try:
            return next(filter(lambda r: r.role_type == role_type, self.role_list))
        except StopIteration:
            raise RoleNotFoundException(f"User not found\n"
                                        f"Server Name: {self.name}\n"
                                        f"Role Type: {role_type.value}")

    @classmethod
    async def get_all_servers(cls, bot: Bot) -> List["Server"]:
        server_list: List[Server] = []
        response: HTTPResponse = await bot.http_session.get(constants.ENDPOINT__SERVER__GET_ALL_SERVERS)

        for data in response.data:
            server: Server = Server.model_construct(id=data["id"],
                                                    name=data["name"])
            server.bot = bot
            await server._initialize()
            server_list.append(server)

        return server_list

    @staticmethod
    def get_default_server_name() -> str:
        server_name: str = common.get_application_name()
        return server_name if common.is_production_environment() else f"{server_name} [Dev]"


class User(DataClass):
    id: int
    username: str
    name: str | None
    is_bot: bool
    server: Server

    @property
    def http_session(self) -> DiscordHTTPSession:
        return self.server.bot.http_session

    @staticmethod
    async def get_all(server: Server) -> List["User"]:
        url: str = constants.ENDPOINT__SERVER__GET_ALL_USERS.replace("$serverID", str(server.id))
        http_session: DiscordHTTPSession = DiscordHTTPSession(url)
        response: HTTPResponse = await http_session.get(url)
        return [User(id=data["user"]["id"],
                     username=data["user"]["username"],
                     name=data["user"]["global_name"],
                     is_bot=data["user"]["bot"] if "bot" in data["user"] else False,
                     server=server) for data in response.data]


class Role(DataClass):
    id: int
    role_type: RoleType
    permissions: str
    server: Server

    @property
    def http_session(self) -> DiscordHTTPSession:
        return self.server.bot.http_session

    @staticmethod
    async def get_all(server: Server) -> List["Role"]:
        role_list: List[Role] = []
        url: str = constants.ENDPOINT__SERVER__GET_ALL_ROLES.replace("$serverID", str(server.id))
        http_session: DiscordHTTPSession = DiscordHTTPSession(url)
        response: HTTPResponse = await http_session.get(url)

        for data in response.data:
            try:
                role_type: RoleType = RoleType(data["name"])
            except ValueError:
                role_type = RoleType.OTHER

            role_list.append(Role(id=data["id"],
                                  role_type=role_type,
                                  permissions=data["permissions"],
                                  server=server))
        return role_list


class Channel(DataClass):
    id: int
    name: str
    type: int
    server: Server

    @property
    def http_session(self) -> DiscordHTTPSession:
        return self.server.bot.http_session

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    @classmethod
    async def get_all_channels(cls, server: Server) -> List["Channel"]:
        url: str = constants.ENDPOINT__CHANNEL__CREATE_CHANNEL_OR_GET_ALL_CHANNELS.replace("$serverID", str(server.id))
        response: HTTPResponse = await server.http_session.get(url)

        return [Channel(id=data["id"],
                        name=data["name"],
                        type=data["type"],
                        server=server) for data in response.data]

    @classmethod
    async def create(cls, channel_name: str, server: Server, type_id: int, is_public_channel: bool = False) -> "Channel":
        url: str = constants.ENDPOINT__CHANNEL__CREATE_CHANNEL_OR_GET_ALL_CHANNELS.replace("$serverID", str(server.id))
        data: Dict[str, Any] = {"name": channel_name, "type": type_id}

        if not is_public_channel:
            data["permission_overwrites"] = [{
                "id": str((await server.get_role(RoleType.EVERYONE)).id),
                "type": 0,
                "allow": 0,
                "deny": 1024
            }]

        response: HTTPResponse = await server.http_session.post(url, data=data)
        return Channel(id=response.data["id"],
                       name=response.data["name"],
                       type=response.data["type"],
                       server=server)


class TextChannel(Channel):

    def __init__(self, **kwargs: Any) -> None:
        super().__init__(**kwargs)

    def send_message(self, message: str, media_list: List[DiscordMedia] | None = None) -> None:
        data: Dict[str, Any] = {"content": message}
        files: Dict[str, Any] = {}
        processed_files: Dict[str, Tuple[str, BinaryIO, str]] = {}
        url: str = constants.ENDPOINT__CHANNEL__SEND_MESSAGE.replace("$channelID", str(self.id))

        if common.is_ci_cd_pipeline_environment():
            return

        if media_list:
            index: int = 0
            for media in media_list:
                files[f"files[{index}]"] = (f"media.{media.file_extension.replace(".", "")}", media.media)

            data = {"payload_json": json.dumps(data)} if data else {}
            for key, (filename, fileobj) in files.items():
                mime_type, _ = mimetypes.guess_type(filename)
                processed_files[key] = (filename, fileobj, mime_type or "application/octet-stream")

        asyncio.ensure_future(self.http_session.post(url, data=data, files=processed_files))

    async def delete(self) -> None:
        await self.http_session.delete(constants.ENDPOINT__CHANNEL__DELETE.replace("$channelID", str(self.id)))
        self.server.text_channel_list.remove(self)

    @classmethod
    async def get_all(cls, server: Server) -> List["TextChannel"]:
        text_channel_list: List[TextChannel] = []
        channel_list: List[Channel] = list(filter(lambda c: c.type == 0, await Channel.get_all_channels(server)))

        for channel in channel_list:
            text_channel: TextChannel = TextChannel.model_construct(**channel.model_dump(exclude={"server"}))
            text_channel.server = server
            text_channel_list.append(text_channel)

        return text_channel_list

    @classmethod
    async def create(cls, text_channel_name: str, server: Server, type_id: int = 0, is_public_channel: bool = False) -> "TextChannel":
        channel: Channel = await Channel.create(text_channel_name, server, type_id, is_public_channel)
        text_channel: TextChannel = TextChannel.model_construct(**channel.model_dump(exclude={"server"}))
        text_channel.server = server
        return text_channel


class Author(DataClass):
    id: int
    display_name: str
    username: str
    is_bot: bool


def get_timestamp_string(timestamp: datetime.datetime | datetime.date) -> str:
    return f"<t:{str(int(time.mktime(timestamp.timetuple())))}:T>"
