# app/src/trading/execution/position_metrics.py

from __future__ import annotations

import time
from dataclasses import dataclass

from src.core.numbers import safe_float
from src.core.types import NumericLike
from src.integrations.exchange.service import ExchangeService
from src.trading.position.state import PositionState


# Единый снимок расчётов по открытой позиции.
# Все числовые показатели позиции должны считаться здесь один раз,
# а остальные части бота должны только использовать готовые значения.
@dataclass(slots=True)
class PositionMetrics:
    symbol: str
    side: str

    entry_price: float | None
    current_price: float | None
    size: float | None
    leverage: float | None

    entry_notional_usd: float
    current_notional_usd: float
    margin_usd: float

    # - price_move_percent = движение цены от входа.
    price_move_percent: float
    # - gross_pnl_usd = PnL без комиссий и overnight.
    gross_pnl_usd: float
    # - commission_usd = комиссия вход + предполагаемый выход.
    commission_usd: float
    # - overnight_cashflow_usd = списание или начисление за leverage.
    overnight_cashflow_usd: float
    # - net_pnl_usd = итоговый PnL после комиссии и overnight.
    net_pnl_usd: float
    # - pnl_percent = net PnL в процентах от notional входа.
    pnl_percent: float

    hold_seconds: int | None
    overnight_count: int


@dataclass(slots=True)
class PlannedPositionMetrics:
    """
    Единый расчёт планируемой позиции до открытия.

    Используется на этапе:
    - подготовки ордера,
    - оценки размера,
    - оценки маржи,
    - оценки комиссии,
    - отображения в UI.
    """

    symbol: str
    side: str

    entry_price: float | None
    size: float | None
    leverage: float | None

    notional_usd: float
    margin_usd: float
    commission_usd: float


def build_position_metrics(
    position: PositionState,
    *,
    current_price: NumericLike | None,
) -> PositionMetrics:
    """
    Главная функция расчёта метрик уже открытой позиции.

    Сюда нужно постепенно перенести все расчёты, которые сейчас разбросаны по:
    - execution/calculations.py
    - execution/position_runtime.py
    - execution/risk_close.py
    - execution/position_protection.py
    - auto/position_health.py

    Последовательность:
    1. Нормализуем входные данные позиции.
    2. Считаем notional и margin.
    3. Считаем движение цены.
    4. Считаем gross PnL.
    5. Считаем комиссии.
    6. Считаем overnight cashflow.
    7. Считаем net PnL.
    8. Считаем PnL % от notional входа.
    """

    price = safe_float(current_price)
    entry = safe_float(position.entry_price)
    size = safe_float(position.size)
    leverage = safe_float(position.leverage) or 1.0

    entry_notional = _notional(entry, size)
    current_notional = _notional(price, size)
    margin = _margin(current_notional, leverage)

    price_move_percent = _price_move_percent(
        side=position.side,
        entry_price=entry,
        current_price=price,
    )

    gross_pnl = _gross_pnl_usd(
        side=position.side,
        entry_price=entry,
        current_price=price,
        size=size,
    )

    commission = _round_trip_commission_usd(
        symbol=position.symbol,
        entry_price=entry,
        current_price=price,
        size=size,
    )

    hold_seconds = _hold_seconds(position)

    overnight_cashflow, overnight_count = _overnight_cashflow_usd(
        symbol=position.symbol,
        side=position.side,
        current_price=price,
        size=size,
        leverage=leverage,
        hold_seconds=hold_seconds,
    )

    net_pnl = round(gross_pnl - commission + overnight_cashflow, 4)

    pnl_percent = _pnl_percent(
        pnl_usd=net_pnl,
        entry_notional_usd=entry_notional,
    )

    return PositionMetrics(
        symbol=position.symbol,
        side=position.side,
        entry_price=entry,
        current_price=price,
        size=size,
        leverage=leverage,
        entry_notional_usd=entry_notional,
        current_notional_usd=current_notional,
        margin_usd=margin,
        price_move_percent=price_move_percent,
        gross_pnl_usd=gross_pnl,
        commission_usd=commission,
        overnight_cashflow_usd=overnight_cashflow,
        net_pnl_usd=net_pnl,
        pnl_percent=pnl_percent,
        hold_seconds=hold_seconds,
        overnight_count=overnight_count,
    )


def build_planned_position_metrics(
    *,
    symbol: str,
    side: str,
    entry_price: NumericLike | None,
    size: NumericLike | None,
    leverage: NumericLike | None,
) -> PlannedPositionMetrics:
    """
    Расчёт планируемой позиции до открытия.

    Здесь нет PnL, потому что позиции ещё нет.
    Считаем только:
    - объём позиции,
    - маржу,
    - примерную round-trip комиссию.
    """

    price = safe_float(entry_price)
    parsed_size = safe_float(size)
    parsed_leverage = safe_float(leverage) or 1.0

    notional = _notional(price, parsed_size)
    margin = _margin(notional, parsed_leverage)

    commission = _round_trip_commission_usd(
        symbol=symbol,
        entry_price=price,
        current_price=price,
        size=parsed_size,
    )

    return PlannedPositionMetrics(
        symbol=symbol,
        side=side,
        entry_price=price,
        size=parsed_size,
        leverage=parsed_leverage,
        notional_usd=notional,
        margin_usd=margin,
        commission_usd=commission,
    )


def _price_move_percent(
    *,
    side: str | None,
    entry_price: float | None,
    current_price: float | None,
) -> float:
    """
    Считает движение цены от входа.

    LONG:
    цена выше входа = плюс.

    SHORT:
    цена ниже входа = плюс.
    """

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

    if current_price is None or current_price <= 0:
        return 0.0

    normalized_side = str(side or "").upper()

    if normalized_side == "LONG":
        return round(((current_price - entry_price) / entry_price) * 100, 4)

    if normalized_side == "SHORT":
        return round(((entry_price - current_price) / entry_price) * 100, 4)

    return 0.0


def _gross_pnl_usd(
    *,
    side: str | None,
    entry_price: float | None,
    current_price: float | None,
    size: float | None,
) -> float:
    """
    Считает PnL без комиссий.

    Это “грязная” прибыль/убыток только от изменения цены.
    """

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

    if current_price is None or current_price <= 0:
        return 0.0

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

    normalized_side = str(side or "").upper()

    if normalized_side == "LONG":
        return round((current_price - entry_price) * size, 4)

    if normalized_side == "SHORT":
        return round((entry_price - current_price) * size, 4)

    return 0.0


def _round_trip_commission_usd(
    *,
    symbol: str | None,
    entry_price: float | None,
    current_price: float | None,
    size: float | None,
) -> float:
    """
    Считает комиссию вход + выход.

    Для открытой позиции:
    - вход уже был по entry_price;
    - выход предполагается по current_price.

    Для планируемой позиции:
    - entry_price и current_price могут быть одинаковыми.
    """

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

    if current_price is None or current_price <= 0:
        return 0.0

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

    fee_percent = _trading_fee_percent(symbol)

    if fee_percent <= 0:
        return 0.0

    entry_notional = entry_price * size
    exit_notional = current_price * size

    return round((entry_notional + exit_notional) * (fee_percent / 100), 4)


def _overnight_cashflow_usd(
    *,
    symbol: str | None,
    side: str | None,
    current_price: float | None,
    size: float | None,
    leverage: float | None,
    hold_seconds: int | None,
) -> tuple[float, int]:
    """
    Считает overnight/leverage cashflow.

    Значение может быть:
    - отрицательным, если биржа списывает funding/overnight;
    - положительным, если ставка по стороне позиции положительная;
    - нулевым, если плечо x1 или срок удержания меньше периода списания.
    """

    parsed_leverage = safe_float(leverage) or 1.0

    if parsed_leverage <= 1:
        return 0.0, 0

    if current_price is None or current_price <= 0:
        return 0.0, 0

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

    if hold_seconds is None or hold_seconds <= 0:
        return 0.0, 0

    rate = _overnight_rate_for_side(
        symbol=symbol,
        side=side,
    )

    if rate is None:
        return 0.0, 0

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

    if overnight_count <= 0:
        return 0.0, 0

    notional = current_price * size
    cashflow = notional * (rate / 100) * overnight_count

    return round(cashflow, 4), overnight_count


def _pnl_percent(
    *,
    pnl_usd: float,
    entry_notional_usd: float,
) -> float:
    """
    Считает net PnL в процентах от notional входа.

    Важно:
    здесь используется net PnL, то есть уже после комиссии и overnight.
    """

    if entry_notional_usd <= 0:
        return 0.0

    return round((pnl_usd / entry_notional_usd) * 100, 4)


def _notional(
    price: float | None,
    size: float | None,
) -> float:
    """
    Считает объём позиции в USD.
    """

    if price is None or price <= 0:
        return 0.0

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

    return round(price * size, 4)


def _margin(
    notional_usd: float,
    leverage: float | None,
) -> float:
    """
    Считает занятые собственные средства.

    Пример:
    notional $1000 при плече x2 = margin $500.
    """

    parsed_leverage = safe_float(leverage) or 1.0

    if parsed_leverage <= 0:
        return 0.0

    if notional_usd <= 0:
        return 0.0

    return round(notional_usd / parsed_leverage, 4)


def _hold_seconds(position: PositionState) -> int | None:
    """
    Считает время удержания позиции.

    Основной источник — opened_monotonic_at.
    Это надёжнее, чем строковое время opened_at.
    """

    opened_at = safe_float(getattr(position, "opened_monotonic_at", None))

    if opened_at is None:
        return None

    return max(0, int(time.monotonic() - opened_at))


def _overnight_period_seconds(symbol: str | None) -> int:
    """
    Возвращает период списания overnight.

    Сейчас логика сохранена как в старом коде:
    - BTC/ETH: каждые 8 часов;
    - остальные активы: раз в 24 часа.
    """

    normalized = str(symbol or "").upper()

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

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

    return 24 * 60 * 60


def _overnight_rate_for_side(
    *,
    symbol: str | None,
    side: str | None,
) -> float | None:
    # Берёт overnight rate для стороны позиции.
    # LONG использует overnight_long_rate.
    # SHORT использует overnight_short_rate.

    fee = _trading_fee(symbol)

    if fee is None:
        return None

    normalized_side = str(side or "").upper()

    if normalized_side == "LONG":
        return safe_float(getattr(fee, "overnight_long_rate", None))

    if normalized_side == "SHORT":
        return safe_float(getattr(fee, "overnight_short_rate", None))

    return None


def _trading_fee_percent(symbol: str | None) -> float:
    # Возвращает торговую комиссию в процентах.
    # Если комиссию получить не удалось — возвращаем 0,
    # чтобы расчёт позиции не падал.

    fee = _trading_fee(symbol)

    if fee is None:
        return 0.0

    return safe_float(getattr(fee, "fee_percent", None)) or 0.0


def _trading_fee(symbol: str | None):
    # Получает объект комиссии с биржи.
    # В этом первом варианте кеш специально не добавлен сюда,
    # чтобы не усложнять файл. Кеш уже есть в ExchangeService/старом коде.
    # Если потребуется — на следующем шаге добавим cache именно здесь.

    normalized_symbol = str(symbol or "").strip()

    if not normalized_symbol:
        return None

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