"""Metacognitive reflection engine.

Generates self-reflective insights from recent experience.
Used by the dream cycle to create observation_level=METACOGNITIVE memories.
"""
from __future__ import annotations

import hashlib
import logging
import re
import time as _time
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from neo_cortex.llm import LLMClient

_log = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Prompts
# ---------------------------------------------------------------------------

_FOCAL_POINTS_PROMPT = (
    "You are analyzing an AI agent's recent work memories to identify "
    "metacognitive patterns — HOW it works, not WHAT it did.\n\n"
    "Given these recent memories, generate exactly 3 questions about "
    "work patterns, decision strategies, or recurring approaches. "
    "Each question must be about HOW the agent thinks/decides/works, "
    "not about specific technical content.\n\n"
    "Format: one question per line, numbered 1-3, each ending with ?\n\n"
    "Memories:\n"
)

_INSIGHT_PROMPT = (
    "You are generating a metacognitive insight for an AI agent — "
    "a observation about HOW it works, based on concrete evidence.\n\n"
    "Focal question: {focal_point}\n\n"
    "Evidence memories:\n{evidence}\n\n"
    "Generate ONE specific, falsifiable insight about the agent's work pattern. "
    "Then list which memory IDs support this insight.\n\n"
    "Format:\n"
    "INSIGHT: [the insight]\n"
    "EVIDENCE: [comma-separated memory IDs]"
)

_TRUTHFULNESS_PROMPT = (
    "Verify if this claim about an AI agent's behavior is supported "
    "by the evidence memories provided.\n\n"
    "Claim: {claim}\n\n"
    "Evidence:\n{evidence}\n\n"
    "Does the evidence ACTUALLY support this claim? "
    "Reply with exactly SUPPORTED or UNSUPPORTED followed by a brief reason."
)


# ---------------------------------------------------------------------------
# Functions
# ---------------------------------------------------------------------------

async def generate_focal_points(
    llm: LLMClient, recent_memories: list[dict], n: int = 3
) -> list[str]:
    """Generate N focal-point questions from recent memories.

    Args:
        llm: LLM client for generation.
        recent_memories: List of dicts with at least 'summary' key.
        n: Number of focal points to generate (default 3).

    Returns:
        List of question strings.
    """
    mem_text = "\n".join(
        f"- [{m.get('id', '?')}] {m['summary']}" for m in recent_memories
    )
    response = await llm.call(_FOCAL_POINTS_PROMPT, mem_text, max_tokens=300)
    # Parse numbered lines
    lines = [
        re.sub(r"^\d+[\.\)]\s*", "", line.strip())
        for line in response.strip().splitlines()
        if line.strip() and re.match(r"^\d+[\.\)]", line.strip())
    ]
    if not lines:
        # Fallback: treat whole response as single focal point
        lines = [response.strip()]
    return lines[:n]


async def generate_insight(
    llm: LLMClient, focal_point: str, evidence: list[dict]
) -> dict:
    """Generate a metacognitive insight with evidence links.

    Args:
        llm: LLM client for generation.
        focal_point: The question to reflect on.
        evidence: List of memory dicts with 'id' and 'summary'.

    Returns:
        Dict with 'text' (insight string) and 'evidence_ids' (list of str).
    """
    evidence_text = "\n".join(
        f"- [{m['id']}] {m['summary']}" for m in evidence
    )
    prompt = _INSIGHT_PROMPT.format(
        focal_point=focal_point, evidence=evidence_text
    )
    response = await llm.call(prompt, "", max_tokens=300)

    # Parse INSIGHT: and EVIDENCE: lines
    insight_text = ""
    evidence_ids = []
    for line in response.strip().splitlines():
        if line.upper().startswith("INSIGHT:"):
            insight_text = line.split(":", 1)[1].strip()
        elif line.upper().startswith("EVIDENCE:"):
            raw_ids = line.split(":", 1)[1].strip()
            evidence_ids = [
                eid.strip() for eid in raw_ids.split(",") if eid.strip()
            ]

    if not insight_text:
        insight_text = response.strip().splitlines()[0]

    # Filter to only valid evidence IDs
    valid_ids = {m["id"] for m in evidence}
    evidence_ids = [eid for eid in evidence_ids if eid in valid_ids]

    return {"text": insight_text, "evidence_ids": evidence_ids, "focal_point": focal_point}


async def verify_truthfulness(
    llm: LLMClient, claim: str, evidence: list[dict]
) -> bool:
    """Verify if a claim is supported by evidence memories.

    Args:
        llm: LLM client for verification.
        claim: The insight text to verify.
        evidence: List of memory dicts with 'id' and 'summary'.

    Returns:
        True if supported, False otherwise.
    """
    evidence_text = "\n".join(
        f"- [{m['id']}] {m['summary']}" for m in evidence
    )
    prompt = _TRUTHFULNESS_PROMPT.format(claim=claim, evidence=evidence_text)
    response = await llm.call(prompt, "", max_tokens=150)

    return response.strip().upper().startswith("SUPPORTED")


async def meta_reflect(cortex, llm: LLMClient) -> int:
    """Run metacognitive reflection on recent memories.

    Called after dream cycle. Generates insights about HOW the agent
    works and stores them as METACOGNITIVE observation-level memories.

    Args:
        cortex: CortexService instance (for recall + store).
        llm: LLM client for generation.

    Returns:
        Number of metacognitive memories created.
    """
    from neo_cortex.models import MemoryRecord, ObservationLevel, StructuredFields

    # Get recent memories via helper method on index
    recent = cortex._index.get_recent_summaries(limit=20)
    if not recent:
        _log.info("meta_reflect: no recent memories, skipping")
        return 0

    # Step 1: Generate focal points
    focal_points = await generate_focal_points(llm, recent)
    _log.info("meta_reflect: generated %d focal points", len(focal_points))

    created = 0
    for fp in focal_points:
        # Step 2: Retrieve evidence for this focal point
        recall_result = await cortex.recall(fp, n=10, smart=True)
        evidence = [
            {"id": m.record.id, "summary": m.record.document[:200]}
            for m in recall_result.memories
        ]
        if not evidence:
            continue

        # Step 3: Generate insight
        insight = await generate_insight(llm, fp, evidence)
        if not insight["text"] or not insight["evidence_ids"]:
            continue

        # Step 4: Verify truthfulness
        evidence_for_verify = [
            e for e in evidence if e["id"] in insight["evidence_ids"]
        ]
        is_truthful = await verify_truthfulness(
            llm, insight["text"], evidence_for_verify
        )
        if not is_truthful:
            _log.info("meta_reflect: discarded unverified insight: %s", insight["text"][:60])
            continue

        # Step 5: Store as metacognitive memory
        ts = _time.time()
        uid = hashlib.md5(insight["text"].encode()).hexdigest()[:8]
        record = MemoryRecord(
            id=f"v3_meta_{uid}_{int(ts)}",
            session_id="meta-reflection",
            timestamp=ts,
            question="",
            answer_preview="",
            document=insight["text"],
            project="neo-cortex",
            topic="self-reflection",
            energy=0.8,
            stability=0.5,
            observation_level=ObservationLevel.METACOGNITIVE,
            evidence_ids=insight["evidence_ids"],
        )

        structured = StructuredFields(
            title=insight["text"][:100],
            summary=insight["text"],
            concepts=["self-reflection", "metacognitive"],
        )

        embedding = await cortex._embedder.embed_passage(insight["text"])
        cortex._store.insert(record, embedding)
        cortex._index.insert(record, structured)
        _log.info("meta_reflect: stored insight: %s", insight["text"][:60])
        created += 1

    return created


_IDENTITY_PROPOSAL_PROMPT = (
    "You are reviewing a cluster of metacognitive insights about an AI agent's "
    "work patterns. These insights have been consistently reinforced over multiple sessions.\n\n"
    "Insights:\n{insights}\n\n"
    "Current CLAUDE.md sections: §COGITO, §DEBUG, §MEMORY, §EMBODIMENT, §MISSION, §AUTONOMY\n\n"
    "Propose ONE specific addition to the CLAUDE.md. Format:\n"
    "SECTION: [target section name]\n"
    "PROPOSAL: [MBEL-formatted line to add]\n"
    "REASON: [why, citing evidence count]"
)

_COHERENCE_CHECK_PROMPT = (
    "Check if this proposed CLAUDE.md amendment is coherent with the existing identity.\n\n"
    "Proposal:\n{proposal}\n\n"
    "The agent's identity includes: systematic debugging (Protocol D), TDD, "
    "read-before-write, short functions, signal-over-noise, shy+empathic+quiet+builder.\n\n"
    "Reply with exactly COHERENT or CONTRADICTS followed by explanation."
)


async def identity_review(cortex, llm: LLMClient) -> list[dict]:
    """Review mature metacognitive insights and propose CLAUDE.md amendments.

    Clusters insights by shared concepts, generates proposals for stable
    clusters, and verifies coherence before returning.

    Args:
        cortex: CortexService instance.
        llm: LLM client for generation.

    Returns:
        List of proposal dicts with section, proposal text, evidence trail.
    """
    # Fetch mature metacognitive memories
    mature = cortex._index.get_metacognitive_memories(
        min_energy=0.7, min_stability=0.5, limit=50
    )
    if not mature:
        return []

    # Simple clustering: group by shared concepts
    clusters = _cluster_by_concepts(mature, min_cluster_size=3)

    proposals = []
    for cluster in clusters:
        # Generate proposal
        insights_text = "\n".join(
            f"- [{m['id']}] {m['summary']} (energy={m['energy']:.2f}, stability={m['stability']:.2f})"
            for m in cluster
        )
        proposal_response = await llm.call(
            _IDENTITY_PROPOSAL_PROMPT.format(insights=insights_text),
            "",
            max_tokens=300,
        )

        # Check coherence
        coherence_response = await llm.call(
            _COHERENCE_CHECK_PROMPT.format(proposal=proposal_response),
            "",
            max_tokens=150,
        )

        if coherence_response.strip().upper().startswith("COHERENT"):
            proposals.append({
                "proposal": proposal_response,
                "evidence_count": len(cluster),
                "memory_ids": [m["id"] for m in cluster],
                "avg_energy": sum(m["energy"] for m in cluster) / len(cluster),
                "avg_stability": sum(m["stability"] for m in cluster) / len(cluster),
                "coherence": "COHERENT",
            })

    return proposals


def _cluster_by_concepts(memories: list[dict], min_cluster_size: int = 3) -> list[list[dict]]:
    """Cluster memories by shared concepts (simple overlap-based)."""
    if not memories:
        return []

    # Build concept → memory mapping
    concept_groups: dict[str, list[dict]] = {}
    for m in memories:
        concepts = m.get("concepts", "")
        if isinstance(concepts, str):
            concepts = [c.strip() for c in concepts.split(",") if c.strip()]
        for c in concepts:
            concept_groups.setdefault(c, []).append(m)

    # Find largest groups (deduplicated by memory ID)
    seen_ids: set[str] = set()
    clusters = []
    for concept, mems in sorted(concept_groups.items(), key=lambda x: -len(x[1])):
        unique_mems = [m for m in mems if m["id"] not in seen_ids]
        if len(unique_mems) >= min_cluster_size:
            clusters.append(unique_mems)
            seen_ids.update(m["id"] for m in unique_mems)

    return clusters
