#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""
Fact Store: Persistent storage for declarative facts (Symposium P0)

Writes to: Hippocampus/Knowledge/auto_facts.json
Thread-safe via filelock (_acquire from brain_io).

NO CAP. Facts grow like all brain knowledge. Managed by:
- Dedup: exact fingerprint match → strengthen existing (bump consolidation_count + confidence)
- Temporal: same entity+type but different value → mark old as historical, keep both
- Quality pruning: handled by consolidation daemon (salience*confidence < 0.1 → archive)

Dependencies: Python stdlib + brain_io (Law 4)
"""

import hashlib
import json
import logging
from typing import Dict, List

from pulse.core.brain_io import _acquire
from pulse.core.brain_utils import FACTS_FILE, text_similarity
from pulse.core.pii_filter import redact_pii

try:
    from pulse.core.brain_db import insert_knowledge as _insert_knowledge_db
except Exception:
    _insert_knowledge_db = None

logger = logging.getLogger("pulse.brain.fact_store")


def _fact_fingerprint(fact: Dict) -> str:
    """Stable fingerprint: fact_type + first 50 chars of fact text."""
    key = f"{fact.get('fact_type', '')}:{fact.get('fact', '')[:50].lower().strip()}"
    return hashlib.md5(key.encode()).hexdigest()


def _entity_key(fact: Dict) -> str:
    """Key for entity+type matching (temporal evolution detection)."""
    entities = sorted(e.lower() for e in fact.get("entities", []))
    return f"{fact.get('fact_type', '')}:{'|'.join(entities)}"




def _check_graduation(item: dict) -> bool:
    """Promote episodic to semantic if accessed 5+ times with conf >= 0.7.

    Models how repeated episodic recall eventually becomes general knowledge
    (Squire 1992: episodic-to-semantic memory consolidation).
    """
    try:
        if (item.get("type") == "episodic" and
                item.get("access_count", 0) >= 5 and
                float(item.get("confidence", 0)) >= 0.7):
            from pulse.core.brain_db import get_conn
            conn = get_conn()
            conn.execute(
                "UPDATE facts SET type = 'semantic' WHERE id = ? AND type = 'episodic'",
                (item.get("id"),)
            )
            conn.commit()
            return True
    except Exception:
        pass
    return False

def load_facts() -> List[Dict]:
    """Load all facts from auto_facts.json. Returns empty list if missing."""
    if not FACTS_FILE.exists():
        return []
    try:
        data = json.loads(FACTS_FILE.read_text(encoding="utf-8"))
        if isinstance(data, list):
            return data
    except (json.JSONDecodeError, OSError) as e:
        logger.warning("Failed to load facts: %s", e)
    return []


def store_facts(facts: List[Dict], source: str = "bridge") -> int:
    """
    Append facts to auto_facts.json (thread-safe, deduplicated, no cap).

    Dedup behavior:
    - Exact fingerprint match → strengthen existing (bump consolidation_count, raise confidence)
    - Same entity+type but different text → temporal evolution (mark old as historical)
    - New fact → append

    Returns number of new facts written + existing facts strengthened.
    """
    if not facts:
        return 0

    FACTS_FILE.parent.mkdir(parents=True, exist_ok=True)
    written = 0

    with _acquire(FACTS_FILE):
        existing = load_facts()
        fp_index = {_fact_fingerprint(f): i for i, f in enumerate(existing)}
        entity_index: Dict[str, List[int]] = {}
        for i, f in enumerate(existing):
            ek = _entity_key(f)
            if ek and ek != ":":
                entity_index.setdefault(ek, []).append(i)

        for fact in facts:
            fp = _fact_fingerprint(fact)

            # Case 1: Exact match → strengthen via Bayesian update (Dimension E)
            if fp in fp_index:
                idx = fp_index[fp]
                existing[idx]["consolidation_count"] = existing[idx].get("consolidation_count", 1) + 1
                from pulse.core.uncertainty import bayesian_update, wilson_interval
                old_conf = existing[idx].get("confidence", 0.5)
                old_ec = existing[idx].get("evidence_count", 1)
                new_conf, new_ec = bayesian_update(old_conf, old_ec, True)
                existing[idx]["confidence"] = new_conf
                existing[idx]["evidence_count"] = new_ec
                existing[idx]["confidence_interval"] = list(wilson_interval(new_conf, new_ec))
                # Dimension D: reinforce temporal decay clock
                from pulse.core.temporal import reinforce
                reinforce(existing[idx])
                written += 1
                continue

            # PII redaction
            fact["fact"] = redact_pii(fact.get("fact", ""))
            fact["source"] = source
            fact.setdefault("salience", 2.0)
            fact.setdefault("consolidation_count", 1)

            # Case 2: Same entity+type → temporal evolution (Dimension D)
            ek = _entity_key(fact)
            if ek and ek != ":" and ek in entity_index:
                from pulse.core.temporal import validity_transition
                for old_idx in entity_index[ek]:
                    old = existing[old_idx]
                    cur_validity = old.get("validity", "valid")
                    if cur_validity not in ("historical", "superseded"):
                        sim = text_similarity(
                            old.get("fact", "")[:80].lower(),
                            fact.get("fact", "")[:80].lower()
                        )
                        if sim < 0.8:  # Different enough to be an evolution
                            new_state = validity_transition(cur_validity, "superseded")
                            old["validity"] = new_state or "historical"
                            old["superseded_by"] = _fact_fingerprint(fact)

            # Case 3: New fact → append with D+E fields
            from pulse.core.temporal import stamp_temporal_fields
            from pulse.core.uncertainty import stamp_uncertainty_fields
            fact.setdefault("type", "fact")
            stamp_temporal_fields(fact, "fact")
            stamp_uncertainty_fields(fact)
            existing.append(fact)
            new_idx = len(existing) - 1
            fp_index[fp] = new_idx
            entity_index.setdefault(ek, []).append(new_idx)
            written += 1

        # SQLite-first: try inserting all facts; fall back to JSON only if SQLite fails
        sqlite_ok = False
        if _insert_knowledge_db is not None:
            try:
                _sq_written = 0
                for fact in facts:
                    _insert_knowledge_db("facts", {
                        "content": fact.get("fact", ""),
                        "title": fact.get("fact", "")[:80],
                        "type": "fact",
                        "domain": fact.get("domain", "general"),
                        "agent": fact.get("source", source),
                        "timestamp": fact.get("timestamp", ""),
                        "salience": fact.get("salience", 2.0),
                        "confidence": fact.get("confidence", 0.5),
                        "consolidation_count": fact.get("consolidation_count", 1),
                        "evidence_count": fact.get("evidence_count", 1),
                        "tags": fact.get("entities", []),
                        "validity": fact.get("validity", "valid"),
                    })
                    _sq_written += 1
                sqlite_ok = _sq_written > 0
            except Exception:
                pass  # SQLite failure → fall through to JSON

        if not sqlite_ok:
            FACTS_FILE.write_text(
                json.dumps(existing, indent=2, ensure_ascii=False),
                encoding="utf-8"
            )

    if written > 0:
        logger.info("Stored %d facts (total: %d)", written, len(existing))

    return written
