from __future__ import annotations

from dataclasses import dataclass, field
from hashlib import sha1
import json
from typing import Any, ClassVar, Mapping


JsonObject = dict[str, Any]
EXPERIMENT_ROW_CONTRACT_VERSION = "v1"
RUN_MANIFEST_CONTRACT_VERSION = "v1"



def _ensure_non_empty_str(value: Any, *, field_name: str) -> str:
    if not isinstance(value, str):
        raise TypeError(f"{field_name} must be a string, got {type(value)}")
    normalized = value.strip()
    if not normalized:
        raise ValueError(f"{field_name} must be a non-empty string")
    return normalized



def _ensure_optional_str(value: Any, *, field_name: str) -> str | None:
    if value is None:
        return None
    return _ensure_non_empty_str(value, field_name=field_name)



def _coerce_mapping(value: Any, *, field_name: str) -> JsonObject:
    if not isinstance(value, Mapping):
        raise TypeError(f"{field_name} must be mapping-like, got {type(value)}")
    return dict(value)



def stable_json_fingerprint(payload: Mapping[str, Any] | list[Any]) -> str:
    """Return a deterministic fingerprint for JSON-like payloads."""
    encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
    return sha1(encoded.encode("utf-8")).hexdigest()


@dataclass(frozen=True)
class ExperimentMeta:
    dataset_ref: str
    usage: str | None = None
    profile: str | None = None
    source_version: str | None = None
    snapshot_hash: str = ""
    source_format: str = ""
    quality: JsonObject = field(default_factory=dict)

    def __post_init__(self) -> None:
        object.__setattr__(
            self,
            "dataset_ref",
            _ensure_non_empty_str(self.dataset_ref, field_name="meta.dataset_ref"),
        )
        object.__setattr__(
            self,
            "usage",
            _ensure_optional_str(self.usage, field_name="meta.usage"),
        )
        object.__setattr__(
            self,
            "profile",
            _ensure_optional_str(self.profile, field_name="meta.profile"),
        )
        object.__setattr__(
            self,
            "source_version",
            _ensure_optional_str(self.source_version, field_name="meta.source_version"),
        )
        object.__setattr__(
            self,
            "snapshot_hash",
            _ensure_non_empty_str(self.snapshot_hash, field_name="meta.snapshot_hash"),
        )
        object.__setattr__(
            self,
            "source_format",
            _ensure_non_empty_str(self.source_format, field_name="meta.source_format"),
        )
        object.__setattr__(
            self,
            "quality",
            _coerce_mapping(self.quality, field_name="meta.quality"),
        )

    @classmethod
    def from_mapping(cls, payload: Mapping[str, Any]) -> "ExperimentMeta":
        data = dict(payload)
        quality = data.get("quality", {})
        return cls(
            dataset_ref=data.get("dataset_ref", ""),
            usage=data.get("usage"),
            profile=data.get("profile"),
            source_version=data.get("source_version"),
            snapshot_hash=data.get("snapshot_hash", ""),
            source_format=data.get("source_format", ""),
            quality=quality,
        )

    def to_dict(self) -> JsonObject:
        return {
            "dataset_ref": self.dataset_ref,
            "usage": self.usage,
            "profile": self.profile,
            "source_version": self.source_version,
            "snapshot_hash": self.snapshot_hash,
            "source_format": self.source_format,
            "quality": dict(self.quality),
        }


@dataclass(frozen=True)
class ExperimentRow:
    contract_version: ClassVar[str] = EXPERIMENT_ROW_CONTRACT_VERSION
    sample_id: str
    payload: JsonObject
    ground_truth: JsonObject | None
    meta: ExperimentMeta

    def __post_init__(self) -> None:
        object.__setattr__(
            self,
            "sample_id",
            _ensure_non_empty_str(self.sample_id, field_name="sample_id"),
        )
        object.__setattr__(self, "payload", _coerce_mapping(self.payload, field_name="payload"))
        if self.ground_truth is not None:
            object.__setattr__(
                self,
                "ground_truth",
                _coerce_mapping(self.ground_truth, field_name="ground_truth"),
            )
        if not isinstance(self.meta, ExperimentMeta):
            raise TypeError("meta must be an ExperimentMeta instance")

    @classmethod
    def from_mapping(cls, payload: Mapping[str, Any]) -> "ExperimentRow":
        data = dict(payload)
        raw_meta = data.get("meta")
        if not isinstance(raw_meta, Mapping):
            raise TypeError("meta must be mapping-like")

        raw_ground_truth = data.get("ground_truth")
        if raw_ground_truth is not None and not isinstance(raw_ground_truth, Mapping):
            raise TypeError("ground_truth must be mapping-like when provided")

        return cls(
            sample_id=data.get("sample_id", ""),
            payload=data.get("payload", {}),
            ground_truth=dict(raw_ground_truth) if isinstance(raw_ground_truth, Mapping) else None,
            meta=ExperimentMeta.from_mapping(raw_meta),
        )

    def to_dict(self) -> JsonObject:
        return {
            "sample_id": self.sample_id,
            "payload": dict(self.payload),
            "ground_truth": dict(self.ground_truth) if self.ground_truth is not None else None,
            "meta": self.meta.to_dict(),
        }


@dataclass(frozen=True)
class RunManifest:
    contract_version: ClassVar[str] = RUN_MANIFEST_CONTRACT_VERSION
    run_id: str
    dataset_ref: str
    usage: str | None
    profile: str | None
    source_version: str | None
    snapshot_hash: str
    connector_policy: JsonObject
    operator_chain_fingerprint: str
    created_at: str

    def __post_init__(self) -> None:
        object.__setattr__(self, "run_id", _ensure_non_empty_str(self.run_id, field_name="run_id"))
        object.__setattr__(
            self,
            "dataset_ref",
            _ensure_non_empty_str(self.dataset_ref, field_name="dataset_ref"),
        )
        object.__setattr__(self, "usage", _ensure_optional_str(self.usage, field_name="usage"))
        object.__setattr__(self, "profile", _ensure_optional_str(self.profile, field_name="profile"))
        object.__setattr__(
            self,
            "source_version",
            _ensure_optional_str(self.source_version, field_name="source_version"),
        )
        object.__setattr__(
            self,
            "snapshot_hash",
            _ensure_non_empty_str(self.snapshot_hash, field_name="snapshot_hash"),
        )
        object.__setattr__(
            self,
            "connector_policy",
            _coerce_mapping(self.connector_policy, field_name="connector_policy"),
        )
        object.__setattr__(
            self,
            "operator_chain_fingerprint",
            _ensure_non_empty_str(
                self.operator_chain_fingerprint,
                field_name="operator_chain_fingerprint",
            ),
        )
        object.__setattr__(
            self,
            "created_at",
            _ensure_non_empty_str(self.created_at, field_name="created_at"),
        )

    @classmethod
    def from_mapping(cls, payload: Mapping[str, Any]) -> "RunManifest":
        data = dict(payload)
        return cls(
            run_id=data.get("run_id", ""),
            dataset_ref=data.get("dataset_ref", ""),
            usage=data.get("usage"),
            profile=data.get("profile"),
            source_version=data.get("source_version"),
            snapshot_hash=data.get("snapshot_hash", ""),
            connector_policy=data.get("connector_policy", {}),
            operator_chain_fingerprint=data.get("operator_chain_fingerprint", ""),
            created_at=data.get("created_at", ""),
        )

    def to_dict(self) -> JsonObject:
        return {
            "run_id": self.run_id,
            "dataset_ref": self.dataset_ref,
            "usage": self.usage,
            "profile": self.profile,
            "source_version": self.source_version,
            "snapshot_hash": self.snapshot_hash,
            "connector_policy": dict(self.connector_policy),
            "operator_chain_fingerprint": self.operator_chain_fingerprint,
            "created_at": self.created_at,
        }

    def fingerprint(self) -> str:
        return stable_json_fingerprint(self.to_dict())


__all__ = [
    "EXPERIMENT_ROW_CONTRACT_VERSION",
    "RUN_MANIFEST_CONTRACT_VERSION",
    "ExperimentMeta",
    "ExperimentRow",
    "RunManifest",
    "stable_json_fingerprint",
]
