import asyncio
import logging
from contextlib import suppress
from datetime import UTC, datetime

from pydantic_ai import Agent
from pydantic_ai_backends import LocalBackend
from telegram import Bot, Update
from telegram.constants import ChatAction
from telegram.ext import (
    BaseHandler,
    CommandHandler,
    ContextTypes,
    MessageHandler,
    filters,
)

from app.agent import run_agent_interaction
from app.agent.deps import Deps
from app.channels.telegram.ask_user import (
    make_ask_user_callback,
    resolve_pending_text_response,
)
from app.channels.telegram.streaming import MessageStreamer
from app.context import Context
from app.context.state import State
from app.settings import Contacts, TelegramContact

logger = logging.getLogger(__name__)

Handler = BaseHandler[Update, ContextTypes.DEFAULT_TYPE, object]


def _upsert_telegram_contact(
    ctx: Context,
    user_id: int,
    first_name: str,
    username: str,
    authorized: bool,
    pending_message: str | None = None,
) -> None:
    try:
        key = str(user_id)
        last_seen = datetime.now(tz=UTC).isoformat()

        def update(contacts: Contacts) -> Contacts:
            entry = contacts.telegram.get(key) or TelegramContact(
                user_id=user_id, authorized=authorized
            )
            entry.user_id = user_id
            entry.first_name = first_name
            entry.username = username
            entry.last_seen = last_seen
            if not authorized and pending_message is not None:
                entry.pending_messages.append(pending_message)
            return contacts.model_copy(update={"telegram": {**contacts.telegram, key: entry}})

        ctx.contacts.set(update)
    except Exception:
        logger.exception("Failed to upsert telegram contact %s", user_id)


async def typing_indicator_loop(bot: Bot, chat_id: int) -> None:
    try:
        while True:
            await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
            await asyncio.sleep(4)
    except asyncio.CancelledError:
        return


def create_start_command_handler(ctx: Context) -> Handler:
    async def handle_start_command(update: Update, _context: ContextTypes.DEFAULT_TYPE) -> None:
        if update.message is None or update.effective_user is None:
            return

        await update.message.reply_text(
            "Olá! Vou continuar a conversa no histórico atual. Como posso ajudar?"
        )

    return CommandHandler("start", handle_start_command)


async def _resolve_input(
    ctx: Context,
    update: Update,
    user_id: int,
) -> str | None:
    config = ctx.settings.get()
    if update.message is None:
        return None

    msg = update.message
    attachment = (
        msg.document
        or (msg.photo[-1] if msg.photo else None)
        or msg.audio
        or msg.voice
        or msg.video
    )

    if msg.text:
        return msg.text

    if attachment is None:
        return None

    if not config.messages.accept_files:
        disabled_notice = (
            "[Usuário tentou enviar um arquivo,"
            " mas o recebimento de arquivos está desabilitado neste canal.]"
        )
        caption = (msg.caption or "").strip()
        if caption:
            return f"{disabled_notice}\n\nMensagem do usuário: {caption}"
        return disabled_notice

    unique_id = str(getattr(attachment, "file_unique_id", "upload"))
    filename = str(getattr(attachment, "file_name", None) or f"file_{unique_id}")

    tg_file = await attachment.get_file()
    data = bytes(await tg_file.download_as_bytearray())

    workspace_dir = ctx.paths.user_workspace(user_id)
    all_dirs = [str(workspace_dir)]
    backend = LocalBackend(
        root_dir=workspace_dir,
        allowed_directories=all_dirs,
        enable_execute=False,
        ask_fallback="deny",
    )
    backend.write(f"uploads/{filename}", data)

    message = f"[Arquivo recebido: {filename} ({len(data)} bytes) — salvo em uploads/{filename}]"
    caption = (msg.caption or "").strip()
    if caption:
        message += f"\n\nMensagem do usuário: {caption}"
    return message


async def _process_message(
    ctx: Context,
    agent: Agent[Deps, str],
    user_input: str,
    user_id: int,
    bot: Bot,
    chat_id: int,
) -> None:
    streamer = MessageStreamer(bot, chat_id, messages=ctx.messages.get)
    await streamer.start()

    typing_task = asyncio.create_task(typing_indicator_loop(bot, chat_id))
    try:
        await run_agent_interaction(
            ctx=ctx,
            agent=agent,
            user_input=user_input,
            user_id=user_id,
            event_stream_handler=streamer.event_handler,
            ask_user=make_ask_user_callback(user_id, bot, chat_id),
            send_file=streamer.send_file,
        )
        await streamer.finalize()
    except Exception as error:
        logger.exception("Error processing message")
        await bot.send_message(
            chat_id=chat_id, text=f"Desculpe, ocorreu um erro: {str(error)[:100]}"
        )
    finally:
        typing_task.cancel()
        with suppress(asyncio.CancelledError):
            await typing_task


def create_message_handler(ctx: Context, agent_state: State[Agent[Deps, str]]) -> Handler:
    async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
        if update.message is None or update.effective_user is None:
            return

        user_id = update.effective_user.id
        first_name = update.effective_user.first_name or ""
        username = update.effective_user.username or ""
        authorized = ctx.contacts.get().is_authorized_for_telegram(user_id)

        user_input = await _resolve_input(ctx, update, user_id)
        if user_input is None:
            return

        logger.info("Message from %s (@%s): %s", first_name, username, user_input)

        _upsert_telegram_contact(
            ctx,
            user_id,
            first_name,
            username,
            authorized,
            pending_message=user_input if not authorized else None,
        )

        if not authorized:
            return

        if update.message.text and resolve_pending_text_response(user_id, user_input):
            return

        await _process_message(
            ctx, agent_state.get(), user_input, user_id, context.bot, update.message.chat_id
        )

    message_filter = (
        (filters.TEXT & ~filters.COMMAND)
        | filters.Document.ALL
        | filters.PHOTO
        | filters.AUDIO
        | filters.VOICE
        | filters.VIDEO
    )
    return MessageHandler(message_filter, handle_message)
