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

from loguru import logger
from twitchio import Channel
from twitchio.ext import commands, routines

from src.huesoporro.actions.get_random_quote import GetRandomQuoteAction
from src.huesoporro.actions.store_quote import StoreQuoteAction
from src.huesoporro.api.dependencies import get_settings
from src.huesoporro.infra.db import Database
from src.huesoporro.infra.repos import QuoteRepo
from src.huesoporro.libs.db import Database as MarkovDB
from src.huesoporro.models import ChatbotSettings, User
from src.huesoporro.svc.backoff_service import BackoffService
from src.huesoporro.svc.generate import SentenceGeneratorSvc
from src.huesoporro.svc.get_random_quote import RandomQuoteGetterSvc
from src.huesoporro.svc.hello import HelloGeneratorSvc
from src.huesoporro.svc.is_mod import IsModSvc
from src.huesoporro.svc.quote_storer_svc import QuoteStorerSvc
from src.huesoporro.svc.store import SentenceStorerSvc


class Bot(commands.Bot):
    def __init__(self, user: User, chatbot_settings: ChatbotSettings, channel: str):
        super().__init__(
            token=user.twitch_access_token, prefix="!", initial_channels=[channel]
        )
        self.channel = channel
        self.user = user
        self.generate_svc = SentenceGeneratorSvc(db=MarkovDB(channel=channel))
        self.hello_svc = HelloGeneratorSvc()
        db = Database()
        self.quote_repo = QuoteRepo(s=get_settings())
        self.get_random_quote_svc = RandomQuoteGetterSvc(quote_repo=self.quote_repo)
        self.get_random_quote_action = GetRandomQuoteAction(
            quote_getter_svc=self.get_random_quote_svc
        )
        self.store_quote_action = StoreQuoteAction(
            quote_storer_svc=QuoteStorerSvc(quote_repo=self.quote_repo),
            is_mod_svc=IsModSvc(db=db),
        )
        self.cbs = chatbot_settings
        self.quote_routine = routines.routine(
            seconds=chatbot_settings.automatic_quote_timer, wait_first=True
        )(self.send_quote)
        self.generation_routine = routines.routine(
            seconds=chatbot_settings.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=["h"])
    async def hello(self, ctx: commands.Context, username: str | None = None):
        username = username or ctx.author.name
        await ctx.send(self.hello_svc.run(username))

    @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)

    @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.store_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()
            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.cbs.automatic_quote_timer > 0:
            logger.info("Starting quote routine")
            self.quote_routine.start(stop_on_error=False)
        if self.cbs.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 MessageType(StrEnum):
    COMMAND = "COMMAND"
    HELLO = "HELLO"
    YES = "YES"
    WHAT = "WHAT"
    LAUGH = "LAUGH"
    OTHER = "OTHER"


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

    def __init__(self, channel_send_func: Callable):
        self.hello_patterns = ["hola", "HOLA", "hiii", "ayo"]
        self.laugh_patterns = [
            "om",
            "KEK",
            "LuL",
            "LUL",
            "OMEGALUL",
            "kek",
            "keking",
            "KEKW",
            "OMEGADANCEBUTFAST",
        ]
        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 self.hello_patterns:
            return MessageType.HELLO
        if content == "Yes":
            return MessageType.YES
        if content.startswith("WHAT"):
            return MessageType.WHAT
        if content in self.laugh_patterns:
            return MessageType.LAUGH
        return MessageType.OTHER

    async def handle_hello(self, author_name: str, hello_svc) -> str:
        """Handles hello messages"""
        return hello_svc.run(author_name)

    async def handle_laugh(self) -> str:
        """Handles laugh messages"""
        return random.choice(self.laugh_patterns)  # noqa: S311


class SaveMessagesCog(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.store_svc = SentenceStorerSvc(db=MarkovDB(channel=bot.channel))
        self.hello_svc = HelloGeneratorSvc()
        self.backoff_svc = BackoffService()
        self.message_handler = MessageHandler(self._send_message)

        # Register a separate send function for each message type
        self.send_functions = {
            MessageType.HELLO: self._create_typed_send("hello"),
            MessageType.YES: self._create_typed_send("yes"),
            MessageType.WHAT: self._create_typed_send("what"),
            MessageType.LAUGH: self._create_typed_send("laugh"),
        }

        # Register each send function with its own backoff
        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)

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

        # Store reference to current message for send functions
        self.current_message = message

        # Store the message content
        await self.store_svc.run(message.content)

        # Determine message type and handle accordingly
        msg_type = self.message_handler.get_message_type(message.content)

        response = None

        match msg_type:
            case MessageType.COMMAND:
                return
            case MessageType.HELLO:
                response = await self.message_handler.handle_hello(
                    message.author.name, self.hello_svc
                )
            case MessageType.YES:
                response = "Indeed"
            case MessageType.WHAT:
                response = "WHAT Ramon"
            case MessageType.LAUGH:
                response = await self.message_handler.handle_laugh()
            case MessageType.OTHER:
                return

        if response and msg_type in self.send_functions:
            # Use the type-specific send function
            await self.backoff_svc.call_async(self.send_functions[msg_type], response)


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

    def add_bot(self, user: User, channel: str, chatbot_settings: ChatbotSettings):
        if user.user in self.bots:
            logger.info(f"Bot for {user.user} already exists")
            return
        logger.info(f"Adding bot for {user.user}")
        bot = Bot(user=user, channel=channel, chatbot_settings=chatbot_settings)
        bot.add_cog(SaveMessagesCog(bot))
        self.bots[user.user] = bot

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

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

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