import os

from dotenv import load_dotenv
load_dotenv()
from telegram import Bot, Update, BotCommand, BotCommandScopeChat
from functools import wraps
from .handler import QH, MH, Conversation
from asgiref.sync import sync_to_async
from django.core.cache import cache
from typing import (
    Any,
    Optional,
)

from .filters import LevelFilter, MatchText,IsConversation
from telegram.ext import filters, ConversationHandler
from bot.component.button import ButtonBuilder
from bot.component.paginator import Paginator, PaginationResult, PaginatorOutput
from django.core.management import call_command
from bot.models import User
from django.contrib.staticfiles import finders
from pathlib import Path
import json
from bot.models import Setting
from ._base import BaseBot
from .middlewares import check_status_middleware, check_maintenance_middleware


class Base(BaseBot):
    DEFAULT_FALLBACK = "default_fallback"
    END_CONVERSATION = 'end'
    def __init__(self, name: str):
        super().__init__(name)
        self.__commands = []
        self.__middlewares:list=[]




    def build(self, bot,  admins):
        self.bot = bot
        self.admins = admins
        self.default_filters = self.chat_filters & LevelFilter(self, self.admins)
        self.__collect_nodes()
        self.__default_nodes()
        self.__not_found()
        self.add_middleware(check_status_middleware)
        self.add_middleware(check_maintenance_middleware)
        del self.entry_MH_filters


    def __not_found(self):
        e = MH()
        e.filters = filters.ALL

        async def act():
            # await self.menu_action()
            if self.update.effective_user.is_bot:
                return 'end'
            await self.send(self.T('not_found_text'), reply_markup=self.menu_button())
            return 'end'

        e.callback = act
        self.node(e)


    def __collect_nodes(self):
        methods_names = self.__get_methods()
        for i in methods_names:
            self.__getattribute__(i)()

    def __user_data(self, key, value=None):
        if value:
            self.context.user_data[key] = value
        else:
            return self.context.user_data.get(key)

    def __decorator(self, func, *args, **kwargs):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            self.update = args[0]
            self.context = args[1]
            await self.__set_user(self.update.effective_user.id)
            await self.__set_setting()

            await self.__setup()

            async def off():
                return -1

            if self.allow_continue:
                out = await func()
                func_name = func.__name__
                sep_name = Base.__decode_idx(func_name)
                try:
                    print(f"set is_conversation for {func_name}")
                    self.context.user_data['is_conversation'] = True if 'isconversation' in sep_name[0] else False
                except Exception as e:
                    pass
                if out == None:
                    if str(sep_name[-2]) != "end":
                        out = f"{self._name}_{sep_name[-2]}"
                        self.now_state(out)
                    else:
                        out = ConversationHandler.END
                if out == "end":
                    out = ConversationHandler.END
                return out
            else:
                out = await off()
                return out

        return wrapper

    def __init_attr(self):
        self.user_id = self.update.effective_user.id
        self.chat_id = self.update.effective_chat.id
        self.message_id = self.update.effective_message.message_id
        self.text = self.update.effective_message.text or self.update.effective_message.caption
        if self.update.effective_message.caption:
            self.message_type = 'media'


    async def __setup(self):
        print('setup')
        self.__init_attr()
        self.language = self.user.language or os.getenv('DEFAULT_LANGUAGE')
        self.T.language=self.language
        for i in self.__middlewares:
            if not await i(self):
                return

    @sync_to_async
    def __set_user(self, user_id: int):
        self._set_users_level()
        user_cache_key = f'{user_id}:user'
        user = cache.get(user_cache_key)
        if user:
            self.user = user
            return
        if not user:
            user = User.objects.filter(user_id=user_id).first()
        if user:
            if self.update:
                update_user_data = self.update.effective_user
                first_name = update_user_data.first_name
                username = update_user_data.username
                last_name = update_user_data.last_name
                user.username = username
                user.last_name = last_name
                user.first_name = first_name
                user.is_active = True
                user.save()
        else:
            User(
                user_id=user_id,
                is_active=True,
            ).save()
            user = User.objects.filter(user_id=user_id).first()
        if user:
            cache.set(user_cache_key, user, 15)
        self.user = user

    @sync_to_async
    def __set_setting(self):
        self.setting = Setting.get_setting()


    @sync_to_async
    def __set_referrer(self, referrer_id: int):
        """
        Set the referrer for the current user
        Args:
            referrer_id: Telegram user_id of the referrer
        """
        if not self.user.referred_by:
            referrer = User.objects.filter(user_id=referrer_id).first()
            if referrer and referrer.id != self.user.id:
                self.user.referred_by = referrer
                self.user.save()
                return True
        return False

    def __get_methods(self):
        return [method for method in dir(self) if
                method.startswith(self.method_prefix) and callable(getattr(self, method))]

    @staticmethod
    def __idx_maker(name, idx_node, idx_items, idx_item,is_conversation):
        add_is_conversation='isconversation' if is_conversation else ''
        out=  f'{name}{add_is_conversation}_{idx_node}_{idx_items}_{idx_item}'
        # print(out)
        return out

    @staticmethod
    def __decode_idx(name):
        return name.split('_')

    def __process_handler(self, handler: list | QH | MH | Any, states_level=False, start_from=0,is_conversation=False):
        if type(handler) == type(None): return
        if not isinstance(handler, list):
            handler = [handler]
        out = []
        idx_node = len(self._handlers)
        for base_items_level, items in enumerate(handler):
            items_level = base_items_level + start_from
            temp = []
            if not isinstance(items, list):
                items = [items]
            for i, item in enumerate(items):
                callback = item.callback
                if type(callback) != type(None):
                    # process callback
                    if states_level and base_items_level == len(handler) - 1:
                        items_level = 'end'
                    callback_name = Base.__idx_maker(self._name, idx_node, items_level, i,is_conversation=is_conversation)
                    callback.__name__ = callback_name
                    item.callback = self.__decorator(callback)

                if type(item.filters) != type(None):
                    # process filters
                    if type(item) == QH:
                        filter_text = item.filters
                        if not isinstance(filter_text, str):
                            raise Exception(f'bad filter: {filter_text}')
                        item.filters = self.inline_filter(filter_text, item._exact_match, self._level,is_conversation=is_conversation)
                    else:
                        if isinstance(item.filters, str):
                            item.filters = self.match_filter(item.filters)
                        # if not states_level:
                        #     self.entry_MH_filters += [item.filters]
                        item.filters = item.filters & self.default_filters
                        if states_level:
                            item.filters = item.filters & ~self.match_filter(self.T('menu_btn')) & IsConversation(self,is_conversation)
                if states_level:
                    temp.append(item.build())
                else:
                    out.append(item.build())
            if temp:
                out.append(temp)
        # print(out)
        return out

    def __default_fallback(self):
        e = MH()
        e.filters = self.match_filter(self.T('menu_btn'))

        async def act():
            self.kill_state()
            await self.send(self.T('menu_text'), reply_markup=self.menu_button())
            return 'end'

        e.callback = act
        return e

    def __default_nodes(self):
        back_menu_node = MH()
        back_menu_node.filters = self.match_filter(self.T('menu_btn'))

        async def act():
            await self.menu_action()
            return 'end'

        back_menu_node.callback = act
        self.node(back_menu_node)
        ######################################
        close_node = QH()
        close_node.filters = 'close'

        async def act():
            await self.bot.deleteMessage(self.update.effective_chat.id, self.update.effective_message.message_id)
            return 'end'

        close_node.callback = act
        self.node(close_node)
        ######################################
        next_page_node = QH()
        next_page_node.filters = 'next_page'

        async def act():
            paginator_name = self.queries('name')
            last_fetched_page = self.get_last_fetched_page(paginator_name)
            msg, btn = await self.make_pagination_pm(paginator_name, last_fetched_page.current_page + 1)
            await self.bot.editMessageText(msg, self.update.effective_chat.id, self.update.effective_message.message_id,
                                           reply_markup=btn)
            return 'end'

        next_page_node.callback = act
        self.node(next_page_node)
        ######################################
        prev_page_node = QH()
        prev_page_node.filters = 'prev_page'

        async def act():
            paginator_name = self.queries('name')
            last_fetched_page = self.get_last_fetched_page(paginator_name)
            msg, btn = await self.make_pagination_pm(paginator_name, last_fetched_page.current_page - 1)
            await self.bot.edit_message_text(msg, self.update.effective_user.id,
                                             self.update.effective_message.message_id, reply_markup=btn)
            return 'end'

        prev_page_node.callback = act
        self.node(prev_page_node)
        ######################################
        prev_page_btn_node = QH()
        prev_page_btn_node.filters = 'prev_page_btn'

        async def act():
            paginator_name = self.queries('name')
            last_fetched_page = self.get_last_fetched_page(paginator_name)
            btn = await self.make_pagination_button(paginator_name, last_fetched_page.current_page - 1)
            await self.bot.edit_message_reply_markup(self.update.effective_user.id,
                                                     self.update.effective_message.message_id, reply_markup=btn)
            return 'end'

        prev_page_btn_node.callback = act
        self.node(prev_page_btn_node)
        ######################################
        next_page_btn_node = QH()
        next_page_btn_node.filters = 'next_page_btn'

        async def act():
            paginator_name = self.queries('name')
            last_fetched_page = self.get_last_fetched_page(paginator_name)
            btn = await self.make_pagination_button(paginator_name, last_fetched_page.current_page + 1)
            await self.bot.edit_message_reply_markup(self.update.effective_user.id,
                                                     self.update.effective_message.message_id, reply_markup=btn)
            return 'end'

        next_page_btn_node.callback = act

        self.node(next_page_btn_node)
        ######################################
        makemessages_node = MH()
        makemessages_node.filters = self.match_filter('/compile')

        async def act():
            if self.__translation_manager:
                for lang in self.__translation_manager.languages:
                    await sync_to_async(call_command)(
                        'compilemessages',
                        verbosity=1
                    )
                await self.send('done')
            else:
                await self.send('Translation Manager not Set')
            return 'end'

        makemessages_node.callback = act
        self.node(makemessages_node)
        ######################################
        select_language_node = QH()
        select_language_node.filters = 'select_language'

        async def act():
            lang = self.queries('lang')
            self.user.language = lang
            self.user.empty_cache()
            await sync_to_async(self.user.save)()
            await self.alert(self.T('success_select_language_text'), t=True)
            await self.bot.deleteMessage(self.chat_id, self.message_id)
            await self.start_action()
            return 'end'

        select_language_node.callback = act
        self.node(select_language_node)

    def state_data(self, main_key, key, value=None, do_empty=False):
        if do_empty:
            self.context.user_data[main_key] = {}

        old_data = self.__user_data(main_key) or {}
        if value is not None:
            old_data[key] = value
            self.__user_data(main_key, old_data)
        else:
            return old_data.get(key)

    def now_state(self, value=None):
        key = '_now_state'
        return self.__user_data(key, value)

    def go_state(self, state):
        if now_state := self.now_state():
            try:
                now_state_int = int(now_state.replace(f'{self._name}_', ''))
                new_state_int = now_state_int + state
                if new_state_int < 0:
                    out = f"{self._name}_" + str(now_state_int)
                else:
                    out = f"{self._name}_" + str(new_state_int)
                self.now_state(out)
                return out
            except Exception as e:
                print(e)

    def kill_state(self):
        key = '_now_state'
        self.context.user_data[key]=None

    def inline_filter(self, filter_text, exact: bool, level: int,is_conversation,*args):
        def checker(*args):
            callback_data = json.loads(args[0])
            if exact:
                if filter_text != callback_data['act']:return False
            else:
                if filter_text not in args[0]: return False
            try:
                print(filter_text,callback_data['act'],self.context.user_data.get('is_conversation',True) ,is_conversation)
                if self.context.user_data.get('is_conversation',True) != is_conversation:
                    return False
            except Exception as e:
                pass
            if level != self._level:
                return False
            if self.user and self.user.level != self._level:
                return False

            R = callback_data.get('R')
            if R and R != self._level:
                return False
            return True


        return checker

    async def send(self,
                   text: str,
                   chat_id: int | str = None,
                   reply_markup=None,
                   parse_mode=None,
                   disable_notification=None,
                   t=True,
                   **kwargs):
        if t:
            text = self.T(text)
        if not chat_id:
            chat_id = self.chat_id
        kwargs['chat_id'] = chat_id
        kwargs['text'] = text
        kwargs['reply_markup'] = reply_markup
        kwargs['parse_mode'] = parse_mode
        kwargs['disable_notification'] = disable_notification

        return await self.bot.send_message(**kwargs)

    async def edit(self,
                   text: str,
                   message_id: int = None,
                   chat_id: int | str = None,
                   reply_markup=None,
                   parse_mode=None,
                   t=True,
                   **kwargs):
        text = self.T(text) if t else text
        kwargs['chat_id'] = chat_id or self.chat_id
        kwargs['message_id'] = message_id or self.message_id
        kwargs['text'] = text
        kwargs['reply_markup'] = reply_markup
        kwargs['parse_mode'] = parse_mode
        return await self.bot.edit_message_text(**kwargs)

    async def edit_caption(self,
                           new_caption: str,
                           message_id: int = None,
                           chat_id: int | str = None,
                           reply_markup=None,
                           parse_mode=None,
                           t=True,
                           **kwargs):
        if t:
            new_caption = self.T(new_caption)
        if not chat_id:
            chat_id = self.chat_id
        if not message_id:
            message_id = self.message_id
        kwargs['chat_id'] = chat_id
        kwargs['message_id'] = message_id
        kwargs['caption'] = new_caption
        kwargs['reply_markup'] = reply_markup
        kwargs['parse_mode'] = parse_mode
        return await self.bot.edit_message_caption(**kwargs)

    async def send_media(self,
                         media: str | bytes,
                         media_type: str = 'photo',
                         caption=None,
                         chat_id: int | str = None,
                         reply_markup=None,
                         parse_mode=None,
                         disable_notification=None,
                         t=True,
                         **kwargs):
        if t:
            caption = self.T(caption)
        if not chat_id:
            chat_id = self.chat_id
        kwargs['chat_id'] = chat_id
        kwargs[media_type] = media
        kwargs['caption'] = caption
        kwargs['reply_markup'] = reply_markup
        kwargs['parse_mode'] = parse_mode
        kwargs['disable_notification'] = disable_notification
        return await getattr(self.bot, f'send_{media_type}')(**kwargs)

    async def copy(self,
                   from_chat_id: int | str,
                   message_id: int,
                   chat_id: int | str = None,
                   reply_markup=None,
                   disable_notification=None,
                   **kwargs):
        kwargs['chat_id'] = chat_id or self.chat_id
        kwargs['from_chat_id'] = from_chat_id
        kwargs['reply_markup'] = reply_markup
        kwargs['message_id'] = message_id
        kwargs['disable_notification'] = disable_notification
        return await self.bot.copy_message(**kwargs)

    async def delete(self,
                     chat_id: str | int = None,
                     message_id: int = None,
                     **kwargs):

        kwargs['chat_id'] = chat_id or self.chat_id
        kwargs['message_id'] = message_id or self.message_id
        return await self.bot.deleteMessage(**kwargs)

    def match_filter(self, text, exact=True):
        return MatchText(text, self, exact)

    def node(self,
             entry_points: list | QH | MH | Any,
             states: Optional[list[QH | MH | Any]] = None,
             fallbacks: Optional[list | QH | MH | Any] = DEFAULT_FALLBACK,
             kwargs: dict[str, Any] = {},
             ):
        is_conversation=type(states) != type(None)

        entry_points = self.__process_handler(entry_points,is_conversation=is_conversation)
        states = self.__process_handler(states, states_level=True, start_from=1,is_conversation=is_conversation)
        if fallbacks == 'default_fallback':
            fallbacks = [self.__default_fallback()]
        fallbacks = self.__process_handler(fallbacks, start_from=2,is_conversation=is_conversation)
        handlers_obj = Conversation.make(self._name, entry_points, states, fallbacks, kwargs)
        for handler_obj in handlers_obj:
            self._handlers.append(handler_obj)

    def button(self, data: list | dict, pattern: int | tuple, reverse: bool = False, close: bool = False, t=True):
        new_data = data.copy()
        if isinstance(data, dict):
            for key, value in data.items():
                if isinstance(value, dict):
                    if 'R' not in value:
                        value['R'] = self._level
                    new_data[key] = value
        if t:
            if isinstance(data, dict):
                new_data = {self.T(k): v for k, v in new_data.items()}
            elif isinstance(data, list):
                new_data = [self.T(i) for i in new_data]
        return ButtonBuilder(new_data, pattern, reverse, close).build()

    def is_query(self) -> bool:
        if self.update.callback_query:
            return True
        return False

    def queries(self, key=None):
        q = json.loads(self.update.callback_query.data)
        if key:
            return q.get(key)
        return q

    def pagination(self, name: str, queryset, formatter_function=None, per_page: int = 10, joiner: str = '\n',
                   close: bool = True, btn_sizes=2, reverse=True):
        """
            Make pagination for telegram messages
            For Buttons Formatter must be return Dict like {key: 'key',data:'value'}
        """
        instance = Paginator(queryset, per_page=per_page, formatter=formatter_function, reverse=reverse)
        self.__pagination[name] = PaginatorOutput(
            instance=instance,
            joiner=joiner,
            close=close,
            btn_sizes=btn_sizes,
        )

    def get_paginator(self, name):
        paginator = self.__pagination.get(name)
        if not paginator:
            raise Exception('Pagination not initialized')
        return paginator

    async def get_page(self, name: str, page_number: int):
        page: PaginationResult = await self.get_paginator(name).instance.paginate(page_number)
        return page

    def get_last_fetched_page(self, name: str) -> PaginationResult:
        return self.get_paginator(name).instance.last_fetched_page

    async def make_pagination_pm(self, name: str, page_number=1):
        page = await self.get_page(name, page_number)
        paginator = self.get_paginator(name)
        msg = paginator.joiner.join(page.items)
        btn = {}
        if page.has_prev:
            btn[self.T('prev_btn')] = {'act': 'prev_page', 'name': name}
        if page.has_next:
            btn[self.T('next_btn')] = {'act': 'next_page', 'name': name}
        x = 2 if len(btn) == 2 else 1
        btn = self.button(btn, (x, 1), close=paginator.close)
        return msg, btn

    async def make_pagination_button(self, name: str, page_number=1):
        page = await self.get_page(name, page_number)
        paginator = self.get_paginator(name)
        btn_sizes = paginator.btn_sizes
        btn = {}
        for i in page.items:
            btn[str(i[0])] = i[1]
        action_btn = {}
        if page.has_prev:
            action_btn[self.T('prev_bn')] = {'act': 'prev_page_btn', 'name': name}
        if page.has_next:
            action_btn[self.T('next_btn')] = {'act': 'next_page_btn', 'name': name}
        p1 = len(btn) // btn_sizes
        p2 = len(btn) % btn_sizes
        sizes = [btn_sizes] * (p1)
        if p2 > 0:
            sizes.append(p2)
        x = 2 if len(action_btn) == 2 else 1
        sizes += [x, 1]
        btn = {**btn, **action_btn}
        btn = self.button(btn, tuple(sizes), close=paginator.close)
        return btn

    async def alert(self, msg, id=0, popup=False, t=False):
        if t:
            msg = self.T(msg)
        if id == 0:
            id = self.update.callback_query.id
        await self.bot.answer_callback_query(
            callback_query_id=id, text=msg, show_alert=popup)

    async def menu_action(self):
        await self.send(self.T('menu_text'), reply_markup=self.menu_button())

    async def start_action(self):
        await self.set_commands()
        btn = self.button(self.menu_btns, self.menu_btns_pattern)
        await self.send(self.T('welcome_text'), reply_markup=btn)

    def get_static_file_bytes(self, static_path):
        # Find the absolute path of the static file
        file_path = finders.find(static_path)
        if file_path:
            # Read file as bytes
            return Path(file_path).read_bytes()
        return None

    def menu_button(self):
        return self.button(self.menu_btns, self.menu_btns_pattern)

    def back_menu_button(self):
        return self.button([self.T('menu_btn')], 1)

    async def toggle(self, queryset, field: str):
        new_value = not getattr(queryset, field)
        setattr(queryset, field, new_value)
        await sync_to_async(queryset.save)()

    async def download(self, effective_message_file, output_path):
        file = effective_message_file
        file_id = file.file_id
        res = await self.bot.get_file(file_id)
        await res.download_to_drive(output_path)

    def add_command(self, command, description):
        self.__commands.append(BotCommand(command, description))

    async def set_commands(self):
        print('Setting commands')
        await self.bot.set_my_commands(self.__commands, BotCommandScopeChat(self.chat_id))

    def add_middleware(self, middleware_func):
        self.__middlewares.append(middleware_func)



