"""
Noetic Eidetic Memory - Conceptual reasoning extraction from CASCADE phases.

Episodic memory captures WHAT happened (narratives, timelines).
Eidetic memory captures FACTS with confidence.

This module extracts NOETIC CONCEPTS - the WHY/WHICH/FOR WHOM reasoning
that explains the epistemic journey, not just records it.

Noetic concepts are stored as eidetic facts because they represent
stable conceptual knowledge derived from reasoning, not temporal narratives.

DESIGN PHILOSOPHY:
- AI-provided concepts (semantic selection) are PRIORITIZED
- Regex extraction is FALLBACK only when AI doesn't provide concepts
- Complex language has too many nuances for keyword matching alone
- Pattern/anti-pattern recognition > keyword heuristics
"""

import logging
import re
import uuid
from typing import Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)


# Fact types for noetic concepts
NOETIC_FACT_TYPES = {
    # Core types (original)
    "task_understanding": "PREFLIGHT: Why this task matters, constraints, stakeholders",
    "decision_rationale": "CHECK: Why proceed/investigate, approach selection reasoning",
    "learning_insight": "POSTFLIGHT: Patterns discovered, generalizable knowledge",
    "epistemic_principle": "Cross-session: Recurring principles about learning/knowing",

    # Chain-reconstruction types (new)
    "rejected_alternative": "Path considered but not taken - preserves decision context",
    "conditional_knowledge": "Knowledge with validity conditions - when/where it applies",
    "concept_link": "Relationship between concepts - enables graph traversal",
    "reasoning_chain": "Hypothesis → Evidence → Conclusion - preserves inference path",
}


def extract_noetic_concepts(
    reasoning: str,
    phase: str,
    task_context: Optional[str] = None,
    vectors: Optional[Dict[str, float]] = None,
) -> List[Dict]:
    """
    Extract noetic concepts (why/which/for whom) from CASCADE phase reasoning.

    Args:
        reasoning: The reasoning text from PREFLIGHT/CHECK/POSTFLIGHT
        phase: CASCADE phase (PREFLIGHT, CHECK, POSTFLIGHT)
        task_context: Optional task description for richer extraction
        vectors: Optional epistemic vectors for confidence weighting

    Returns:
        List of noetic concepts ready for eidetic embedding
    """
    if not reasoning:
        return []

    concepts = []

    # Phase-specific extraction
    if phase == "PREFLIGHT":
        concepts.extend(_extract_preflight_concepts(reasoning, task_context, vectors))
    elif phase == "CHECK":
        concepts.extend(_extract_check_concepts(reasoning, vectors))
    elif phase == "POSTFLIGHT":
        concepts.extend(_extract_postflight_concepts(reasoning, task_context, vectors))

    return concepts


def _extract_preflight_concepts(
    reasoning: str,
    task_context: Optional[str],
    vectors: Optional[Dict[str, float]],
) -> List[Dict]:
    """Extract task understanding concepts from PREFLIGHT reasoning."""
    concepts = []

    # Base confidence from vectors (higher know = more confident understanding)
    base_confidence = 0.6
    if vectors and "know" in vectors:
        base_confidence = min(0.9, 0.5 + vectors["know"] * 0.4)

    # Look for "because", "since", "due to" patterns (WHY)
    why_patterns = [
        r"because\s+(.{20,150}?)(?:\.|$|,\s*(?:and|but|so))",
        r"since\s+(.{20,150}?)(?:\.|$|,\s*(?:and|but|so))",
        r"due to\s+(.{20,100}?)(?:\.|$)",
        r"the reason\s+(?:is|being)?\s*(.{20,150}?)(?:\.|$)",
    ]

    for pattern in why_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:2]:  # Limit to 2 per pattern
            content = f"Task understanding: {match.strip()}"
            concepts.append({
                "fact_type": "task_understanding",
                "content": content,
                "confidence": base_confidence,
                "tags": ["noetic", "preflight", "why"],
            })

    # Look for constraint/requirement mentions (WHICH)
    constraint_patterns = [
        r"(?:must|need to|required to|should)\s+(.{15,100}?)(?:\.|$)",
        r"constraint[s]?:?\s*(.{15,100}?)(?:\.|$)",
    ]

    for pattern in constraint_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:2]:
            content = f"Constraint: {match.strip()}"
            concepts.append({
                "fact_type": "task_understanding",
                "content": content,
                "confidence": base_confidence * 0.9,
                "tags": ["noetic", "preflight", "constraint"],
            })

    # If task_context provided, extract domain/purpose
    if task_context:
        # Simple heuristic: first clause often states purpose
        purpose_match = re.match(r"^(.{20,100}?)(?:\.|,|$)", task_context)
        if purpose_match:
            concepts.append({
                "fact_type": "task_understanding",
                "content": f"Task purpose: {purpose_match.group(1).strip()}",
                "confidence": base_confidence,
                "tags": ["noetic", "preflight", "purpose"],
            })

    return concepts


def _extract_check_concepts(
    reasoning: str,
    vectors: Optional[Dict[str, float]],
) -> List[Dict]:
    """Extract decision rationale concepts from CHECK reasoning."""
    concepts = []

    # Confidence based on uncertainty (lower uncertainty = more confident decision)
    base_confidence = 0.65
    if vectors:
        uncertainty = vectors.get("uncertainty", 0.5)
        base_confidence = min(0.9, 0.5 + (1 - uncertainty) * 0.4)

    # Decision patterns (WHY proceed/investigate)
    decision_patterns = [
        r"(?:decided|choosing|opted)\s+to\s+(\w+)\s+because\s+(.{20,150}?)(?:\.|$)",
        r"proceeding\s+(?:because|since)\s+(.{20,150}?)(?:\.|$)",
        r"investigating\s+(?:because|since|due to)\s+(.{20,150}?)(?:\.|$)",
        r"ready\s+to\s+(?:proceed|continue)\s+(.{15,100}?)(?:\.|$)",
    ]

    for pattern in decision_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:2]:
            if isinstance(match, tuple):
                content = f"Decision: {match[0]} - {match[1].strip()}"
            else:
                content = f"Decision rationale: {match.strip()}"
            concepts.append({
                "fact_type": "decision_rationale",
                "content": content,
                "confidence": base_confidence,
                "tags": ["noetic", "check", "decision"],
            })

    # Approach selection patterns (WHICH)
    approach_patterns = [
        r"(?:chose|selected|using)\s+(.{10,50}?)\s+(?:approach|method|strategy)",
        r"approach:?\s+(.{15,100}?)(?:\.|$)",
    ]

    for pattern in approach_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:1]:
            content = f"Approach chosen: {match.strip()}"
            concepts.append({
                "fact_type": "decision_rationale",
                "content": content,
                "confidence": base_confidence * 0.9,
                "tags": ["noetic", "check", "approach"],
            })

    # CHAIN RECONSTRUCTION: Rejected alternatives (paths not taken)
    # Critical for understanding WHY this path vs others
    rejected_patterns = [
        r"(?:instead of|rather than|not|didn't)\s+(.{10,80}?)\s+because\s+(.{15,100}?)(?:\.|$)",
        r"(?:considered|tried|thought about)\s+(.{10,60}?)\s+but\s+(.{15,100}?)(?:\.|$)",
        r"(?:rejected|ruled out|avoided)\s+(.{10,60}?)(?:\s+(?:because|since|as)\s+(.{15,100}?))?(?:\.|$)",
        r"(?:could have|might have)\s+(.{10,60}?)\s+but\s+(.{15,100}?)(?:\.|$)",
    ]

    for pattern in rejected_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:2]:
            if isinstance(match, tuple) and len(match) >= 2:
                alt = match[0].strip()
                reason = match[1].strip() if match[1] else "unspecified"
                content = f"Rejected: {alt} | Reason: {reason}"
            else:
                content = f"Rejected alternative: {match.strip()}"
            concepts.append({
                "fact_type": "rejected_alternative",
                "content": content,
                "confidence": base_confidence * 0.85,
                "tags": ["noetic", "check", "alternative", "chain"],
            })

    return concepts


def _extract_postflight_concepts(
    reasoning: str,
    task_context: Optional[str],
    vectors: Optional[Dict[str, float]],
) -> List[Dict]:
    """Extract learning insight concepts from POSTFLIGHT reasoning."""
    concepts = []

    # Higher confidence for learning insights (they're validated by completion)
    base_confidence = 0.75
    if vectors and "completion" in vectors:
        base_confidence = min(0.95, 0.6 + vectors["completion"] * 0.35)

    # Learning patterns (generalizable insights)
    learning_patterns = [
        r"learned\s+(?:that\s+)?(.{20,150}?)(?:\.|$)",
        r"discovered\s+(?:that\s+)?(.{20,150}?)(?:\.|$)",
        r"realized\s+(?:that\s+)?(.{20,150}?)(?:\.|$)",
        r"insight:?\s+(.{20,150}?)(?:\.|$)",
        r"pattern:?\s+(.{20,100}?)(?:\.|$)",
    ]

    for pattern in learning_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:2]:
            content = f"Learning: {match.strip()}"
            concepts.append({
                "fact_type": "learning_insight",
                "content": content,
                "confidence": base_confidence,
                "tags": ["noetic", "postflight", "learning"],
            })

    # Causal patterns (X leads to Y)
    causal_patterns = [
        r"(.{10,50}?)\s+(?:leads to|causes|results in|enables)\s+(.{10,100}?)(?:\.|$)",
        r"when\s+(.{10,50}?),?\s+(?:then\s+)?(.{10,100}?)(?:\.|$)",
    ]

    for pattern in causal_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:1]:
            if isinstance(match, tuple) and len(match) == 2:
                content = f"Causal pattern: {match[0].strip()} → {match[1].strip()}"
                concepts.append({
                    "fact_type": "learning_insight",
                    "content": content,
                    "confidence": base_confidence * 0.85,
                    "tags": ["noetic", "postflight", "causal"],
                })

    # Domain applicability (FOR WHOM)
    domain_patterns = [
        r"applies to\s+(.{10,50}?)(?:\.|$)",
        r"relevant for\s+(.{10,50}?)(?:\.|$)",
        r"useful for\s+(.{10,50}?)(?:\.|$)",
    ]

    for pattern in domain_patterns:
        matches = re.findall(pattern, reasoning, re.IGNORECASE)
        for match in matches[:1]:
            content = f"Applies to: {match.strip()}"
            concepts.append({
                "fact_type": "learning_insight",
                "content": content,
                "confidence": base_confidence * 0.8,
                "tags": ["noetic", "postflight", "domain"],
            })

    return concepts


def embed_noetic_concepts(
    project_id: str,
    session_id: str,
    concepts: List[Dict],
    domain: Optional[str] = None,
) -> Tuple[int, int]:
    """
    Embed extracted noetic concepts into eidetic memory.

    Args:
        project_id: Project UUID
        session_id: Session UUID (for source tracking)
        concepts: List of concept dicts from extract_noetic_concepts
        domain: Optional domain tag for all concepts

    Returns:
        Tuple of (embedded_count, failed_count)
    """
    if not concepts:
        return (0, 0)

    try:
        from empirica.core.qdrant.vector_store import embed_eidetic
    except ImportError:
        logger.warning("Qdrant not available for noetic embedding")
        return (0, len(concepts))

    embedded = 0
    failed = 0

    for concept in concepts:
        fact_id = f"noetic_{session_id}_{uuid.uuid4().hex[:8]}"

        success = embed_eidetic(
            project_id=project_id,
            fact_id=fact_id,
            content=concept["content"],
            fact_type=concept["fact_type"],
            domain=domain or "noetic",
            confidence=concept["confidence"],
            confirmation_count=1,
            source_sessions=[session_id],
            tags=concept.get("tags", []),
        )

        if success:
            embedded += 1
        else:
            failed += 1

    logger.info(f"Noetic concepts embedded: {embedded}, failed: {failed}")
    return (embedded, failed)


def normalize_ai_concepts(
    ai_concepts: List[Dict],
    phase: str,
    vectors: Optional[Dict[str, float]] = None,
) -> List[Dict]:
    """
    Normalize and validate AI-provided noetic concepts.

    AI provides concepts in structured format; this ensures they meet
    embedding requirements and adds phase-specific tags.

    Args:
        ai_concepts: List of concept dicts from AI (fact_type, content, confidence, tags)
        phase: CASCADE phase for tag injection
        vectors: Optional vectors for confidence calibration

    Returns:
        List of normalized concepts ready for embedding
    """
    normalized = []

    for concept in ai_concepts:
        # Validate required fields
        if not isinstance(concept, dict):
            logger.warning(f"Skipping non-dict concept: {concept}")
            continue

        fact_type = concept.get("fact_type", "")
        content = concept.get("content", "")

        if not fact_type or not content:
            logger.warning(f"Skipping concept missing fact_type or content: {concept}")
            continue

        # Validate fact_type
        valid_types = set(NOETIC_FACT_TYPES.keys())
        if fact_type not in valid_types:
            logger.warning(f"Unknown fact_type '{fact_type}', defaulting to learning_insight")
            fact_type = "learning_insight"

        # Get confidence (default 0.7, cap at 0.95)
        confidence = float(concept.get("confidence", 0.7))
        confidence = min(0.95, max(0.1, confidence))

        # Calibrate with vectors if available
        if vectors:
            # Higher know/lower uncertainty = trust AI's confidence more
            know = vectors.get("know", 0.5)
            uncertainty = vectors.get("uncertainty", 0.5)
            calibration = (know + (1 - uncertainty)) / 2
            confidence = confidence * (0.7 + calibration * 0.3)
            confidence = min(0.95, confidence)

        # Build tags (inject phase tag)
        tags = list(concept.get("tags", []) or [])
        tags.extend(["noetic", phase.lower(), "ai_provided"])

        normalized.append({
            "fact_type": fact_type,
            "content": content.strip()[:500],  # Cap length
            "confidence": round(confidence, 3),
            "tags": tags,
        })

    return normalized


def hook_cascade_noetic(
    phase: str,
    session_id: str,
    project_id: str,
    reasoning: str,
    task_context: Optional[str] = None,
    vectors: Optional[Dict[str, float]] = None,
    domain: Optional[str] = None,
    ai_concepts: Optional[List[Dict]] = None,
) -> Dict:
    """
    Main hook to call from CASCADE phase handlers.

    PRIORITY ORDER:
    1. AI-provided concepts (semantic selection) - preferred
    2. Regex extraction (fallback) - only if AI provides nothing

    Args:
        phase: CASCADE phase (PREFLIGHT, CHECK, POSTFLIGHT)
        session_id: Current session UUID
        project_id: Project UUID
        reasoning: The reasoning text from the phase
        task_context: Optional task description
        vectors: Optional epistemic vectors
        domain: Optional domain tag
        ai_concepts: AI-provided noetic concepts (takes priority over extraction)

    Returns:
        Dict with extraction and embedding results
    """
    concepts = []
    source = "none"

    # PRIORITY 1: AI-provided concepts (semantic selection)
    if ai_concepts:
        concepts = normalize_ai_concepts(ai_concepts, phase, vectors)
        source = "ai_provided"
        logger.info(f"Using {len(concepts)} AI-provided noetic concepts for {phase}")

    # PRIORITY 2: Regex extraction (fallback)
    if not concepts and reasoning:
        concepts = extract_noetic_concepts(
            reasoning=reasoning,
            phase=phase,
            task_context=task_context,
            vectors=vectors,
        )
        source = "regex_fallback"
        logger.info(f"Fallback: Extracted {len(concepts)} concepts via regex for {phase}")

    if not concepts:
        return {
            "ok": True,
            "phase": phase,
            "concepts_extracted": 0,
            "concepts_embedded": 0,
            "source": source,
            "note": "No noetic concepts (AI didn't provide any, regex found none)",
        }

    # Embed concepts
    embedded, failed = embed_noetic_concepts(
        project_id=project_id,
        session_id=session_id,
        concepts=concepts,
        domain=domain,
    )

    return {
        "ok": True,
        "phase": phase,
        "source": source,
        "concepts_extracted": len(concepts),
        "concepts_embedded": embedded,
        "concepts_failed": failed,
        "concepts": [c["content"][:100] for c in concepts],  # Summary
    }
