# app/src/telegram/handlers/portfolio.py
from __future__ import annotations

from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import (
    CallbackQuery,
    InaccessibleMessage,
    InlineKeyboardMarkup,
    Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder

from src.core.numbers import safe_float
from src.core.types import NumericLike
from src.integrations.exchange.models import BalanceSummary
from src.integrations.exchange.service import ExchangeService
from src.integrations.exchange.runtime_ui import (
    build_runtime_exchange_alerts,
    format_runtime_exchange_alerts,
)
from src.integrations.exchange.status import classify_exchange_error
from src.telegram.live.active_screen import ActiveScreenManager
from src.telegram.live.runner import LiveScreen, LiveScreenRunner, ScreenRegistry
from src.telegram.ui.common import mode_line, now_line
from src.telegram.ui.currency_ui import (
    balance_total,
    estimate_balance_usd,
    format_usd_amount,
    is_zero_balance,
)
from src.trading.accounts.service import AccountsService
from src.trading.journal.service import JournalService


router = Router(name="portfolio")


PINNED_ORDER = {
    "USD": 1,
    "USDT": 2,
    "BTC": 3,
    "ETH": 4,
}


# Получить доступное Telegram-сообщение из callback.
def _require_message(
    callback: CallbackQuery,
) -> Message | None:
    message = callback.message

    if (
        message is None
        or isinstance(message, InaccessibleMessage)
    ):
        return None

    return message


# Отформатировать количество актива компактно для портфеля.
def _compact_amount(currency: str, value: NumericLike) -> str:
    number = safe_float(value) or 0.0
    currency = currency.upper()

    if currency in {"USD", "USDT", "EUR"}:
        return format_usd_amount(number)

    text = f"{number:.8f}"

    if "." in text:
        integer, frac = text.split(".")
        integer = f"{int(integer):,}".replace(",", " ")

        stripped = frac.rstrip("0")

        if stripped == "":
            return f"{integer}.00"

        if len(stripped) <= 2:
            return f"{integer}.{stripped.ljust(2, '0')}"

        return f"{integer}.{stripped}"

    return f"{int(text):,}".replace(",", " ")


# Клавиатура портфеля при частичных данных или ошибке.
def _portfolio_warning_keyboard() -> InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()
    builder.button(text="🔁 Обновить", callback_data="portfolio:retry")
    builder.adjust(1)
    return builder.as_markup()


# Отсортировать балансы: сначала основные активы, потом остальные по алфавиту.
def sort_balances(items: list[BalanceSummary]) -> list[BalanceSummary]:
    def sort_key(item: BalanceSummary) -> tuple[int, str]:
        currency = item.currency.upper()
        priority = PINNED_ORDER.get(currency, 999)
        return (priority, currency)

    return sorted(items, key=sort_key)


# Собрать текст ошибки портфеля через единый exchange status layer.
def _build_portfolio_exchange_error_text(exc: Exception) -> str:
    alerts = build_runtime_exchange_alerts(exc=exc)
    body = format_runtime_exchange_alerts(alerts)

    if not body:
        body = "⛔️ Биржа недоступна"

    return (
        "<b>💼 Портфель</b>\n"
        f"{mode_line()}"
        f"{body}\n\n"
        f"{now_line()}"
    )



# Собрать live-текст портфеля.
# raise_errors=True используется при ручном открытии экрана,
# чтобы handler смог записать ошибку в журнал и показать стандартный error UI.
def _build_portfolio_live_text(
    *,
    raise_errors: bool = False,
) -> tuple[str, InlineKeyboardMarkup | None]:
    service = AccountsService()
    exchange_service = ExchangeService()

    try:
        balances = service.get_live_balance_summary()

    except Exception as exc:
        if raise_errors:
            raise

        return _build_portfolio_exchange_error_text(exc), _portfolio_warning_keyboard()

    if not balances:
        text = (
            "<b>💼 Портфель</b>\n"
            f"{mode_line()}"
            "Нет данных по балансу.\n\n"
            f"{now_line()}"
        )
        return text, None

    visible_balances = [
        item
        for item in balances
        if not is_zero_balance(item)
    ]
    visible_balances = sort_balances(visible_balances)

    if not visible_balances:
        text = (
            "<b>💼 Портфель</b>\n"
            f"{mode_line()}"
            "Нет активов с балансом.\n\n"
            f"{now_line()}"
        )
        return text, None

    price_cache: dict[str, float | None] = {}
    total_estimated_usd = 0.0
    has_any_estimate = False
    missing_estimate_assets: list[str] = []

    lines: list[str] = [
        "<b>💼 Портфель</b>",
        mode_line().rstrip(),
        "",
    ]

    for item in visible_balances:
        currency = item.currency.upper()
        total = safe_float(balance_total(item)) or 0.0
        locked = safe_float(item.locked) or 0.0

        try:
            estimated_usd = estimate_balance_usd(
                item,
                exchange_service,
                price_cache,
            )
        except Exception:
            estimated_usd = None

        if estimated_usd is not None:
            total_estimated_usd += estimated_usd
            has_any_estimate = True
        elif total > 0:
            missing_estimate_assets.append(currency)

        line = f"{currency}: {_compact_amount(currency, total)}"

        if locked > 0:
            line += f" · locked {_compact_amount(currency, locked)}"

        if estimated_usd is not None and currency not in {"USD", "USDT"}:
            line += f" ≈ $ {format_usd_amount(estimated_usd)}"

        if currency == "BTC" and any("USD:" in x or "USDT:" in x for x in lines):
            lines.append("")

        lines.append(line)

    has_partial_data = len(missing_estimate_assets) > 0

    if missing_estimate_assets:
        lines.append("🟡 <b>Данные загружены частично</b>")

    if has_any_estimate:
        lines.insert(3, "")
        lines.insert(
            3,
            f"Оценка: <b>≈ $ {format_usd_amount(total_estimated_usd)}</b>",
        )

    if missing_estimate_assets:
        lines.append(f"Нет оценки: {', '.join(missing_estimate_assets)}")

    lines.extend(["", now_line()])

    reply_markup = (
        _portfolio_warning_keyboard()
        if has_partial_data
        else None
    )

    return "\n".join(lines).rstrip(), reply_markup


# Render-функция текста для live runner.
def _portfolio_live_text() -> str:
    text, _ = _build_portfolio_live_text()
    return text


# Render-функция клавиатуры для live runner.
def _portfolio_live_markup() -> InlineKeyboardMarkup | None:
    _, markup = _build_portfolio_live_text()
    return markup


def _register_portfolio_live_screen(message: Message) -> None:
    bot = message.bot

    if bot is None:
        return

    LiveScreenRunner.unregister_message(
        chat_id=message.chat.id,
        message_id=message.message_id,
    )

    ScreenRegistry.unregister_message(
        chat_id=message.chat.id,
        message_id=message.message_id,
    )

    LiveScreenRunner.register_screen(
        LiveScreen(
            screen="portfolio",
            bot=bot,
            chat_id=message.chat.id,
            message_id=message.message_id,
            render_text=_portfolio_live_text,
            render_markup=_portfolio_live_markup,
            interval_seconds=10,
        )
    )

    LiveScreenRunner.start("portfolio")


# Подготовить новый экран портфеля из обычного сообщения.
async def _prepare_portfolio_from_message(
    message: Message,
) -> bool:
    bot = message.bot

    if bot is None:
        return False

    await ActiveScreenManager.prepare_new_screen(
        screen="portfolio",
        bot=bot,
        chat_id=message.chat.id,
    )

    return True


# Подготовить экран портфеля из callback.
async def _prepare_portfolio_from_callback(
    callback: CallbackQuery,
) -> bool:
    message = _require_message(callback)

    if message is None:
        await callback.answer("Сообщение недоступно", show_alert=True)
        return False

    bot = message.bot

    if bot is None:
        await callback.answer("Bot недоступен", show_alert=True)
        return False

    await ActiveScreenManager.prepare_new_screen(
        screen="portfolio",
        bot=bot,
        chat_id=message.chat.id,
        keep_message_id=message.message_id,
    )

    return True


# Отрисовать экран портфеля и зарегистрировать live обновления.
async def _render_portfolio_screen(
    target_message: Message,
    *,
    user_id: int | None,
    chat_id: int | None,
    edit_mode: bool,
    action: str,
) -> None:
    journal = JournalService()

    journal.log_ui_info(
        event_type="portfolio_open_requested",
        message="Запрошено открытие экрана портфеля.",
        screen="portfolio",
        action=action,
        user_id=user_id,
        chat_id=chat_id,
    )

    text, reply_markup = _build_portfolio_live_text(raise_errors=True)

    journal.log_ui_info(
        event_type="portfolio_open_success",
        message="Портфель загружен.",
        screen="portfolio",
        action=action,
        user_id=user_id,
        chat_id=chat_id,
    )

    if edit_mode:
        await target_message.edit_text(text, reply_markup=reply_markup)
        _register_portfolio_live_screen(target_message)
        ActiveScreenManager.register(screen="portfolio", message=target_message)
        return

    sent_message = await target_message.answer(text, reply_markup=reply_markup)
    _register_portfolio_live_screen(sent_message)
    ActiveScreenManager.register(screen="portfolio", message=sent_message)


@router.message(F.text == "💼 Портфель")
async def open_portfolio(
    message: Message,
    state: FSMContext,
) -> None:
    await state.clear()

    if not await _prepare_portfolio_from_message(message):
        return

    user_id = message.from_user.id if message.from_user else None
    chat_id = message.chat.id

    try:
        await _render_portfolio_screen(
            message,
            user_id=user_id,
            chat_id=chat_id,
            edit_mode=False,
            action="open",
        )

    except Exception as exc:
        JournalService().log_ui_error(
            event_type="portfolio_open_error",
            message="Не удалось загрузить портфель.",
            screen="portfolio",
            action="open",
            user_id=user_id,
            chat_id=chat_id,
            error_type=classify_exchange_error(exc),
            raw_error=str(exc),
        )

        sent_message = await message.answer(
            _build_portfolio_exchange_error_text(exc),
            reply_markup=_portfolio_warning_keyboard(),
        )

        ActiveScreenManager.register(
            screen="portfolio",
            message=sent_message,
        )


@router.callback_query(F.data == "portfolio:retry")
async def retry_portfolio(
    callback: CallbackQuery,
    state: FSMContext,
) -> None:
    await state.clear()

    if not await _prepare_portfolio_from_callback(callback):
        return

    message = _require_message(callback)

    if message is None:
        await callback.answer("Сообщение недоступно", show_alert=True)
        return

    user_id = callback.from_user.id if callback.from_user else None
    chat_id = message.chat.id

    try:
        await _render_portfolio_screen(
            message,
            user_id=user_id,
            chat_id=chat_id,
            edit_mode=True,
            action="retry",
        )
        await callback.answer()

    except Exception as exc:
        JournalService().log_ui_error(
            event_type="portfolio_retry_error",
            message="Не удалось обновить портфель.",
            screen="portfolio",
            action="retry",
            user_id=user_id,
            chat_id=chat_id,
            error_type=classify_exchange_error(exc),
            raw_error=str(exc),
        )

        await message.edit_text(
            _build_portfolio_exchange_error_text(exc),
            reply_markup=_portfolio_warning_keyboard(),
        )

        ActiveScreenManager.register(
            screen="portfolio",
            message=message,
        )

        await callback.answer()