# app/src/trading/market_intelligence/common/scores.py

from __future__ import annotations

from dataclasses import dataclass, field

from src.trading.market_intelligence.common.constants import (
    DEFAULT_CONFIDENCE,
    DEFAULT_PROBABILITY,
    DEFAULT_SCORE,
    DEFAULT_WEIGHT,
    EXCELLENT_SCORE_THRESHOLD,
    GOOD_SCORE_THRESHOLD,
    HIGH_CONFIDENCE_THRESHOLD,
    LOW_CONFIDENCE_THRESHOLD,
    MAX_CONFIDENCE,
    MAX_PROBABILITY,
    MAX_SCORE,
    MAX_WEIGHT,
    MIN_CONFIDENCE,
    MIN_PROBABILITY,
    MIN_SCORE,
    MIN_WEIGHT,
    NORMAL_CONFIDENCE_THRESHOLD,
    NORMAL_SCORE_THRESHOLD,
    VERY_HIGH_CONFIDENCE_THRESHOLD,
    VERY_LOW_CONFIDENCE_THRESHOLD,
    WEAK_SCORE_THRESHOLD,
)
from src.trading.market_intelligence.common.enums import (
    ConfidenceLevel,
    MarketQuality,
)
from src.trading.market_intelligence.common.reasons import ReasonCode
from src.trading.market_intelligence.common.types import (
    ConfidenceValue,
    ProbabilityValue,
    ReasonCode as ReasonCodeType,
    ScoreValue,
    WeightValue,
)


def clamp_score(value: float | int | None) -> ScoreValue:
    # Приводим оценку к безопасному диапазону 0...100.
    # Это защищает все будущие движки от случайного выхода за границы шкалы.
    if value is None:
        return DEFAULT_SCORE

    return max(MIN_SCORE, min(MAX_SCORE, float(value)))


def clamp_confidence(value: float | int | None) -> ConfidenceValue:
    # Приводим уверенность к безопасному диапазону 0...1.
    # Уверенность показывает качество вывода, а не силу движения рынка.
    if value is None:
        return DEFAULT_CONFIDENCE

    return max(MIN_CONFIDENCE, min(MAX_CONFIDENCE, float(value)))


def clamp_probability(value: float | int | None) -> ProbabilityValue:
    # Приводим вероятность к безопасному диапазону 0...100.
    # Это единая шкала для будущих движков вероятности.
    if value is None:
        return DEFAULT_PROBABILITY

    return max(MIN_PROBABILITY, min(MAX_PROBABILITY, float(value)))


def clamp_weight(value: float | int | None) -> WeightValue:
    # Приводим вес показателя к безопасному диапазону 0...1.
    # Вес показывает, насколько сильно показатель влияет на итоговую оценку.
    if value is None:
        return DEFAULT_WEIGHT

    return max(MIN_WEIGHT, min(MAX_WEIGHT, float(value)))


def classify_score_quality(value: float | int | None) -> MarketQuality:
    # Переводим числовую оценку в понятное качество.
    # Это нужно для журнала и диагностики, чтобы не читать только сухие числа.
    score = clamp_score(value)

    if score >= EXCELLENT_SCORE_THRESHOLD:
        return MarketQuality.EXCELLENT

    if score >= GOOD_SCORE_THRESHOLD:
        return MarketQuality.GOOD

    if score >= NORMAL_SCORE_THRESHOLD:
        return MarketQuality.NORMAL

    if score >= WEAK_SCORE_THRESHOLD:
        return MarketQuality.WEAK

    return MarketQuality.POOR


def classify_confidence_level(value: float | int | None) -> ConfidenceLevel:
    # Переводим числовую уверенность в понятный уровень.
    # Это помогает человеку быстро понять, насколько надёжен вывод движка.
    confidence = clamp_confidence(value)

    if confidence >= VERY_HIGH_CONFIDENCE_THRESHOLD:
        return ConfidenceLevel.VERY_HIGH

    if confidence >= HIGH_CONFIDENCE_THRESHOLD:
        return ConfidenceLevel.HIGH

    if confidence >= NORMAL_CONFIDENCE_THRESHOLD:
        return ConfidenceLevel.NORMAL

    if confidence >= LOW_CONFIDENCE_THRESHOLD:
        return ConfidenceLevel.LOW

    if confidence >= VERY_LOW_CONFIDENCE_THRESHOLD:
        return ConfidenceLevel.VERY_LOW

    return ConfidenceLevel.VERY_LOW


@dataclass(frozen=True, slots=True)
class EngineScore:
    # Единая модель оценки движка.
    # Оценка всегда хранится в диапазоне 0...100 и дополнительно имеет
    # человекочитаемое качество для диагностики.
    value: ScoreValue = DEFAULT_SCORE
    quality: MarketQuality = MarketQuality.POOR
    reason: ReasonCode = ReasonCode.UNKNOWN

    @classmethod
    def create(
        cls,
        value: float | int | None,
        *,
        reason: ReasonCode = ReasonCode.UNKNOWN,
    ) -> EngineScore:
        safe_value = clamp_score(value)

        return cls(
            value=safe_value,
            quality=classify_score_quality(safe_value),
            reason=reason,
        )


@dataclass(frozen=True, slots=True)
class EngineConfidence:
    # Единая модель уверенности движка.
    # Уверенность показывает, насколько результат можно считать надёжным.
    value: ConfidenceValue = DEFAULT_CONFIDENCE
    level: ConfidenceLevel = ConfidenceLevel.VERY_LOW
    reason: ReasonCode = ReasonCode.UNKNOWN

    @property
    def is_reliable(self) -> bool:
        # Считаем результат достаточно надёжным, если уверенность
        # не ниже нормального уровня.
        return self.value >= NORMAL_CONFIDENCE_THRESHOLD

    @classmethod
    def create(
        cls,
        value: float | int | None,
        *,
        reason: ReasonCode = ReasonCode.UNKNOWN,
    ) -> EngineConfidence:
        safe_value = clamp_confidence(value)

        return cls(
            value=safe_value,
            level=classify_confidence_level(safe_value),
            reason=reason,
        )


@dataclass(frozen=True, slots=True)
class ProbabilityScore:
    # Единая модель вероятности.
    # Используется для будущих движков продолжения и изменения направления.
    value: ProbabilityValue = DEFAULT_PROBABILITY
    confidence: EngineConfidence = field(default_factory=EngineConfidence)
    reason: ReasonCode = ReasonCode.UNKNOWN

    @classmethod
    def create(
        cls,
        value: float | int | None,
        *,
        confidence: EngineConfidence | None = None,
        reason: ReasonCode = ReasonCode.UNKNOWN,
    ) -> ProbabilityScore:
        return cls(
            value=clamp_probability(value),
            confidence=confidence or EngineConfidence(),
            reason=reason,
        )


@dataclass(frozen=True, slots=True)
class WeightedScore:
    # Одна часть итоговой оценки.
    # Например, общий результат может состоять из оценки тренда,
    # оценки изменчивости рынка и оценки ликвидности.
    name: str
    value: ScoreValue
    weight: WeightValue = DEFAULT_WEIGHT
    reason: ReasonCodeType = ReasonCode.UNKNOWN

    @property
    def weighted_value(self) -> float:
        # Возвращает вклад этой части в итоговую оценку.
        return self.value * self.weight

    @classmethod
    def create(
        cls,
        *,
        name: str,
        value: float | int | None,
        weight: float | int | None = DEFAULT_WEIGHT,
        reason: ReasonCode = ReasonCode.UNKNOWN,
    ) -> WeightedScore:
        return cls(
            name=name,
            value=clamp_score(value),
            weight=clamp_weight(weight),
            reason=reason,
        )


@dataclass(frozen=True, slots=True)
class ScoreBreakdown:
    # Подробная структура итоговой оценки.
    # Нужна для диагностики: система должна объяснять, из каких частей
    # получилась итоговая оценка.
    items: tuple[WeightedScore, ...] = ()

    @property
    def total_weight(self) -> float:
        return sum(item.weight for item in self.items)

    @property
    def total_score(self) -> float:
        return sum(item.weighted_value for item in self.items)

    @property
    def final_score(self) -> ScoreValue:
        # Если веса отсутствуют, безопасно возвращаем нулевую оценку.
        # Это лучше, чем делить на ноль или создавать ложный результат.
        if self.total_weight <= 0:
            return DEFAULT_SCORE

        return clamp_score(self.total_score / self.total_weight)

    @property
    def quality(self) -> MarketQuality:
        return classify_score_quality(self.final_score)

    @classmethod
    def create(cls, items: tuple[WeightedScore, ...]) -> ScoreBreakdown:
        return cls(items=items)