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

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Protocol

from src.trading.market_intelligence.common.enums import (
    EngineStatus,
    MarketBias,
    MarketDirection,
    MarketPhase,
    MarketRegime,
    RiskLevel,
)
from src.trading.market_intelligence.common.reasons import ReasonCode
from src.trading.market_intelligence.common.scores import (
    EngineConfidence,
    EngineScore,
)
from src.trading.market_intelligence.common.types import (
    ContextDict,
    DiagnosticMessages,
    DiagnosticValue,
    DurationMs,
    EngineName,
    EngineVersion,
    MarketData,
    MetricsDict,
    PayloadDict,
    SymbolName,
    TimeframeName,
)


@dataclass(frozen=True, slots=True)
class EngineMetadata:
    # Описание Engine как компонента платформы.
    # Metadata не содержит аналитической логики и не является результатом анализа.
    # Она нужна Runtime, Registry, Coordinator и документации.
    name: EngineName
    version: EngineVersion
    description: str = ""
    supported_timeframes: tuple[TimeframeName, ...] = ()
    required_dependencies: tuple[EngineName, ...] = ()
    optional_dependencies: tuple[EngineName, ...] = ()
    enabled_by_default: bool = True


class EngineTypeProtocol(Protocol):
    # Минимальный контракт класса Engine для хранения в Common Layer.
    # Common не должен импортировать EngineProtocol из Engine Layer.
    @classmethod
    def get_metadata(cls) -> EngineMetadata:
        # Вернуть metadata Engine без создания экземпляра.
        ...

    async def analyze(
        self,
        context: EngineContext,
    ) -> EngineResult:
        # Выполнить анализ рыночного контекста.
        ...


@dataclass(frozen=True, slots=True)
class EngineRegistration:
    # Атомарная запись регистрации Engine.
    # Не дублирует данные из EngineMetadata.
    engine_type: type[EngineTypeProtocol]
    metadata: EngineMetadata


@dataclass(frozen=True, slots=True)
class EngineMetric:
    # Одна измеримая величина внутри движка.
    # Например: сила движения, ширина спреда, возраст данных или качество структуры.
    # Метрика не является решением, она только объясняет часть расчёта.
    name: str
    value: DiagnosticValue
    unit: str | None = None
    description: str | None = None


@dataclass(frozen=True, slots=True)
class EngineDiagnostics:
    # Диагностика объясняет, что произошло во время работы движка.
    # Она нужна для журнала, отладки и будущих проверок качества.
    reason: ReasonCode = ReasonCode.UNKNOWN
    details: ContextDict = field(default_factory=dict)
    warnings: DiagnosticMessages = field(default_factory=list)
    errors: DiagnosticMessages = field(default_factory=list)

    @property
    def has_warnings(self) -> bool:
        # Отдельный признак помогает быстро понять,
        # были ли у движка некритичные проблемы.
        return bool(self.warnings)

    @property
    def has_errors(self) -> bool:
        # Ошибки не должны ломать всю платформу.
        # Но результат должен честно сообщать, что они были.
        return bool(self.errors)


@dataclass(frozen=True, slots=True)
class EngineEvaluationMeta:
    # Служебная информация о расчёте.
    # Она помогает понять, какой движок, какой версии и когда сформировал результат.
    engine_name: EngineName
    engine_version: EngineVersion
    calculated_at: float | None = None
    duration_ms: DurationMs | None = None
    input_age_seconds: float | None = None


@dataclass(frozen=True, slots=True)
class EngineDependencyResult:
    # Краткое описание результата другого движка.
    # Это позволяет использовать зависимости без прямого вызова соседних Engine.
    engine_name: EngineName
    status: EngineStatus = EngineStatus.UNKNOWN
    score: EngineScore = field(default_factory=EngineScore)
    confidence: EngineConfidence = field(default_factory=EngineConfidence)
    reason: ReasonCode = ReasonCode.UNKNOWN
    updated_at: float | None = None


@dataclass(frozen=True, slots=True)
class EngineContext:
    # Единый входной объект для любого Engine.
    # Важно: здесь нет позиции, плеча, баланса, ордеров или Telegram.
    # Market Intelligence получает только данные для анализа рынка.
    symbol: SymbolName
    timeframe: TimeframeName
    market_data: MarketData = field(default_factory=dict)
    previous_snapshot: Any | None = None
    dependency_results: tuple[EngineDependencyResult, ...] = ()
    settings: ContextDict = field(default_factory=dict)
    metadata: ContextDict = field(default_factory=dict)
    created_at: float | None = None


@dataclass(frozen=True, slots=True)
class EngineResult:
    # Единый результат любого аналитического движка.
    # Это мнение о рынке, а не торговое действие.
    engine_name: EngineName
    engine_version: EngineVersion
    symbol: SymbolName
    timeframe: TimeframeName

    status: EngineStatus = EngineStatus.UNKNOWN
    score: EngineScore = field(default_factory=EngineScore)
    confidence: EngineConfidence = field(default_factory=EngineConfidence)
    reason: ReasonCode = ReasonCode.UNKNOWN

    metrics: tuple[EngineMetric, ...] = ()
    diagnostics: EngineDiagnostics = field(default_factory=EngineDiagnostics)
    dependencies: tuple[EngineDependencyResult, ...] = ()
    meta: EngineEvaluationMeta | None = None
    payload: PayloadDict = field(default_factory=dict)

    direction: MarketDirection = MarketDirection.UNKNOWN
    bias: MarketBias = MarketBias.UNKNOWN
    phase: MarketPhase = MarketPhase.UNKNOWN
    regime: MarketRegime = MarketRegime.UNKNOWN
    risk_level: RiskLevel = RiskLevel.UNKNOWN

    @property
    def is_ok(self) -> bool:
        # Удобный признак для Coordinator.
        # Он показывает, что движок отработал без критической ошибки.
        return self.status == EngineStatus.OK

    @property
    def is_usable(self) -> bool:
        # Результат может быть полезен даже если он частичный.
        # Например, часть данных отсутствовала, но базовая оценка всё равно рассчитана.
        return self.status in {
            EngineStatus.OK,
            EngineStatus.PARTIAL,
            EngineStatus.STALE,
        }

    @property
    def has_errors(self) -> bool:
        # Быстрый доступ к признаку ошибок внутри диагностики.
        return self.diagnostics.has_errors


@dataclass(frozen=True, slots=True)
class RuntimeResult:
    # Единый результат выполнения Runtime.
    # RuntimeResult агрегирует результаты нескольких Engine,
    # но не содержит аналитики и не принимает торговых решений.
    engine_results: tuple[EngineResult, ...] = ()
    diagnostics: ContextDict = field(default_factory=dict)
    started_at: float | None = None
    finished_at: float | None = None
    duration_ms: DurationMs | None = None
    metadata: ContextDict = field(default_factory=dict)

    @property
    def successful_results(self) -> tuple[EngineResult, ...]:
        # Результаты Engine, которые успешно завершили анализ.
        return tuple(
            result
            for result in self.engine_results
            if result.status == EngineStatus.OK
        )

    @property
    def failed_results(self) -> tuple[EngineResult, ...]:
        # Результаты Engine, которые завершились ошибкой.
        return tuple(
            result
            for result in self.engine_results
            if result.status == EngineStatus.ERROR
        )

    @property
    def partial_results(self) -> tuple[EngineResult, ...]:
        # Частичные, но потенциально пригодные результаты Engine.
        return tuple(
            result
            for result in self.engine_results
            if result.status == EngineStatus.PARTIAL
        )

    @property
    def stale_results(self) -> tuple[EngineResult, ...]:
        # Результаты Engine, построенные на устаревших данных.
        return tuple(
            result
            for result in self.engine_results
            if result.status == EngineStatus.STALE
        )

    @property
    def executed_engines(self) -> tuple[EngineName, ...]:
        # Имена Engine, которые вернули результат.
        return tuple(result.engine_name for result in self.engine_results)

    @property
    def failed_engines(self) -> tuple[EngineName, ...]:
        # Имена Engine, которые завершились ошибкой.
        return tuple(result.engine_name for result in self.failed_results)

    @property
    def successful_engines(self) -> tuple[EngineName, ...]:
        # Имена Engine, которые завершились успешно.
        return tuple(result.engine_name for result in self.successful_results)

    @property
    def has_errors(self) -> bool:
        # Быстрый признак наличия ошибок Runtime-выполнения.
        return bool(self.failed_results)

    @property
    def is_successful(self) -> bool:
        # Runtime считается успешным, если ни один Engine не завершился ERROR.
        return not self.has_errors

    @property
    def total_engines(self) -> int:
        # Количество Engine, вернувших результат.
        return len(self.engine_results)

    @property
    def successful_count(self) -> int:
        # Количество успешно завершённых Engine.
        return len(self.successful_results)


@dataclass(frozen=True, slots=True)
class CoordinatorDiagnostics:
    # Диагностика Coordinator.
    reason: ReasonCode = ReasonCode.UNKNOWN
    details: ContextDict = field(default_factory=dict)
    warnings: DiagnosticMessages = field(default_factory=list)
    errors: DiagnosticMessages = field(default_factory=list)

    @property
    def has_warnings(self) -> bool:
        return bool(self.warnings)

    @property
    def has_errors(self) -> bool:
        return bool(self.errors)


@dataclass(frozen=True, slots=True)
class CoordinatorEvaluationMeta:
    # Служебная информация о расчёте Coordinator.
    coordinator_version: str
    calculated_at: float | None = None
    duration_ms: DurationMs | None = None


@dataclass(frozen=True, slots=True)
class CoordinatorResult:
    # Единый результат Coordinator Layer.
    # Это итоговая аналитическая интерпретация RuntimeResult,
    # но ещё не торговое решение.
    runtime_result: RuntimeResult
    diagnostics: CoordinatorDiagnostics = field(default_factory=CoordinatorDiagnostics)
    meta: CoordinatorEvaluationMeta | None = None
    payload: PayloadDict = field(default_factory=dict)

    status: EngineStatus = EngineStatus.UNKNOWN
    score: EngineScore = field(default_factory=EngineScore)
    confidence: EngineConfidence = field(default_factory=EngineConfidence)
    reason: ReasonCode = ReasonCode.UNKNOWN

    direction: MarketDirection = MarketDirection.UNKNOWN
    bias: MarketBias = MarketBias.UNKNOWN
    phase: MarketPhase = MarketPhase.UNKNOWN
    regime: MarketRegime = MarketRegime.UNKNOWN
    risk_level: RiskLevel = RiskLevel.UNKNOWN

    @property
    def is_usable(self) -> bool:
        return self.status in {
            EngineStatus.OK,
            EngineStatus.PARTIAL,
            EngineStatus.STALE,
        }

    @property
    def has_errors(self) -> bool:
        return self.diagnostics.has_errors