"""
Обработчики действий (ActionInstruction)
"""

import hashlib
import logging
from typing import Any
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, ReplyKeyboardMarkup
from aiogram.types import (
    InlineKeyboardButton,
    KeyboardButton,
    SwitchInlineQueryChosenChat as AiogramSwitchInlineQueryChosenChat,
    WebAppInfo,
)

from ui_router.exceptions import (
    ActionError,
    ContentError,
    MissingActionFieldError,
    UnknownActionTypeError,
)
from ui_router.schema import (
    ActionInstruction,
    ActionType,
    TransitionType,
    DynamicContent,
    ContentTemplate,
    LocalizedContent,
    LocalizedTemplate,
    VariableScope,
    ScheduleType,
    GotoSceneAction,
    SendMessageAction,
    EditMessageAction,
    SendPhotoAction,
    SendVideoAction,
    SendDocumentAction,
    DeleteMessageAction,
    AnswerCallbackAction,
    BackAction,
    SaveInputAction,
    ClearDataAction,
    SetVariableAction,
    ScheduleEventAction,
    EmitEventAction,
    BusinessAction,
    ApproveJoinRequestAction,
    DeclineJoinRequestAction,
    AnswerInlineQueryAction,
    InlineArticleResult,
    CustomAction,
)
from ui_router.state.context import ExecutionContext
from .callbacks import CallbackDataManager
from ui_router.events.bus import EventData, EventSourceType
from .formatting import flag_formatter


logger = logging.getLogger(__name__)


class ContentResolver:
    """Резолвер контента (замена флагов в темплейтах)"""

    def __init__(self, callback_manager: CallbackDataManager, fluent_bundles: dict[str, Any] | None = None) -> None:
        self.callback_manager = callback_manager
        self.fluent_bundles = fluent_bundles

    def resolve_string(self, text: str | DynamicContent | LocalizedContent, context: ExecutionContext) -> str:
        """Разрешить строку (подставить флаги)"""
        if isinstance(text, str):
            return self._replace_flags(text, context)

        if isinstance(text, LocalizedContent):
            return self._resolve_localized(text, context)

        if text.type == "template":
            return self._resolve_template(text.template, context)

        if text.type == "localized_template":
            return self._resolve_localized_template(text.localized_template, context)

        msg = "Cannot resolve function-based content here"
        raise ContentError(msg)

    def _replace_flags(self, text: str, context: ExecutionContext) -> str:
        """Заменить флаги в тексте"""
        flags = context.get_all_flags()
        return flag_formatter.format(text, **flags)

    def _resolve_template(self, template: ContentTemplate, context: ExecutionContext) -> str:
        """Разрешить темплейт"""
        flags = {name: context.get_flag(name, "") for name in template.flags}
        return flag_formatter.format(template.template, **flags)

    def _resolve_localized(self, content: LocalizedContent, context: ExecutionContext) -> str:
        """Разрешить LocalizedContent через функцию-геттер"""
        locale = context.get_flag("locale", content.default_locale)
        return content.translations.get(locale, content.translations.get(content.default_locale, ""))

    def _resolve_localized_template(self, template: LocalizedTemplate, context: ExecutionContext) -> str:
        """Разрешить LocalizedTemplate через Fluent или fallback"""
        locale = context.get_flag(template.locale_flag, template.default_locale)

        if self.fluent_bundles and locale in self.fluent_bundles:
            return self._resolve_via_fluent(template, context, locale)

        template_str = template.translations.get(locale, template.translations.get(template.default_locale, ""))
        flags = {name: context.get_flag(name, "") for name in template.flags}
        return flag_formatter.format(template_str, **flags)

    def _resolve_via_fluent(self, template: LocalizedTemplate, context: ExecutionContext, locale: str) -> str:
        """Резолв через Fluent"""
        bundle = self.fluent_bundles[locale]

        base_text = template.translations.get("en") or next(iter(template.translations.values()))
        hash_str = hashlib.md5(base_text.encode(), usedforsecurity=False).hexdigest()[:12]
        fluent_id = f"msg_{hash_str}"

        fluent_args = {flag: context.get_flag(flag, "") for flag in template.flags}

        try:
            msg = bundle.get_message(fluent_id)
            if not msg or not msg.value:
                template_str = template.translations.get(locale, template.translations.get(template.default_locale, ""))
                return flag_formatter.format(template_str, **dict(fluent_args.items()))

            result, errors = bundle.format_pattern(msg.value, fluent_args)

            if errors:
                logger.error("Fluent formatting errors for %s: %s", fluent_id, errors)
        except Exception:
            template_str = template.translations.get(locale, template.translations.get(template.default_locale, ""))
            return flag_formatter.format(template_str, **dict(fluent_args.items()))
        else:
            return result

    async def resolve_keyboard(
        self,
        keyboard: Any,
        context: ExecutionContext,
        scene_id: str,
    ) -> InlineKeyboardMarkup | ReplyKeyboardMarkup:
        """Разрешить клавиатуру"""
        if hasattr(keyboard, "type") and keyboard.type == "dynamic":
            from .dynamic_keyboard import DynamicKeyboardRenderer

            renderer = DynamicKeyboardRenderer(self.callback_manager)
            return await renderer.render(keyboard, context, scene_id)

        if keyboard.is_inline:
            buttons = []
            for row in keyboard.buttons:
                button_row = []
                for btn in row:
                    text = self.resolve_string(btn.text, context)

                    if btn.url:
                        url = self.resolve_string(btn.url, context)
                        button_row.append(
                            InlineKeyboardButton(
                                text=text,
                                url=url,
                                style=btn.style,
                                icon_custom_emoji_id=btn.icon_custom_emoji_id,
                            )
                        )
                    elif btn.web_app_url:
                        button_row.append(
                            InlineKeyboardButton(
                                text=text,
                                web_app=WebAppInfo(url=btn.web_app_url),
                                style=btn.style,
                                icon_custom_emoji_id=btn.icon_custom_emoji_id,
                            )
                        )
                    elif btn.switch_inline_query is not None:
                        button_row.append(
                            InlineKeyboardButton(
                                text=text,
                                switch_inline_query=btn.switch_inline_query,
                                style=btn.style,
                                icon_custom_emoji_id=btn.icon_custom_emoji_id,
                            )
                        )
                    elif btn.switch_inline_query_current_chat is not None:
                        button_row.append(
                            InlineKeyboardButton(
                                text=text,
                                switch_inline_query_current_chat=btn.switch_inline_query_current_chat,
                                style=btn.style,
                                icon_custom_emoji_id=btn.icon_custom_emoji_id,
                            )
                        )
                    elif btn.switch_inline_query_chosen_chat is not None:
                        chosen = btn.switch_inline_query_chosen_chat
                        button_row.append(
                            InlineKeyboardButton(
                                text=text,
                                switch_inline_query_chosen_chat=AiogramSwitchInlineQueryChosenChat(
                                    query=chosen.query or "",
                                    allow_user_chats=chosen.allow_user_chats,
                                    allow_bot_chats=chosen.allow_bot_chats,
                                    allow_group_chats=chosen.allow_group_chats,
                                    allow_channel_chats=chosen.allow_channel_chats,
                                ),
                                style=btn.style,
                                icon_custom_emoji_id=btn.icon_custom_emoji_id,
                            )
                        )
                    elif btn.callback_action or btn.callback_global:
                        handler_name = btn.callback_action or btn.callback_global
                        is_global = btn.callback_global is not None

                        params = {}
                        for key, value_template in btn.callback_params.items():
                            params[key] = self.resolve_string(value_template, context)

                        callback_data = await self.callback_manager.encode(
                            scene_id=scene_id,
                            handler_name=handler_name,
                            params=params,
                            is_global=is_global,
                        )

                        button_row.append(
                            InlineKeyboardButton(
                                text=text,
                                callback_data=callback_data,
                                style=btn.style,
                                icon_custom_emoji_id=btn.icon_custom_emoji_id,
                            )
                        )

                buttons.append(button_row)

            return InlineKeyboardMarkup(inline_keyboard=buttons)
        buttons = []
        for row in keyboard.buttons:
            button_row = []
            for btn in row:
                text = self.resolve_string(btn.text, context)
                button_row.append(KeyboardButton(text=text))
            buttons.append(button_row)

        return ReplyKeyboardMarkup(
            keyboard=buttons,
            resize_keyboard=keyboard.resize_keyboard,
            one_time_keyboard=keyboard.one_time_keyboard,
        )


class ActionExecutor:
    """Исполнитель действий"""

    def __init__(
        self,
        content_resolver: ContentResolver | None = None,
        registry: Any = None,
        event_bus: Any = None,
        event_scheduler: Any = None,
        variable_repository: Any = None,
    ) -> None:
        self.registry = registry
        self.event_bus = event_bus
        self.event_scheduler = event_scheduler
        self.variable_repository = variable_repository
        self.content_resolver = content_resolver

    async def execute(
        self,
        action: ActionInstruction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Any:
        """Выполнить действие"""

        if action.type == ActionType.SEND_MESSAGE:
            return await self._send_message(action, context, event)

        if action.type == ActionType.EDIT_MESSAGE:
            return await self._edit_message(action, context, event)

        if action.type == ActionType.SEND_PHOTO:
            return await self._send_photo(action, context, event)

        if action.type == ActionType.SEND_VIDEO:
            return await self._send_video(action, context, event)

        if action.type == ActionType.SEND_DOCUMENT:
            return await self._send_document(action, context, event)

        if action.type == ActionType.DELETE_MESSAGE:
            return await self._delete_message(action, context, event)

        if action.type == ActionType.ANSWER_CALLBACK:
            return await self._answer_callback(action, context, event)

        if action.type == ActionType.GOTO_SCENE:
            return await self._goto_scene(action, context, event)

        if action.type == ActionType.BACK:
            return await self._go_back(action, context, event)

        if action.type == ActionType.SAVE_INPUT:
            return await self._save_input(action, context, event)

        if action.type == ActionType.CLEAR_DATA:
            return await self._clear_data(action, context, event)

        if action.type == ActionType.SET_VARIABLE:
            return await self._set_variable(action, context, event)

        if action.type == ActionType.SCHEDULE_EVENT:
            return await self._schedule_event(action, context, event)

        if action.type == ActionType.EMIT_EVENT:
            return await self._emit_event(action, context, event)

        if action.type == ActionType.BUSINESS_ACTION:
            return await self._execute_business_action(action, context, event)

        if action.type == ActionType.APPROVE_JOIN_REQUEST:
            return await self._approve_join_request(action, context, event)

        if action.type == ActionType.DECLINE_JOIN_REQUEST:
            return await self._decline_join_request(action, context, event)

        if action.type == ActionType.ANSWER_INLINE_QUERY:
            return await self._answer_inline_query(action, context, event)

        if action.type == ActionType.CUSTOM:
            return await self._execute_custom(action, context, event)

        raise UnknownActionTypeError(action.type)

    async def _send_message(
        self,
        action: SendMessageAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Message:
        """Отправить сообщение"""
        if not action.content:
            msg = "send_message"
            raise ActionError(msg, "content is required")

        text = self.content_resolver.resolve_string(action.content.text, context)
        keyboard = None

        if action.keyboard:
            keyboard = await self.content_resolver.resolve_keyboard(action.keyboard, context, context.scene_id)

        return await context.bot.send_message(
            chat_id=context.chat_id,
            text=text,
            parse_mode=action.content.parse_mode,
            reply_markup=keyboard,
            disable_web_page_preview=action.content.disable_web_page_preview,
        )

    async def _edit_message(
        self,
        action: EditMessageAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Message:
        """Редактировать сообщение"""
        if not isinstance(event, CallbackQuery):
            msg = "edit_message"
            raise ActionError(msg, "Edit message only works with CallbackQuery")
        if not action.content:
            msg = "edit_message"
            raise ActionError(msg, "content is required")

        text = self.content_resolver.resolve_string(action.content.text, context)
        keyboard = None

        if action.keyboard:
            keyboard = await self.content_resolver.resolve_keyboard(action.keyboard, context, context.scene_id)

        return await event.message.edit_text(
            text=text,
            parse_mode=action.content.parse_mode,
            reply_markup=keyboard,
            disable_web_page_preview=action.content.disable_web_page_preview,
        )

    async def _send_photo(
        self,
        action: SendPhotoAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Message:
        """Отправить фото"""
        if not action.content or not action.content.media:
            msg = "send_photo"
            raise ActionError(msg, "content with media is required")

        photo = self.content_resolver.resolve_string(action.content.media.source, context)
        caption = None
        if action.content.media.caption:
            caption = self.content_resolver.resolve_string(action.content.media.caption, context)

        keyboard = None
        if action.keyboard:
            keyboard = await self.content_resolver.resolve_keyboard(action.keyboard, context, context.scene_id)

        return await context.bot.send_photo(
            chat_id=context.chat_id,
            photo=photo,
            caption=caption,
            parse_mode=action.content.parse_mode,
            reply_markup=keyboard,
        )

    async def _send_video(
        self,
        action: SendVideoAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Message:
        """Отправить видео"""
        if not action.content or not action.content.media:
            msg = "send_video"
            raise ActionError(msg, "content with media is required")

        video = self.content_resolver.resolve_string(action.content.media.source, context)
        caption = None
        if action.content.media.caption:
            caption = self.content_resolver.resolve_string(action.content.media.caption, context)

        keyboard = None
        if action.keyboard:
            keyboard = await self.content_resolver.resolve_keyboard(action.keyboard, context, context.scene_id)

        return await context.bot.send_video(
            chat_id=context.chat_id,
            video=video,
            caption=caption,
            parse_mode=action.content.parse_mode,
            reply_markup=keyboard,
        )

    async def _send_document(
        self,
        action: SendDocumentAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Message:
        """Отправить документ"""
        if not action.content or not action.content.media:
            msg = "send_document"
            raise ActionError(msg, "content with media is required")

        document = self.content_resolver.resolve_string(action.content.media.source, context)
        caption = None
        if action.content.media.caption:
            caption = self.content_resolver.resolve_string(action.content.media.caption, context)

        keyboard = None
        if action.keyboard:
            keyboard = await self.content_resolver.resolve_keyboard(action.keyboard, context, context.scene_id)

        return await context.bot.send_document(
            chat_id=context.chat_id,
            document=document,
            caption=caption,
            parse_mode=action.content.parse_mode,
            reply_markup=keyboard,
        )

    async def _delete_message(
        self,
        action: DeleteMessageAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Удалить сообщение"""
        if isinstance(event, CallbackQuery):
            await event.message.delete()
        elif isinstance(event, Message):
            await event.delete()

    async def _answer_callback(
        self,
        action: AnswerCallbackAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Ответить на callback"""
        if not isinstance(event, CallbackQuery):
            return

        text = None
        if action.callback_text:
            text = self.content_resolver.resolve_string(action.callback_text, context)

        await event.answer(text=text, show_alert=action.show_alert)

    async def _goto_scene(
        self,
        action: GotoSceneAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Перейти к сцене"""
        transition = action.transition or TransitionType.SEND

        context.set_flag("_pending_scene", action.scene_id)
        context.set_flag("_pending_transition", transition.value)

    async def _go_back(
        self,
        action: BackAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Вернуться назад"""
        transition = action.transition or None
        context.set_flag("_pending_back", True)
        if transition:
            context.set_flag("_pending_transition", transition.value)

    async def _save_input(
        self,
        action: SaveInputAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Сохранить ввод пользователя"""
        if not action.save_as:
            msg = "save_input"
            raise MissingActionFieldError(msg, "save_as")

        if isinstance(event, Message):
            context.save_user_input(action.save_as, event.text)

        elif isinstance(event, CallbackQuery) and action.params and "from_param" in action.params:
            source_key = action.params["from_param"]
            value = context.get_user_input(source_key)
            if value:
                context.save_user_input(action.save_as, value)

    async def _clear_data(
        self,
        action: ClearDataAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Очистить данные"""
        context.clear_user_input(action.data_keys)

    async def _set_variable(
        self,
        action: SetVariableAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Установить значение переменной"""
        variable_name = action.variable_name
        value = action.value
        operation = action.operation
        scope = action.scope

        if not variable_name:
            msg = "set_variable"
            raise MissingActionFieldError(msg, "variable_name")

        if operation != "set":
            current = await self.variable_repository.get(
                bot_id=context.bot.id,
                name=variable_name,
                scope=scope,
                user_id=context.user_id if scope == VariableScope.USER else None,
                chat_id=context.chat_id if scope == VariableScope.CHAT else None,
            )

            if current is None:
                current = 0

            if operation == "add":
                value = current + value
            elif operation == "subtract":
                value = current - value
            elif operation == "multiply":
                value = current * value
            else:
                msg = "set_variable"
                raise ActionError(msg, f"Unknown operation: {operation}")

        await self.variable_repository.set(
            bot_id=context.bot.id,
            name=variable_name,
            value=value,
            scope=scope,
            user_id=context.user_id if scope == VariableScope.USER else None,
            chat_id=context.chat_id if scope == VariableScope.CHAT else None,
        )

    async def _schedule_event(
        self,
        action: ScheduleEventAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Запланировать событие"""
        event_name = action.event_name
        schedule_type = action.schedule_type
        schedule_value = action.schedule_value
        event_data = action.event_data

        if not event_name:
            msg = "schedule_event"
            raise MissingActionFieldError(msg, "event_name")
        if not schedule_value:
            msg = "schedule_event"
            raise MissingActionFieldError(msg, "schedule_value")

        user_id = context.user_id
        chat_id = context.chat_id
        bot_id = context.bot.id if context.bot else context.event_data.get("bot_id")

        if schedule_type in {"once", ScheduleType.ONCE}:
            task_id = await self.event_scheduler.schedule_once(
                event_name=event_name,
                delay=schedule_value,
                event_data=event_data,
                bot_id=bot_id,
                user_id=user_id,
                chat_id=chat_id,
            )
            logger.debug("Scheduled event '%s' with delay %s, task_id: %s", event_name, schedule_value, task_id)

        elif schedule_type in {"cron", ScheduleType.CRON}:
            task_id = await self.event_scheduler.schedule_cron(
                event_name=event_name,
                cron_expr=schedule_value,
                event_data=event_data,
                bot_id=bot_id,
                user_id=user_id,
                chat_id=chat_id,
            )
            logger.debug("Scheduled cron event '%s' with expr %s, task_id: %s", event_name, schedule_value, task_id)

        elif schedule_type in {"interval", ScheduleType.INTERVAL}:
            task_id = await self.event_scheduler.schedule_interval(
                event_name=event_name,
                interval_seconds=int(schedule_value),
                event_data=event_data,
                bot_id=bot_id,
                user_id=user_id,
                chat_id=chat_id,
            )
            logger.debug("Scheduled interval event '%s' every %ss, task_id: %s", event_name, schedule_value, task_id)

        else:
            msg = "schedule_event"
            raise ActionError(msg, f"Unknown schedule_type: {schedule_type}")

    async def _emit_event(
        self,
        action: EmitEventAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> None:
        """Вызвать событие немедленно"""
        event_name = action.event_name
        event_data = action.event_data

        if not event_name:
            msg = "emit_event"
            raise MissingActionFieldError(msg, "event_name")

        bot_id = context.bot.id if context.bot else context.event_data.get("bot_id")

        event_obj = EventData(
            event_name=event_name,
            source_type=EventSourceType.INTERNAL,
            data=event_data,
            bot_id=bot_id,
            user_id=context.user_id,
            chat_id=context.chat_id,
        )

        await self.event_bus.emit(event_obj)
        logger.debug("Emitted event '%s'", event_name)

    async def _approve_join_request(
        self,
        action: ApproveJoinRequestAction,
        context: ExecutionContext,
        event: Any,
    ) -> None:
        """Одобрить запрос на вступление в чат."""
        from aiogram.types import ChatJoinRequest

        if not isinstance(event, ChatJoinRequest):
            msg = "approve_join_request"
            raise ActionError(msg, "Action requires ChatJoinRequest event")
        await event.approve()
        logger.debug("Approved join request from user %s in chat %s", event.from_user.id, event.chat.id)

    async def _decline_join_request(
        self,
        action: DeclineJoinRequestAction,
        context: ExecutionContext,
        event: Any,
    ) -> None:
        """Отклонить запрос на вступление в чат."""
        from aiogram.types import ChatJoinRequest

        if not isinstance(event, ChatJoinRequest):
            msg = "decline_join_request"
            raise ActionError(msg, "Action requires ChatJoinRequest event")
        await event.decline()
        logger.debug("Declined join request from user %s in chat %s", event.from_user.id, event.chat.id)

    async def _answer_inline_query(
        self,
        action: AnswerInlineQueryAction,
        context: ExecutionContext,
        event: Any,
    ) -> None:
        """Ответить на inline-запрос."""
        from aiogram.types import InlineQuery, InlineQueryResultArticle, InputTextMessageContent

        if not isinstance(event, InlineQuery):
            msg = "answer_inline_query"
            raise ActionError(msg, "Action requires InlineQuery event")

        results = []

        if action.results_function:
            results = await self.registry.business_actions.execute(
                action_name=action.results_function,
                context=context,
                event=event,
                params={},
            )
        elif action.results:
            for i, item in enumerate(action.results):
                title = self.content_resolver.resolve_string(item.title, context)
                description = (
                    self.content_resolver.resolve_string(item.description, context)
                    if item.description
                    else None
                )
                message_text = self.content_resolver.resolve_string(item.message_text, context)
                results.append(InlineQueryResultArticle(
                    id=item.id or str(i),
                    title=title,
                    description=description,
                    input_message_content=InputTextMessageContent(
                        message_text=message_text,
                        parse_mode=item.parse_mode,
                    ),
                    thumbnail_url=item.thumbnail_url,
                ))

        await event.answer(
            results=results,
            cache_time=action.cache_time,
            is_personal=action.is_personal,
            next_offset=action.next_offset or "",
        )

    async def _execute_custom(
        self,
        action: CustomAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Any:
        """Выполнить кастомное действие"""
        if not action.function:
            msg = "custom"
            raise MissingActionFieldError(msg, "function")

        params = {}
        for key, value_template in action.function_params.items():
            params[key] = self.content_resolver.resolve_string(value_template, context)

        return await self.registry.actions.execute(action.function, context, params)

    async def _execute_business_action(
        self,
        action: BusinessAction,
        context: ExecutionContext,
        event: Message | CallbackQuery,
    ) -> Any:
        """
        Выполнить бизнес-действие через BusinessActionsRegistry

        action.business_action должен содержать имя действия
        action.params передаются в функцию как **params
        """
        if not action.business_action:
            msg = "business_action"
            raise MissingActionFieldError(msg, "business_action")

        return await self.registry.business_actions.execute(
            action_name=action.business_action,
            context=context,
            event=event,
            params=action.params,
        )
