"""
hcp.seed — Holographic seed computation engine.

Implements Phase 1 (İlim / Distinction) and Phase 2 (İrade / Specification)
of the Holographic Context Protocol.

Phase 1 — Single O(N) pass over the full context:
  1. Classify each chunk's structural type (harfî, KV₁)
  2. Extract globally significant keywords
  3. Identify instruction and constraint positions
  4. Detect entity mentions and their positions
  5. Infer structural links (cascade dependencies, AX23)

Phase 2 — Compress into a HolographicSeed:
  1. Compute type_signature (7-dim distribution over ContentType)
  2. Compute position_importance (weighted by type + keyword salience)
  3. Select global_keywords (top-k by frequency × spread)
  4. Package structural links, instruction/constraint/entity positions

Framework Axioms:
  AX2:  İlim (Distinction) — classification pass
  AX3:  İrade (Specification) — seed compression
  AX12: Vahidiyet (global unity) — seed captures global structure
  AX13: Ehadiyet (local reflection) — seed will be broadcast to each chunk
  AX17: Hakikat = Esmâ — reality is the set of Names, not surface form
  AX23: Cascade DAG — directed dependencies between content
  AX37: Holographic Structure — parts mirror the whole
  KV₁:  Harfî classification — classify by structural function, not surface
  KV₃:  Observer non-interference — classification detects, never creates
  T6/KV₄: All scores bounded < 1.0

KV₇ compliance: This module imports ONLY from hcp.types and standard library.
"""

from __future__ import annotations

import math
import re
from collections import Counter
from typing import Dict, List, Optional, Set, Tuple

from hcp.types import (
    ContentType,
    ContextChunk,
    HCPConfig,
    HolographicSeed,
    clamp_score,
)


# ---------------------------------------------------------------------------
# Keyword patterns for content type classification (KV₁ — harfî mode)
# ---------------------------------------------------------------------------

# These classify by STRUCTURAL FUNCTION (what role the content plays),
# not by surface form (what words appear).  Harfî: what-it-points-to.

_INSTRUCTION_PATTERNS: Tuple[str, ...] = (
    r"\b(must|shall|should|ensure|always|never|do not|don't|require)\b",
    r"\b(implement|create|build|write|generate|produce|output)\b",
    r"\b(use|follow|apply|adopt|prefer|avoid)\b",
    r"\b(step \d|first|then|next|finally|before|after)\b",
    r"\b(instruction|directive|command|rule|guideline)\b",
)

_CONSTRAINT_PATTERNS: Tuple[str, ...] = (
    r"\b(constraint|limit|restrict|bound|maximum|minimum|at most|at least)\b",
    r"\b(cannot|must not|forbidden|prohibited|not allowed|illegal)\b",
    r"\b(only if|unless|except|provided that|on condition)\b",
    r"\b(boundary|threshold|ceiling|floor|cap)\b",
)

_ENTITY_PATTERNS: Tuple[str, ...] = (
    r"\b(is defined as|refers to|means|denotes|represents)\b",
    r"\b(class|type|category|kind|sort|species)\b",
    r"\b(called|named|known as|termed|labeled)\b",
)

_RELATIONSHIP_PATTERNS: Tuple[str, ...] = (
    r"\b(depends on|requires|causes|leads to|results in)\b",
    r"\b(connects|links|relates|maps|corresponds)\b",
    r"\b(if.*then|because|therefore|thus|hence|consequently)\b",
    r"\b(parent|child|sibling|ancestor|descendant)\b",
)

_METADATA_PATTERNS: Tuple[str, ...] = (
    r"^#{1,6}\s",
    r"^[-*]\s",
    r"^\d+\.\s",
    r"^(note|warning|important|todo|fixme|hack):",
    r"^(section|chapter|part|appendix)\b",
)

_FACT_PATTERNS: Tuple[str, ...] = (
    r"\b(\d+\.?\d*%|\d+\.?\d*\s*(ms|seconds|bytes|MB|GB|tokens))\b",
    r"\b(measured|observed|recorded|found|showed|demonstrated)\b",
    r"\b(according to|per|based on|as reported|data shows)\b",
    r"\b(result|finding|evidence|metric|benchmark|score)\b",
)


# ---------------------------------------------------------------------------
# Stop words for keyword extraction
# ---------------------------------------------------------------------------

_STOP_WORDS: frozenset[str] = frozenset({
    "a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
    "have", "has", "had", "do", "does", "did", "will", "would", "could",
    "should", "may", "might", "shall", "can", "to", "of", "in", "for",
    "on", "with", "at", "by", "from", "as", "into", "through", "during",
    "before", "after", "above", "below", "between", "under", "again",
    "further", "then", "once", "and", "but", "or", "nor", "not", "so",
    "very", "just", "about", "up", "out", "if", "each", "which", "their",
    "there", "this", "that", "these", "those", "it", "its", "all", "any",
    "both", "few", "more", "most", "other", "some", "such", "no", "only",
    "same", "than", "too", "also", "how", "what", "when", "where", "who",
    "they", "them", "he", "she", "we", "you", "i", "me", "my", "your",
    "his", "her", "our", "us",
})


# ---------------------------------------------------------------------------
# Public API: classify_content
# ---------------------------------------------------------------------------

def classify_content(
    text: str,
    hint: Optional[str] = None,
    hint_confidence: float = 0.0,
) -> ContentType:
    """Classify a text chunk's structural type.

    Operates in harfî mode (KV₁): classifies by structural FUNCTION
    (what role this content plays in the whole), not by surface keywords.

    The classification follows priority ordering that mirrors the
    ontological stack (AGENTS.md §1.5): instructions and constraints
    (higher structural importance) take priority over facts and narrative
    (lower structural importance).

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

    Args:
        text: The text content to classify.
        hint: Optional ContentType value string (e.g. "instruction").
        hint_confidence: Confidence in the hint [0, 1]. Default 0.0.

    Returns:
        The ContentType with the strongest signal. Defaults to NARRATIVE.
    """
    # --- Tier 1: high-confidence hint bypass (>= 0.8) ---
    if hint and hint_confidence >= 0.8:
        for ct in ContentType:
            if ct.value == hint:
                return ct

    text_lower = text.lower().strip()
    if not text_lower:
        return ContentType.NARRATIVE

    # Score each type by pattern matches (KV₃: detect, don't create order)
    scores: Dict[ContentType, int] = {ct: 0 for ct in ContentType}

    for pattern in _INSTRUCTION_PATTERNS:
        scores[ContentType.INSTRUCTION] += len(re.findall(pattern, text_lower))

    for pattern in _CONSTRAINT_PATTERNS:
        scores[ContentType.CONSTRAINT] += len(re.findall(pattern, text_lower))

    for pattern in _ENTITY_PATTERNS:
        scores[ContentType.ENTITY] += len(re.findall(pattern, text_lower))

    for pattern in _RELATIONSHIP_PATTERNS:
        scores[ContentType.RELATIONSHIP] += len(re.findall(pattern, text_lower))

    for pattern in _METADATA_PATTERNS:
        for line in text_lower.split("\n"):
            if re.search(pattern, line.strip()):
                scores[ContentType.METADATA] += 1

    for pattern in _FACT_PATTERNS:
        scores[ContentType.FACT] += len(re.findall(pattern, text_lower))

    # --- Tier 2: medium-confidence hint boost (0.3 – 0.8) ---
    if hint and 0.3 <= hint_confidence < 0.8:
        for ct in ContentType:
            if ct.value == hint:
                scores[ct] += 3  # +3 boost (typical regex scores are 1–5)
                break

    # Priority ordering: INSTRUCTION > CONSTRAINT > others (structural > detail, T15)
    priority = [
        ContentType.INSTRUCTION,
        ContentType.CONSTRAINT,
        ContentType.RELATIONSHIP,
        ContentType.ENTITY,
        ContentType.FACT,
        ContentType.METADATA,
        ContentType.NARRATIVE,
    ]

    best_type = ContentType.NARRATIVE
    best_score = 0
    for ct in priority:
        if scores[ct] > best_score:
            best_score = scores[ct]
            best_type = ct
        elif scores[ct] == best_score and scores[ct] > 0:
            # Tie-breaking: higher structural priority wins (T15)
            if priority.index(ct) < priority.index(best_type):
                best_type = ct

    return best_type


# ---------------------------------------------------------------------------
# Public API: extract_keywords
# ---------------------------------------------------------------------------

def extract_keywords(
    text: str,
    top_k: int = 15,
) -> Tuple[str, ...]:
    """Extract globally significant keywords from text.

    Uses frequency × spread heuristic: words that appear frequently
    and across multiple sentences are more structurally significant.

    Per AX22 (Unreachable Supremum): keyword extraction is bounded —
    we extract top_k, not all.

    Args:
        text: The text to extract keywords from.
        top_k: Maximum keywords to return.

    Returns:
        Tuple of top-k keywords ordered by significance score.
    """
    # Tokenize: alphanumeric words of length >= 3
    words = re.findall(r"\b[a-zA-Z]{3,}\b", text.lower())
    words = [w for w in words if w not in _STOP_WORDS]

    if not words:
        return ()

    # Frequency
    freq: Counter[str] = Counter(words)

    # Spread: how many sentences contain the word
    sentences = re.split(r"[.!?]+", text.lower())
    spread: Counter[str] = Counter()
    for word in freq:
        for sentence in sentences:
            if word in sentence:
                spread[word] += 1

    # Score: frequency × log(1 + spread)
    scores: Dict[str, float] = {}
    for word in freq:
        scores[word] = freq[word] * math.log1p(spread.get(word, 0))

    # Top-k by score
    sorted_words = sorted(scores, key=lambda w: scores[w], reverse=True)
    return tuple(sorted_words[:top_k])


# ---------------------------------------------------------------------------
# Internal: _detect_entities
# ---------------------------------------------------------------------------

def _detect_entities(chunks: List[ContextChunk]) -> Dict[str, List[int]]:
    """Detect named entities and their positions across chunks.

    Simple heuristic: capitalized multi-word phrases that appear in
    more than one chunk are likely entities.
    """
    # Find capitalized phrases (2+ words)
    entity_pattern = re.compile(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b")
    entity_positions: Dict[str, List[int]] = {}

    for chunk in chunks:
        matches = entity_pattern.findall(chunk.content)
        for entity in matches:
            entity_lower = entity.lower()
            if entity_lower not in entity_positions:
                entity_positions[entity_lower] = []
            if chunk.position not in entity_positions[entity_lower]:
                entity_positions[entity_lower].append(chunk.position)

    # Also detect single capitalized words that appear frequently
    single_cap = re.compile(r"\b([A-Z][a-z]{2,})\b")
    for chunk in chunks:
        matches = single_cap.findall(chunk.content)
        for word in matches:
            word_lower = word.lower()
            if word_lower in _STOP_WORDS:
                continue
            if word_lower not in entity_positions:
                entity_positions[word_lower] = []
            if chunk.position not in entity_positions[word_lower]:
                entity_positions[word_lower].append(chunk.position)

    # Keep only entities appearing in 2+ chunks (cross-referential significance)
    return {k: v for k, v in entity_positions.items() if len(v) >= 2}


# ---------------------------------------------------------------------------
# Internal: _infer_structural_links
# ---------------------------------------------------------------------------

def _infer_structural_links(
    chunks: List[ContextChunk],
    entity_positions: Dict[str, List[int]],
) -> List[Tuple[int, int]]:
    """Infer directed structural links between chunks.

    Maps to AX23 (Cascade DAG): content elements motivate and depend
    on each other, forming a directed acyclic graph.

    Link types:
      1. Entity co-reference: if entity E appears in chunks i and j (i < j),
         then i → j (earlier mention establishes, later references).
      2. Instruction→target: if an instruction chunk references content
         in another chunk (via shared keywords), create a link.
    """
    links: List[Tuple[int, int]] = []
    seen: Set[Tuple[int, int]] = set()

    # Entity co-reference links
    for positions in entity_positions.values():
        sorted_pos = sorted(positions)
        for i in range(len(sorted_pos) - 1):
            edge = (sorted_pos[i], sorted_pos[i + 1])
            if edge not in seen:
                links.append(edge)
                seen.add(edge)

    # Instruction → later chunk links (instructions structurally "govern" later content)
    instruction_chunks = [c for c in chunks if c.content_type == ContentType.INSTRUCTION]
    for ic in instruction_chunks:
        ic_words = set(re.findall(r"\b[a-z]{3,}\b", ic.content.lower())) - _STOP_WORDS
        for other in chunks:
            if other.position <= ic.position:
                continue
            other_words = set(re.findall(r"\b[a-z]{3,}\b", other.content.lower())) - _STOP_WORDS
            overlap = ic_words & other_words
            if len(overlap) >= 3:
                edge = (ic.position, other.position)
                if edge not in seen:
                    links.append(edge)
                    seen.add(edge)

    return links


# ---------------------------------------------------------------------------
# Internal: _compute_position_importance
# ---------------------------------------------------------------------------

def _compute_position_importance(
    chunks: List[ContextChunk],
    entity_positions: Dict[str, List[int]],
    structural_links: List[Tuple[int, int]],
) -> List[float]:
    """Compute per-position importance weights.

    Importance is determined by structural role, not by position.
    This is the KEY innovation: importance is CONTENT-DRIVEN, not
    POSITION-DRIVEN.  This directly counters the U-shaped attention
    bias (Liu et al. TACL 2024).

    Factors:
      1. Content type weight: instructions/constraints > facts > narrative
      2. Entity centrality: positions mentioned by many entities are more important
      3. Link degree: positions with more structural links are more important
    """
    n = len(chunks)
    if n == 0:
        return []

    # Content type weights (T15: structural > detail)
    type_weights = {
        ContentType.INSTRUCTION: 1.0,
        ContentType.CONSTRAINT: 0.9,
        ContentType.RELATIONSHIP: 0.7,
        ContentType.ENTITY: 0.6,
        ContentType.FACT: 0.5,
        ContentType.METADATA: 0.3,
        ContentType.NARRATIVE: 0.2,
    }

    importance = [0.0] * n

    # Factor 1: content type
    for chunk in chunks:
        importance[chunk.position] += type_weights.get(chunk.content_type, 0.2)

    # Factor 2: entity centrality (how many distinct entities mention this position)
    entity_count: Counter[int] = Counter()
    for positions in entity_positions.values():
        for pos in positions:
            entity_count[pos] += 1

    max_entity = max(entity_count.values()) if entity_count else 1
    for pos, count in entity_count.items():
        if pos < n:
            importance[pos] += 0.5 * (count / max_entity)

    # Factor 3: link degree (in-degree + out-degree)
    degree: Counter[int] = Counter()
    for src, tgt in structural_links:
        degree[src] += 1
        degree[tgt] += 1

    max_degree = max(degree.values()) if degree else 1
    for pos, deg in degree.items():
        if pos < n:
            importance[pos] += 0.3 * (deg / max_degree)

    # Normalize to [0, 0.9999] — KV₄ compliance
    max_imp = max(importance) if importance else 1.0
    if max_imp > 0:
        importance = [clamp_score(v / max_imp) for v in importance]

    return importance


# ---------------------------------------------------------------------------
# Public API: compute_seed
# ---------------------------------------------------------------------------

def compute_seed(
    chunks: List[ContextChunk],
    config: Optional[HCPConfig] = None,
) -> HolographicSeed:
    """Compute the holographic seed for a list of context chunks.

    This is the CORE ALGORITHM of HCP: a single O(N) pass that
    extracts the structural skeleton of the entire context.

    Three-Phase Actualization (AX2–4):
      Phase 1 — İlim (Distinction):
        Classify each chunk and extract structural metadata.
      Phase 2 — İrade (Specification):
        Select what goes into the seed.
      Phase 3 — Kudret (Actualization):
        Package into a HolographicSeed ready for broadcast.

    Args:
        chunks: List of ContextChunk objects with content and positions.
        config: HCPConfig for top_k and other parameters.

    Returns:
        HolographicSeed encoding the entire context's structure.

    Raises:
        ValueError: If chunks is empty.
    """
    if not chunks:
        raise ValueError("Cannot compute seed from empty context")

    if config is None:
        config = HCPConfig()

    n = len(chunks)

    # -----------------------------------------------------------------------
    # Phase 1 — İlim (Distinction): Classify and analyze
    # -----------------------------------------------------------------------

    # Classify each chunk's content type (if not already classified)
    for chunk in chunks:
        if chunk.content_type == ContentType.NARRATIVE:
            # Extract hint from source_metadata if available (E1)
            hint = None
            hint_conf = 0.0
            if chunk.source_metadata:
                hint = chunk.source_metadata.content_type_hint or None
                hint_conf = chunk.source_metadata.hint_confidence
            chunk.content_type = classify_content(
                chunk.content, hint=hint, hint_confidence=hint_conf,
            )

    # Extract per-chunk keywords
    for chunk in chunks:
        if not chunk.keywords:
            chunk.keywords = extract_keywords(chunk.content, top_k=10)

    # Detect entities across chunks
    entity_positions = _detect_entities(chunks)

    # Infer structural links (AX23: cascade DAG)
    structural_links = _infer_structural_links(chunks, entity_positions)

    # -----------------------------------------------------------------------
    # Phase 2 — İrade (Specification): Select and compress
    # -----------------------------------------------------------------------

    # Type signature: distribution over 7 ContentTypes
    type_counts = Counter(chunk.content_type for chunk in chunks)
    type_order = list(ContentType)
    type_signature = tuple(
        type_counts.get(ct, 0) / n for ct in type_order
    )

    # Instruction and constraint positions
    instruction_positions = tuple(
        c.position for c in chunks if c.content_type == ContentType.INSTRUCTION
    )
    constraint_positions = tuple(
        c.position for c in chunks if c.content_type == ContentType.CONSTRAINT
    )

    # Global keywords: merge all chunk keywords, re-rank by global frequency
    all_text = " ".join(c.content for c in chunks)
    global_keywords = extract_keywords(all_text, top_k=config.seed_top_k)

    # -----------------------------------------------------------------------
    # Phase 3 — Kudret (Actualization): Package the seed
    # -----------------------------------------------------------------------

    # Position importance (content-driven, not position-driven — the KEY insight)
    position_importance = _compute_position_importance(
        chunks, entity_positions, structural_links
    )

    # Freeze entity positions for immutability
    frozen_entities = {
        k: tuple(v) for k, v in entity_positions.items()
    }

    return HolographicSeed(
        n_chunks=n,
        type_signature=type_signature,
        instruction_positions=instruction_positions,
        constraint_positions=constraint_positions,
        entity_positions=frozen_entities,
        structural_links=tuple(structural_links),
        global_keywords=global_keywords,
        position_importance=tuple(position_importance),
    )
