import sys
import argparse
import tomlkit
import discord
import logging
import re
import random
import time
import asyncio
import math
from discord import Message, TextChannel, DMChannel
from discord.ext import commands
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from .ai_responder import AIMessage
from .openai_responder import OpenAIResponder
from typing import Optional, Union


class ConfigFileHandler(FileSystemEventHandler):
    def __init__(self, on_modified):
        self._on_modified = on_modified

    def on_modified(self, event):
        self._on_modified(event)


class FjerkroaBot(commands.Bot):
    def __init__(self, config_file: str):
        self.config_file = config_file
        self.config = self.load_config(self.config_file)
        intents = discord.Intents.default()
        intents.message_content = True
        intents.members = True
        intents.reactions = True
        self._re_user = re.compile(r"[<][@][!]?\s*([0-9]+)[>]")

        self.init_observer()
        self.init_aichannels()

        super().__init__(command_prefix="!", case_insensitive=True, intents=intents)

    def init_observer(self):
        self.observer = Observer()
        self.file_handler = ConfigFileHandler(self.on_config_file_modified)
        self.observer.schedule(self.file_handler, path=self.config_file, recursive=False)
        self.observer.start()

    def init_aichannels(self):
        self.airesponder = OpenAIResponder(self.config)
        self.aichannels = {chan_name: OpenAIResponder(self.config, chan_name) for chan_name in self.config['additional-responders']}

    def init_channels(self):
        if 'chat-channel' in self.config:
            self.chat_channel = self.channel_by_name(self.config['chat-channel'], no_ignore=True)
        else:
            self.chat_channel = None
        self.staff_channel = self.channel_by_name(self.config['staff-channel'], no_ignore=True)
        self.welcome_channel = self.channel_by_name(self.config['welcome-channel'], no_ignore=True)

    def init_boreness(self):
        if 'chat-channel' not in self.config:
            return
        self.last_activity_time = time.monotonic()
        self.loop.create_task(self.on_boreness())
        logging.info('Boreness initialised.')

    async def on_boreness(self):
        logging.info(f'Boreness started on channel: {repr(self.chat_channel)}')
        while True:
            if self.chat_channel is None:
                await asyncio.sleep(7)
                continue
            boreness_interval = float(self.config.get('boreness-interval', 12.0))
            elapsed_time = (time.monotonic() - self.last_activity_time) / 3600.0
            probability = 1 / (1 + math.exp(-1 * (elapsed_time - (boreness_interval / 2.0)) + math.log(1 / 0.2 - 1)))
            if random.random() < probability:
                prev_messages = [msg async for msg in self.chat_channel.history(limit=2)]
                last_author = prev_messages[1].author.id if len(prev_messages) > 1 else None
                if last_author and last_author != self.user.id:
                    logging.info(f'Borred with {probability} probability after {elapsed_time}')
                    boreness_prompt = self.config.get('boreness-prompt', 'Pretend that you just now thought of something, be creative.')
                    message = AIMessage('system', boreness_prompt, self.config.get('chat-channel', 'chat'), True, False)
                    try:
                        await self.respond(message, self.chat_channel)
                    except Exception as err:
                        logging.warning(f"Failed to activate borringness: {repr(err)}")
            await asyncio.sleep(7)

    async def on_ready(self):
        self.init_channels()
        self.init_boreness()
        logging.info(f"We have logged in as {self.user}"
                     f" ({repr(self.staff_channel)}, {repr(self.welcome_channel)}, {repr(self.chat_channel)})")

    async def on_member_join(self, member):
        logging.info(f"User {member.name} joined")
        if self.welcome_channel is not None:
            msg = AIMessage(member.name,
                            self.config['join-message'].replace('{name}', member.name),
                            str(self.welcome_channel.name),
                            historise_question=False)
            await self.respond(msg, self.welcome_channel)

    async def on_message(self, message: Message) -> None:
        if self.user is not None and message.author.id == self.user.id:
            return
        if not isinstance(message.channel, (TextChannel, DMChannel)):
            return
        await self.handle_message_through_responder(message)

    async def on_reaction_operation(self, reaction, user, operation):
        logging.info(f'{operation} reaction {reaction} by {user}.')
        airesponder = self.get_ai_responder(self.get_channel_name(reaction.message.channel))
        message = str(reaction.message.content) if reaction.message.content else ''
        if len(message) > 1:
            await airesponder.memoize_reaction(reaction.message.author.name, user.name, operation, str(reaction.emoji), message)

    async def on_reaction_add(self, reaction, user):
        await self.on_reaction_operation(reaction, user, 'adding')

    async def on_reaction_remove(self, reaction, user):
        await self.on_reaction_operation(reaction, user, 'removing')

    async def on_reaction_clear(self, reaction, user):
        await self.on_reaction_operation(reaction, user, 'clearing')

    def on_config_file_modified(self, event):
        if event.src_path == self.config_file:
            new_config = self.load_config(self.config_file)
            if repr(new_config) != repr(self.config):
                logging.info(f"config file {self.config_file} changed, reloading.")
                self.config = new_config
                self.airesponder.config = self.config
                for responder in self.aichannels.values():
                    responder.config = self.config

    @classmethod
    def load_config(self, config_file: str = "config.toml"):
        with open(config_file, encoding='utf-8') as file:
            return tomlkit.load(file)

    def channel_by_name(self,
                        channel_name: Optional[str],
                        fallback_channel: Optional[Union[TextChannel, DMChannel]] = None,
                        no_ignore: bool = False
                        ) -> Optional[Union[TextChannel, DMChannel]]:
        """Fetch a channel by name, or return the fallback channel if not found."""
        if channel_name is None:
            return fallback_channel
        if channel_name.startswith("#"):
            channel_name = channel_name[1:]
        if not no_ignore and channel_name in self.config.get("ignore-channels", []):
            return fallback_channel
        for guild in self.guilds:
            channel = discord.utils.get(guild.channels, name=channel_name)
            if channel is not None and isinstance(channel, TextChannel):
                return channel
        return fallback_channel

    def get_channel_name(self, channel):
        if isinstance(channel, DMChannel) and channel.recipient is not None:
            return str(channel.recipient.name)
        return str(channel.id) if isinstance(channel, DMChannel) else str(channel.name)

    def ignore_message(self, channel_name, message):
        return channel_name in self.config.get("ignore-channels", []) and not message.direct

    def log_message_action(self, action, message, channel_name):
        logging.info(f"{action} message {repr(message)} for channel {channel_name}")

    def get_ai_responder(self, channel_name):
        return self.aichannels[channel_name] if channel_name in self.aichannels else self.airesponder

    async def handle_message_through_responder(self, message):
        """Handle a message through the AI responder"""
        message_content = str(message.content).strip()
        if message.reference and message.reference.resolved and type(message.reference.resolved.content) == str:
            reference_content = str(message.reference.resolved.content).replace("\n", "> \n")
            message_content = f'> {reference_content}\n\n{message_content}'
        if len(message_content) < 1:
            return
        for ma_user in self._re_user.finditer(message_content):
            uid = int(ma_user.group(1))
            for guild in self.guilds:
                user = guild.get_member(uid)
                if user is not None:
                    break
            if user is not None:
                message_content = re.sub(f'[<][@][!]? *{uid} *[>]', f'@{user.name}', message_content)
        channel_name = self.get_channel_name(message.channel)
        msg = AIMessage(message.author.name, message_content, channel_name, self.user in message.mentions)
        await self.respond(msg, message.channel)

    async def send_message_with_typing(self, airesponder, channel, message):
        """Send the user message to the AI responder with typing animation in discord"""
        async with channel.typing():
            return await airesponder.send(message)

    async def send_answer_with_typing(self, response, answer_channel, airesponder):
        """Send an answer from AI to discord channel with typing animation"""
        async with answer_channel.typing():
            if response.picture is not None:
                # Generate the image with the AI and send it with the answer
                images = [discord.File(fp=await airesponder.draw(response.picture), filename="image.png")]
                await answer_channel.send(response.answer, files=images, suppress_embeds=True)
            else:
                await answer_channel.send(response.answer, suppress_embeds=True)
            self.last_activity_time = time.monotonic()

    async def respond(
        self,
        message: AIMessage,  # Incoming message object with user message and metadata
        channel: Union[TextChannel, DMChannel]  # Channel (Text or Direct Message) the message is coming from
    ) -> None:
        """Handle a message from a user with an AI responder"""

        # Get the name of the channel where the message was sent
        channel_name = self.get_channel_name(channel)

        # Check if the message should be ignored, log and return if so
        if self.ignore_message(channel_name, message):
            self.log_message_action("ignore", message, channel_name)
            return

        # In case the message shouldn't be ignored, log the handling action
        self.log_message_action("handle", message, channel_name)

        # Get the AI responder based on the channel name
        airesponder = self.get_ai_responder(channel_name)

        # Send the user message to the AI responder, with typing indicators
        response = await self.send_message_with_typing(airesponder, channel, message)

        # Check if the user tried to hack the system, log if so
        if response.hack:
            logging.warning(f"User {message.user} tried to hack the system.")
            if response.staff is None:
                response.staff = f"User {message.user} try to hack the AI."

        # If there is a staff message, send it to the staff channel, with typing indicators
        if response.staff is not None and self.staff_channel is not None:
            async with self.staff_channel.typing():
                await self.staff_channel.send(response.staff, suppress_embeds=True)

        # Get the answer channel based on the requested response channel
        answer_channel = self.channel_by_name(response.channel, channel)

        # If there is no need for an answer or the answer channel is not found, return
        if not (response.answer_needed and answer_channel is not None):
            return

        # Send the AI's answer to the specified answer channel, with typing indicators
        await self.send_answer_with_typing(response, answer_channel, airesponder)

    async def close(self):
        self.observer.stop()
        await super().close()


def main() -> int:
    from .bot_logging import setup_logging
    setup_logging()
    parser = argparse.ArgumentParser(description='Fjerkroa AI bot')
    parser.add_argument('--config', type=str, default='config.toml', help='Config file.')
    args = parser.parse_args()

    config = FjerkroaBot.load_config(args.config)
    bot = FjerkroaBot(args.config)
    bot.run(config["discord-token"])

    return 0


if __name__ == "__main__":
    sys.exit(main())
