"""Observations retain provenance, time, uncertainty, identity and history."""
from dataclasses import asdict, dataclass
from datetime import datetime
import math


def timestamp(value):
    dt = datetime.fromisoformat(value)
    if dt.tzinfo is None:
        raise ValueError("Observation time requires an explicit timezone")
    return dt


@dataclass(frozen=True)
class Observation:
    id: str
    key: str
    value: object
    source: str
    source_version: str
    observed_at: str
    scope: str
    session: str
    confidence: float = 1.0
    uncertainty: str = ""

    def __post_init__(self):
        timestamp(self.observed_at)
        if not all(isinstance(x, str) and x for x in (self.id, self.key, self.source, self.source_version, self.session)):
            raise ValueError("Observation identity/source/version is required")
        if self.scope not in ("static", "synthetic", "recorded", "current"):
            raise ValueError("Unknown observation scope")
        if type(self.confidence) not in (int, float) or not math.isfinite(self.confidence) or not 0 <= self.confidence <= 1:
            raise ValueError("Invalid confidence")


class State:
    def __init__(self, schema, session, history=()):
        self.schema, self.session = schema, session
        self.history, self.latest, self.ids, self.conflicts = [], {}, {}, set()
        for value in history:
            self.add(Observation(**value))

    def add(self, observation):
        o = observation
        if o.key not in self.schema:
            raise ValueError("Undeclared state field: " + o.key)
        expected = {"bool": bool, "int": int, "str": str, "list": list}[self.schema[o.key]["type"]]
        if o.value is not None and type(o.value) is not expected:
            raise ValueError("Invalid state type: " + o.key)
        if o.id in self.ids:
            if self.ids[o.id] != o:
                raise ValueError("Observation id reused with different evidence")
            return
        self.history.append(o)
        self.ids[o.id] = o
        # Keep other sessions/static data for review, never use as live state.
        if o.session != self.session or o.scope not in ("synthetic", "current"):
            return
        old = self.latest.get(o.key)
        if old is None or timestamp(o.observed_at) > timestamp(old.observed_at):
            self.latest[o.key] = o
            self.conflicts.discard(o.key)
        elif timestamp(o.observed_at) == timestamp(old.observed_at) and o.value != old.value:
            self.conflicts.add(o.key)

    def value(self, key):
        o = self.latest.get(key)
        if o is None or key in self.conflicts or o.confidence < 1 or o.uncertainty:
            return None
        return o.value

    def holds(self, condition, parameters):
        actual = self.value(condition["key"])
        if actual is None:
            return False
        expected = parameters[condition["param"]] if "param" in condition else condition.get("value")
        op = condition.get("op", "eq")
        if op == "eq":
            return type(actual) is type(expected) and actual == expected
        if op == "gte":
            return type(actual) is int and type(expected) is int and actual >= expected
        raise ValueError("Unknown predicate: " + op)

    def export(self):
        return [asdict(x) for x in self.history]
