#!/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).

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

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

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

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

MAX_FACTS = 500  # FIFO eviction cap


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 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).

    Returns number of facts actually written (after dedup).
    """
    if not facts:
        return 0

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

    with _acquire(FACTS_FILE):
        existing = load_facts()
        existing_fps = {_fact_fingerprint(f) for f in existing}

        for fact in facts:
            fp = _fact_fingerprint(fact)
            if fp in existing_fps:
                continue

            # PII redaction
            fact["fact"] = redact_pii(fact.get("fact", ""))

            # Attach source + storage metadata
            fact["source"] = source
            fact.setdefault("salience", 2.0)
            fact.setdefault("consolidation_count", 1)

            existing.append(fact)
            existing_fps.add(fp)
            written += 1

        # FIFO eviction: remove lowest-salience items when over cap
        if len(existing) > MAX_FACTS:
            existing.sort(key=lambda f: f.get("salience", 0), reverse=True)
            existing = existing[:MAX_FACTS]

        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
