"""Declarative event-reaction contract (ARCH-ER1).

This is composition metadata, not an inline workflow engine. Reactions match
authoritative domain events and schedule already-admitted capabilities through
the Cloud outbox. They cannot contain arbitrary code or unrestricted URLs.
"""
from __future__ import annotations

import hashlib
import json
from typing import Any

from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, model_validator

REACTION_MAX_DEPTH = 8
REACTION_MAX_ACTIONS = 16
REACTION_MAX_MATCHING_PER_EVENT = 32

ADMITTED_REACTION_CAPABILITIES = frozenset(
    {
        "workflow.assignment.create",
        "task.create",
        "notifications.in_app.create",
        "email.send",
        "collaboration.conversation.message.create",
        "requirements.checklist.instantiate",
        "record.create",
        "record.update",
        "workflow.transition",
        "integration.execute",
    }
)


class ReactionCondition(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    field: StrictStr = Field(min_length=1, max_length=128)
    equals: Any


class ReactionAction(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    capability: StrictStr = Field(min_length=1, max_length=128)
    input: dict[str, Any] = Field(default_factory=dict)
    recipients: tuple[StrictStr, ...] = ()
    template: StrictStr | None = None

    def validate_capability(self) -> "ReactionAction":
        if self.capability not in ADMITTED_REACTION_CAPABILITIES:
            raise ValueError("reaction_capability_not_admitted")
        return self


class EventReaction(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    id: StrictStr = Field(min_length=1, max_length=128, pattern=r"^[a-z0-9][a-z0-9._-]*$")
    version: StrictInt = Field(default=1, ge=1)
    event: StrictStr = Field(min_length=1, max_length=128)
    conditions: tuple[ReactionCondition, ...] = ()
    actions: tuple[ReactionAction, ...] = Field(min_length=1, max_length=REACTION_MAX_ACTIONS)
    enabled: StrictBool = True
    max_depth: StrictInt = Field(default=REACTION_MAX_DEPTH, ge=1, le=REACTION_MAX_DEPTH)

    @model_validator(mode="after")
    def validate_actions(self) -> "EventReaction":
        for action in self.actions:
            action.validate_capability()
        return self


def reaction_matches(reaction: EventReaction, event: dict[str, Any]) -> bool:
    """Pure event matcher. Missing condition fields fail closed."""
    if not reaction.enabled or event.get("type") != reaction.event:
        return False
    data = event.get("data") if isinstance(event.get("data"), dict) else event
    return all(data.get(condition.field) == condition.equals for condition in reaction.conditions)


def reaction_execution_context(
    reaction: EventReaction,
    *,
    source_event_id: str,
    causation_id: str | None = None,
    correlation_id: str | None = None,
    depth: int = 0,
    attempt: int = 1,
    action_index: int = 0,
) -> dict[str, Any]:
    """Build retry/idempotency metadata for a Cloud outbox reaction attempt."""
    if depth >= reaction.max_depth:
        raise ValueError("reaction_depth_exceeded")
    idem = hashlib.sha256(
        json.dumps(
            {
                "reaction_id": reaction.id,
                "reaction_version": reaction.version,
                "source_event_id": source_event_id,
                "action_index": action_index,
            },
            sort_keys=True,
            separators=(",", ":"),
        ).encode()
    ).hexdigest()
    return {
        "reaction_id": reaction.id,
        "reaction_version": reaction.version,
        "source_event_id": source_event_id,
        "causation_id": causation_id or source_event_id,
        "correlation_id": correlation_id or source_event_id,
        "depth": depth + 1,
        "attempt": attempt,
        "action_index": action_index,
        "idempotency_key": f"reaction:{reaction.id}:{reaction.version}:{source_event_id}:{action_index}:{idem}",
    }


EVENT_REACTIONS_CONTRACT = {
    "id": "event.reaction",
    "status": "admitted_contract_runtime_pending",
    "mutation_boundary": "after_authoritative_event",
    "execution": "Cloud_outbox",
    "arbitrary_code": False,
    "unrestricted_http": False,
    "max_depth": REACTION_MAX_DEPTH,
    "max_actions_per_reaction": REACTION_MAX_ACTIONS,
    "max_matching_reactions_per_event": REACTION_MAX_MATCHING_PER_EVENT,
    "idempotency": "reaction_id_plus_source_event_id",
    "capabilities": sorted(ADMITTED_REACTION_CAPABILITIES),
}
