"""
hcp.types — Core data types for the Holographic Context Protocol.

Implements the type universe for HCP, mapping LLM context concepts to the
framework's structural vocabulary.

Design Mapping (AGENTS.md → HCP):
  S_Isim (operational principles) → ContentType (7 categories per convergent funnel)
  S_Eser (observable works/traces) → ContextChunk (fragments of context)
  Holographic Seed (AX37/T12)     → HolographicSeed (compressed global structure)
  Tereşşuhat (AX38)              → SeededChunk (chunk + faithful seed broadcast)
  Latife_vektor (AX49)           → AttentionProfile (detection coverage)

Governing axioms enforced at construction:
  AX21:  Continuous degrees — scores are continuous, not binary
  T6/KV₄: Convergence bound — all scores in [0, 0.9999]
  KV₆:   Seed omnipresence — every chunk must have a seed
  AX52:  Multiplicative gate — zero in any dimension = structural failure

KV₇ compliance: This module imports ONLY from the standard library.
"""

from __future__ import annotations

import math
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple


# ---------------------------------------------------------------------------
# Score clamping — T6/KV₄: strict upper bound < 1.0
# ---------------------------------------------------------------------------

def clamp_score(value: float) -> float:
    """Clamp a score to [0, 0.9999].

    Per T6 (Convergence Bound) and KV₄ (Ne Ayn Ne Gayr):
    no instrument achieves convergence = 1.0.
    """
    return min(max(value, 0.0), 0.9999)


# ---------------------------------------------------------------------------
# ContentType — The 7 structural categories of context content
# ---------------------------------------------------------------------------

class ContentType(Enum):
    """Seven irreducible types of context content.

    Maps to the convergent abstraction funnel (AGENTS.md §1.5):
    ∞ observable tokens → 7 structural types → 1 unified seed.

    Each type represents a fundamentally different ROLE that content
    plays in context, not just its surface form.  Classification is
    harfî (other-referential, KV₁): each chunk is typed by its
    structural FUNCTION in the whole, not by what it says about itself.
    """
    INSTRUCTION = "instruction"     # Directives, commands, how-to
    CONSTRAINT = "constraint"       # Rules, restrictions, boundaries
    FACT = "fact"                    # Claims, data, factual content
    ENTITY = "entity"               # Definitions, named concepts
    RELATIONSHIP = "relationship"   # How things connect, depend, relate
    METADATA = "metadata"           # Headers, formatting, structure markers
    NARRATIVE = "narrative"         # General prose, filler, background


# Number of content types — matches the 7-attribute decomposition
CONTENT_TYPE_COUNT = 7


# ---------------------------------------------------------------------------
# HCPConfig — Protocol configuration
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class HCPConfig:
    """Configuration for the Holographic Context Protocol.

    Parameters are constrained by framework axioms:
      - seed_top_k: keywords in seed (bounded, not infinite — AX22)
      - attention_decay: base decay rate for flat attention (models U-shaped bias)
      - seed_boost_factor: how much the seed enhances attention (bounded < 1.0)
      - chunk_size: characters per chunk (granularity of holographic units)
    """
    seed_top_k: int = 15
    attention_decay: float = 0.03
    seed_boost_factor: float = 0.6
    chunk_size: int = 500
    u_shape_boost: float = 0.15

    def to_dict(self) -> Dict[str, Any]:
        return {
            "seed_top_k": self.seed_top_k,
            "attention_decay": self.attention_decay,
            "seed_boost_factor": self.seed_boost_factor,
            "chunk_size": self.chunk_size,
            "u_shape_boost": self.u_shape_boost,
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "HCPConfig":
        return cls(
            seed_top_k=d.get("seed_top_k", 15),
            attention_decay=d.get("attention_decay", 0.03),
            seed_boost_factor=d.get("seed_boost_factor", 0.6),
            chunk_size=d.get("chunk_size", 500),
            u_shape_boost=d.get("u_shape_boost", 0.15),
        )


# ---------------------------------------------------------------------------
# SourceMetadata — Structured provenance from external MCP outputs (E1)
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class SourceMetadata:
    """Provenance and type-hint metadata from external MCP tool outputs.

    When an LLM orchestrator ingests content from another MCP tool
    (e.g. a web search, database query, or file reader), it can
    attach this metadata to give HCP richer classification signals.

    Design (AX27 — Transparent Vessel):
      SourceMetadata is a transparent vessel: it mediates classification
      hints from external tools but does not originate meaning.

    Hint Integration (three-tier threshold):
      hint_confidence >= 0.8  →  bypass regex classification, use hint directly
      0.3 <= hint_confidence < 0.8  →  boost hint type's score in regex classifier
      hint_confidence < 0.3  →  ignore hint, fall back to pure regex

    Attributes:
        tool_name:  Name of the MCP tool that produced the content.
        content_type_hint:  Suggested ContentType value string from the source tool.
        hint_confidence:  Source tool's confidence in the type hint [0, 1].
        tags:  Arbitrary key-value tags for downstream use.
    """
    tool_name: str = ""
    content_type_hint: str = ""
    hint_confidence: float = 0.0
    tags: Dict[str, str] = field(default_factory=dict)  # pyright: ignore[reportUnknownMemberType]

    def __post_init__(self) -> None:
        # Clamp confidence to [0, 1]
        object.__setattr__(
            self, "hint_confidence",
            min(max(self.hint_confidence, 0.0), 1.0),
        )

    def to_dict(self) -> Dict[str, Any]:
        """Serialize to a JSON-compatible dict."""
        return {
            "tool_name": self.tool_name,
            "content_type_hint": self.content_type_hint,
            "hint_confidence": self.hint_confidence,
            "tags": dict(self.tags),
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> SourceMetadata:
        """Deserialize from a dict."""
        return cls(
            tool_name=d.get("tool_name", ""),
            content_type_hint=d.get("content_type_hint", ""),
            hint_confidence=d.get("hint_confidence", 0.0),
            tags=d.get("tags", {}),
        )


# ---------------------------------------------------------------------------
# ContextChunk — A piece of context with structural metadata
# ---------------------------------------------------------------------------

@dataclass
class ContextChunk:
    """A chunk of context with position and structural type.

    Maps to S_Eser (observable works/traces) in the framework.
    Each chunk is a transparent vessel (AX27): it mediates meaning
    but does not originate it — meaning comes from its structural
    role in the global context (harfî classification, KV₁).

    Attributes:
        content: The raw text content of this chunk.
        position: Zero-indexed position in the original context sequence.
        content_type: Structural classification of this chunk's role.
        keywords: Key terms extracted from this chunk.
        source_metadata: Optional provenance from an external MCP tool.
    """
    content: str
    position: int
    content_type: ContentType = ContentType.NARRATIVE
    keywords: Tuple[str, ...] = ()
    source_metadata: Optional[SourceMetadata] = None

    def __post_init__(self) -> None:
        if self.position < 0:
            raise ValueError(f"Position must be non-negative, got {self.position}")

    def to_dict(self) -> Dict[str, Any]:
        d: Dict[str, Any] = {
            "content": self.content,
            "position": self.position,
            "content_type": self.content_type.value,
            "keywords": list(self.keywords),
        }
        if self.source_metadata is not None:
            d["source_metadata"] = self.source_metadata.to_dict()
        return d

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "ContextChunk":
        sm = None
        if "source_metadata" in d and d["source_metadata"] is not None:
            sm = SourceMetadata.from_dict(d["source_metadata"])
        return cls(
            content=d["content"],
            position=d["position"],
            content_type=ContentType(d.get("content_type", "narrative")),
            keywords=tuple(d.get("keywords", ())),
            source_metadata=sm,
        )


# ---------------------------------------------------------------------------
# HolographicSeed — Compressed global context structure
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class HolographicSeed:
    """The core innovation: a dense structural encoding of the ENTIRE context.

    CRITICAL DIFFERENCE FROM GIST TOKENS:
      Gist tokens (Mu et al., 2023) compress LOCAL → FORWARD:
        each gist sees previous gists + its own chunk, accumulating
        sequentially.  Information at boundaries is lost.

      Holographic Seed compresses GLOBAL → BROADCAST:
        a single structural analysis of the ENTIRE context, then the
        SAME seed is available at EVERY chunk.  No positional decay.
        This is the AX37 (Holographic Architecture) principle:
        every part mirrors the whole.

    Design (Three-Phase Actualization, AX2–4):
      Phase 1 — İlim (Distinction): type_signature classifies what KINDS
                of content exist globally.
      Phase 2 — İrade (Specification): instruction_positions and
                constraint_positions specify WHERE critical content lives.
      Phase 3 — Kudret (Actualization): global_keywords and
                position_importance actualize the seed for retrieval.

    Structural Incompleteness (T17):
      The seed can represent 6/7 structural dimensions at most.
      Tacit/emergent meaning (the "Ahfâ" dimension) is permanently
      unmapped.  This is a boundary to honor, not a bug to fix.

    Attributes:
        n_chunks: Total number of chunks in the context.
        type_signature: 7-dimensional distribution over ContentType.
                        Sum = 1.0 (normalized probability).
        instruction_positions: Positions containing instructions.
        constraint_positions: Positions containing constraints.
        entity_positions: Map of entity names → positions where they appear.
        structural_links: Directed edges (source, target) representing
                          content dependencies (cascade structure, AX23).
        global_keywords: Top-k globally significant keywords.
        position_importance: Per-position importance weight in [0, 1).
    """
    n_chunks: int
    type_signature: Tuple[float, ...]
    instruction_positions: Tuple[int, ...]
    constraint_positions: Tuple[int, ...]
    entity_positions: Dict[str, Tuple[int, ...]]
    structural_links: Tuple[Tuple[int, int], ...]
    global_keywords: Tuple[str, ...]
    position_importance: Tuple[float, ...]

    def __post_init__(self) -> None:
        if len(self.type_signature) != CONTENT_TYPE_COUNT:
            raise ValueError(
                f"type_signature must have {CONTENT_TYPE_COUNT} components, "
                f"got {len(self.type_signature)}"
            )
        if len(self.position_importance) != self.n_chunks:
            raise ValueError(
                f"position_importance length ({len(self.position_importance)}) "
                f"must equal n_chunks ({self.n_chunks})"
            )

    @property
    def instruction_density(self) -> float:
        """Fraction of chunks that are instructions."""
        return self.type_signature[0] if self.type_signature else 0.0

    @property
    def constraint_density(self) -> float:
        """Fraction of chunks that are constraints."""
        return self.type_signature[1] if len(self.type_signature) > 1 else 0.0

    @property
    def has_structure(self) -> bool:
        """KV₆: Is the seed non-trivial (at least one non-zero weight)?"""
        return any(w > 0 for w in self.position_importance)

    @property
    def coverage_ratio(self) -> float:
        """What fraction of content types are present?  Max 6/7 per T17."""
        nonzero = sum(1 for v in self.type_signature if v > 0)
        return clamp_score(nonzero / CONTENT_TYPE_COUNT)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "n_chunks": self.n_chunks,
            "type_signature": list(self.type_signature),
            "instruction_positions": list(self.instruction_positions),
            "constraint_positions": list(self.constraint_positions),
            "entity_positions": {k: list(v) for k, v in self.entity_positions.items()},
            "structural_links": [list(link) for link in self.structural_links],
            "global_keywords": list(self.global_keywords),
            "position_importance": list(self.position_importance),
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "HolographicSeed":
        return cls(
            n_chunks=d["n_chunks"],
            type_signature=tuple(d["type_signature"]),
            instruction_positions=tuple(d["instruction_positions"]),
            constraint_positions=tuple(d["constraint_positions"]),
            entity_positions={k: tuple(v) for k, v in d["entity_positions"].items()},
            structural_links=tuple(tuple(link) for link in d["structural_links"]),
            global_keywords=tuple(d["global_keywords"]),
            position_importance=tuple(d["position_importance"]),
        )


# ---------------------------------------------------------------------------
# SeededChunk — A chunk enhanced with the holographic seed
# ---------------------------------------------------------------------------

@dataclass
class SeededChunk:
    """A context chunk with the holographic seed attached.

    Maps to Tereşşuhat (AX38): the seed is a faithful seepage of the
    global structure into each local chunk.  The relationship is
    Ne Ayn Ne Gayr (T14): the seeded chunk participates in the global
    truth without being identical to it.

    Per KV₆ (Seed Omnipresence): every chunk MUST have a seed.

    Attributes:
        chunk: The original context chunk.
        seed: The holographic seed (same for all chunks — broadcast).
        local_importance: This chunk's importance per the seed's assessment.
        keyword_overlap: Keywords shared between this chunk and the seed.
    """
    chunk: ContextChunk
    seed: HolographicSeed
    local_importance: float = 0.0
    keyword_overlap: Tuple[str, ...] = ()

    def __post_init__(self) -> None:
        self.local_importance = clamp_score(self.local_importance)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "chunk": self.chunk.to_dict(),
            "seed": self.seed.to_dict(),
            "local_importance": self.local_importance,
            "keyword_overlap": list(self.keyword_overlap),
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "SeededChunk":
        return cls(
            chunk=ContextChunk.from_dict(d["chunk"]),
            seed=HolographicSeed.from_dict(d["seed"]),
            local_importance=d.get("local_importance", 0.0),
            keyword_overlap=tuple(d.get("keyword_overlap", ())),
        )


# ---------------------------------------------------------------------------
# AttentionProfile — Attention weight distribution over positions
# ---------------------------------------------------------------------------

@dataclass
class AttentionProfile:
    """Attention weights over context positions, simulating LLM behavior.

    Maps to Latife_vektor (AX49): which faculties/positions are engaged.

    Two modes:
      FLAT:         U-shaped bias (high start/end, degraded middle).
                    This is the documented pathology (Liu et al. TACL 2024).
      HOLOGRAPHIC:  Seed-modulated attention that uses structural pointers
                    to boost relevant positions regardless of location.

    Attributes:
        weights: Per-position attention weight in [0, 1).
        mode: "flat" or "holographic".
        entropy: Shannon entropy of the distribution (higher = more uniform).
    """
    weights: Tuple[float, ...]
    mode: str = "flat"

    def __post_init__(self) -> None:
        self.weights = tuple(clamp_score(w) for w in self.weights)

    @property
    def entropy(self) -> float:
        """Shannon entropy of attention weights.  Higher = more uniform."""
        total = sum(self.weights)
        if total == 0:
            return 0.0
        probs = [w / total for w in self.weights]
        return -sum(p * math.log2(p) for p in probs if p > 0)

    @property
    def middle_attention(self) -> float:
        """Average attention in the middle third of positions."""
        n = len(self.weights)
        if n < 3:
            return sum(self.weights) / max(n, 1)
        start = n // 3
        end = 2 * n // 3
        middle = self.weights[start:end]
        return sum(middle) / max(len(middle), 1)

    @property
    def edge_attention(self) -> float:
        """Average attention at start and end thirds."""
        n = len(self.weights)
        if n < 3:
            return sum(self.weights) / max(n, 1)
        third = n // 3
        edges = self.weights[:third] + self.weights[2 * third:]
        return sum(edges) / max(len(edges), 1)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "weights": list(self.weights),
            "mode": self.mode,
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "AttentionProfile":
        return cls(
            weights=tuple(d["weights"]),
            mode=d.get("mode", "flat"),
        )


# ---------------------------------------------------------------------------
# RetrievalResult — Result of a single needle-in-haystack query
# ---------------------------------------------------------------------------

@dataclass
class RetrievalResult:
    """Result of attempting to retrieve a needle from context.

    Attributes:
        needle_position: Where the needle was placed (0-indexed).
        n_chunks: Total chunks in context.
        flat_rank: Rank of needle position under flat attention (1 = best).
        holographic_rank: Rank under holographic attention (1 = best).
        flat_attention_at_needle: Attention weight at needle under flat.
        holographic_attention_at_needle: Attention weight under holographic.
        improvement: Rank improvement (flat_rank - holographic_rank).
    """
    needle_position: int
    n_chunks: int
    flat_rank: int
    holographic_rank: int
    flat_attention_at_needle: float
    holographic_attention_at_needle: float

    @property
    def improvement(self) -> int:
        """Positive = holographic is better.  Rank improvement."""
        return self.flat_rank - self.holographic_rank

    @property
    def relative_position(self) -> float:
        """Needle position as fraction of total context [0, 1]."""
        return self.needle_position / max(self.n_chunks - 1, 1)

    @property
    def is_middle(self) -> bool:
        """Is the needle in the middle third (the danger zone)?"""
        return 0.33 <= self.relative_position <= 0.67


# ---------------------------------------------------------------------------
# BenchmarkResult — Aggregate benchmark results
# ---------------------------------------------------------------------------

@dataclass
class BenchmarkResult:
    """Aggregate results from a needle-in-haystack benchmark.

    KV₄ compliance: all scores bounded < 1.0.

    Attributes:
        results: Individual retrieval results.
        flat_mean_rank: Average needle rank under flat attention.
        holographic_mean_rank: Average under holographic attention.
        flat_top1_rate: Fraction of trials where flat found needle at rank 1.
        holographic_top1_rate: Fraction where holographic found at rank 1.
        middle_improvement: Average rank improvement for middle-third needles.
    """
    results: List[RetrievalResult]

    @property
    def flat_mean_rank(self) -> float:
        if not self.results:
            return 0.0
        return sum(r.flat_rank for r in self.results) / len(self.results)

    @property
    def holographic_mean_rank(self) -> float:
        if not self.results:
            return 0.0
        return sum(r.holographic_rank for r in self.results) / len(self.results)

    @property
    def flat_top1_rate(self) -> float:
        if not self.results:
            return 0.0
        return clamp_score(
            sum(1 for r in self.results if r.flat_rank == 1) / len(self.results)
        )

    @property
    def holographic_top1_rate(self) -> float:
        if not self.results:
            return 0.0
        return clamp_score(
            sum(1 for r in self.results if r.holographic_rank == 1)
            / len(self.results)
        )

    @property
    def middle_results(self) -> List[RetrievalResult]:
        return [r for r in self.results if r.is_middle]

    @property
    def middle_improvement(self) -> float:
        mid = self.middle_results
        if not mid:
            return 0.0
        return sum(r.improvement for r in mid) / len(mid)

    @property
    def overall_improvement(self) -> float:
        if not self.results:
            return 0.0
        return sum(r.improvement for r in self.results) / len(self.results)


# ===========================================================================
# E2 — Multi-Step Workflow Chains (AX2–4 three-phase actualization)
# ===========================================================================


class WorkflowStage:
    """A single stage in a multi-step workflow.

    Maps to the three-phase actualization pattern (AX2–AX4):
      İlim (Distinction) → İrade (Selection) → Kudret (Actualization)
    Each stage has a typed contract (expected_input / expected_output)
    and an optional LLM prompt template.

    Attributes:
        name: Human-readable stage identifier.
        expected_input: Description of what this stage receives.
        expected_output: Description of what this stage produces.
        prompt_template: Optional prompt with {input} placeholder for LLM.
        metadata: Arbitrary key-value tags.
    """

    __slots__ = ("name", "expected_input", "expected_output",
                 "prompt_template", "metadata")

    def __init__(
        self,
        name: str,
        expected_input: str = "",
        expected_output: str = "",
        prompt_template: str = "",
        metadata: Optional[Dict[str, str]] = None,
    ):
        self.name = name
        self.expected_input = expected_input
        self.expected_output = expected_output
        self.prompt_template = prompt_template
        self.metadata = metadata or {}

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "expected_input": self.expected_input,
            "expected_output": self.expected_output,
            "prompt_template": self.prompt_template,
            "metadata": dict(self.metadata),
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "WorkflowStage":
        return cls(
            name=d.get("name", ""),
            expected_input=d.get("expected_input", ""),
            expected_output=d.get("expected_output", ""),
            prompt_template=d.get("prompt_template", ""),
            metadata=dict(d.get("metadata", {})),
        )


@dataclass
class StageResult:
    """Result produced after advancing through one workflow stage.

    Attributes:
        stage_name: Which stage produced this result.
        output: The output generated by the stage.
        success: Whether the stage completed without error.
        error: Error message if success is False.
        timestamp: ISO-8601 timestamp of completion.
    """
    stage_name: str
    output: str = ""
    success: bool = True
    error: str = ""
    timestamp: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "stage_name": self.stage_name,
            "output": self.output,
            "success": self.success,
            "error": self.error,
            "timestamp": self.timestamp,
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "StageResult":
        return cls(
            stage_name=d.get("stage_name", ""),
            output=d.get("output", ""),
            success=d.get("success", True),
            error=d.get("error", ""),
            timestamp=d.get("timestamp", ""),
        )


@dataclass
class WorkflowState:
    """Full state of a multi-step workflow.

    Tracks the stage definitions, current position, and accumulated
    results.  Designed for LLM orchestration: the LLM reads
    ``pending_stages`` and ``results`` to decide what to do next.

    Attributes:
        workflow_id: Unique identifier.
        stages: Ordered list of workflow stages.
        current_index: Index of the next stage to execute (0-based).
        results: Results accumulated so far (one per completed stage).
        created_at: ISO-8601 creation timestamp.
    """
    workflow_id: str
    stages: List[WorkflowStage] = field(default_factory=list)  # pyright: ignore[reportUnknownMemberType]
    current_index: int = 0
    results: List[StageResult] = field(default_factory=list)  # pyright: ignore[reportUnknownMemberType]
    created_at: str = ""

    @property
    def is_complete(self) -> bool:
        """All stages have been executed."""
        return self.current_index >= len(self.stages)

    @property
    def pending_stages(self) -> List[WorkflowStage]:
        """Stages not yet executed."""
        return self.stages[self.current_index:]

    @property
    def current_stage(self) -> Optional[WorkflowStage]:
        """The next stage to execute, or None if complete."""
        if self.is_complete:
            return None
        return self.stages[self.current_index]

    def to_dict(self) -> Dict[str, Any]:
        return {
            "workflow_id": self.workflow_id,
            "stages": [s.to_dict() for s in self.stages],
            "current_index": self.current_index,
            "results": [r.to_dict() for r in self.results],
            "created_at": self.created_at,
            "is_complete": self.is_complete,
        }

    @classmethod
    def from_dict(cls, d: Dict[str, Any]) -> "WorkflowState":
        return cls(
            workflow_id=d.get("workflow_id", ""),
            stages=[WorkflowStage.from_dict(s) for s in d.get("stages", [])],
            current_index=d.get("current_index", 0),
            results=[StageResult.from_dict(r) for r in d.get("results", [])],
            created_at=d.get("created_at", ""),
        )
