"""
Pattern Retrieval for Cognitive Workflow Hooks

Provides pattern retrieval for PREFLIGHT (proactive loading) and CHECK (reactive validation).
Integrates with Qdrant memory collections for lessons, dead_ends, and findings.

ANTI-GAMING: Calibration-specific feedback (calibration_warnings in PREFLIGHT, calibration_bias
in CHECK) has been removed from AI-facing output. Specific vector gaps and directions gave the
AI an "answer key" for gaming CHECK gates. Calibration data is now user-facing only
(calibration-report, statusline). The Sentinel uses it internally for threshold inflation.

Defaults:
- similarity_threshold: 0.7
- limit: 3
- optional: True (graceful fail if Qdrant unavailable)
"""
from __future__ import annotations

import logging
import os
from typing import Any

logger = logging.getLogger(__name__)

# Defaults
# NOTE: Threshold lowered to 0.5 because placeholder embeddings (hash-based)
# produce max scores of ~0.55-0.60. Real ML embeddings would score 0.7-0.9.
DEFAULT_THRESHOLD = 0.5
DEFAULT_LIMIT = 3

# Time gap thresholds for human context awareness (in seconds)
# These are metadata signals for Claude, not retrieval quantity controls
TIME_GAP_THRESHOLDS = {
    "continuation": 30 * 60,      # < 30 minutes = likely same work session
    "short_break": 4 * 60 * 60,   # < 4 hours = human took a break
    # > 4 hours = human was away for extended period
}


def compute_time_gap_info(last_session_timestamp: float | None = None) -> dict[str, Any]:
    """
    Compute time gap information since last session.

    Returns metadata for Claude to understand human time context.
    This is a SIGNAL for awareness, not a control for retrieval quantity.

    Args:
        last_session_timestamp: Unix timestamp of last session end (or None if unknown)

    Returns:
        {
            "gap_seconds": float,
            "gap_human_readable": "4h 23m",
            "gap_category": "continuation" | "short_break" | "extended_away",
            "note": "Human-friendly context note"
        }
    """
    import time

    if last_session_timestamp is None:
        return {
            "gap_seconds": None,
            "gap_human_readable": "unknown",
            "gap_category": "unknown",
            "note": "No previous session timestamp available"
        }

    gap_seconds = time.time() - last_session_timestamp

    # Format human-readable
    hours = int(gap_seconds // 3600)
    minutes = int((gap_seconds % 3600) // 60)
    if hours > 0:
        gap_human_readable = f"{hours}h {minutes}m"
    else:
        gap_human_readable = f"{minutes}m"

    # Categorize
    if gap_seconds < TIME_GAP_THRESHOLDS["continuation"]:
        category = "continuation"
        note = "Continuing recent work session"
    elif gap_seconds < TIME_GAP_THRESHOLDS["short_break"]:
        category = "short_break"
        note = f"Returning after {gap_human_readable} break"
    else:
        category = "extended_away"
        note = f"Human was away for {gap_human_readable} - may benefit from context recap"

    return {
        "gap_seconds": gap_seconds,
        "gap_human_readable": gap_human_readable,
        "gap_category": category,
        "note": note
    }


def get_qdrant_url() -> str | None:
    """Check if Qdrant is configured."""
    return os.getenv("EMPIRICA_QDRANT_URL")


def _search_memory_by_type(
    project_id: str,
    query_text: str,
    memory_type: str,
    limit: int = DEFAULT_LIMIT,
    min_score: float = DEFAULT_THRESHOLD
) -> list[dict]:
    """
    Search memory collection filtered by type.
    Returns empty list if Qdrant not available (optional behavior).
    """
    try:
        from .vector_store import _check_qdrant_available, _get_embedding_safe, _get_qdrant_client, _memory_collection

        if not _check_qdrant_available():
            return []

        qvec = _get_embedding_safe(query_text)
        if qvec is None:
            return []

        from qdrant_client.models import FieldCondition, Filter, MatchValue
        client = _get_qdrant_client()
        coll = _memory_collection(project_id)

        if not client.collection_exists(coll):
            return []

        query_filter = Filter(must=[
            FieldCondition(key="type", match=MatchValue(value=memory_type))
        ])

        results = client.query_points(
            collection_name=coll,
            query=qvec,
            query_filter=query_filter,
            limit=limit,
            with_payload=True
        )

        # Filter by min_score and return
        return [
            {
                "score": getattr(r, 'score', 0.0) or 0.0,
                **(r.payload or {})
            }
            for r in results.points
            if (getattr(r, 'score', 0.0) or 0.0) >= min_score
        ]
    except Exception as e:
        logger.debug(f"_search_memory_by_type({memory_type}) failed: {e}")
        return []


def _search_related_docs(
    project_id: str,
    query_text: str,
    limit: int = DEFAULT_LIMIT,
    min_score: float = DEFAULT_THRESHOLD
) -> list[dict]:
    """
    Search docs collection for documents related to a query.
    Used to find supporting documentation for retrieved memory entries.

    Returns list of related docs with path, description, and relevance score.
    """
    try:
        from .vector_store import _check_qdrant_available, _docs_collection, _get_embedding_safe, _get_qdrant_client

        if not _check_qdrant_available():
            return []

        qvec = _get_embedding_safe(query_text)
        if qvec is None:
            return []

        client = _get_qdrant_client()
        coll = _docs_collection(project_id)

        if not client.collection_exists(coll):
            return []

        results = client.query_points(
            collection_name=coll,
            query=qvec,
            limit=limit,
            with_payload=True
        )

        # Format results
        return [
            {
                "doc_path": (r.payload or {}).get("doc_path", ""),
                "description": (r.payload or {}).get("description", ""),
                "doc_type": (r.payload or {}).get("doc_type", ""),
                "tags": (r.payload or {}).get("tags", []),
                "score": getattr(r, 'score', 0.0) or 0.0
            }
            for r in results.points
            if (getattr(r, 'score', 0.0) or 0.0) >= min_score
        ]
    except Exception as e:
        logger.debug(f"_search_related_docs failed: {e}")
        return []


def _compute_adaptive_limits(vectors: dict | None, base_limit: int) -> dict[str, int]:
    """Compute per-collection retrieval limits based on vector state.

    Higher uncertainty → more context from all collections (up to 2x).
    Low know → more lessons, dead-ends, assumptions.
    Low context → more episodic, goals, decisions.
    """
    if not vectors:
        return dict.fromkeys(["lessons", "dead_ends", "findings", "eidetic", "episodic", "goals", "assumptions", "decisions", "global_dead_ends", "docs"], base_limit)

    uncertainty = vectors.get("uncertainty", 0.5)
    know = vectors.get("know", 0.5)
    context = vectors.get("context", 0.5)

    # Base multiplier: scales 1.0x at u=0.0 to 2.0x at u=1.0
    uncertainty_mult = 1.0 + uncertainty

    # Knowledge gap: low know → more procedural/warning context
    know_gap = max(0.0, 1.0 - know)  # 0.0 at know=1.0, 1.0 at know=0.0

    # Context gap: low context → more situational awareness
    context_gap = max(0.0, 1.0 - context)  # 0.0 at context=1.0, 1.0 at context=0.0

    def _limit(base_mult: float, gap_bonus: float = 0.0) -> int:
        return max(1, int(base_limit * base_mult * uncertainty_mult + gap_bonus))

    return {
        "lessons": _limit(1.0, know_gap * 2),
        "dead_ends": _limit(1.0, know_gap * 2),
        "findings": _limit(1.0),
        "eidetic": _limit(1.0, know_gap),
        "episodic": _limit(1.0, context_gap * 2),
        "goals": _limit(1.0, context_gap * 2),
        "assumptions": _limit(1.0, know_gap * 2),
        "decisions": _limit(1.0, context_gap),
        "global_dead_ends": max(1, int(2 * uncertainty_mult)),
        "docs": _limit(1.0),
    }


def _enrich_memory_types(result, project_id, task_context, limits, include_eidetic, include_episodic):
    """Enrich with eidetic facts, episodic narratives, and global dead-ends."""
    if include_eidetic:
        try:
            from .vector_store import search_eidetic
            result["eidetic_facts"] = [
                {"content": e.get("content", ""), "confidence": e.get("confidence", 0.5),
                 "domain": e.get("domain"), "confirmation_count": e.get("confirmation_count", 1), "score": e.get("score", 0.0)}
                for e in search_eidetic(project_id, task_context, min_confidence=0.5, limit=limits["eidetic"])]
        except Exception as e:
            logger.debug(f"Eidetic retrieval failed: {e}")
            result["eidetic_facts"] = []
    if include_episodic:
        try:
            from .vector_store import search_episodic
            result["episodic_narratives"] = [
                {"narrative": ep.get("narrative", ""), "outcome": ep.get("outcome"),
                 "learning_delta": ep.get("learning_delta", {}), "recency_weight": ep.get("recency_weight", 1.0), "score": ep.get("score", 0.0)}
                for ep in search_episodic(project_id, task_context, limit=limits["episodic"], apply_recency_decay=True)]
        except Exception as e:
            logger.debug(f"Episodic retrieval failed: {e}")
            result["episodic_narratives"] = []
    try:
        from .vector_store import search_global_dead_ends
        raw = search_global_dead_ends(f"Approach for: {task_context}", limit=limits["global_dead_ends"])
        if raw:
            result["global_dead_ends"] = [{"approach": g.get("approach", g.get("text", "")), "why_failed": g.get("why_failed", ""),
                "project": g.get("project_name", "other project"), "score": g.get("score", 0.0)} for g in raw]
    except Exception as e:
        logger.debug(f"Global dead-ends retrieval failed: {e}")


def _enrich_knowledge_graph(result, project_id, task_context, threshold, limits,
                            include_goals, include_assumptions, include_decisions, include_related_docs):
    """Enrich with goals, assumptions, decisions, and docs."""
    if include_goals:
        try:
            from .vector_store import search_goals
            raw = search_goals(project_id, task_context, include_subtasks=True, limit=limits["goals"])
            if raw:
                result["related_goals"] = [{"objective": g.get("objective") or g.get("description", ""), "status": g.get("status", ""),
                    "type": g.get("type", "goal"), "goal_id": g.get("goal_id", ""), "score": g.get("score", 0.0)} for g in raw]
        except Exception as e:
            logger.debug(f"Goals retrieval failed: {e}")
    if include_assumptions:
        try:
            from .vector_store import search_assumptions
            raw = search_assumptions(project_id, task_context, status="unverified", limit=limits["assumptions"])
            if raw:
                result["unverified_assumptions"] = [{"assumption": a.get("assumption", ""), "confidence": a.get("confidence", 0.5),
                    "urgency_signal": a.get("urgency_signal", 0.0), "domain": a.get("domain"), "score": a.get("score", 0.0)} for a in raw]
        except Exception as e:
            logger.debug(f"Assumptions retrieval failed: {e}")
    if include_decisions:
        try:
            from .vector_store import search_decisions
            raw = search_decisions(project_id, task_context, limit=limits["decisions"])
            if raw:
                result["prior_decisions"] = [{"choice": d.get("choice", ""), "rationale": d.get("rationale", ""),
                    "reversibility": d.get("reversibility", ""), "confidence_at_decision": d.get("confidence_at_decision", 0.5),
                    "score": d.get("score", 0.0)} for d in raw]
        except Exception as e:
            logger.debug(f"Decisions retrieval failed: {e}")
    if include_related_docs:
        try:
            raw = _search_related_docs(project_id, task_context, limit=limits["docs"], min_score=threshold)
            result["related_docs"] = [{"doc_path": d.get("doc_path", ""), "description": d.get("description", ""),
                "doc_type": d.get("doc_type", ""), "tags": d.get("tags", []), "score": d.get("score", 0.0)} for d in raw]
        except Exception as e:
            logger.debug(f"Related docs retrieval failed: {e}")
            result["related_docs"] = []


def _enrich_task_patterns(result, project_id, task_context, threshold, limits,
                          include_eidetic, include_episodic, include_related_docs,
                          include_goals, include_assumptions, include_decisions):
    """Enrich task patterns with optional retrieval types."""
    _enrich_memory_types(result, project_id, task_context, limits, include_eidetic, include_episodic)
    _enrich_knowledge_graph(result, project_id, task_context, threshold, limits,
                            include_goals, include_assumptions, include_decisions, include_related_docs)


def retrieve_task_patterns(
    project_id: str,
    task_context: str,
    threshold: float = DEFAULT_THRESHOLD,
    limit: int = DEFAULT_LIMIT,
    last_session_timestamp: float | None = None,
    include_eidetic: bool = False,
    include_episodic: bool = False,
    include_related_docs: bool = False,
    include_goals: bool = False,
    include_assumptions: bool = False,
    include_decisions: bool = False,
    vectors: dict | None = None,
) -> dict[str, Any]:
    """
    PREFLIGHT hook: Retrieve relevant patterns for a task (Noetic RAG).

    Returns patterns that should inform the AI before starting work:
    - lessons: Procedural knowledge (HOW to do things)
    - dead_ends: Failed approaches (what NOT to try)
    - relevant_findings: High-impact facts
    - eidetic_facts: Stable facts with confidence (optional)
    - episodic_narratives: Recent session arcs (optional)
    - related_docs: Reference documents related to retrieved memory (optional)
    - related_goals: Goals/subtasks related to task context (optional)
    - unverified_assumptions: Unverified beliefs that may affect this work (optional)
    - prior_decisions: Past decisions relevant to this area (optional)
    - time_gap: Metadata about time since last session (for human context awareness)

    Args:
        project_id: Project ID
        task_context: Description of the task being undertaken
        threshold: Minimum similarity score (default 0.5)
        limit: Max patterns per type (default 3)
        last_session_timestamp: Used to compute time gap metadata
        include_eidetic: Include eidetic facts in retrieval
        include_episodic: Include episodic narratives in retrieval
        include_related_docs: Include related reference docs in retrieval
        include_goals: Include related goals/subtasks
        include_assumptions: Include unverified assumptions ("What are you assuming?")
        include_decisions: Include prior decisions ("What was already decided?")
        vectors: Current epistemic vectors for adaptive depth scaling
    """
    # Compute time gap metadata (signal for Claude, not retrieval control)
    time_gap_info = compute_time_gap_info(last_session_timestamp)

    if not get_qdrant_url():
        return {"lessons": [], "dead_ends": [], "relevant_findings": [], "time_gap": time_gap_info}

    # Adaptive limits: scale retrieval depth by vector state
    limits = _compute_adaptive_limits(vectors, limit)

    # Search for lessons (procedural knowledge)
    lessons_raw = _search_memory_by_type(
        project_id,
        f"How to: {task_context}",
        "lesson",
        limits["lessons"],
        threshold
    )
    lessons = [
        {
            "name": l.get("text", "").replace("LESSON: ", "").split(" - ")[0] if l.get("text") else "",
            "description": l.get("text", "").split(" - ")[1].split(" Domain:")[0] if " - " in l.get("text", "") else "",
            "domain": l.get("domain", ""),
            "confidence": l.get("confidence", 0.8),
            "score": l.get("score", 0.0)
        }
        for l in lessons_raw
    ]

    # Search for dead ends (what NOT to try)
    dead_ends_raw = _search_memory_by_type(
        project_id,
        f"Approach for: {task_context}",
        "dead_end",
        limits["dead_ends"],
        threshold
    )
    dead_ends = [
        {
            "approach": d.get("text", "").replace("DEAD END: ", "").split(" Why failed:")[0] if d.get("text") else "",
            "why_failed": d.get("text", "").split("Why failed: ")[1] if "Why failed:" in d.get("text", "") else "",
            "score": d.get("score", 0.0)
        }
        for d in dead_ends_raw
    ]

    # Search for relevant findings (high-impact facts)
    findings_raw = _search_memory_by_type(
        project_id,
        task_context,
        "finding",
        limits["findings"],
        threshold
    )
    relevant_findings = [
        {
            "finding": f.get("text", ""),
            "impact": f.get("impact", 0.5),
            "score": f.get("score", 0.0)
        }
        for f in findings_raw
    ]

    # ANTI-GAMING: Calibration warnings (specific overestimate/underestimate patterns from
    # similar past tasks) are no longer surfaced to the AI. They provide an "answer key"
    # for gaming self-assessment vectors. Calibration data is available to the USER via
    # calibration-report and statusline. The Sentinel uses it for threshold inflation.

    # Build result
    result = {
        "lessons": lessons,
        "dead_ends": dead_ends,
        "relevant_findings": relevant_findings,
        "time_gap": time_gap_info,
    }

    # Enrich with optional retrieval types
    _enrich_task_patterns(result, project_id, task_context, threshold, limits,
        include_eidetic, include_episodic, include_related_docs,
        include_goals, include_assumptions, include_decisions)
    return result


def _enrich_check_warnings(warnings, project_id, current_approach, threshold, limit,
                           include_findings, include_eidetic, include_goals, include_assumptions):
    """Enrich CHECK warnings with optional findings, eidetic, goals, and assumptions."""
    if include_findings and current_approach:
        try:
            raw = _search_memory_by_type(project_id, current_approach, "finding", limit, threshold)
            if raw:
                warnings["related_findings"] = [{"finding": f.get("text", ""), "impact": f.get("impact", 0.5), "score": f.get("score", 0.0)} for f in raw]
        except Exception as e:
            logger.debug(f"CHECK findings retrieval failed: {e}")
    if include_eidetic and current_approach:
        try:
            from .vector_store import search_eidetic
            raw = search_eidetic(project_id, current_approach, min_confidence=0.5, limit=limit)
            if raw:
                warnings["eidetic_context"] = [{"content": e.get("content", ""), "confidence": e.get("confidence", 0.5),
                    "domain": e.get("domain"), "score": e.get("score", 0.0)} for e in raw]
        except Exception as e:
            logger.debug(f"CHECK eidetic retrieval failed: {e}")
    if include_goals:
        try:
            from .vector_store import search_goals
            raw = search_goals(project_id, current_approach or "current work", status="in_progress", include_subtasks=True, limit=limit)
            if raw:
                warnings["active_goals"] = [{"objective": g.get("objective") or g.get("description", ""), "status": g.get("status", ""),
                    "type": g.get("type", "goal"), "score": g.get("score", 0.0)} for g in raw]
        except Exception as e:
            logger.debug(f"CHECK goals retrieval failed: {e}")
    if include_assumptions:
        try:
            from .vector_store import search_assumptions
            raw = search_assumptions(project_id, current_approach or "current approach", status="unverified", limit=limit)
            if raw:
                warnings["unverified_assumptions"] = [{"assumption": a.get("assumption", ""), "confidence": a.get("confidence", 0.5),
                    "urgency_signal": a.get("urgency_signal", 0.0), "score": a.get("score", 0.0)} for a in raw]
        except Exception as e:
            logger.debug(f"CHECK assumptions retrieval failed: {e}")


def check_against_patterns(
    project_id: str,
    current_approach: str,
    vectors: dict | None = None,
    threshold: float = DEFAULT_THRESHOLD,
    limit: int = DEFAULT_LIMIT,
    include_findings: bool = False,
    include_eidetic: bool = False,
    include_goals: bool = False,
    include_assumptions: bool = False,
) -> dict[str, Any]:
    """
    CHECK hook: Validate current approach against known patterns (Noetic RAG).

    Returns warnings if the approach matches known failures or
    if vector patterns indicate risk. Optionally enriches with
    findings, eidetic facts, goals, and unverified assumptions.

    Args:
        project_id: Project ID
        current_approach: Description of current approach/plan
        vectors: Current epistemic vectors (know, uncertainty, etc.)
        threshold: Minimum similarity for dead_end match (default 0.7)
        limit: Max warnings to return (default 3)
        include_findings: Include related findings as context
        include_eidetic: Include eidetic facts (stable knowledge)
        include_goals: Include active goals for alignment check
        include_assumptions: Include unverified assumptions as risk signal
    """
    if not get_qdrant_url():
        return {"dead_end_matches": [], "mistake_risk": None, "has_warnings": False}

    warnings = {
        "dead_end_matches": [],
        "mistake_risk": None,
        "has_warnings": False
    }

    # Check if current approach matches known dead ends
    if current_approach:
        dead_ends = _search_memory_by_type(
            project_id,
            f"Approach: {current_approach}",
            "dead_end",
            limit,
            threshold
        )

        warnings["dead_end_matches"] = [
            {
                "approach": d.get("text", "").replace("DEAD END: ", "").split(" Why failed:")[0] if d.get("text") else "",
                "why_failed": d.get("text", "").split("Why failed: ")[1] if "Why failed:" in d.get("text", "") else "",
                "similarity": d.get("score", 0.0)
            }
            for d in dead_ends
        ]

    # Check vector patterns for mistake risk
    if vectors:
        know = vectors.get("know", 0.5)
        uncertainty = vectors.get("uncertainty", 0.5)

        # High uncertainty + low know = historical mistake pattern
        if uncertainty >= 0.5 and know <= 0.4:
            warnings["mistake_risk"] = (
                f"High risk pattern: uncertainty={uncertainty:.2f}, know={know:.2f}. "
                "Historical data shows mistakes occur when acting with high uncertainty and low knowledge. "
                "Consider more investigation before proceeding."
            )
        # Acting with very low context awareness
        elif vectors.get("context", 0.5) <= 0.3:
            warnings["mistake_risk"] = (
                f"Low context awareness ({vectors.get('context', 0):.2f}). "
                "Proceeding without understanding current state increases mistake probability."
            )

    # ANTI-GAMING: Calibration bias details (specific vectors, directions, magnitudes)
    # are no longer surfaced to the AI. They provide an "answer key" for gaming CHECK.
    # Calibration bias data is available to the USER via calibration-report and statusline.
    # The Sentinel uses this data internally for threshold inflation (dynamic_thresholds.py)
    # but does not expose specifics to the AI.

    # Enrich with optional retrieval types
    _enrich_check_warnings(warnings, project_id, current_approach, threshold, limit,
                           include_findings, include_eidetic, include_goals, include_assumptions)

    # Set has_warnings flag
    warnings["has_warnings"] = (
        bool(warnings["dead_end_matches"])
        or bool(warnings["mistake_risk"])
        or bool(warnings.get("unverified_assumptions"))
    )

    return warnings


def search_lessons_for_task(
    project_id: str,
    task_context: str,
    domain: str | None = None,
    limit: int = DEFAULT_LIMIT,
    min_score: float = DEFAULT_THRESHOLD
) -> list[dict]:
    """
    Search for relevant lessons for a specific task.
    Optionally filter by domain.

    Args:
        project_id: Project ID
        task_context: What you're trying to do
        domain: Optional domain filter (e.g., "notebooklm", "git")
        limit: Max results
        min_score: Minimum similarity score

    Returns:
        List of lessons with name, description, domain, confidence, score
    """
    try:
        from .vector_store import _check_qdrant_available, _get_embedding_safe, _get_qdrant_client, _memory_collection

        if not _check_qdrant_available():
            return []

        qvec = _get_embedding_safe(f"Lesson for: {task_context}")
        if qvec is None:
            return []

        from qdrant_client.models import FieldCondition, Filter, MatchValue
        client = _get_qdrant_client()
        coll = _memory_collection(project_id)

        if not client.collection_exists(coll):
            return []

        # Build filter
        conditions = [FieldCondition(key="type", match=MatchValue(value="lesson"))]
        if domain:
            conditions.append(FieldCondition(key="domain", match=MatchValue(value=domain)))

        query_filter = Filter(must=conditions)

        results = client.query_points(
            collection_name=coll,
            query=qvec,
            query_filter=query_filter,
            limit=limit,
            with_payload=True
        )

        lessons = []
        for r in results.points:
            score = getattr(r, 'score', 0.0) or 0.0
            if score < min_score:
                continue

            payload = r.payload or {}
            text = payload.get("text", "")

            # Parse the embedded text format: "LESSON: name - description Domain: domain"
            name = text.replace("LESSON: ", "").split(" - ")[0] if text else ""
            desc = text.split(" - ")[1].split(" Domain:")[0] if " - " in text else ""

            lessons.append({
                "name": name,
                "description": desc,
                "domain": payload.get("domain", ""),
                "confidence": payload.get("confidence", 0.8),
                "tags": payload.get("tags", []),
                "score": score
            })

        return lessons
    except Exception as e:
        logger.debug(f"search_lessons_for_task failed: {e}")
        return []
