"""Core types for contextweaver.

Defines all enums and dataclasses used across both the Context Engine and the
Routing Engine.  Every dataclass implements :meth:`to_dict` / :meth:`from_dict`
for easy serialisation to JSON-compatible dicts.

Output / result types (:class:`ResultEnvelope`, :class:`BuildStats`,
:class:`ContextPack`, :class:`ChoiceCard`) live in :mod:`contextweaver.envelope`
and are re-exported here for backward compatibility.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Literal

# ---------------------------------------------------------------------------
# Enumerations
# ---------------------------------------------------------------------------


class Sensitivity(str, Enum):
    """Data sensitivity level attached to a :class:`ContextItem`."""

    public = "public"
    internal = "internal"
    confidential = "confidential"
    restricted = "restricted"


class ItemKind(str, Enum):
    """The semantic kind of a :class:`ContextItem`."""

    user_turn = "user_turn"
    agent_msg = "agent_msg"
    tool_call = "tool_call"
    tool_result = "tool_result"
    doc_snippet = "doc_snippet"
    memory_fact = "memory_fact"
    plan_state = "plan_state"
    policy = "policy"


class Phase(str, Enum):
    """Execution phase that determines the active token budget."""

    route = "route"
    call = "call"
    interpret = "interpret"
    answer = "answer"


# ---------------------------------------------------------------------------
# Dataclasses
# ---------------------------------------------------------------------------


@dataclass
class SelectableItem:
    """A unified representation of a tool, agent, skill, flow, or internal function.

    This is the single vocabulary the Routing Engine operates on.  Use the
    :data:`ToolCard` alias when you want to emphasise the tool-card framing.

    ``kind="flow"`` denotes a multi-step capability executed by an external
    runtime (e.g. a ChainWeaver flow imported via
    :mod:`contextweaver.adapters.chainweaver`); contextweaver routes to it
    like any other candidate but never executes it.

    The optional ``depends_on`` / ``provides`` / ``requires`` fields (issue
    #27 Phase 2) carry tool-dependency metadata used by history-aware
    routing.  All three default to ``None`` so existing catalogs round-trip
    unchanged.
    """

    id: str
    kind: Literal["tool", "agent", "skill", "internal", "flow"]
    name: str
    description: str
    tags: list[str] = field(default_factory=list)
    namespace: str = ""
    args_schema: dict[str, Any] = field(default_factory=dict)
    output_schema: dict[str, Any] | None = None
    examples: list[str] = field(default_factory=list)
    constraints: dict[str, Any] = field(default_factory=dict)
    side_effects: bool = False
    cost_hint: float = 0.0
    metadata: dict[str, Any] = field(default_factory=dict)
    depends_on: list[str] | None = None
    provides: list[str] | None = None
    requires: list[str] | None = None

    def to_dict(self) -> dict[str, Any]:
        """Serialise to a JSON-compatible dict.

        ``depends_on`` / ``provides`` / ``requires`` are omitted from the
        payload when ``None`` so older consumers see the same shape as
        before issue #27 Phase 2.
        """
        payload: dict[str, Any] = {
            "id": self.id,
            "kind": self.kind,
            "name": self.name,
            "description": self.description,
            "tags": list(self.tags),
            "namespace": self.namespace,
            "args_schema": dict(self.args_schema),
            "output_schema": dict(self.output_schema) if self.output_schema is not None else None,
            "examples": list(self.examples),
            "constraints": dict(self.constraints),
            "side_effects": self.side_effects,
            "cost_hint": self.cost_hint,
            "metadata": dict(self.metadata),
        }
        if self.depends_on is not None:
            payload["depends_on"] = list(self.depends_on)
        if self.provides is not None:
            payload["provides"] = list(self.provides)
        if self.requires is not None:
            payload["requires"] = list(self.requires)
        return payload

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> SelectableItem:
        """Deserialise from a JSON-compatible dict.

        Missing ``depends_on`` / ``provides`` / ``requires`` keys round-trip
        to ``None`` (issue #27 Phase 2).
        """
        return cls(
            id=data["id"],
            kind=data["kind"],
            name=data["name"],
            description=data["description"],
            tags=list(data.get("tags", [])),
            namespace=data.get("namespace", ""),
            args_schema=dict(data.get("args_schema", {})),
            output_schema=dict(data["output_schema"])
            if data.get("output_schema") is not None
            else None,
            examples=list(data.get("examples", [])),
            constraints=dict(data.get("constraints", {})),
            side_effects=bool(data.get("side_effects", False)),
            cost_hint=float(data.get("cost_hint", 0.0)),
            metadata=dict(data.get("metadata", {})),
            depends_on=list(data["depends_on"]) if data.get("depends_on") is not None else None,
            provides=list(data["provides"]) if data.get("provides") is not None else None,
            requires=list(data["requires"]) if data.get("requires") is not None else None,
        )


#: Alias — use when emphasising the LLM-facing card framing.
ToolCard = SelectableItem


@dataclass
class ArtifactRef:
    """A lightweight reference to an out-of-band artifact stored in an ArtifactStore.

    Attributes:
        handle: Opaque key used to retrieve the artifact via
            :meth:`ArtifactStore.get` / :meth:`drilldown`.
        media_type: MIME type of the stored bytes.
        size_bytes: Size of the stored bytes in bytes.
        label: Optional human-readable label.
        content_hash: ``sha256`` digest of the stored bytes as a
            lowercase hex string.  Empty when not populated by the
            writing path (legacy ``ArtifactRef`` instances pre-#190).
            Populated by :func:`~contextweaver.context.firewall.apply_firewall`
            and used to short-circuit re-firewall passes on items that
            already carry a firewall-processed body (issue #190).
    """

    handle: str
    media_type: str
    size_bytes: int
    label: str = ""
    content_hash: str = ""

    def to_dict(self) -> dict[str, Any]:
        """Serialise to a JSON-compatible dict."""
        out: dict[str, Any] = {
            "handle": self.handle,
            "media_type": self.media_type,
            "size_bytes": self.size_bytes,
            "label": self.label,
        }
        if self.content_hash:
            out["content_hash"] = self.content_hash
        return out

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ArtifactRef:
        """Deserialise from a JSON-compatible dict."""
        return cls(
            handle=data["handle"],
            media_type=data["media_type"],
            size_bytes=int(data["size_bytes"]),
            label=data.get("label", ""),
            content_hash=data.get("content_hash", ""),
        )


@dataclass
class ContextItem:
    """A single entry in the event log / context pipeline.

    *parent_id* enables the dependency-closure pass that pulls in prerequisite
    items even when they fall outside the budget window.
    """

    id: str
    kind: ItemKind
    text: str
    token_estimate: int = 0
    sensitivity: Sensitivity = Sensitivity.public
    metadata: dict[str, Any] = field(default_factory=dict)
    parent_id: str | None = None
    artifact_ref: ArtifactRef | None = None

    def to_dict(self) -> dict[str, Any]:
        """Serialise to a JSON-compatible dict."""
        return {
            "id": self.id,
            "kind": self.kind.value,
            "text": self.text,
            "token_estimate": self.token_estimate,
            "sensitivity": self.sensitivity.value,
            "metadata": dict(self.metadata),
            "parent_id": self.parent_id,
            "artifact_ref": self.artifact_ref.to_dict() if self.artifact_ref else None,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ContextItem:
        """Deserialise from a JSON-compatible dict."""
        artifact_raw = data.get("artifact_ref")
        return cls(
            id=data["id"],
            kind=ItemKind(data["kind"]),
            text=data["text"],
            token_estimate=int(data.get("token_estimate", 0)),
            sensitivity=Sensitivity(data.get("sensitivity", Sensitivity.public.value)),
            metadata=dict(data.get("metadata", {})),
            parent_id=data.get("parent_id"),
            artifact_ref=ArtifactRef.from_dict(artifact_raw) if artifact_raw else None,
        )


@dataclass
class ViewSpec:
    """Specifies a named view (a filtered/projected representation) of an artifact."""

    view_id: str
    label: str
    selector: dict[str, Any] = field(default_factory=dict)
    artifact_ref: ArtifactRef | None = None

    def to_dict(self) -> dict[str, Any]:
        """Serialise to a JSON-compatible dict."""
        return {
            "view_id": self.view_id,
            "label": self.label,
            "selector": dict(self.selector),
            "artifact_ref": self.artifact_ref.to_dict() if self.artifact_ref else None,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ViewSpec:
        """Deserialise from a JSON-compatible dict."""
        artifact_raw = data.get("artifact_ref")
        return cls(
            view_id=data["view_id"],
            label=data["label"],
            selector=dict(data.get("selector", {})),
            artifact_ref=ArtifactRef.from_dict(artifact_raw) if artifact_raw else None,
        )


# ---------------------------------------------------------------------------
# Re-exports from envelope.py (backward compatibility)
# ---------------------------------------------------------------------------

from contextweaver.envelope import (  # noqa: E402
    BuildStats,
    ChoiceCard,
    ContextPack,
    HydrationResult,
    ResultEnvelope,
    RoutingDecision,
)

__all__ = [
    "ArtifactRef",
    "BuildStats",
    "ChoiceCard",
    "ContextItem",
    "ContextPack",
    "HydrationResult",
    "ItemKind",
    "Phase",
    "ResultEnvelope",
    "RoutingDecision",
    "SelectableItem",
    "Sensitivity",
    "ToolCard",
    "ViewSpec",
]
