"""Frozen domain records with explicit validation at construction time."""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from typing import Iterable

from ..errors import ContractViolation, UnsupportedOperation
from .enums import (
    OsmObjectType,
    PlannedActionType,
    ProfileEligibilityState,
    SourcePresenceState,
)


def _nonblank(value: object, field_name: str) -> str:
    if not isinstance(value, str) or not value.strip():
        raise ContractViolation(f"{field_name} must be a nonblank string")
    return value.strip()


def _text_tuple(values: Iterable[str], field_name: str) -> tuple[str, ...]:
    if isinstance(values, str):
        raise ContractViolation(f"{field_name} must be an iterable of strings, not a string")
    normalized = tuple(_nonblank(value, field_name) for value in values)
    return normalized


def _require_enum(value: object, enum_type: type[object], field_name: str) -> object:
    if isinstance(value, enum_type):
        return value
    try:
        return enum_type(value)  # type: ignore[call-arg]
    except (TypeError, ValueError) as exc:
        raise ContractViolation(f"{field_name} is not a supported value") from exc


def _require_positive_id(value: object, field_name: str) -> int:
    if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
        raise ContractViolation(f"{field_name} must be a positive integer")
    return value


@dataclass(frozen=True, slots=True)
class OsmIdentity:
    """An OSM object identity independent of a configured region."""

    osm_type: OsmObjectType
    osm_id: int

    def __post_init__(self) -> None:
        object.__setattr__(
            self, "osm_type", _require_enum(self.osm_type, OsmObjectType, "osm_type")
        )
        object.__setattr__(self, "osm_id", _require_positive_id(self.osm_id, "osm_id"))


@dataclass(frozen=True, slots=True)
class RegionScopedIdentity:
    """An OSM identity qualified by a nonblank source region."""

    region_id: str
    osm_type: OsmObjectType
    osm_id: int

    def __post_init__(self) -> None:
        object.__setattr__(self, "region_id", _nonblank(self.region_id, "region_id"))
        object.__setattr__(
            self, "osm_type", _require_enum(self.osm_type, OsmObjectType, "osm_type")
        )
        object.__setattr__(self, "osm_id", _require_positive_id(self.osm_id, "osm_id"))

    @property
    def osm_identity(self) -> OsmIdentity:
        """Return the unscoped OSM identity represented by this record."""

        return OsmIdentity(osm_type=self.osm_type, osm_id=self.osm_id)


@dataclass(frozen=True, slots=True)
class ProfileVersion:
    """A profile identifier paired with its immutable rule version."""

    profile_id: str
    version: str

    def __post_init__(self) -> None:
        object.__setattr__(self, "profile_id", _nonblank(self.profile_id, "profile_id"))
        object.__setattr__(self, "version", _nonblank(self.version, "version"))


@dataclass(frozen=True, slots=True)
class SourcePresenceRecord:
    """Evidence that a region-scoped identity had one source state."""

    identity: RegionScopedIdentity
    state: SourcePresenceState
    snapshot_references: tuple[str, ...] = ()
    evidence: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if not isinstance(self.identity, RegionScopedIdentity):
            raise ContractViolation("identity must be a RegionScopedIdentity")
        object.__setattr__(self, "state", _require_enum(self.state, SourcePresenceState, "state"))
        object.__setattr__(
            self,
            "snapshot_references",
            _text_tuple(self.snapshot_references, "snapshot_references"),
        )
        object.__setattr__(self, "evidence", _text_tuple(self.evidence, "evidence"))


@dataclass(frozen=True, slots=True)
class ProfileEligibilityRecord:
    """A profile-specific eligibility decision for a region-scoped identity."""

    identity: RegionScopedIdentity
    profile: ProfileVersion
    state: ProfileEligibilityState
    rule_evidence: tuple[str, ...] = ()

    def __post_init__(self) -> None:
        if not isinstance(self.identity, RegionScopedIdentity):
            raise ContractViolation("identity must be a RegionScopedIdentity")
        if not isinstance(self.profile, ProfileVersion):
            raise ContractViolation("profile must be a ProfileVersion")
        object.__setattr__(
            self, "state", _require_enum(self.state, ProfileEligibilityState, "state")
        )
        object.__setattr__(self, "rule_evidence", _text_tuple(self.rule_evidence, "rule_evidence"))

    @property
    def profile_version(self) -> ProfileVersion:
        """Return the profile/version contract for this eligibility record."""

        return self.profile


@dataclass(frozen=True, slots=True)
class PlannedAction:
    """A deterministic, report-only reconciliation plan item.

    Phase 2A creates planning records only.  ``executable`` is permanently
    false in this package, and ``execute`` raises a typed error if called.
    """

    action: PlannedActionType
    reason_codes: tuple[str, ...]
    evidence: tuple[str, ...]
    planning_key: str | None = None
    idempotency_key: str = field(init=False)

    def __post_init__(self) -> None:
        object.__setattr__(self, "action", _require_enum(self.action, PlannedActionType, "action"))
        normalized_reasons = _text_tuple(self.reason_codes, "reason_codes")
        normalized_evidence = _text_tuple(self.evidence, "evidence")
        if not normalized_reasons:
            raise ContractViolation("reason_codes must contain at least one reason code")
        if not normalized_evidence:
            raise ContractViolation("evidence must contain at least one evidence item")
        object.__setattr__(self, "reason_codes", normalized_reasons)
        object.__setattr__(self, "evidence", normalized_evidence)

        supplied_key = self.planning_key
        if supplied_key is not None:
            supplied_key = _nonblank(supplied_key, "planning_key")
        else:
            canonical = json.dumps(
                {
                    "action": self.action.value,
                    "evidence": normalized_evidence,
                    "reason_codes": normalized_reasons,
                },
                sort_keys=True,
                separators=(",", ":"),
            ).encode("utf-8")
            supplied_key = hashlib.sha256(canonical).hexdigest()
        object.__setattr__(self, "planning_key", supplied_key)
        object.__setattr__(self, "idempotency_key", supplied_key)

    @property
    def executable(self) -> bool:
        """Whether this action may be executed by the current phase."""

        return False

    @property
    def action_type(self) -> PlannedActionType:
        """Return the planned action vocabulary member."""

        return self.action

    def execute(self) -> None:
        """Reject execution because this package is report-only."""

        raise UnsupportedOperation(
            f"planned action {self.action.value} is report-only in Phase 2A.1"
        )


__all__ = [
    "OsmIdentity",
    "PlannedAction",
    "ProfileEligibilityRecord",
    "ProfileVersion",
    "RegionScopedIdentity",
    "SourcePresenceRecord",
]
