"""Persistent Activity entity/revision contracts, separate from domain Activity identity."""

from __future__ import annotations

import hashlib
import json
import re
from dataclasses import asdict, dataclass
from datetime import datetime
from typing import Any
from uuid import uuid4

from mountain_twin.activity.contracts import (
    Activity,
    ActivityPrivacy,
    ActivityProvenance,
    ActivitySourceKind,
    ActivityType,
    AvailabilityState,
    ExternalActivityReference,
    FieldAvailability,
    GeoPoint,
    RawProviderRecord,
    TrackReference,
)

ACTIVITY_SNAPSHOT_FORMAT = "activity_snapshot_v0_1"
ActivityEntityId = str
ActivityRevisionId = str
ActivitySourceLinkId = str

RAW_PAYLOAD_ENCODING_JSON = "application/json"
RAW_PAYLOAD_COMPRESSION_GZIP = "gzip"


def _aware(value: str, label: str) -> None:
    instant = datetime.fromisoformat(value)
    if instant.tzinfo is None or instant.utcoffset() is None:
        raise ValueError(f"{label} must be timezone-aware")


@dataclass(frozen=True)
class ActivityEntity:
    """Stable durable identity for one logical observed activity."""

    activity_entity_id: ActivityEntityId
    user_id: str
    created_at: str
    current_revision_id: ActivityRevisionId | None = None

    def __post_init__(self) -> None:
        if not self.activity_entity_id or not self.user_id:
            raise ValueError("activity entity requires stable identity and owner")
        _aware(self.created_at, "activity entity creation")


@dataclass(frozen=True)
class ActivityRevision:
    """Immutable canonical interpretation of an ActivityEntity at one versioned point."""

    activity_revision_id: ActivityRevisionId
    activity_entity_id: ActivityEntityId
    canonical_activity: Activity
    normalizer_version: str
    taxonomy_version: str
    normalized_at: str

    def __post_init__(self) -> None:
        if not all((
            self.activity_revision_id, self.activity_entity_id,
            self.normalizer_version, self.taxonomy_version,
        )):
            raise ValueError("activity revision requires identity and interpretation versions")
        _aware(self.normalized_at, "activity revision normalization")
        if self.canonical_activity.provenance.normalization_version != self.normalizer_version:
            raise ValueError("activity revision normalizer version differs from canonical Activity provenance")
        if self.canonical_activity.provenance.normalized_at != self.normalized_at:
            raise ValueError("activity revision normalization time differs from canonical Activity provenance")
        expected_identity = activity_revision_id_for(
            self.activity_entity_id, self.canonical_activity, self.normalizer_version, self.taxonomy_version
        )
        if self.activity_revision_id != expected_identity:
            raise ValueError("activity revision identity must match its immutable interpretation inputs")


@dataclass(frozen=True)
class ActivitySourceLink:
    """Stable provider-neutral source identity attached to one ActivityEntity."""

    source_link_id: ActivitySourceLinkId
    activity_entity_id: ActivityEntityId
    user_id: str
    source_kind: ActivitySourceKind
    provider: str | None
    source_activity_id: str
    first_seen_at: str

    def __post_init__(self) -> None:
        if not all((self.source_link_id, self.activity_entity_id, self.user_id, self.source_activity_id)):
            raise ValueError("activity source link requires stable identity, owner and source activity ID")
        if self.provider == "":
            raise ValueError("activity source link provider must be None or a non-empty string")
        if self.source_kind is ActivitySourceKind.PROVIDER and not self.provider:
            raise ValueError("provider source link requires provider identity")
        _aware(self.first_seen_at, "activity source link first seen")
        expected_identity = activity_source_link_id_for(
            self.user_id, self.source_kind, self.provider, self.source_activity_id
        )
        if self.source_link_id != expected_identity:
            raise ValueError("activity source link identity must match its stable source identity")


@dataclass(frozen=True)
class RawPayloadBlob:
    """Metadata for protected immutable content-addressed raw JSON payload bytes."""

    payload_sha256: str
    encoding: str
    compression: str
    byte_count: int

    def __post_init__(self) -> None:
        if not re.fullmatch(r"[0-9a-f]{64}", self.payload_sha256):
            raise ValueError("raw payload SHA-256 must be a lowercase hex digest")
        if self.encoding != RAW_PAYLOAD_ENCODING_JSON or self.compression != RAW_PAYLOAD_COMPRESSION_GZIP:
            raise ValueError("unsupported raw payload encoding or compression")
        if self.byte_count < 0:
            raise ValueError("raw payload byte count cannot be negative")


@dataclass(frozen=True)
class ActivityRevisionProvenance:
    """Relational source-truth evidence for one immutable ActivityRevision."""

    activity_revision_id: ActivityRevisionId
    activity_entity_id: ActivityEntityId
    raw_record_id: str
    source_link_id: ActivitySourceLinkId

    def __post_init__(self) -> None:
        if not all((
            self.activity_revision_id, self.activity_entity_id,
            self.raw_record_id, self.source_link_id,
        )):
            raise ValueError("activity revision provenance requires revision, entity, source and raw identities")


def new_activity_entity_id() -> ActivityEntityId:
    """Generate an opaque provider-neutral durable entity identifier."""
    return f"activity-entity-v0_1:{uuid4()}"


def activity_source_link_id_for(
    user_id: str,
    source_kind: ActivitySourceKind,
    provider: str | None,
    source_activity_id: str,
) -> ActivitySourceLinkId:
    identity = json.dumps(
        {
            "format": "activity_source_link_identity_v0_1",
            "provider": provider,
            "source_activity_id": source_activity_id,
            "source_kind": source_kind.value,
            "user_id": user_id,
        },
        sort_keys=True,
        separators=(",", ":"),
    )
    return f"activity-source-link-v0_1:{hashlib.sha256(identity.encode()).hexdigest()}"


def new_activity_source_link(
    activity_entity_id: ActivityEntityId,
    user_id: str,
    source_kind: ActivitySourceKind,
    provider: str | None,
    source_activity_id: str,
    first_seen_at: str,
) -> ActivitySourceLink:
    return ActivitySourceLink(
        activity_source_link_id_for(user_id, source_kind, provider, source_activity_id),
        activity_entity_id,
        user_id,
        source_kind,
        provider,
        source_activity_id,
        first_seen_at,
    )


def canonical_raw_payload_bytes(payload: Any) -> bytes:
    """Encode JSON source content deterministically before its content hash is calculated."""
    return json.dumps(payload, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8")


def raw_payload_sha256(payload: Any) -> str:
    return hashlib.sha256(canonical_raw_payload_bytes(payload)).hexdigest()


def raw_provider_record_id_for(
    user_id: str,
    source_link_id: ActivitySourceLinkId,
    record_kind: str,
    payload_sha256: str,
) -> str:
    identity = json.dumps(
        {
            "format": "raw_provider_record_identity_v0_1",
            "payload_sha256": payload_sha256,
            "record_kind": record_kind,
            "source_link_id": source_link_id,
            "user_id": user_id,
        },
        sort_keys=True,
        separators=(",", ":"),
    )
    return f"raw-provider-record-v0_1:{hashlib.sha256(identity.encode()).hexdigest()}"


def new_raw_provider_record(
    source_link: ActivitySourceLink,
    payload: RawPayloadBlob,
    record_kind: str,
    fetched_at: str,
    source_schema_version: str | None = None,
) -> RawProviderRecord:
    if not record_kind:
        raise ValueError("raw provider record requires a record kind")
    return RawProviderRecord(
        raw_provider_record_id_for(
            source_link.user_id, source_link.source_link_id, record_kind, payload.payload_sha256
        ),
        source_link.user_id,
        source_link.source_kind,
        source_link.provider,
        source_link.source_activity_id,
        fetched_at,
        f"sqlite:raw-payload:{payload.payload_sha256}",
        payload.payload_sha256,
        source_schema_version,
        record_kind,
    )


def serialize_activity_snapshot(activity: Activity) -> str:
    """Deterministic, inspectable Activity snapshot; never a product projection or log value."""
    return json.dumps(
        {"format": ACTIVITY_SNAPSHOT_FORMAT, "activity": asdict(activity)},
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    )


def serialize_activity_semantic_identity_snapshot(activity: Activity) -> str:
    """Return deterministic semantic Activity content for immutable revision identity.

    ActivityProvenance records source acquisition evidence. ``normalization_version``
    remains interpretation-version evidence; raw/source references, native provider
    type and ``normalized_at`` remain in the full durable snapshot only.
    """
    full_activity = asdict(activity)
    semantic_provenance = {
        "normalization_version": full_activity["provenance"]["normalization_version"],
    }
    semantic_activity = {**full_activity, "provenance": semantic_provenance}
    return json.dumps(
        {"format": ACTIVITY_SNAPSHOT_FORMAT, "activity": semantic_activity},
        sort_keys=True,
        separators=(",", ":"),
        allow_nan=False,
    )


def deserialize_activity_snapshot(payload: str) -> Activity:
    """Round-trip a deterministic stored canonical Activity snapshot without pickle."""
    envelope = json.loads(payload)
    if envelope.get("format") != ACTIVITY_SNAPSHOT_FORMAT:
        raise ValueError("unsupported Activity snapshot format")
    value = envelope["activity"]
    provenance_value = value["provenance"]
    sources = tuple(
        ExternalActivityReference(
            ActivitySourceKind(item["source_kind"]), item["provider"],
            item["source_activity_id"], item["raw_record_id"],
        )
        for item in provenance_value["source_references"]
    )
    provenance = ActivityProvenance(
        sources,
        tuple(provenance_value["normalized_from_raw_record_ids"]),
        provenance_value["normalization_version"],
        provenance_value["normalized_at"],
        provenance_value.get("provider_native_activity_type"),
    )
    privacy_value = value["privacy"]
    privacy = ActivityPrivacy(
        privacy_value["is_private"],
        privacy_value["redact_start_end_locations"],
        privacy_value["allow_precise_location_in_product"],
    )
    track_value = value.get("track")
    track = TrackReference(**track_value) if track_value is not None else None
    start_value, end_value = value.get("start_location"), value.get("end_location")
    return Activity(
        activity_id=value["activity_id"],
        user_id=value["user_id"],
        activity_type=ActivityType(value["activity_type"]),
        provenance=provenance,
        started_at=value.get("started_at"),
        timezone=value.get("timezone"),
        title=value.get("title"),
        elapsed_duration_s=value.get("elapsed_duration_s"),
        moving_duration_s=value.get("moving_duration_s"),
        distance_m=value.get("distance_m"),
        elevation_gain_m=value.get("elevation_gain_m"),
        elevation_loss_m=value.get("elevation_loss_m"),
        min_elevation_m=value.get("min_elevation_m"),
        max_elevation_m=value.get("max_elevation_m"),
        average_speed_mps=value.get("average_speed_mps"),
        max_speed_mps=value.get("max_speed_mps"),
        average_pace_s_per_km=value.get("average_pace_s_per_km"),
        average_heart_rate_bpm=value.get("average_heart_rate_bpm"),
        max_heart_rate_bpm=value.get("max_heart_rate_bpm"),
        track=track,
        start_location=GeoPoint(**start_value) if start_value is not None else None,
        end_location=GeoPoint(**end_value) if end_value is not None else None,
        privacy=privacy,
        field_availability=tuple(
            FieldAvailability(
                item["field_name"], AvailabilityState(item["state"]), tuple(item["reason_codes"])
            )
            for item in value.get("field_availability", ())
        ),
    )


def activity_revision_id_for(
    activity_entity_id: ActivityEntityId,
    canonical_activity: Activity,
    normalizer_version: str,
    taxonomy_version: str,
) -> ActivityRevisionId:
    semantic_snapshot = serialize_activity_semantic_identity_snapshot(canonical_activity)
    identity = json.dumps(
        {
            "activity_entity_id": activity_entity_id,
            "semantic_canonical_snapshot_sha256": hashlib.sha256(semantic_snapshot.encode()).hexdigest(),
            "format": "activity_revision_identity_v0_1",
            "normalizer_version": normalizer_version,
            "taxonomy_version": taxonomy_version,
        },
        sort_keys=True,
        separators=(",", ":"),
    )
    return f"activity-revision-v0_1:{hashlib.sha256(identity.encode()).hexdigest()}"


def new_activity_revision(
    activity_entity_id: ActivityEntityId,
    canonical_activity: Activity,
    taxonomy_version: str,
) -> ActivityRevision:
    """Create a deterministic immutable revision from the existing canonical domain value."""
    normalizer_version = canonical_activity.provenance.normalization_version
    return ActivityRevision(
        activity_revision_id=activity_revision_id_for(
            activity_entity_id, canonical_activity, normalizer_version, taxonomy_version
        ),
        activity_entity_id=activity_entity_id,
        canonical_activity=canonical_activity,
        normalizer_version=normalizer_version,
        taxonomy_version=taxonomy_version,
        normalized_at=canonical_activity.provenance.normalized_at,
    )
