# app/src/telegram/handlers/auto/ui.py

from __future__ import annotations

import math
import time
import re

from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder

from src.integrations.exchange.service import ExchangeService
from src.market_data.acquisition.models.quote import Quote
from src.integrations.exchange.runtime_ui import build_runtime_exchange_alert_lines
from src.telegram.ui.common import mode_line
from src.trading.auto.service import AutoTradeService
from src.core.numbers import safe_float


def build_auto_notification_text() -> str:
    state = AutoTradeService().get_state()

    signal = str(getattr(state, "last_signal", "HOLD") or "HOLD").upper()

    if signal in {"BUY", "SELL"}:
        return _build_signal_notification_text(state, signal)

    cycle_trades = int(getattr(state, "cycle_closed_trades", 0) or 0)
    cycle_pnl = float(getattr(state, "cycle_realized_pnl_usd", 0.0) or 0.0)

    parts = [
        "Автоторговля",
        _cycle_number_text(state),
    ]

    if cycle_trades > 0:
        parts.append(f"{cycle_trades} {_trade_word(cycle_trades)}")
        parts.append(_format_pnl_line(cycle_pnl))

    return " · ".join(parts)


def _build_signal_notification_text(state, signal: str) -> str:
    quote = _market_quote(getattr(state, "symbol", None))

    bid_price = _price_from_quote(quote, "bid_price")
    ask_price = _price_from_quote(quote, "ask_price")

    side = "Long" if signal == "BUY" else "Short"
    side_icon = _signal_icon(signal)
    asset = _asset_symbol(getattr(state, "symbol", None))

    confidence = safe_float(getattr(state, "last_signal_confidence", None)) or 0.0
    reason = str(getattr(state, "last_signal_reason", "") or "").strip()

    if signal == "BUY":
        entry_price = ask_price
        entry_source = "Ask"
        second_price_label = "Bid"
        second_price = bid_price
    else:
        entry_price = bid_price
        entry_source = "Bid"
        second_price_label = "Ask"
        second_price = ask_price

    lines = [
        f"Сигнал {side_icon} {asset} · {side}",
        "",
        f"Цена входа {side} · {_format_plain_or_dash(entry_price)} ({entry_source})",
        f"Цена {second_price_label} · {_format_plain_or_dash(second_price)}",
        "",
        _signal_strength_line(confidence),
    ]

    # Общая оценка рынка на момент сигнала.
    # Это не факт входа, а качество рыночного контекста 0..100.
    market_score_line = _market_score_notification_line(state)
    if market_score_line:
        lines.append(market_score_line)

    compact_reason = _notification_signal_reason(reason)
    if compact_reason:
        lines.append(compact_reason)

    return "\n".join(lines)


def _price_from_quote(
    quote: Quote | None,
    key: str,
) -> float | None:
    if quote is None:
        return None

    return safe_float(getattr(quote, key, None))


def _position_current_price(state) -> float | None:
    quote = _market_quote(getattr(state, "symbol", None))

    if quote is not None:
        side = str(getattr(state, "position_side", "") or "").upper()

        if side == "LONG":
            price = quote.bid_price or quote.last_price
        elif side == "SHORT":
            price = quote.ask_price or quote.last_price
        else:
            price = quote.last_price

        parsed = safe_float(price)
        if parsed is not None:
            return parsed

    return _current_price(getattr(state, "symbol", None))


def _signal_strength_line(confidence: float) -> str:
    filled = min(3, max(0, round(confidence * 3)))
    bar = "●" * filled + "○" * (3 - filled)

    if confidence >= 0.8:
        level = "Сильный"
    elif confidence >= 0.5:
        level = "Средний"
    else:
        level = "Слабый"

    return f"{bar} {level} · {confidence:.2f}"


def _notification_signal_reason(reason: str) -> str:
    reason_upper = reason.upper()

    if "BREAKOUT_UP" in reason_upper:
        return "Пробой вверх"

    if "BREAKOUT_DOWN" in reason_upper:
        return "Пробой вниз"

    if "TREND_UP" in reason_upper:
        return "Рынок растёт"

    if "TREND_DOWN" in reason_upper:
        return "Рынок снижается"

    return _compact_entry_block_message(reason)


def auto_keyboard() -> InlineKeyboardMarkup:
    state = AutoTradeService().get_state()

    builder = InlineKeyboardBuilder()

    status = (state.status or "").upper()
    block_reason = _auto_block_reason()
    diagnostics_available = status in {"RUNNING", "OBSERVING"}

    if status == "OFF":
        if block_reason:
            builder.button(text="⛔ Запуск недоступен", callback_data="auto:start_blocked")
        else:
            builder.button(text="▶️ Запустить", callback_data="auto:start")

        builder.button(text="👀 Наблюдать", callback_data="auto:observe")

    elif status == "RUNNING":
        builder.button(text="👀 Наблюдать", callback_data="auto:observe")
        builder.button(text="🛑 Остановить", callback_data="auto:stop")

    elif status == "OBSERVING":
        if block_reason:
            builder.button(text="⛔ Запуск недоступен", callback_data="auto:start_blocked")
        else:
            builder.button(text="▶️ Запустить", callback_data="auto:start")

        builder.button(text="🛑 Остановить", callback_data="auto:stop")

    else:
        builder.button(text="▶️ Запустить", callback_data="auto:start")
        builder.button(text="👀 Наблюдать", callback_data="auto:observe")
        builder.button(text="🛑 Остановить", callback_data="auto:stop")

    builder.button(text="🛠️ Настройки", callback_data="settings:auto")
    builder.button(text="🧯 Защита", callback_data="auto:risk")

    if diagnostics_available:
        builder.button(text="📊 Анализ рынка", callback_data="auto:diagnostics")
        builder.adjust(2, 2, 1)
    elif status in {"OFF", "RUNNING", "OBSERVING"}:
        builder.adjust(2, 2)
    else:
        builder.adjust(2, 2, 1)

    return builder.as_markup()


def auto_diagnostics_keyboard() -> InlineKeyboardMarkup:
    builder = InlineKeyboardBuilder()

    builder.button(text="🔄 Обновить", callback_data="auto:diagnostics")
    builder.button(text="⬅️ Назад", callback_data="auto:home")

    builder.adjust(2)
    return builder.as_markup()


def is_auto_configured(state) -> bool:
    if not state.symbol:
        return False

    if not state.strategy:
        return False

    if state.risk_percent is None:
        return False

    strategy = state.strategy.upper()

    if strategy == "TREND":
        return (
            state.stop_loss_percent is not None
            and state.stop_loss_percent > 0
        )

    return True


def _auto_block_reason(state: object | None = None) -> str | None:
    state_to_use = state or AutoTradeService().get_state()

    lines = build_runtime_exchange_alert_lines(
        symbol=getattr(state_to_use, "symbol", None),
        include_exchange_unavailable=True,
    )

    if not lines:
        return None

    return "\n".join(lines)


def _append_auto_block_reason(parts: list[str], state: object) -> None:
    block_reason = _auto_block_reason(state)

    if block_reason:
        parts.extend([
            "",
            block_reason,
        ])




def _build_not_configured_text(state) -> str:
    symbol_ready = state.symbol is not None
    strategy_ready = state.strategy is not None
    risk_ready = state.risk_percent is not None

    symbol_icon = "" if symbol_ready else "⚠️"
    strategy_icon = "" if strategy_ready else "⚠️"
    risk_icon = "" if risk_ready else "⚠️"

    parts = [
        "🤖 Автоторговля ⚪ Не настроена",
        _account_mode_line(),
    ]

    _append_auto_block_reason(parts, state)

    parts.extend([
        "",
        f"{symbol_icon} Актив · {_asset_symbol(state.symbol)}",
        f"{strategy_icon} Стратегия · {_required_value(_strategy_short(state.strategy))}",
        f"{risk_icon} Риск · {_required_value(_risk_percent_text(state))}",
    ])

    strategy = (state.strategy or "").upper()

    if strategy == "TREND":
        sl_value = (
            _format_percent(state.stop_loss_percent)
            if state.stop_loss_percent is not None
            else "⏤"
        )

        sl_icon = "" if sl_value != "⏤" else "⚠️"
        parts.append(f"{sl_icon} SL · {sl_value}")

    parts.extend([
        "",
        "⚠️ Требуется настройка параметров",
    ])

    return "\n".join(parts)


def _build_stopped_without_position_text(state) -> str:
    available = _allocated_balance(state) + _realized_pnl(state)

    parts = [
        "⚪️ Автоторговля остановлена",
        _account_mode_line(),
    ]

    _append_auto_block_reason(parts, state)

    parts.extend([
        "",
        f"Доступно 💰 {_format_money_compact(available)}",
        "",
        "Настройки 🛠️",
        f"Стратегия · {_strategy_short(state.strategy)}",
        f"Актив · {_asset_symbol(state.symbol)}",
        f"Плечо · {_leverage_text(state.leverage)}",
        (
            f"Лимит · "
            f"{_format_percent(state.max_reserved_balance_percent)}"
        ),
        f"Риск · {_format_percent(state.risk_percent)}",
        "",
        _settings_risk_percent_line(state),
    ])

    return "\n".join(parts)


def build_auto_text() -> str:
    state = AutoTradeService().get_state()

    if not is_auto_configured(state):
        return _build_not_configured_text(state)

    if state.position_side != "NONE" and state.entry_price is not None:
        return _build_active_position_text(state)

    if state.status == "OFF":
        return _build_stopped_without_position_text(state)

    return _build_waiting_text(state)


def build_auto_semantic_text() -> str:
    text = build_auto_text()

    return re.sub(
        r" · \d+с| · \d+м \d+с| · \d+ч \d+м",
        "",
        text,
    )


def _settings_risk_percent_line(state) -> str:
    sl = _format_percent(state.stop_loss_percent)
    tp = _format_percent(state.take_profit_percent)

    ml = (
        f"-{_format_money_compact(abs(state.max_loss_usd))}"
        if state.max_loss_usd is not None
        else "выкл"
    )

    return f"SL {sl} · TP {tp} · ML {ml}"


def _build_waiting_text(state) -> str:
    price = _signal_entry_price(state)

    available = _allocated_balance(state) + _realized_pnl(state)
    estimated_size = _estimated_size(state, price)

    cycle_trades = int(getattr(state, "cycle_closed_trades", 0) or 0)
    cycle_pnl = float(getattr(state, "cycle_realized_pnl_usd", 0.0) or 0.0)

    parts = [
        f"{_status_text(state)}",
        _account_mode_line(),
    ]

    _append_auto_block_reason(parts, state)

    execution_block_lines = _execution_block_lines(state)
    if execution_block_lines:
        parts.extend(["", *execution_block_lines])

    parts.extend([
        "",
        f"Доступно 💰 {_format_money_compact(available)}",
    ])

    parts.extend([
        "",
        *_cycle_summary_lines(state),
    ])

    parts.extend([
        "",
        _signal_line(state),
    ])

    execution_runtime_line = _execution_runtime_line(state)
    if execution_runtime_line:
        parts.extend([
            "",
            execution_runtime_line,
        ])

    block_title = (
        "Прогноз сделки 🔮"
        if state.status == "OBSERVING"
        else "Подготовка ордера 🧾"
    )

    notional = (
        estimated_size * price
        if estimated_size is not None and price is not None and price > 0
        else None
    )

    commission_lines = _commission_lines_for_order(state, notional)

    order_lines = [
        "",
        block_title,
        _order_header_line(state),
        f"Цена · {_format_plain_or_dash(price)}",
        _estimated_size_text(state, price),
        _max_reserved_line(state, price),
    ]

    if commission_lines:
        order_lines.extend([
            "",
            *commission_lines,
            "",
        ])

    order_lines.extend([
        _effective_risk_line(state),
    ])

    execution_confidence_line = _execution_confidence_line(state)
    if execution_confidence_line:
        order_lines.append(execution_confidence_line)

    order_lines.append(
        _risk_summary_line(
            state,
            estimated_size,
            entry_price_override=price,
        )
    )

    parts.extend(order_lines)

    adjustment_visible = _adaptive_adjustment_visible(state)

    if adjustment_visible:
        reason = getattr(state, "adaptive_size_reason", "") or ""
        multiplier = float(getattr(state, "adaptive_size_multiplier", 1.0) or 1.0)

        parts.extend([
            "",
            (
                f"Коррекция ⚠️ "
                f"x{multiplier:.2f} "
                f"{_short_adaptive_reason(reason, multiplier)}"
            ),
        ])

    return "\n".join(parts)


def _execution_runtime_line(state) -> str:
    quality = str(
        getattr(state, "execution_quality", "") or ""
    ).upper()

    reason = str(
        getattr(state, "execution_quality_reason", "") or ""
    ).upper()

    market_status_message = str(
        getattr(state, "market_status_message", "") or ""
    ).strip()

    if quality == "GOOD":
        return ""

    if reason in {
        "MARKET_BREAK",
        "MARKET_CLOSED",
        "EXCHANGE_UNAVAILABLE",
        "AUTH_ERROR",
        "TIME_ERROR",
        "INVALID_SYMBOL",
    }:
        return market_status_message or "⏸️ Перерыв в торгах"

    if reason == "MARKET_STATUS_UNKNOWN":
        return market_status_message or "⚠️ Статус торгов неизвестен"

    if quality == "WARNING":
        if reason == "AGING_SNAPSHOT":
            return "⚠️ Котировки обновляются с задержкой"

        if reason == "WIDE_SPREAD":
            return "⚠️ Повышенный spread"

        if reason == "SNAPSHOT_UNAVAILABLE":
            return "⚠️ Нет данных стакана"

        return "⚠️ Предупреждение"

    if quality == "BLOCKED":
        if reason in {"STALE_SNAPSHOT", "SNAPSHOT_ERROR"}:
            return "⛔️ Нет актуальных котировок"

        if reason == "HIGH_SPREAD":
            return "⛔️ Высокий spread"

        return "⛔️ Вход заблокирован"

    return ""


def _build_active_position_text(state) -> str:
    current_price = _position_current_price(state)
    price_for_calc = current_price or state.entry_price or 0.0

    size = state.position_size or 0.0
    notional = size * price_for_calc

    reserved = _position_reserved_usd(state, current_price)

    available = (
        _allocated_balance(state)
        + _realized_pnl(state)
        - reserved
    )

    pnl = state.unrealized_pnl_usd or 0.0

    cycle_trades = int(getattr(state, "cycle_closed_trades", 0) or 0)

    cycle_pnl = float(
        getattr(state, "cycle_realized_pnl_usd", 0.0) or 0.0
    )

    side_icon = (
        "🟢"
        if state.position_side == "LONG"
        else "🔴"
    )

    adaptive_warning = (
        " ⚠️"
        if _adaptive_size_active(state) and not _show_adaptive_banner(state)
        else ""
    )

    parts = [
        _status_text(state),
        _account_mode_line(),
    ]

    _append_auto_block_reason(parts, state)

    execution_block_lines = _execution_block_lines(state)
    if execution_block_lines:
        parts.extend(["", *execution_block_lines])

    parts.extend([
        "",
        f"Доступно 💰 {_format_money_compact(available)}",
        f"Маржа · {_format_usd_compact(reserved)}",
    ])

    parts.extend([
        "",
        *_cycle_summary_lines(state),
    ])

    separator = " " if adaptive_warning else " · "

    parts.extend([
        "",
        (
            f"{side_icon} "
            f"{_asset_symbol(state.symbol)} · "
            f"{_strategy_short(state.strategy)} · "
            f"{_position_side_text(state.position_side)} "
            f"{_leverage_text(state.leverage)}"
        ),
        f"Вход · {_format_plain_or_dash(state.entry_price)}",
        f"Цена · {_format_plain_or_dash(price_for_calc)}",
        (
            f"Размер{adaptive_warning}{separator}"
            f"{_format_crypto_size(size)}"
        ),
        f"Объём · {_format_usd_compact(notional)}",
        _format_pnl_line(pnl),
        
    ])

    commission_lines = _commission_lines_for_position(state, notional)

    if commission_lines:
        parts.extend([
            "",
            *commission_lines,
            "",
        ])

    execution_runtime_line = _execution_runtime_line(state)
    if execution_runtime_line:
        parts.append(execution_runtime_line)

    parts.append(_risk_summary_line(state, size))

    if pnl < 0:
        reason_block = _position_warning_reason(state)

        if reason_block:
            parts.extend([
                "",
                reason_block,
            ])

    if _show_flip_banner(state):
        old_side = state.last_flip_old_side or "SHORT"
        new_side = state.last_flip_new_side or "LONG"

        old_icon = "🟢" if old_side == "LONG" else "🔴"
        new_icon = "🟢" if new_side == "LONG" else "🔴"

        parts.extend([
            "",
            "Позиция развернута 🔄",
            f"{old_icon} {old_side} → {new_icon} {new_side}",
        ])

    if _adaptive_adjustment_visible(state):
        reason = getattr(state, "adaptive_size_reason", "") or ""

        multiplier = float(
            getattr(state, "adaptive_size_multiplier", 1.0) or 1.0
        )

        parts.extend([
            "",
            f"Коррекция ⚠️ x{multiplier:.2f}",
            _short_adaptive_reason(reason, multiplier),
        ])

    return "\n".join(parts)


def _compact_entry_block_message(message: str) -> str:
    normalized = message.strip().lower()

    mapping = {
        "trend есть, но движение шумное": "Шумное движение по тренду",
        "рынок сейчас не подходит для входа": "Слабый импульс",
        "слабый импульс вверх": "Слабый импульс вверх",
        "слабый импульс вниз": "Слабый импульс вниз",
        "недостаточно live-данных": "Мало данных",
        "мало live-данных": "Мало данных",
        "высокая волатильность": "Высокая волатильность",
        "низкая активность": "Низкая активность",
        "market_structure_conflict": "Структура против входа",
        "market_structure_mixed": "Структура не подтверждает вход",
        "структура рынка против входа": "Структура против входа",
        "структура рынка не подтверждает вход": "Структура не подтверждает вход",
        "counter_trend_breakout": "Пробой против тренда",
    }

    result = mapping.get(normalized, message)
    return result.strip().rstrip(".")


def _allocated_balance(state) -> float:
    return float(getattr(state, "allocated_balance_usd", 1000.0) or 1000.0)


def _realized_pnl(state) -> float:
    return float(getattr(state, "realized_pnl_usd", 0.0) or 0.0)


def _position_reserved_usd(state, current_price: float | None) -> float:
    if (
        state.position_side == "NONE"
        or state.position_size is None
        or state.position_size <= 0
    ):
        return 0.0

    price = current_price or state.entry_price or 0.0
    leverage = state.leverage or 1.0

    if price <= 0 or leverage <= 0:
        return 0.0

    return (state.position_size * price) / leverage


def _max_reserved_line(state, price: float | None = None) -> str:
    size = _estimated_size(state, price)

    if size is None or price is None or price <= 0:
        return "Маржа · —"

    leverage = state.leverage or 1.0
    if leverage <= 0:
        return "Маржа · —"

    position_size_usd = size * price
    own_funds_usd = position_size_usd / leverage

    return f"Маржа · {_format_usd_compact(own_funds_usd)}"


def _market_quote(symbol: str | None) -> Quote | None:
    if not symbol:
        return None

    try:
        return ExchangeService().get_quote(
            symbol,
            runtime_key="auto",
        )
    except Exception:
        return None
    

def _trading_fee(symbol: str | None):
    if not symbol:
        return None

    try:
        return ExchangeService().get_trading_fee(symbol)
    except Exception:
        return None
    

def _trade_fee_rt_usd(symbol: str | None, notional: float | None) -> float | None:
    if notional is None or notional <= 0:
        return None

    fee = _trading_fee(symbol)
    if fee is None or fee.fee_percent is None:
        return None

    return abs(notional * (fee.fee_percent / 100) * 2)


def _overnight_period_seconds(symbol: str | None) -> int:
    normalized = str(symbol or "").upper()

    if normalized.startswith("BTC/") or normalized.startswith("ETH/"):
        return 8 * 60 * 60

    return 24 * 60 * 60


def _overnight_rate_for_side(fee, side: str | None) -> float | None:
    normalized_side = str(side or "").upper()

    if normalized_side in {"LONG", "BUY"}:
        return safe_float(fee.overnight_long_rate)

    if normalized_side in {"SHORT", "SELL"}:
        return safe_float(fee.overnight_short_rate)

    return None


def _signed_usd_compact(value: float | None) -> str:
    if value is None:
        return "$ —"

    if value > 0:
        return f"+{_format_usd_compact(value)}"

    if value < 0:
        return f"-{_format_usd_compact(abs(value))}"

    return _format_usd_compact(0)


def _predicted_overnight_fee_both_sides_lines(
    symbol: str | None,
    notional: float | None,
) -> list[str]:
    if notional is None or notional <= 0:
        return []

    fee = _trading_fee(symbol)
    if fee is None:
        return []

    lines: list[str] = []

    long_rate = safe_float(fee.overnight_long_rate)
    short_rate = safe_float(fee.overnight_short_rate)

    if long_rate is not None:
        long_value = notional * (long_rate / 100)
        lines.append(f" · Левередж Long · {_signed_usd_compact(long_value)}")

    if short_rate is not None:
        short_value = notional * (short_rate / 100)
        lines.append(f" · Левередж Short · {_signed_usd_compact(short_value)}")

    return lines


def _position_overnight_fee_usd(
    state,
    notional: float | None,
) -> tuple[float | None, int]:
    if notional is None or notional <= 0:
        return None, 0

    fee = _trading_fee(state.symbol)
    if fee is None:
        return None, 0

    rate = _overnight_rate_for_side(fee, state.position_side)
    if rate is None:
        return None, 0

    hold_seconds = safe_float(getattr(state, "position_hold_seconds", None))

    if hold_seconds is None:
        opened_at = safe_float(getattr(state, "position_opened_monotonic_at", None))
        if opened_at is not None:
            hold_seconds = max(0, time.monotonic() - opened_at)

    if hold_seconds is None:
        return 0.0, 0

    period_seconds = _overnight_period_seconds(state.symbol)
    overnight_count = int(hold_seconds // period_seconds)

    return notional * (rate / 100) * overnight_count, overnight_count


def _commission_lines_for_order(
    state,
    notional: float | None,
) -> list[str]:
    trade_fee = _trade_fee_rt_usd(state.symbol, notional)

    leverage = safe_float(getattr(state, "leverage", None)) or 1.0

    if leverage <= 1:
        leverage_fees: list[str] = []
    else:
        leverage_fees = _predicted_overnight_fee_both_sides_lines(
            state.symbol,
            notional,
        )

    if trade_fee is None and not leverage_fees:
        return []

    lines = ["Комиссии:"]

    if trade_fee is not None:
        lines.append(f" · Сделка (RT) · {_format_usd_compact(trade_fee)}")

    lines.extend(leverage_fees)

    return lines


def _commission_lines_for_position(
    state,
    notional: float | None,
) -> list[str]:
    trade_fee = _trade_fee_rt_usd(state.symbol, notional)

    leverage = safe_float(getattr(state, "leverage", None)) or 1.0

    if leverage <= 1:
        leverage_fee = None
        overnight_count = 0
    else:
        leverage_fee, overnight_count = _position_overnight_fee_usd(state, notional)

    if trade_fee is None and leverage_fee is None:
        return []

    lines = ["Комиссии:"]

    if trade_fee is not None:
        lines.append(f" · Сделка (RT) · {_format_usd_compact(trade_fee)}")

    # Показываем комиссию за левередж только после первого фактического списания.
    # До этого строка "$0 / 0 спис." не несёт пользы и визуально засоряет UI.
    if leverage_fee is not None and overnight_count > 0:
        side = _position_side_text(getattr(state, "position_side", None))
        lines.append(
            f" · Левередж {side} · {_signed_usd_compact(leverage_fee)} / "
            f"{overnight_count} спис."
        )

    return lines


def _current_price(symbol: str | None) -> float | None:
    quote = _market_quote(symbol)

    if quote is not None:
        price = quote.last_price
        if price is not None:
            try:
                parsed = safe_float(price)
                return parsed
            except (TypeError, ValueError):
                return None

    if not symbol:
        return None

    try:
        return float(ExchangeService().get_quote(symbol).last_price)
    except Exception:
        return None


def _signal_entry_price(state) -> float | None:
    quote = _market_quote(state.symbol)

    if quote is None:
        return _current_price(state.symbol)

    signal = (state.last_signal or "HOLD").upper()

    if signal == "BUY":
        price = quote.ask_price
    elif signal == "SELL":
        price = quote.bid_price
    else:
        price = quote.last_price

    if price is None:
        return None

    try:
        parsed = safe_float(price)
        return parsed
    except (TypeError, ValueError):
        return None


def _target_risk_usd(state) -> float:
    if state.risk_percent is None:
        return 0.0

    return _allocated_balance(state) * (state.risk_percent / 100)


def _effective_risk_line(state) -> str:
    effective_risk_usd = getattr(state, "effective_target_risk_usd", None)

    if effective_risk_usd is not None:
        return f"Риск · {_format_usd_compact(effective_risk_usd)}"

    return f"Риск · {_format_usd_compact(_target_risk_usd(state))}"


def _execution_confidence_line(state) -> str:
    score = safe_float(
        getattr(state, "execution_confidence_score", None)
    )

    if score is None:
        return ""

    level = str(
        getattr(state, "execution_confidence_level", "") or ""
    ).upper()

    if level == "HIGH":
        icon = "🟢"
        level_text = "Высокая"

    elif level == "NORMAL":
        icon = "🟡"
        level_text = "Нормальная"

    elif level == "LOW":
        icon = "🔴"
        level_text = "Низкая"

    else:
        icon = "⚪"
        level_text = "Неизвестно"

    return (
        f"Уверенность исполнения "
        f"{icon} {level_text} · {score:.2f}"
    )


def _estimated_size(state, price: float | None) -> float | None:
    if (
        price is None
        or price <= 0
        or state.risk_percent is None
        or state.risk_percent <= 0
        or state.stop_loss_percent is None
        or state.stop_loss_percent <= 0
    ):
        return None

    stop_loss_distance_usd = price * (state.stop_loss_percent / 100)

    if stop_loss_distance_usd <= 0:
        return None

    risk_size = _target_risk_usd(state) / stop_loss_distance_usd

    multiplier = getattr(state, "adaptive_size_multiplier", None)

    if multiplier is not None:
        risk_size *= float(multiplier)

    max_percent = getattr(state, "max_reserved_balance_percent", None)

    if max_percent is None or max_percent <= 0:
        return _round_size(risk_size)

    leverage = state.leverage or 1.0

    if leverage <= 0:
        return _round_size(risk_size)

    max_reserved_usd = _allocated_balance(state) * (max_percent / 100)
    max_notional_usd = max_reserved_usd * leverage
    max_size = max_notional_usd / price

    return _round_size(min(risk_size, max_size))


def _estimated_size_text(state, price: float | None) -> str:
    size = _estimated_size(state, price)

    if size is None or price is None:
        return "Размер · —\nОбъём · —"

    notional = size * price

    size_label = "Размер"

    if _adaptive_size_active(state) and not _show_adaptive_banner(state):
        size_label = "Размер ⚠️"

    separator = " " if "⚠️" in size_label else " · "

    return (
        f"{size_label}{separator}{_format_crypto_size(size)}\n"
        f"Объём · {_format_usd_compact(notional)}"
    )


def _risk_summary_line(
    state,
    size: float | None,
    *,
    entry_price_override: float | None = None,
) -> str:
    entry_price = entry_price_override or state.entry_price

    effective_risk = getattr(
        state,
        "effective_target_risk_usd",
        None,
    )

    sl_value = (
        abs(float(effective_risk))
        if effective_risk is not None and effective_risk > 0
        else _risk_loss_value(
            percent=state.stop_loss_percent,
            fixed_loss=None,
            size=size,
            entry_price=entry_price,
        )
    )

    tp_value = _risk_profit_value(
        percent=state.take_profit_percent,
        size=size,
        entry_price=entry_price,
    )

    ml_value = _risk_loss_value(
        percent=None,
        fixed_loss=state.max_loss_usd,
        size=size,
        entry_price=entry_price,
    )

    enabled: list[tuple[str, float]] = []

    if sl_value is not None:
        enabled.append(("SL", sl_value))

    if tp_value is not None:
        enabled.append(("TP", tp_value))

    if ml_value is not None:
        enabled.append(("ML", ml_value))

    if len(enabled) == 1:
        key, value = enabled[0]

        if key == "SL":
            return f"Stop Loss −{_format_usd_compact(value)}"

        if key == "TP":
            return f"Take Profit +{_format_usd_compact(value)}"

        if key == "ML":
            return f"Макс. убыток −{_format_usd_compact(value)}"

    items: list[str] = []

    if sl_value is not None:
        items.append(f"SL -{_format_usd_compact(sl_value)}")

    if tp_value is not None:
        items.append(f"TP +{_format_usd_compact(tp_value)}")

    if ml_value is not None:
        items.append(f"ML -{_format_usd_compact(ml_value)}")

    if not items:
        return "SL выкл · TP выкл · ML выкл"

    return " · ".join(items)


def _risk_loss_value(
    *,
    percent: float | None,
    fixed_loss: float | None,
    size: float | None,
    entry_price: float | None,
) -> float | None:
    if fixed_loss is not None:
        return abs(float(fixed_loss))

    if percent is None:
        return None

    if size is None or size <= 0:
        return None

    if entry_price is None or entry_price <= 0:
        return None

    move = entry_price * (percent / 100)

    return abs(move * size)


def _risk_profit_value(
    *,
    percent: float | None,
    size: float | None,
    entry_price: float | None,
) -> float | None:
    if percent is None:
        return None

    if size is None or size <= 0:
        return None

    if entry_price is None or entry_price <= 0:
        return None

    move = entry_price * (percent / 100)

    return abs(move * size)


def _order_header_line(state) -> str:
    signal = (state.last_signal or "").upper()

    if signal == "BUY":
        return (
            f"🟢 <b>{_asset_symbol(state.symbol)}</b> · "
            f"{_strategy_short(state.strategy)} · "
            f"Long {_leverage_text(state.leverage)}"
        )

    if signal == "SELL":
        return (
            f"🔴 <b>{_asset_symbol(state.symbol)}</b> · "
            f"{_strategy_short(state.strategy)} · "
            f"Short {_leverage_text(state.leverage)}"
        )

    return (
        f"<b>{_asset_symbol(state.symbol)}</b> · "
        f"{_strategy_short(state.strategy)} · "
        f"{_leverage_text(state.leverage)}"
    )


def _signal_text(signal: str) -> str:
    mapping = {
        "BUY": "Long",
        "SELL": "Short",
        "HOLD": "Ожидание",
    }

    return mapping.get(signal.upper(), signal.title())


def _signal_line(state) -> str:
    signal = (state.last_signal or "HOLD").upper()

    signal_text = (
        f"Сигнал {_signal_icon(signal)} "
        f"{_signal_text(signal)}"
    )

    if signal in {"BUY", "SELL"} and (
        state.decision_status == "READY"
        or getattr(state, "is_signal_ready", False)
    ):
        return f"{signal_text} · готов"

    duration = _signal_duration_text(state)

    return f"{signal_text} · {duration}"


def _position_warning_reason(state) -> str:
    signal = (state.last_signal or "HOLD").upper()

    confidence = float(
        getattr(state, "last_signal_confidence", 0.0) or 0.0
    )

    reason = str(
        getattr(state, "last_signal_reason", "") or ""
    ).strip()

    lines: list[str] = []

    if signal == "BUY":
        lines.append("Сигнал 🟢 Long")

    elif signal == "SELL":
        lines.append("Сигнал 🔴 Short")

    if signal in {"BUY", "SELL"}:
        filled = min(3, max(0, round(confidence * 3)))

        bar = (
            "●" * filled
            + "○" * (3 - filled)
        )

        if confidence >= 0.8:
            level = "Сильный"
        elif confidence >= 0.5:
            level = "Средний"
        else:
            level = "Слабый"

        lines.append(
            f"{bar} {level} · {confidence:.2f}"
        )

    reason_upper = reason.upper()
    compact_reason = _compact_entry_block_message(reason)

    if "BREAKOUT_UP" in reason_upper:
        lines.append("Пробой вверх")

    elif "BREAKOUT_DOWN" in reason_upper:
        lines.append("Пробой вниз")

    elif "TREND_UP" in reason_upper:
        lines.append("Рынок растёт")

    elif "TREND_DOWN" in reason_upper:
        lines.append("Рынок снижается")

    elif compact_reason:
        lines.append(compact_reason)

    return "\n".join(lines)


def _signal_duration_text(state) -> str:
    started_at = getattr(state, "signal_started_at", None)

    if started_at is not None:
        total_seconds = max(0, int(time.monotonic() - float(started_at)))
    else:
        repeat_count = state.last_signal_repeat_count or 0
        total_seconds = max(0, repeat_count * 5)

    hours = total_seconds // 3600
    minutes = (total_seconds % 3600) // 60
    seconds = total_seconds % 60

    if hours > 0:
        return f"{hours}ч {minutes:02d}м"

    if minutes > 0:
        return f"{minutes}м {seconds:02d}с"

    return f"{seconds}с"


def _status_text(state) -> str:
    runtime = _cycle_runtime_text(state)

    status = (state.status or "").upper()

    if status == "RUNNING":
        return f"🟢 Автоторговля ⏳ {runtime}"

    if status == "OBSERVING":
        return f"👀 Автоторговля ⏳ {runtime}"

    return "⚪️ Автоторговля остановлена"


def _cycle_number_text(state) -> str:
    cycle_number = int(getattr(state, "cycle_number", 0) or 0)

    if cycle_number <= 0:
        cycle_number = 1

    return f"Цикл {cycle_number}"


def _cycle_runtime_text(state) -> str:
    started_at = getattr(state, "cycle_started_at", None)

    if started_at is None:
        return "0м"

    seconds = max(0, int(time.monotonic() - float(started_at)))

    days = seconds // 86400
    hours = (seconds % 86400) // 3600
    minutes = (seconds % 3600) // 60

    if days > 0:
        return f"{days}д {hours}ч"

    if hours > 0:
        return f"{hours}ч {minutes}м"

    return f"{minutes}м"


def _show_flip_banner(state) -> bool:
    ts = getattr(state, "last_flip_monotonic_at", None)

    if ts is None:
        return False

    return (time.monotonic() - float(ts)) <= 600


def _show_adaptive_banner(state) -> bool:
    ts = getattr(state, "adaptive_size_changed_at", None)

    if ts is None:
        return False

    return (time.monotonic() - float(ts)) <= 600


def _adaptive_size_active(state) -> bool:
    multiplier = getattr(state, "adaptive_size_multiplier", None)

    if multiplier is None:
        return False

    return abs(float(multiplier) - 1.0) > 0.001


def _account_mode_line() -> str:
    return "DEMO аккаунт" if "DEMO" in mode_line().upper() else "LIVE аккаунт"


def _asset_symbol(symbol: str | None) -> str:
    if not symbol:
        return "—"

    base = symbol.split("_", 1)[0].upper()

    if "/" in base:
        return base.split("/", 1)[0]

    for suffix in ("USDT", "USD", "EUR", "BTC"):
        if base.endswith(suffix) and len(base) > len(suffix):
            return base[: -len(suffix)]

    return base


def _strategy_short(strategy: str | None) -> str:
    if not strategy:
        return "—"

    mapping = {
        "TREND": "Trend",
        "GRID": "Grid",
        "SCALP": "Scalp",
    }
    return mapping.get(strategy.upper(), strategy.title())


def _leverage_text(value: float | None) -> str:
    if value is None:
        return "x—"

    return f"x{value:g}"


def _risk_percent_text(state) -> str:
    if state.risk_percent is None:
        return "—"

    return _format_percent(state.risk_percent)


def _required_value(value: str) -> str:
    if not value or value == "—":
        return "⏤"

    return value


def _signal_icon(signal: str | None) -> str:
    mapping = {
        "BUY": "🟢",
        "SELL": "🔴",
        "HOLD": "🟡",
    }

    return mapping.get(signal or "", "")


def _position_side_text(value: str | None) -> str:
    text = (value or "").upper()

    if text == "LONG":
        return "Long"

    if text == "SHORT":
        return "Short"

    return text.title()


def _round_size(value: float | int | None) -> float | None:
    if value is None:
        return None

    precision = 5
    factor = 10 ** precision

    return math.floor(float(value) * factor) / factor


def _format_crypto_size(value: float | int | None) -> str:
    rounded = _round_size(value)

    if rounded is None:
        return "—"

    return f"{rounded:.5f}".rstrip("0").rstrip(".")


def _format_percent(value: float | int | None) -> str:
    if value is None:
        return "выкл"

    number = float(value)

    if abs(number - round(number)) < 1e-9:
        return f"{int(round(number))}%"

    return f"{number:.2f}".rstrip("0").rstrip(".") + "%"


def _format_money_compact(value: float | int | None) -> str:
    if value is None:
        return "—"

    number = float(value)

    if abs(number - round(number)) < 1e-9:
        return f"{number:,.0f}".replace(",", " ")

    return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".")


def _format_usd_compact(value: float | int | None) -> str:
    if value is None:
        return "$ —"

    return f"${_format_money_compact(value)}"


def _format_plain_or_dash(value: float | int | None) -> str:
    if value is None:
        return "—"

    return _format_usd_compact(value)


def _trade_word(value: int) -> str:
    value = abs(int(value))

    if value % 10 == 1 and value % 100 != 11:
        return "сделка"

    if value % 10 in {2, 3, 4} and value % 100 not in {12, 13, 14}:
        return "сделки"

    return "сделок"


def _cycle_summary_lines(state) -> list[str]:
    status = str(getattr(state, "status", "") or "").upper()

    # В режиме наблюдения торгового цикла нет:
    # бот только анализирует рынок и не исполняет сделки.
    if status == "OBSERVING":
        return ["🔬 Анализ рынка"]

    if status != "RUNNING":
        return []

    cycle_trades = int(getattr(state, "cycle_closed_trades", 0) or 0)
    cycle_pnl = float(getattr(state, "cycle_realized_pnl_usd", 0.0) or 0.0)

    wins = int(getattr(state, "cycle_winning_trades", 0) or 0)
    losses = int(getattr(state, "cycle_losing_trades", 0) or 0)

    if cycle_trades <= 0:
        return [f"🔄 {_cycle_number_text(state)}"]

    lines = [
        (
            f"🔄 {_cycle_number_text(state)} · "
            f"{cycle_trades} {_trade_word(cycle_trades)} · "
            f"🟢 {wins} 🔴 {losses}"
        ),
        *_cycle_trade_block_lines(state),
        "",
        _format_pnl_line(cycle_pnl),
        *_cycle_commission_lines(state),
    ]

    return lines


def _format_pnl_line(value: float | int | None) -> str:
    # Показываем именно итог цикла/позиции.
    # Комиссии уже включены в net PnL, а ниже отдельным блоком показываем,
    # какая часть результата пришлась на комиссии.
    amount = float(value or 0.0)

    if amount > 0:
        return f"🟢 Итог · +{_format_usd_compact(amount)}"

    if amount < 0:
        return f"🔴 Итог · −{_format_usd_compact(abs(amount))}"

    return "⚪ Итог · $0"


def _adaptive_adjustment_visible(state) -> bool:
    return _adaptive_size_active(state) and _show_adaptive_banner(state)


def _short_adaptive_reason(
    reason: str,
    multiplier: float | None = None,
) -> str:
    value = safe_float(multiplier)

    if value is not None:
        if value < 0.15:
            return "Вход заблокирован"

        if value < 0.75:
            return "Риск снижен"

        if value < 1.0:
            return "Размер уменьшен"

        if value > 1.05:
            return "Размер увеличен"

    text = str(reason or "").lower()

    if "margin" in text:
        return "Лимит маржи"

    if "spread" in text:
        return "Высокий spread"

    if "шум" in text or "noisy" in text:
        return "Шумный рынок"

    if "низк" in text or "low" in text:
        return "Низкая уверенность"

    if not reason:
        return "Размер скорректирован"

    return reason[:1].upper() + reason[1:]


def _execution_block_lines(state) -> list[str]:
    title = str(getattr(state, "execution_block_title", "") or "").strip()
    message = str(getattr(state, "execution_block_message", "") or "").strip()
    action = str(getattr(state, "execution_block_action", "") or "").strip()

    if not title or not message:
        return []

    lines = [
        f"⛔ {title}",
        message,
    ]

    if action:
        lines.append(action)

    return lines


def _cycle_commission_lines(state) -> list[str]:
    trade_fees = safe_float(getattr(state, "cycle_trade_fees_usd", None)) or 0.0
    overnight_fees = safe_float(getattr(state, "cycle_overnight_fees_usd", None)) or 0.0

    if abs(trade_fees) < 0.0001 and abs(overnight_fees) < 0.0001:
        return []

    lines = ["Включая комиссии:"]

    if abs(trade_fees) >= 0.0001:
        lines.append(f"· сделки (RT) · {_format_usd_compact(abs(trade_fees))}")

    if abs(overnight_fees) >= 0.0001:
        lines.append(f"· левередж · {_signed_usd_compact(overnight_fees)}")

    return lines


def _cycle_trade_block_lines(state) -> list[str]:
    # Этот блок показываем только при реальной блокировке по серии убытков.
    # Обычная пауза/cooldown после одной сделки сюда не попадает.
    if not bool(getattr(state, "loss_cooldown_active", False)):
        return []

    consecutive_losses = int(
        getattr(state, "cycle_consecutive_losses", 0) or 0
    )

    if consecutive_losses <= 0:
        return []

    return [
        "",
        "⛔️ Блокировка сделок",
        f"· {consecutive_losses} убыточных сделок подряд",
        "· перезапусти цикл",
    ]


def _market_score_notification_line(state) -> str:
    score = safe_float(getattr(state, "market_score", None))

    if score is None:
        return ""

    label = str(getattr(state, "market_score_label", "") or "").strip()

    if not label:
        label = _market_score_label(score)

    return f"Рынок · {label.lower()} · {score:.0f}%"


def _market_score_label(score: float) -> str:
    # Единая шкала общей оценки рынка:
    # 90-100 — отличный рынок
    # 75-89  — благоприятный
    # 55-74  — нейтральный
    # 35-54  — сложный
    # 0-34   — неблагоприятный
    if score >= 90:
        return "Отличный"

    if score >= 75:
        return "Благоприятный"

    if score >= 55:
        return "Нейтральный"

    if score >= 35:
        return "Сложный"

    return "Неблагоприятный"