"""SourceMonitor — memory provenance tracking and confabulation detection.

v0.15.0: The Reflective Mind

Where did I learn this? Was it observed, inferred, told to me, or did I
reason it out? Source monitoring is the cognitive system that tracks memory
provenance — and its failure is the root of confabulation, false memories,
and the uncomfortable feeling of "I know it but don't know where from."

The SourceMonitor tags memories with provenance metadata, auto-detects likely
source types from memory content and metadata heuristics, and audits the
memory store for uncertain or potentially confabulated items — flagging those
whose source confidence falls below a threshold.

Source types
------------
``observation``  — directly witnessed / first-hand input
``inference``    — reasoned or derived from other knowledge
``instruction``  — explicitly told by an external agent
``reflection``   — emerged from the agent's own self-review process
``dream``        — produced during dream consolidation / offline replay
``insight``      — cross-domain bridging insight (generated by InsightEngine)
``unknown``      — provenance cannot be determined

Biological analogue: Source Monitoring Framework (Johnson, Hashtroudi &
Lindsay 1993) — the set of processes that attribute memories to their origin.
Damage to prefrontal systems causes source amnesia: knowing a fact but not
knowing where it came from; also implicated in false-memory formation.
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import Any, Optional


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

SOURCE_TYPES: frozenset[str] = frozenset({
    "observation", "inference", "instruction", "reflection",
    "dream", "insight", "unknown",
})


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class SourceTag:
    """Provenance tag for a single memory."""

    memory_id: str
    source_type: str          # One of SOURCE_TYPES
    confidence: float         # 0..1 — how confident the attribution is
    provenance_note: str      # Free-text note about the source
    tagged_at: float = field(default_factory=time.time)

    def summary(self) -> str:
        return (
            f"SourceTag [{self.memory_id[:12]}] "
            f"source={self.source_type} conf={self.confidence:.2f}  "
            f"note={self.provenance_note[:60]!r}"
        )

    def to_dict(self) -> dict:
        return {
            "memory_id": self.memory_id, "source_type": self.source_type,
            "confidence": self.confidence, "provenance_note": self.provenance_note,
            "tagged_at": self.tagged_at,
        }

    @classmethod
    def from_dict(cls, d: dict) -> "SourceTag":
        return cls(
            memory_id=d["memory_id"], source_type=d["source_type"],
            confidence=d.get("confidence", 0.5),
            provenance_note=d.get("provenance_note", ""),
            tagged_at=d.get("tagged_at", 0.0),
        )


@dataclass
class SourceAuditEntry:
    """Audit result for a single memory."""

    memory_id: str
    source_type: str
    source_confidence: float
    content_excerpt: str      # First 80 chars of memory content
    flagged: bool             # True if source is uncertain / confabulation risk
    flag_reason: str          # Why it was flagged (empty if not flagged)


@dataclass
class SourceReport:
    """Result of a SourceMonitor.audit() run."""

    total_audited: int
    flagged_count: int
    source_distribution: dict[str, int]  # source_type → count
    high_risk_entries: list[SourceAuditEntry]
    duration_seconds: float

    def summary(self) -> str:
        dist_str = ", ".join(
            f"{k}={v}" for k, v in sorted(
                self.source_distribution.items(), key=lambda x: x[1], reverse=True
            )
        )
        lines = [
            f"SourceReport: {self.total_audited} memories audited, "
            f"{self.flagged_count} flagged in {self.duration_seconds:.3f}s",
            f"  Distribution: {dist_str}",
        ]
        for entry in self.high_risk_entries[:5]:
            lines.append(
                f"  ⚑ [{entry.memory_id[:12]}] {entry.source_type} "
                f"(conf={entry.source_confidence:.2f}): {entry.flag_reason}"
            )
        return "\n".join(lines)


# ---------------------------------------------------------------------------
# SourceMonitor
# ---------------------------------------------------------------------------

class SourceMonitor:
    """Tracks and audits memory provenance.

    Tags are stored in-memory as ``self._tags``.  They are not automatically
    persisted — call :meth:`tag` to assign provenance, and :meth:`auto_tag`
    to apply heuristic attribution to untagged memories.

    Parameters
    ----------
    memory:
        The :class:`HierarchicalMemory` instance.
    flag_threshold:
        Source-confidence below which a memory is flagged as confabulation
        risk during :meth:`audit` (default 0.5).
    """

    def __init__(
        self,
        memory: Any,
        flag_threshold: float = 0.5,
    ) -> None:
        self.memory = memory
        self.flag_threshold = flag_threshold
        self._tags: dict[str, SourceTag] = {}

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def tag(
        self,
        memory_id: str,
        source_type: str,
        confidence: float = 0.8,
        note: str = "",
    ) -> SourceTag:
        """Assign a provenance tag to a memory.

        Args:
            memory_id:   Memory ID to tag.
            source_type: One of the valid SOURCE_TYPES strings.
            confidence:  Confidence in the attribution (default 0.8).
            note:        Optional free-text provenance note.

        Returns:
            The created :class:`SourceTag`.

        Raises:
            ValueError: If ``source_type`` is not a recognised type.
        """
        if source_type not in SOURCE_TYPES:
            raise ValueError(
                f"Unknown source_type {source_type!r}. "
                f"Valid types: {sorted(SOURCE_TYPES)}"
            )
        tag = SourceTag(
            memory_id=memory_id,
            source_type=source_type,
            confidence=max(0.0, min(1.0, confidence)),
            provenance_note=note,
        )
        self._tags[memory_id] = tag
        return tag

    def get_tag(self, memory_id: str) -> Optional[SourceTag]:
        """Return the provenance tag for a memory, or ``None`` if untagged.

        Args:
            memory_id: Memory ID to look up.

        Returns:
            :class:`SourceTag` or ``None``.
        """
        return self._tags.get(memory_id)

    def auto_tag(self) -> int:
        """Apply heuristic source attribution to all untagged memories.

        Heuristics (in priority order):

        - domain ``"dream"`` → ``"dream"``, conf 0.90
        - domain ``"insight"`` → ``"insight"``, conf 0.85
        - domain ``"reflection"`` → ``"reflection"``, conf 0.90
        - title/content contains ``"reflection"`` or ``"lesson"`` → ``"reflection"``, conf 0.70
        - importance ≥ 0.85 → ``"instruction"`` (likely explicitly told), conf 0.60
        - Else → ``"observation"``, conf 0.50

        Returns:
            Number of memories newly tagged.
        """
        items = self._collect_all()
        tagged = 0
        for item in items:
            if item.id in self._tags:
                continue
            src_type, conf = self._infer_source(item)
            self._tags[item.id] = SourceTag(
                memory_id=item.id,
                source_type=src_type,
                confidence=conf,
                provenance_note="auto-tagged by heuristic",
            )
            tagged += 1
        return tagged

    def audit(self, max_flagged: int = 20) -> SourceReport:
        """Audit all memories for source uncertainty.

        Memories without a tag are assigned ``"unknown"`` with confidence 0
        for audit purposes (but are not permanently tagged).

        Args:
            max_flagged: Maximum high-risk entries to include (default 20).

        Returns:
            :class:`SourceReport` with distribution and flagged entries.
        """
        t0 = time.time()
        items = self._collect_all()
        distribution: dict[str, int] = {st: 0 for st in SOURCE_TYPES}
        high_risk: list[SourceAuditEntry] = []
        flagged = 0

        for item in items:
            tag = self._tags.get(item.id)
            if tag is None:
                # Temporarily infer
                src_type, conf = self._infer_source(item)
            else:
                src_type, conf = tag.source_type, tag.confidence

            distribution[src_type] = distribution.get(src_type, 0) + 1
            excerpt = (item.experience.content or "")[:80]

            is_flagged = (
                conf < self.flag_threshold
                or src_type == "unknown"
            )
            flag_reason = ""
            if src_type == "unknown":
                flag_reason = "source type unknown — possible confabulation risk"
            elif conf < self.flag_threshold:
                flag_reason = f"low source confidence ({conf:.2f} < {self.flag_threshold:.2f})"

            if is_flagged:
                flagged += 1
                if len(high_risk) < max_flagged:
                    high_risk.append(SourceAuditEntry(
                        memory_id=item.id,
                        source_type=src_type,
                        source_confidence=conf,
                        content_excerpt=excerpt,
                        flagged=True,
                        flag_reason=flag_reason,
                    ))

        return SourceReport(
            total_audited=len(items),
            flagged_count=flagged,
            source_distribution={k: v for k, v in distribution.items() if v > 0},
            high_risk_entries=high_risk,
            duration_seconds=time.time() - t0,
        )

    def source_profile(self) -> dict[str, int]:
        """Return the distribution of tagged source types.

        Only counts memories that have been explicitly or auto-tagged.

        Returns:
            Dict mapping source_type → count, sorted descending by count.
        """
        counter: dict[str, int] = {}
        for tag in self._tags.values():
            counter[tag.source_type] = counter.get(tag.source_type, 0) + 1
        return dict(sorted(counter.items(), key=lambda x: x[1], reverse=True))

    # ------------------------------------------------------------------
    # Persistence
    # ------------------------------------------------------------------

    def save_state(self, path: "str | Path") -> None:
        """Persist source tags to a JSON file (atomic write)."""
        import json
        import os
        import tempfile
        from pathlib import Path as _P

        path = _P(path)
        data = {"version": "0.15.0", "tags": [t.to_dict() for t in self._tags.values()]}
        tmp_fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
        try:
            with os.fdopen(tmp_fd, "w") as f:
                json.dump(data, f, indent=2)
                f.flush()
                os.fsync(f.fileno())
            os.replace(tmp_path, str(path))
        except Exception:
            try:
                os.unlink(tmp_path)
            except OSError:
                pass
            raise

    def load_state(self, path: "str | Path") -> bool:
        """Restore source tags from a JSON file."""
        import json
        from pathlib import Path as _P

        p = _P(path)
        if not p.exists():
            return False
        try:
            data = json.loads(p.read_text())
            tags = data.get("tags", data if isinstance(data, list) else [])
            self._tags = {d["memory_id"]: SourceTag.from_dict(d) for d in tags}
            return True
        except Exception:
            return False

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _infer_source(self, item: Any) -> tuple[str, float]:
        """Heuristically infer source type and confidence from item metadata."""
        domain = (getattr(item.experience, "domain", None) or "").lower()
        importance = getattr(item.experience, "importance", 0.0) or 0.0
        content = (item.experience.content or "").lower()
        title = (getattr(item.experience, "title", None) or "").lower()

        if domain == "dream":
            return "dream", 0.90
        if domain == "insight":
            return "insight", 0.85
        if domain == "reflection":
            return "reflection", 0.90

        # Keyword hints in content or title
        reflection_hints = {"reflection", "lesson", "pattern", "principle", "recurring"}
        if any(h in content[:200] or h in title for h in reflection_hints):
            return "reflection", 0.70

        instruction_hints = {"instruction", "told", "asked", "requested", "commanded"}
        if any(h in content[:200] for h in instruction_hints):
            return "instruction", 0.65

        inference_hints = {"therefore", "implies", "inferred", "conclude", "reasoning", "suggests"}
        if any(h in content[:200] for h in inference_hints):
            return "inference", 0.65

        if importance >= 0.85:
            return "instruction", 0.60

        return "observation", 0.50

    def _collect_all(self) -> list[Any]:
        items: list[Any] = []
        for tier in (self.memory.working, self.memory.short_term):
            items.extend(tier)
        for tier in (self.memory.long_term, self.memory.semantic):
            items.extend(tier.values())
        return items
