"""Immutable provider-neutral Personal Intelligence contracts."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from enum import Enum

from mountain_twin.activity import (
    ActivityFamily,
    ActivityType,
    AvailabilityState,
)

PERSONAL_INTELLIGENCE_ALGORITHM_VERSION = "personal_intelligence_algorithm_v0_1"
WINDOW_POLICY_VERSION = "utc_rolling_windows_v0_1"
BASELINE_POLICY_VERSION = "non_overlapping_personal_baseline_v0_1"
QUANTILE_POLICY_VERSION = "linear_r7_v1"
WINDOW_DAYS = (7, 28, 90, 365)


class IntelligenceAvailability(str, Enum):
    AVAILABLE = "AVAILABLE"
    PARTIAL = "PARTIAL"
    UNAVAILABLE = "UNAVAILABLE"
    INSUFFICIENT_HISTORY = "INSUFFICIENT_HISTORY"
    NOT_APPLICABLE = "NOT_APPLICABLE"


class SignalName(str, Enum):
    ACTIVITY_COUNT = "ACTIVITY_COUNT"
    MOVING_DURATION = "MOVING_DURATION"
    ELAPSED_DURATION = "ELAPSED_DURATION"
    DISTANCE = "DISTANCE"
    ELEVATION_GAIN = "ELEVATION_GAIN"


class ComparisonRelation(str, Enum):
    BELOW_REFERENCE_RANGE = "BELOW_REFERENCE_RANGE"
    WITHIN_REFERENCE_RANGE = "WITHIN_REFERENCE_RANGE"
    ABOVE_REFERENCE_RANGE = "ABOVE_REFERENCE_RANGE"


@dataclass(frozen=True)
class FrozenActivity:
    """The exact canonical summary fields consumed after current-revision selection."""

    activity_entity_id: str
    activity_revision_id: str
    activity_type: ActivityType
    activity_family: ActivityFamily
    started_at: datetime | None
    moving_duration_s: float | None
    elapsed_duration_s: float | None
    distance_m: float | None
    elevation_gain_m: float | None
    field_availability: tuple[tuple[str, AvailabilityState], ...] = ()

    def __post_init__(self) -> None:
        if not self.activity_entity_id or not self.activity_revision_id:
            raise ValueError("frozen activity requires entity and revision identity")
        if self.started_at is not None and (
            self.started_at.tzinfo is None or self.started_at.utcoffset() is None
        ):
            raise ValueError("frozen activity start must be timezone-aware")
        if len({name for name, _ in self.field_availability}) != len(self.field_availability):
            raise ValueError("frozen activity availability fields must be unique")


@dataclass(frozen=True)
class PersonalIntelligenceInput:
    user_id: str
    as_of: datetime
    taxonomy_version: str
    activities: tuple[FrozenActivity, ...]

    def __post_init__(self) -> None:
        if not self.user_id:
            raise ValueError("Personal Intelligence input requires a user")
        if self.as_of.tzinfo is None or self.as_of.utcoffset() is None:
            raise ValueError("Personal Intelligence as_of must be timezone-aware")
        pairs = tuple(
            (item.activity_entity_id, item.activity_revision_id) for item in self.activities
        )
        if pairs != tuple(sorted(pairs)):
            raise ValueError("frozen activities must use canonical entity/revision ordering")
        if len({item.activity_entity_id for item in self.activities}) != len(self.activities):
            raise ValueError("frozen input permits one current revision per entity")


@dataclass(frozen=True)
class SignalCoverage:
    candidate_activity_count: int
    evaluable_activity_count: int
    missing_activity_count: int
    history_complete: bool

    def __post_init__(self) -> None:
        if (
            min(
                self.candidate_activity_count,
                self.evaluable_activity_count,
                self.missing_activity_count,
            )
            < 0
        ):
            raise ValueError("signal coverage counts cannot be negative")
        if (
            self.evaluable_activity_count + self.missing_activity_count
            != self.candidate_activity_count
        ):
            raise ValueError("signal coverage must partition candidate activities")


@dataclass(frozen=True)
class SignalMeasurement:
    signal: SignalName
    value: float | None
    unit: str
    availability: IntelligenceAvailability
    coverage: SignalCoverage


@dataclass(frozen=True)
class BaselineDistribution:
    availability: IntelligenceAvailability
    sample_count: int
    median: float | None = None
    minimum: float | None = None
    maximum: float | None = None
    lower_quartile: float | None = None
    upper_quartile: float | None = None


@dataclass(frozen=True)
class SignalComparison:
    signal: SignalName
    availability: IntelligenceAvailability
    baseline: BaselineDistribution
    relation: ComparisonRelation | None = None


@dataclass(frozen=True)
class WindowSignalSet:
    window_days: int
    measurements: tuple[SignalMeasurement, ...]
    comparisons: tuple[SignalComparison, ...]
    history_complete: bool


@dataclass(frozen=True)
class CompositionEntry:
    classification: str
    activity_count: int
    moving_duration_s: float | None
    moving_coverage: SignalCoverage


@dataclass(frozen=True)
class ActivityComposition:
    by_family: tuple[CompositionEntry, ...]
    by_type: tuple[CompositionEntry, ...]


@dataclass(frozen=True)
class ExposureDistribution:
    signal: SignalName
    availability: IntelligenceAvailability
    coverage: SignalCoverage
    minimum: float | None = None
    lower_quartile: float | None = None
    median: float | None = None
    upper_quartile: float | None = None
    maximum: float | None = None


@dataclass(frozen=True)
class HistoricalRequirement:
    min_moving_duration_s: float | None = None
    min_elevation_gain_m: float | None = None

    def __post_init__(self) -> None:
        if self.min_moving_duration_s is None and self.min_elevation_gain_m is None:
            raise ValueError("a requirement needs at least one explicit threshold")
        for value in (self.min_moving_duration_s, self.min_elevation_gain_m):
            if value is not None and value < 0:
                raise ValueError("requirement thresholds cannot be negative")


@dataclass(frozen=True)
class RequirementEvidence:
    requirement: HistoricalRequirement
    availability: IntelligenceAvailability
    total_candidate_count: int
    evaluable_candidate_count: int
    non_evaluable_candidate_count: int
    qualifying_activity_count: int
    qualifying_revision_refs: tuple[tuple[str, str], ...]
    field_coverage: tuple[tuple[SignalName, SignalCoverage], ...]

    def __post_init__(self) -> None:
        if (
            self.evaluable_candidate_count + self.non_evaluable_candidate_count
            != self.total_candidate_count
        ):
            raise ValueError("requirement evidence must partition candidates")


@dataclass(frozen=True)
class PersonalIntelligenceSnapshot:
    snapshot_id: str
    input_digest: str
    user_id: str
    as_of: datetime
    algorithm_version: str
    taxonomy_version: str
    window_policy_version: str
    baseline_policy_version: str
    frozen_input: PersonalIntelligenceInput
    undated_activity_count: int
    future_activity_count: int
    all_history_measurements: tuple[SignalMeasurement, ...]
    windows: tuple[WindowSignalSet, ...]
    composition: ActivityComposition
    moving_duration_exposure: ExposureDistribution
    elevation_gain_exposure: ExposureDistribution
    requirement_evidence: RequirementEvidence | None = None
