"""Directory browser callback handlers.

Handles all inline keyboard callbacks for the directory browser UI:
  - CB_DIR_SELECT: Navigate into a subdirectory
  - CB_DIR_UP: Navigate to parent directory
  - CB_DIR_PAGE: Paginate directory listing
  - CB_DIR_CONFIRM: Confirm directory selection, show provider picker
  - CB_PROV_SELECT: Select provider and create tmux window
  - CB_DIR_CANCEL: Cancel directory browsing
  - CB_DIR_FAV: Select a favorite directory
  - CB_DIR_STAR: Star/unstar a directory

Key function: handle_directory_callback (uniform callback handler signature).
"""

import structlog
from pathlib import Path

from telegram import CallbackQuery, Update
from telegram.error import TelegramError
from telegram.ext import ContextTypes

from ..providers import registry as provider_registry
from ..session import session_manager
from ..tmux_manager import tmux_manager
from .callback_data import (
    CB_DIR_CANCEL,
    CB_DIR_CONFIRM,
    CB_DIR_FAV,
    CB_DIR_PAGE,
    CB_DIR_SELECT,
    CB_DIR_STAR,
    CB_DIR_UP,
    CB_PROV_SELECT,
)
from .callback_helpers import get_thread_id
from .directory_browser import (
    BROWSE_DIRS_KEY,
    BROWSE_PAGE_KEY,
    BROWSE_PATH_KEY,
    build_directory_browser,
    build_provider_picker,
    clear_browse_state,
    get_favorites,
)
from .message_sender import safe_edit, safe_send
from .user_state import PENDING_THREAD_ID, PENDING_THREAD_TEXT

logger = structlog.get_logger()


async def handle_directory_callback(
    query: CallbackQuery,
    user_id: int,
    data: str,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle directory browser callbacks.

    Dispatches to the appropriate sub-handler based on callback data prefix.
    """
    if data.startswith(CB_DIR_FAV):
        await _handle_fav(query, user_id, data, update, context)
    elif data.startswith(CB_DIR_STAR):
        await _handle_star(query, user_id, data, update, context)
    elif data.startswith(CB_DIR_SELECT):
        await _handle_select(query, user_id, data, update, context)
    elif data == CB_DIR_UP:
        await _handle_up(query, user_id, update, context)
    elif data.startswith(CB_DIR_PAGE):
        await _handle_page(query, user_id, data, update, context)
    elif data == CB_DIR_CONFIRM:
        await _handle_confirm(query, user_id, update, context)
    elif data.startswith(CB_PROV_SELECT):
        await _handle_provider_select(query, user_id, data, update, context)
    elif data == CB_DIR_CANCEL:
        await _handle_cancel(query, update, context)


async def _resolve_fav_index(
    query: CallbackQuery,
    user_id: int,
    data: str,
    prefix: str,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> str | None:
    """Validate pending thread, parse fav index, and return the fav path or None."""
    pending_tid = (
        context.user_data.get(PENDING_THREAD_ID) if context.user_data else None
    )
    if pending_tid is not None and get_thread_id(update) != pending_tid:
        await query.answer("Stale browser (topic mismatch)", show_alert=True)
        return None
    try:
        idx = int(data[len(prefix) :])
    except ValueError:
        await query.answer("Invalid data")
        return None

    favorites, _starred = get_favorites(user_id)
    if idx < 0 or idx >= len(favorites):
        await query.answer("Favorite not found", show_alert=True)
        return None
    return favorites[idx]


async def _handle_fav(
    query: CallbackQuery,
    user_id: int,
    data: str,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_DIR_FAV: select a favorite directory and navigate into it."""
    fav_path = await _resolve_fav_index(
        query, user_id, data, CB_DIR_FAV, update, context
    )
    if fav_path is None:
        return
    if not Path(fav_path).is_dir():
        await query.answer("Directory no longer exists", show_alert=True)
        return

    if context.user_data is not None:
        context.user_data[BROWSE_PATH_KEY] = fav_path
        context.user_data[BROWSE_PAGE_KEY] = 0

    msg_text, keyboard, subdirs = build_directory_browser(fav_path, user_id=user_id)
    if context.user_data is not None:
        context.user_data[BROWSE_DIRS_KEY] = subdirs
    await safe_edit(query, msg_text, reply_markup=keyboard)
    await query.answer()


async def _handle_star(
    query: CallbackQuery,
    user_id: int,
    data: str,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_DIR_STAR: toggle star on a favorite directory."""
    fav_path = await _resolve_fav_index(
        query, user_id, data, CB_DIR_STAR, update, context
    )
    if fav_path is None:
        return
    now_starred = session_manager.toggle_user_star(user_id, fav_path)

    # Rebuild browser at current path to update star icons
    default_path = str(Path.cwd())
    current_path = (
        context.user_data.get(BROWSE_PATH_KEY, default_path)
        if context.user_data
        else default_path
    )
    current_page = context.user_data.get(BROWSE_PAGE_KEY, 0) if context.user_data else 0
    msg_text, keyboard, subdirs = build_directory_browser(
        current_path, current_page, user_id=user_id
    )
    if context.user_data is not None:
        context.user_data[BROWSE_DIRS_KEY] = subdirs
    await safe_edit(query, msg_text, reply_markup=keyboard)
    await query.answer("⭐ Starred" if now_starred else "☆ Unstarred")


async def _handle_select(
    query: CallbackQuery,
    user_id: int,
    data: str,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_DIR_SELECT: navigate into a subdirectory."""
    pending_tid = (
        context.user_data.get(PENDING_THREAD_ID) if context.user_data else None
    )
    if pending_tid is not None and get_thread_id(update) != pending_tid:
        await query.answer("Stale browser (topic mismatch)", show_alert=True)
        return
    try:
        idx = int(data[len(CB_DIR_SELECT) :])
    except ValueError:
        await query.answer("Invalid data")
        return

    cached_dirs: list[str] = (
        context.user_data.get(BROWSE_DIRS_KEY, []) if context.user_data else []
    )
    if idx < 0 or idx >= len(cached_dirs):
        await query.answer("Directory list changed, please refresh", show_alert=True)
        return
    subdir_name = cached_dirs[idx]

    default_path = str(Path.cwd())
    current_path = (
        context.user_data.get(BROWSE_PATH_KEY, default_path)
        if context.user_data
        else default_path
    )
    new_path = (Path(current_path) / subdir_name).resolve()

    if not new_path.exists() or not new_path.is_dir():
        await query.answer("Directory not found", show_alert=True)
        return

    new_path_str = str(new_path)
    if context.user_data is not None:
        context.user_data[BROWSE_PATH_KEY] = new_path_str
        context.user_data[BROWSE_PAGE_KEY] = 0

    msg_text, keyboard, subdirs = build_directory_browser(new_path_str, user_id=user_id)
    if context.user_data is not None:
        context.user_data[BROWSE_DIRS_KEY] = subdirs
    await safe_edit(query, msg_text, reply_markup=keyboard)
    await query.answer()


async def _handle_up(
    query: CallbackQuery,
    user_id: int,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_DIR_UP: navigate to parent directory."""
    pending_tid = (
        context.user_data.get(PENDING_THREAD_ID) if context.user_data else None
    )
    if pending_tid is not None and get_thread_id(update) != pending_tid:
        await query.answer("Stale browser (topic mismatch)", show_alert=True)
        return
    default_path = str(Path.cwd())
    current_path = (
        context.user_data.get(BROWSE_PATH_KEY, default_path)
        if context.user_data
        else default_path
    )
    current = Path(current_path).resolve()
    parent = current.parent

    parent_path = str(parent)
    if context.user_data is not None:
        context.user_data[BROWSE_PATH_KEY] = parent_path
        context.user_data[BROWSE_PAGE_KEY] = 0

    msg_text, keyboard, subdirs = build_directory_browser(parent_path, user_id=user_id)
    if context.user_data is not None:
        context.user_data[BROWSE_DIRS_KEY] = subdirs
    await safe_edit(query, msg_text, reply_markup=keyboard)
    await query.answer()


async def _handle_page(
    query: CallbackQuery,
    user_id: int,
    data: str,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_DIR_PAGE: paginate directory listing."""
    pending_tid = (
        context.user_data.get(PENDING_THREAD_ID) if context.user_data else None
    )
    if pending_tid is not None and get_thread_id(update) != pending_tid:
        await query.answer("Stale browser (topic mismatch)", show_alert=True)
        return
    try:
        pg = int(data[len(CB_DIR_PAGE) :])
    except ValueError:
        await query.answer("Invalid data")
        return
    default_path = str(Path.cwd())
    current_path = (
        context.user_data.get(BROWSE_PATH_KEY, default_path)
        if context.user_data
        else default_path
    )
    if context.user_data is not None:
        context.user_data[BROWSE_PAGE_KEY] = pg

    msg_text, keyboard, subdirs = build_directory_browser(
        current_path, pg, user_id=user_id
    )
    if context.user_data is not None:
        context.user_data[BROWSE_DIRS_KEY] = subdirs
    await safe_edit(query, msg_text, reply_markup=keyboard)
    await query.answer()


async def _handle_confirm(
    query: CallbackQuery,
    user_id: int,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_DIR_CONFIRM: confirm directory, show provider picker."""
    default_path = str(Path.cwd())
    selected_path = (
        context.user_data.get(BROWSE_PATH_KEY, default_path)
        if context.user_data
        else default_path
    )
    pending_thread_id: int | None = (
        context.user_data.get(PENDING_THREAD_ID) if context.user_data else None
    )

    confirm_thread_id = get_thread_id(update)
    if pending_thread_id is not None and confirm_thread_id != pending_thread_id:
        clear_browse_state(context.user_data)
        if context.user_data is not None:
            context.user_data.pop(PENDING_THREAD_ID, None)
            context.user_data.pop(PENDING_THREAD_TEXT, None)
        await query.answer("Stale browser (topic mismatch)", show_alert=True)
        return

    await query.answer()

    # Guard against double-click: if thread already has a window, skip
    if pending_thread_id is not None:
        existing_wid = session_manager.get_window_for_thread(user_id, pending_thread_id)
        if existing_wid is not None:
            display = session_manager.get_display_name(existing_wid)
            logger.warning(
                "Thread %d already bound to window %s (%s), ignoring duplicate confirm",
                pending_thread_id,
                existing_wid,
                display,
            )
            clear_browse_state(context.user_data)
            await safe_edit(
                query,
                f"✅ Already bound to window {display}.",
            )
            return

    # Show provider selection keyboard (keep browse state for _handle_provider_select)
    text, keyboard = build_provider_picker(selected_path)
    await safe_edit(query, text, reply_markup=keyboard)


async def _validate_provider_select(
    query: CallbackQuery,
    user_id: int,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
    pending_thread_id: int | None,
) -> bool:
    """Validate provider select callback; returns True if request should proceed."""
    confirm_thread_id = get_thread_id(update)
    if pending_thread_id is not None and confirm_thread_id != pending_thread_id:
        if context.user_data is not None:
            context.user_data.pop(PENDING_THREAD_ID, None)
            context.user_data.pop(PENDING_THREAD_TEXT, None)
        await query.answer("Stale browser (topic mismatch)", show_alert=True)
        return False

    await query.answer()

    # Guard against double-click: if thread already has a window, skip
    if pending_thread_id is not None:
        existing_wid = session_manager.get_window_for_thread(user_id, pending_thread_id)
        if existing_wid is not None:
            display = session_manager.get_display_name(existing_wid)
            logger.warning(
                "Thread %d already bound to window %s (%s), ignoring duplicate provider select",
                pending_thread_id,
                existing_wid,
                display,
            )
            await safe_edit(query, f"✅ Already bound to window {display}.")
            return False

    return True


async def _handle_provider_select(
    query: CallbackQuery,
    user_id: int,
    data: str,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_PROV_SELECT: select provider and create tmux window."""
    provider_name = data[len(CB_PROV_SELECT) :]
    if not provider_registry.is_valid(provider_name):
        await query.answer("Unknown provider", show_alert=True)
        return

    default_path = str(Path.cwd())
    selected_path = (
        context.user_data.get(BROWSE_PATH_KEY, default_path)
        if context.user_data
        else default_path
    )
    pending_thread_id: int | None = (
        context.user_data.get(PENDING_THREAD_ID) if context.user_data else None
    )

    # Clear browse state now that we've read the path
    clear_browse_state(context.user_data)

    if not await _validate_provider_select(
        query, user_id, update, context, pending_thread_id
    ):
        return

    # Resolve launch command (env override > provider default)
    from ccbot.providers import resolve_launch_command

    launch_command = resolve_launch_command(provider_name)

    success, message, created_wname, created_wid = await tmux_manager.create_window(
        selected_path, launch_command=launch_command
    )
    if success:
        session_manager.update_user_mru(user_id, selected_path)
        # Set cwd before set_window_provider so it's included in the save.
        # Recovery needs cwd for hookless providers (codex/gemini) that never
        # write session_map.json. For Claude, the hook overwrites via
        # load_session_map — harmless duplication.
        window_state = session_manager.get_window_state(created_wid)
        window_state.cwd = selected_path
        session_manager.set_window_provider(created_wid, provider_name)
        logger.info(
            "Window created: %s (id=%s) at %s provider=%s (user=%d, thread=%s)",
            created_wname,
            created_wid,
            selected_path,
            provider_name,
            user_id,
            pending_thread_id,
        )
        if provider_registry.get(provider_name).capabilities.supports_hook:
            await session_manager.wait_for_session_map_entry(created_wid)

        if pending_thread_id is not None:
            session_manager.bind_thread(
                user_id, pending_thread_id, created_wid, window_name=created_wname
            )

            try:
                await context.bot.edit_forum_topic(
                    chat_id=session_manager.resolve_chat_id(user_id, pending_thread_id),
                    message_thread_id=pending_thread_id,
                    name=created_wname,
                )
            except TelegramError as e:
                logger.debug("Failed to rename topic: %s", e)

            await safe_edit(
                query,
                f"✅ {message}\n\nBound to this topic. Send messages here.",
            )

            pending_text = (
                context.user_data.get(PENDING_THREAD_TEXT)
                if context.user_data
                else None
            )
            if pending_text:
                logger.debug(
                    "Forwarding pending text to window %s (len=%d)",
                    created_wname,
                    len(pending_text),
                )
                if context.user_data is not None:
                    context.user_data.pop(PENDING_THREAD_TEXT, None)
                    context.user_data.pop(PENDING_THREAD_ID, None)
                send_ok, send_msg = await session_manager.send_to_window(
                    created_wid,
                    pending_text,
                )
                if not send_ok:
                    logger.warning("Failed to forward pending text: %s", send_msg)
                    await safe_send(
                        context.bot,
                        session_manager.resolve_chat_id(user_id, pending_thread_id),
                        f"❌ Failed to send pending message: {send_msg}",
                        message_thread_id=pending_thread_id,
                    )
            elif context.user_data is not None:
                context.user_data.pop(PENDING_THREAD_ID, None)
        else:
            await safe_edit(query, f"✅ {message}")
    else:
        await safe_edit(query, f"❌ {message}")
        if pending_thread_id is not None and context.user_data is not None:
            context.user_data.pop(PENDING_THREAD_ID, None)
            context.user_data.pop(PENDING_THREAD_TEXT, None)


async def _handle_cancel(
    query: CallbackQuery,
    update: Update,
    context: ContextTypes.DEFAULT_TYPE,
) -> None:
    """Handle CB_DIR_CANCEL: cancel directory browsing."""
    pending_tid = (
        context.user_data.get(PENDING_THREAD_ID) if context.user_data else None
    )
    if pending_tid is not None and get_thread_id(update) != pending_tid:
        await query.answer("Stale browser (topic mismatch)", show_alert=True)
        return
    clear_browse_state(context.user_data)
    if context.user_data is not None:
        context.user_data.pop(PENDING_THREAD_ID, None)
        context.user_data.pop(PENDING_THREAD_TEXT, None)
    await safe_edit(query, "Cancelled")
    await query.answer("Cancelled")
