import os
from dotenv import load_dotenv
load_dotenv()
from telegram import BotCommand, BotCommandScopeChat
from functools import wraps
from .handler import QH, MH, Conversation
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 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

    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 = await User.objects.filter(user_id=user_id).afirst()
        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
                await user.asave()
        else:
            user = User(
                user_id=user_id,
                is_active=True,
            )
            await user.asave()
        if user:
            cache.set(user_cache_key, user, 15)
        self.user = user

    async def __set_setting(self):
        self.setting = await Setting.aget_setting()



    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)


    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:
                if not 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 queryset.asave()

    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)



