"""Main bot orchestrator: Telegram polling, handlers, optional API server."""
from __future__ import annotations

import asyncio
import signal
import traceback
from threading import Event, Thread
from typing import Any

from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    CallbackQueryHandler,
    MessageHandler,
    filters,
)

from TgrEzLi.api_server import ApiServer
from TgrEzLi.defaults import DEFAULT_BANNER_FILLER
from TgrEzLi.handlers import HandlerRegistry, call_user_function
from TgrEzLi.logger import get_default_logger
from TgrEzLi.types import TgCbData, TgCmdData, TgMsgData, TgrezliConfig

__version__ = "0.4.0"


def _print_banner(
    logger: Any,
    name: str,
    version: str,
    author: str,
    filler: str = DEFAULT_BANNER_FILLER,
) -> None:
    banner = f"\n{filler * 50}\n{name} {version} by {author}\n{filler * 50}\n"
    logger.info(banner)


class TEL:
    """
    Orchestrates Telegram polling, message/command/callback handlers,
    and an optional embedded HTTP API server for custom POST routes.
    """

    def __init__(self, config: TgrezliConfig | None = None) -> None:
        self.config = config or TgrezliConfig()
        self.logger = get_default_logger(
            save_log=self.config.save_log,
            log_file=self.config.log_file,
        )
        self.registry = HandlerRegistry()
        self._credential_manager: Any = None

        self.chat_ids: dict[str, str] = {}
        self.default_chat_name: str | None = None

        self.application: Any = None
        self.loop: asyncio.AbstractEventLoop | None = None
        self.polling_thread: Thread | None = None
        self._running = False  # True while polling thread is active (send_msg works)
        self._stop_event = Event()  # set by stop() so run() returns

        self.api_server: ApiServer | None = None

        _print_banner(self.logger, "TgrEzLi", f"v{__version__}", "eaannist")

    def _get_credential_manager(self) -> Any:
        if self._credential_manager is None:
            from TgrEzLi.crypto import CredentialManager
            self._credential_manager = CredentialManager()
        return self._credential_manager

    def set_save_log(self, flag: bool) -> None:
        """Enable or disable writing logs to the log file."""
        self.config.save_log = flag
        self.logger.set_save_log(flag)

    def set_host(self, host: str) -> None:
        """Set the API server host."""
        self.config.api_host = host

    def set_port(self, port: int) -> None:
        """Set the API server port."""
        self.config.api_port = port

    def signup(self, token: str, chat_dict: dict[str, str], password: str) -> None:
        """Encrypt and save token + chat_dict, then connect."""
        self._get_credential_manager().signup(token, chat_dict, password)
        self.login(password)

    def login(self, password: str) -> None:
        """Load token + chat_dict from encrypted file and connect."""
        token, chat_dict = self._get_credential_manager().login(password)
        self.connect(token, chat_dict)

    def connect(self, token: str, chat_dict: dict[str, str]) -> None:
        """Prepare the bot and start polling in a background thread. send_msg() works immediately; use run() to block the main thread until stop()."""
        if not chat_dict:
            raise ValueError("Chat dictionary is empty.")
        self.chat_ids = {str(k): str(v) for k, v in chat_dict.items()}
        self.default_chat_name = next(iter(self.chat_ids))

        self.loop = asyncio.new_event_loop()
        self.application = ApplicationBuilder().token(token).build()

        self.application.add_handler(MessageHandler(filters.COMMAND, self._command_handler), group=0)
        self.application.add_handler(
            MessageHandler(filters.TEXT & ~filters.COMMAND, self._message_handler),
            group=1,
        )
        self.application.add_handler(CallbackQueryHandler(self._callback_handler), group=2)
        self.application.add_error_handler(self._error_handler)

        self.polling_thread = Thread(target=self._run_polling, daemon=True)
        self.polling_thread.start()
        self._running = True
        self.logger.info("Bot connected and polling (background). send_msg() works; call run() to block until stop().")

    def _run_polling(self) -> None:
        try:
            asyncio.set_event_loop(self.loop)
            if self.application and self.loop:
                self.loop.run_until_complete(self.application.run_polling())
        except Exception as e:
            self.logger.error("Error in run_polling: %s", e)
            self.logger.debug(traceback.format_exc())
        finally:
            self._running = False
            self.logger.info("Polling thread exiting.")
            if self.application and self.loop:
                try:
                    self.loop.run_until_complete(self.application.shutdown())
                except Exception:
                    pass
            if self.loop and self.loop.is_running():
                self.loop.stop()
            if self.loop:
                self.loop.close()
            self.loop = None
            self.application = None

    def run(self) -> None:
        """Block the main thread until stop() is called (e.g. Ctrl+C). Starts the API server in a background thread if routes are registered. Polling is already active after connect()."""
        if not self._running:
            raise RuntimeError("Call connect() before run().")
        if self.registry.api_routes and not self.api_server:
            self.api_server = ApiServer(
                self.logger, self.registry,
                host=self.config.api_host,
                port=self.config.api_port,
            )
            self.api_server.start()

        def _on_signal(_sig: int, _frame: object) -> None:
            self.logger.info("Shutdown requested (signal).")
            self.stop()

        try:
            signal.signal(signal.SIGINT, _on_signal)
        except ValueError:
            pass

        self._stop_event.clear()
        self.logger.info("Main thread blocking until stop() (Ctrl+C).")
        self._stop_event.wait()

    def stop(self) -> None:
        """Stop polling and API server; unblocks run() if it was waiting. Safe to call from a signal handler or another thread."""
        if self.application and getattr(self.application, "updater", None):
            self.application.updater.stop()
        if self.application:
            self.application.stop()
        if self.api_server:
            self.api_server.stop()
            self.api_server = None
        self._stop_event.set()
        if self.polling_thread and self.polling_thread.is_alive():
            self.logger.info("Waiting for polling thread to exit...")
            self.polling_thread.join(timeout=5)
        self._running = False
        self.logger.info("Bot stopped.")

    async def _error_handler(self, update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
        self.logger.error("Unhandled error!")
        self.logger.error(str(context.error))
        self.logger.debug(traceback.format_exc())

    async def _message_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        if not update.message:
            return
        chat_id = str(update.effective_chat.id)
        chat_name = self._get_chat_name_by_id(chat_id)
        if not chat_name:
            return
        text = update.message.text or ""
        tgmsg_data = TgMsgData(
            text=text,
            msg_id=update.message.message_id,
            chat_id=chat_id,
            user_id=update.effective_user.id if update.effective_user else None,
            user_name=update.effective_user.username if update.effective_user else None,
            timestamp=update.message.date,
            raw_update=update,
        )
        for chats, func in self.registry.message_handlers:
            if chat_name in chats:
                call_user_function(self.logger, func, TgMsg=tgmsg_data)

    async def _command_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        if not update.message:
            return
        text = update.message.text or ""
        parts = text.strip().split(maxsplit=1)
        command_part = parts[0].lower()
        args_part = parts[1] if len(parts) > 1 else ""
        if "@" in command_part:
            command_part = command_part.split("@", 1)[0]
        command_name = command_part.lstrip("/")

        chat_id = str(update.effective_chat.id)
        chat_name = self._get_chat_name_by_id(chat_id)
        if not chat_name:
            return

        tgcmd_data = TgCmdData(
            command=command_part,
            args=args_part,
            msg_id=update.message.message_id,
            chat_id=chat_id,
            user_id=update.effective_user.id if update.effective_user else None,
            user_name=update.effective_user.username if update.effective_user else None,
            timestamp=update.message.date,
            raw_update=update,
        )
        if command_name in self.registry.command_handlers:
            for chats, func in self.registry.command_handlers[command_name]:
                if chat_name in chats:
                    call_user_function(self.logger, func, TgCmd=tgcmd_data)

    async def _callback_handler(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        if not update.callback_query:
            return
        cb_query = update.callback_query
        chat_id = str(update.effective_chat.id)
        chat_name = self._get_chat_name_by_id(chat_id)
        if not chat_name:
            return

        button_text = None
        if cb_query.message and cb_query.message.reply_markup:
            for row in cb_query.message.reply_markup.inline_keyboard:
                for btn in row:
                    if btn.callback_data == cb_query.data:
                        button_text = btn.text
                        break

        tgcb_data = TgCbData(
            text=button_text,
            value=cb_query.data,
            msg_id=cb_query.message.message_id if cb_query.message else None,
            chat_id=chat_id,
            user_id=cb_query.from_user.id if cb_query.from_user else None,
            user_name=cb_query.from_user.username if cb_query.from_user else None,
            timestamp=cb_query.message.date if cb_query.message else None,
            raw_update=update,
        )
        await cb_query.answer()

        for chats, func in self.registry.callback_handlers:
            if chat_name in chats:
                call_user_function(self.logger, func, TgCb=tgcb_data)

    def on_message(self, chat: str | list[str] | set[str] | None = None) -> Any:
        """Decorator for message handlers."""
        def decorator(func: Any) -> Any:
            self.registry.register_message_handler(func, self._parse_chat(chat))
            return func
        return decorator

    def on_command(self, command: str, chat: str | list[str] | set[str] | None = None) -> Any:
        """Decorator for command handlers."""
        cmd = command.lstrip("/")
        def decorator(func: Any) -> Any:
            self.registry.register_command_handler(cmd, func, self._parse_chat(chat))
            return func
        return decorator

    def on_callback(self, chat: str | list[str] | set[str] | None = None) -> Any:
        """Decorator for callback (inline keyboard) handlers."""
        def decorator(func: Any) -> Any:
            self.registry.register_callback_handler(func, self._parse_chat(chat))
            return func
        return decorator

    def on_api_req(
        self,
        endpoint: str,
        args: list[str] | None = None,
        host: str | None = None,
        port: int | None = None,
    ) -> Any:
        """Decorator for custom API POST endpoints. Starts the API server if needed."""
        if args is None:
            args = []
        def decorator(func: Any) -> Any:
            self.registry.register_api_route(endpoint, args, func)
            if not self.api_server:
                h = host or self.config.api_host
                p = port or self.config.api_port
                self.api_server = ApiServer(self.logger, self.registry, host=h, port=p)
                self.api_server.start()
            return func
        return decorator

    def send_msg(self, text: str, chat: str | list[str] | set[str] | None = None) -> None:
        self._send(self.application.bot.send_message, {"text": text}, chat)

    def reply_to_msg(
        self,
        text: str,
        msg_id: int,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self._send(
            self.application.bot.send_message,
            {"text": text, "reply_to_message_id": msg_id},
            chat,
        )

    def send_img(
        self,
        photo_path: str,
        caption: str | None = None,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self._send(
            self.application.bot.send_photo,
            {"photo": photo_path, "caption": caption},
            chat,
        )

    def send_file(
        self,
        file_path: str,
        caption: str | None = None,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self._send(
            self.application.bot.send_document,
            {"document": file_path, "caption": caption},
            chat,
        )

    def send_position(
        self,
        latitude: float,
        longitude: float,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self._send(
            self.application.bot.send_location,
            {"latitude": latitude, "longitude": longitude},
            chat,
        )

    def send_buttons(
        self,
        text: str,
        buttons: list[list[dict[str, str]]],
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        kb = [
            [InlineKeyboardButton(btn["text"], callback_data=btn["value"]) for btn in row]
            for row in buttons
        ]
        markup = InlineKeyboardMarkup(kb)
        self._send(
            self.application.bot.send_message,
            {"text": text, "reply_markup": markup},
            chat,
        )

    def _send(
        self,
        send_func: Any,
        params: dict[str, Any],
        chat: str | list[str] | set[str] | None,
    ) -> None:
        if not self._running or not self.application or not self.loop:
            raise RuntimeError("Bot is not connected. Call connect() first.")
        chats = self._parse_chat(chat)
        for chat_name in chats:
            if chat_name not in self.chat_ids:
                self.logger.error("Chat '%s' not found. Message not sent.", chat_name)
                continue
            params["chat_id"] = self.chat_ids[chat_name]
            coro = send_func(**params)
            asyncio.run_coroutine_threadsafe(coro, self.loop)

    def send_log(
        self,
        limit: int | None = None,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        """Send the last `limit` lines of the log file, or the full log if limit is None."""
        try:
            with open(self.config.log_file, "r", encoding="utf-8") as f:
                lines = f.readlines()
            text = "".join(lines[-limit:]) if limit else "".join(lines)
        except OSError as e:
            text = f"Error reading log file: {e}"
        self.send_msg(text, chat)

    def send_info(self, chat: str | list[str] | set[str] | None = None) -> None:
        """Send bot info (chats, handlers, API server, log saving) to the given chat."""
        info = "Bot Info:\n"
        info += f"Chat IDs: {self.chat_ids}\n"
        info += f"onMessage handlers: {len(self.registry.message_handlers)}\n"
        info += "onCommand handlers: {\n"
        for cmd, funcs in self.registry.command_handlers.items():
            info += f"  /{cmd} : {len(funcs)} handlers\n"
        info += "}\n"
        info += f"onCallback handlers: {len(self.registry.callback_handlers)}\n"
        info += f"API Routes: {list(self.registry.api_routes.keys())}\n"
        if self.api_server:
            info += f"API Server: {self.api_server.host}:{self.api_server.port}\n"
        else:
            info += "API Server: Not started\n"
        info += f"Log saving: {self.config.save_log}\n"
        self.send_msg(info, chat)

    def send_registered_handlers(
        self,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        """Send a list of registered handlers for debugging."""
        txt = "Registered handlers:\n\nonMessage:\n"
        for chats, func in self.registry.message_handlers:
            txt += f" - {func.__name__} on {chats}\n"
        txt += "\nonCommand:\n"
        for cmd, lst in self.registry.command_handlers.items():
            for chats, func in lst:
                txt += f" - /{cmd} -> {func.__name__} on {chats}\n"
        txt += "\nonCallback:\n"
        for chats, func in self.registry.callback_handlers:
            txt += f" - {func.__name__} on {chats}\n"
        txt += "\nAPI Routes:\n"
        for route in self.registry.api_routes:
            txt += f" - {route}\n"
        self.send_msg(txt, chat)

    def _get_chat_name_by_id(self, chat_id: str) -> str | None:
        for name, cid in self.chat_ids.items():
            if cid == chat_id:
                return name
        return None

    def _parse_chat(
        self,
        chat: str | list[str] | set[str] | None,
    ) -> set[str]:
        if chat is None:
            return {self.default_chat_name} if self.default_chat_name else set()
        if isinstance(chat, str):
            return {chat}
        if isinstance(chat, (list, tuple, set)):
            return set(chat)
        raise ValueError(f"Invalid chat parameter: {chat}")

    # Aliases for backward-friendly API (camelCase)
    def setSaveLog(self, flag: bool) -> None:
        self.set_save_log(flag)

    def setHost(self, host: str) -> None:
        self.set_host(host)

    def setPort(self, port: int) -> None:
        self.set_port(port)

    def onMessage(self, chat: str | list[str] | set[str] | None = None) -> Any:
        return self.on_message(chat)

    def onCommand(self, command: str, chat: str | list[str] | set[str] | None = None) -> Any:
        return self.on_command(command, chat)

    def onCallback(self, chat: str | list[str] | set[str] | None = None) -> Any:
        return self.on_callback(chat)

    def onApiReq(
        self,
        endpoint: str,
        args: list[str] | None = None,
        host: str | None = None,
        port: int | None = None,
    ) -> Any:
        return self.on_api_req(endpoint, args, host, port)

    def sendMsg(self, text: str, chat: str | list[str] | set[str] | None = None) -> None:
        self.send_msg(text, chat)

    def replyToMsg(
        self,
        text: str,
        msg_id: int,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self.reply_to_msg(text, msg_id, chat)

    def sendImg(
        self,
        photo_path: str,
        caption: str | None = None,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self.send_img(photo_path, caption, chat)

    def sendFile(
        self,
        file_path: str,
        caption: str | None = None,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self.send_file(file_path, caption, chat)

    def sendPosition(
        self,
        latitude: float,
        longitude: float,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self.send_position(latitude, longitude, chat)

    def sendButtons(
        self,
        text: str,
        buttons: list[list[dict[str, str]]],
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self.send_buttons(text, buttons, chat)

    def sendLog(
        self,
        limit: int | None = None,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self.send_log(limit, chat)

    def sendInfo(self, chat: str | list[str] | set[str] | None = None) -> None:
        self.send_info(chat)

    def sendRegisteredHandlers(
        self,
        chat: str | list[str] | set[str] | None = None,
    ) -> None:
        self.send_registered_handlers(chat)


__all__ = ["TEL", "__version__"]
