"""Pure import-trust reference for ci-app-access-policy/v1 (no I/O).

The integration constructs SourceBinding from its registered application and
authenticated transport, NEVER from exported JSON or browser metadata. This
library cannot authenticate a source. A snapshot is not an access token:
consumers must also resolve current identity, legacy ACL and execution binding.
invalidate() is mandatory on resolver/read errors. Absence can enable legacy
handling only if legacy is explicitly enabled and no policy was ever declared.
Persist the adoption marker and revision/digest watermark across restarts;
recreating an empty store must not reset an existing registration's history.
"""
from dataclasses import dataclass
import hashlib
import json

from reference import validate_policy


@dataclass(frozen=True)
class SourceBinding:
    app_id: str
    issuer: str
    resource: str
    connection: str

    def __post_init__(self):
        if not all(isinstance(value, str) and value.strip() for value in
                   (self.app_id, self.issuer, self.resource, self.connection)):
            raise ValueError("invalid_registered_source")


@dataclass(frozen=True)
class PolicySnapshot:
    revision: int
    digest: str
    generation: int
    canonical_json: str

    def policy(self):
        """Return a fresh copy; modifying it cannot mutate this snapshot."""
        return json.loads(self.canonical_json)


class PolicyImportError(ValueError):
    pass


class PolicyStore:
    """One registered app connection; loss of trust never revives old rights.

    accept(policy, source=...) is called only after an authenticated read.
    Same revision/content is idempotent while current. Recovery after invalidate
    creates a new generation; it never resumes an existing suspended window.
    A valid higher revision may remove optional constraints. Removing the entire
    declared contract cannot restore legacy handling in this store.
    """

    def __init__(self, binding, *, legacy_enabled=False):
        if not isinstance(binding, SourceBinding) or type(legacy_enabled) is not bool:
            raise ValueError("invalid_store_configuration")
        self._binding = binding
        self._legacy_enabled = legacy_enabled
        self._declared = False
        self._revision = 0
        self._digest = None
        self._generation = 0
        self._current = None
        self.reason = "not_loaded"

    @property
    def binding(self):
        return self._binding

    @property
    def current(self):
        return self._current

    def invalidate(self, reason="source_unavailable"):
        """Called on unavailable identity/policy/owner/grant resolvers or read errors.

        Unknown state cannot be interpreted as a missing optional restriction.
        This conservatively records a declaration and disables legacy fallback.
        """
        self._declared = True
        self._generation += 1
        self._current = None
        self.reason = reason

    def legacy_available(self, *, declaration_present):
        """Check a successfully read declaration, never an error-derived default."""
        if type(declaration_present) is not bool:
            self.invalidate("invalid_declaration")
            return False
        if declaration_present:
            self._declared = True
            return False
        if self._declared:
            self.invalidate("declared_policy_missing")
            return False
        return self._legacy_enabled

    def is_current(self, snapshot):
        """Revision/generation guard only; not complete action authorization."""
        return self._current is not None and snapshot is self._current

    def _reject(self, reason):
        self.invalidate(reason)
        raise PolicyImportError(reason)

    def accept(self, policy, *, source):
        self._declared = True
        if not isinstance(source, SourceBinding) or source != self.binding:
            self._reject("source_binding_mismatch")
        if not validate_policy(policy):
            self._reject("invalid_policy")
        if policy["app_id"] != self.binding.app_id:
            self._reject("app_id_mismatch")
        normalized_policy = dict(policy)
        normalized_policy["revision"] = int(policy["revision"])
        try:
            encoded = json.dumps(normalized_policy, ensure_ascii=False, sort_keys=True,
                                 separators=(",", ":"), allow_nan=False)
            digest = hashlib.sha256(encoded.encode("utf-8")).hexdigest()
        except (ValueError, TypeError, UnicodeError):
            self._reject("invalid_policy_encoding")
        revision = normalized_policy["revision"]
        if revision < self._revision:
            self._reject("stale_revision")
        if revision == self._revision and digest != self._digest:
            self._reject("revision_content_mismatch")
        if self._current is not None and revision == self._revision:
            return self._current
        self._generation += 1
        snapshot = PolicySnapshot(revision, digest, self._generation, encoded)
        self._revision, self._digest = revision, digest
        self._current = snapshot
        self.reason = "ready"
        return snapshot
