import asyncio
import random
from collections.abc import Callable
from enum import StrEnum
from typing import ClassVar

from loguru import logger
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)
from twitchio import Channel, Message
from twitchio.ext import commands, routines

from huesoporro.actions.quotes.create_quote_action import CreateQuoteAction
from huesoporro.actions.quotes.get_random_quote import GetRandomQuoteAction
from huesoporro.infra.repos import ChatbotRepo, QuoteRepo
from huesoporro.libs.db import MarkovDatabase
from huesoporro.models import Chatbot, User
from huesoporro.settings import Settings
from huesoporro.svc.backoff_service import BackoffService
from huesoporro.svc.generate import SentenceGeneratorSvc
from huesoporro.svc.hello import get_hello_generator_svc
from huesoporro.svc.is_mod import IsModSvc
from huesoporro.svc.quotes_svcs import CreateQuoteSvc, GetRandomQuoteSvc
from huesoporro.svc.store import SentenceStorerSvc


class Bot(commands.Bot):
    def __init__dependencies(self, channel: str, settings: Settings):
        self.quote_repo = QuoteRepo(s=settings)
        self.chatbot_repo = ChatbotRepo(s=settings)
        self.get_random_quote_action = GetRandomQuoteAction(
            get_random_quote_svc=GetRandomQuoteSvc(repo=self.quote_repo)
        )
        self.create_quote_action = CreateQuoteAction(
            create_quote_svc=CreateQuoteSvc(repo=self.quote_repo),
            is_mod_svc=IsModSvc(repo=self.chatbot_repo),
        )
        self.generate_svc = SentenceGeneratorSvc(db=MarkovDatabase(channel=channel))
        self.hello_svc = get_hello_generator_svc()

    def __init__(self, user: User, chatbot: Chatbot, channel: str, settings: Settings):
        super().__init__(
            token=user.twitch_access_token, prefix="!", initial_channels=[channel]
        )
        self.__init__dependencies(channel=channel, settings=settings)
        self.channel = channel
        self.user = user
        self.chatbot = chatbot
        self.quote_routine = routines.routine(
            seconds=chatbot.automatic_quote_timer, wait_first=True
        )(self.send_quote)
        self.generation_routine = routines.routine(
            seconds=chatbot.automatic_generation_timer, wait_first=True
        )(self.send_generation)

    async def event_ready(self):
        logger.info(f"Logged in as {self.nick}")
        logger.info(f"User id is {self.user_id}")

    @commands.command(aliases=["g"])
    async def generate(self, ctx: commands.Context, *, words: str | None = None):
        sentence = await self.generate_svc.run(words)
        if not sentence:
            logger.warning(
                f"Could not generate sentence for {words or 'no words provided'}"
            )
            return
        await ctx.send(sentence)
        # Wait for the specified time
        await asyncio.sleep(60)

    @commands.command(aliases=["qadd"])
    async def add_quote(self, ctx: commands.Context, *, quote: str):
        # extract author from quote; the author is the last word
        quote, author = quote.rsplit(" ", 1)
        new_quote = await self.create_quote_action.run(
            user=self.user,
            channel=self.channel,
            quote=quote,
            author=author,
            username=ctx.author.name,
        )
        if new_quote:
            await ctx.send(new_quote.as_pretty_saved())
        else:
            await ctx.send(f"@{ctx.author.name} no tienes permisos para añadir citas")

    @commands.command(aliases=["q", "quote"])
    async def get_random_quote(self, ctx: commands.Context):
        quote = await self.get_random_quote_action.run(channel_name=self.channel)
        if quote:
            await ctx.send(quote.as_pretty())

    def get_channel_conn(self) -> Channel:
        return Channel(name=self.channel, websocket=self._connection)

    async def send_quote(self):
        quote = await self.get_random_quote_action.run(channel_name=self.channel)
        if quote:
            channel = self.get_channel_conn()
            if channel:
                logger.info(f"Sending random quote {quote.quote}")
                await channel.send(quote.quote)

    async def send_generation(self):
        sentence = await self.generate_svc.run()
        if not sentence:
            return
        channel = self.get_channel_conn()
        logger.info(f"Sending generated sentence {sentence}")
        await channel.send(sentence)

    def start_routines(self):
        if self.chatbot.automatic_quote_timer > 0:
            logger.info("Starting quote routine")
            self.quote_routine.start(stop_on_error=False)
        if self.chatbot.automatic_generation_timer > 0:
            logger.info("Starting generation routine")
            self.generation_routine.start(stop_on_error=False)

    def stop_routines(self):
        logger.info("Stopping routines")
        self.quote_routine.cancel()
        self.generation_routine.cancel()


class HelloMessagesCog(commands.Cog):
    hello_patterns: ClassVar[list[str]] = ["hola", "HOLA", "hiii", "ayo"]

    def __init__(self, bot):
        self.bot = bot
        self.hello_svc = get_hello_generator_svc()

    @commands.Cog.event()
    async def event_message(self, message):
        if not message.author:
            return
        if message.content in self.hello_patterns:
            hello = self.hello_svc.run(message.author.name)
            if hello:
                await message.channel.send(hello)


class MessageType(StrEnum):
    COMMAND = "COMMAND"
    HELLO = "HELLO"
    YES = "YES"
    WHAT = "WHAT"
    LAUGH = "LAUGH"
    ANO_SUFFIX = "ANO_SUFFIX"
    OTHER = "OTHER"


class MessageHandler:
    """Handles different types of messages with their corresponding responses"""

    def __init__(self, channel_send_func: Callable):
        self.laugh_patterns = [
            "om",
            "KEK",
            "LuL",
            "LUL",
            "OMEGALUL",
            "kek",
            "keking",
            "KEKW",
            "OMEGADANCEBUTFAST",
            "xdd",
            "xdding",
        ]
        self.ano_suffix_reply_patterns = [
            "me la agarras con la mano. venga, tira",
            "me la agarras con la mano, espabila",
            "me la agarras con la mano y te falta calle",
            "vegetasmile",
        ]
        self.send = channel_send_func

    def get_message_type(self, content: str) -> MessageType:
        """Determines the type of message based on its content"""
        if content.startswith("!"):
            return MessageType.COMMAND
        if content in ["Yes", "yes"]:
            return MessageType.YES
        if content.startswith("WHAT"):
            return MessageType.WHAT
        if content.endswith("ano") and len(content) > 3:  # noqa: PLR2004
            return MessageType.ANO_SUFFIX
        if content in self.laugh_patterns:
            return MessageType.LAUGH
        return MessageType.OTHER

    def handle_laugh(self) -> str:
        return random.choice(self.laugh_patterns)  # noqa: S311

    def handle_ano_suffix(self) -> str:
        return random.choice(self.ano_suffix_reply_patterns)  # noqa: S311


class SaveMessagesCog(commands.Cog):
    def __init__(self, bot: Bot):
        self.bot = bot
        self.store_svc = SentenceStorerSvc(db=MarkovDatabase(channel=bot.channel))
        self.generate_svc = SentenceGeneratorSvc(db=MarkovDatabase(channel=bot.channel))
        self.backoff_svc = BackoffService()
        self.message_handler = MessageHandler(self._send_message)

        self.send_functions = {
            MessageType.YES: self._create_typed_send("yes"),
            MessageType.WHAT: self._create_typed_send("what"),
            MessageType.LAUGH: self._create_typed_send("laugh"),
            MessageType.ANO_SUFFIX: self._create_typed_send("ano_suffix"),
        }

        for func in self.send_functions.values():
            self.backoff_svc.add_callable(func, backoff_seconds=10)

    def _create_typed_send(self, type_name: str):
        """Creates a send function for a specific message type"""

        async def typed_send(content: str):
            if hasattr(self, "current_message"):
                await self.current_message.channel.send(content)

        # Set a unique name for the function to ensure it's treated as distinct
        typed_send.__name__ = f"send_{type_name}"
        return typed_send

    async def _send_message(self, content: str):
        """Generic send message function (for non-backoff uses)"""
        if hasattr(self, "current_message"):
            await self.current_message.channel.send(content)

    def is_bot_mention(self, tok: str) -> bool:
        return tok.lower() == str(self.bot.nick).lower()

    async def _handle_bot_mention(self, message: Message) -> str | None:
        content = (message.content or "").strip()
        if not content:
            return None

        tokens = content.split()
        contains_mention = any(self.is_bot_mention(t) for t in tokens)
        if not contains_mention:
            return None

        # Find the first non-mention token as seed
        non_mention_tokens = (
            t.strip(".,!?;:") for t in tokens if not self.is_bot_mention(t)
        )
        seed = next((t for t in non_mention_tokens if t), None)

        if not seed:
            return None

        sentence = await self.generate_svc.run(seed)
        if not sentence:
            return None

        await message.channel.send(f"@{message.author.name} {sentence}")

        return sentence

    @commands.Cog.event()
    async def event_message(self, message):
        """Main message event handler"""
        if not message.author:
            return

        self.current_message = message

        await self.store_svc.run(message.content)

        # If the message contains a mention to this bot, reply by generating
        # a sentence from the first word that is not the bot username itself.
        if await self._handle_bot_mention(message):
            # If the bot actually replies with something, it should not try to send
            # any other type of reply
            return

        msg_type = self.message_handler.get_message_type(message.content)

        response = None

        match msg_type:
            case MessageType.COMMAND:
                return
            case MessageType.YES:
                response = "Indeed"
            case MessageType.WHAT:
                response = "WHAT Ramon"
            case MessageType.LAUGH:
                response = self.message_handler.handle_laugh()
            case MessageType.ANO_SUFFIX:
                response = (
                    f"@{message.author.name} {self.message_handler.handle_ano_suffix()}"
                )
            case MessageType.OTHER:
                return

        if response and msg_type in self.send_functions:
            await self.backoff_svc.call_async(self.send_functions[msg_type], response)


class BotsManager:
    def __init__(self, s: Settings):
        self.bots: dict[str, Bot] = {}
        self.s = s

    def add_bot(self, user: User, channel: str, chatbot: Chatbot):
        if user.username in self.bots:
            logger.info(f"Bot for {user.username} already exists")
            return

        logger.info(f"Adding bot for {user.username}")
        bot = Bot(user=user, channel=channel, chatbot=chatbot, settings=self.s)
        bot.add_cog(SaveMessagesCog(bot))
        bot.add_cog(HelloMessagesCog(bot))
        self.bots[user.username] = bot

    async def run_user_bot(self, user: User):
        if user.username not in self.bots:
            return None

        logger.info(f"Starting bot for {user.username}")

        bot = self.bots[user.username]

        @retry(
            stop=stop_after_attempt(5),
            wait=wait_exponential(multiplier=2, min=2, max=60),
            retry=retry_if_exception_type((ConnectionError, TimeoutError, OSError)),
        )
        async def start_bot_with_retry():
            await bot.start()

        task = asyncio.create_task(start_bot_with_retry())

        def on_bot_done(future):
            try:
                if future.cancelled():
                    logger.warning(f"Bot for {user.username} was cancelled")
                elif future.exception():
                    logger.error(
                        f"Bot for {user.username} failed: {future.exception()}"
                    )
                else:
                    logger.info(f"Bot for {user.username} stopped normally")
            except Exception as e:  # noqa: BLE001
                logger.error(f"Error in bot completion callback: {e}")

        task.add_done_callback(on_bot_done)

        bot.start_routines()

        return task

    async def run_user_bot2(self, user: User):
        if user.username not in self.bots:
            return

        logger.info(f"Starting bot for {user.username}")
        bot = self.bots[user.username]
        task = asyncio.create_task(bot.start())
        task.add_done_callback(
            lambda x: logger.info(f"Bot for {user.username} stopped")
        )

        bot.start_routines()

    async def stop_user_bot(self, user: User):
        if user.username not in self.bots:
            return
        bot = self.bots.pop(user.username)
        await bot.close()
        bot.stop_routines()
