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

import asyncio
import signal
import traceback
from importlib.metadata import PackageNotFoundError, version
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, merge_config
from TgrEzLi.handlers import HandlerRegistry, call_user_function
from TgrEzLi.logger import get_default_logger
from TgrEzLi.types import TgCbData, TgCmdData, TgMsgData, TgrEzLiConfig, TgrezliConfig


def _get_version() -> str:
    try:
        return version("TgrEzLi")
    except PackageNotFoundError:
        return "0.0.0"


__version__ = _get_version()


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


def _legacy_config_to_dict(config: TgrezliConfig) -> TgrEzLiConfig:
    """Convert legacy TgrezliConfig to TgrEzLiConfig-like dict for merge_config."""
    return {
        "logging": {
            "save_log_file": config.save_log,
            "log_file_path": config.log_file,
        },
        "api_server": {
            "host": config.api_host,
            "port": config.api_port,
        },
    }


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 | TgrezliConfig | None = None,
    ) -> None:
        if isinstance(config, TgrezliConfig):
            user_config: TgrEzLiConfig | None = _legacy_config_to_dict(config)
        else:
            user_config = config
        self._config = merge_config(user_config)

        log_cfg = self._config["logging"]
        self.logger = get_default_logger(
            save_log_file=log_cfg.get("save_log_file", False),
            log_file_path=log_cfg.get("log_file_path"),
        )
        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")

        conn = self._config.get("connection") or {}
        token = conn.get("token")
        chats = conn.get("chats")
        if token and chats:
            self.connect(token, chats)

    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 (save_log_file)."""
        self._config["logging"]["save_log_file"] = flag
        self.logger.set_save_log_file(flag)

    def set_host(self, host: str) -> None:
        """Set the API server host."""
        self._config["api_server"]["host"] = host

    def set_port(self, port: int) -> None:
        """Set the API server port."""
        self._config["api_server"]["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().")
        api = self._config.get("api_server") or {}
        if self.registry.api_routes and not self.api_server:
            self.api_server = ApiServer(
                self.logger,
                self.registry,
                host=api.get("host", "localhost"),
                port=api.get("port", 9999),
            )
            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:
                api = self._config.get("api_server") or {}
                h = host or api.get("host", "localhost")
                p = port or api.get("port", 9999)
                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."""
        log_path = (self._config.get("logging") or {}).get("log_file_path", "TgrEzLi.log")
        try:
            with open(log_path, "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.get('logging') or {}).get('save_log_file', False)}\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 set_save_log_file(self, flag: bool) -> None:
        """Alias for set_save_log (save_log_file)."""
        self.set_save_log(flag)

    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__"]
