#!/usr/bin/env python3
"""
EvidenceBus - Unified Write Path for Echo Evidence Stores

WO-ECHO-BRIDGE-03: Single write path that fans out to all evidence stores.

Usage:
    from helix_cdc.echo.tools.evidence_bus import EvidenceBus, EvidenceEvent

    bus = EvidenceBus()
    event = EvidenceEvent(
        event_id="abc123",
        timestamp_utc="2026-01-29T12:00:00Z",
        kind="note",
        category="learning",
        tags=["svd", "compression"],
        text="SVD on V/Output fails due to high rank",
        sha256="...",
        provenance={"source": "claude_session"},
    )
    receipts = bus.emit(event)
"""

import hashlib
import json
import sqlite3
import time
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional


# Default paths
PROJECT_ROOT = Path(__file__).resolve().parents[3]


@dataclass
class EvidenceEvent:
    """
    Canonical evidence event for the EvidenceBus.

    Represents any fact, note, checkpoint, or conversation piece
    that should be persisted across Echo's memory stores.
    """
    event_id: str          # sha256(text + category + session_id + ts_bucket)[:16]
    timestamp_utc: str     # ISO 8601 timestamp
    kind: str              # "note" | "checkpoint" | "conversation" | "receipt_ref"
    category: str          # learning | decision | context | warning | preference
    tags: List[str]
    text: str
    sha256: str            # hash of text
    provenance: Dict[str, Any]
    session_id: Optional[str] = None
    ttl_days: Optional[int] = None

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_note(cls, note: str, category: str, tags: List[str],
                  session_id: Optional[str] = None,
                  ttl_days: Optional[int] = None) -> "EvidenceEvent":
        """Factory method to create an EvidenceEvent from a note."""
        timestamp = datetime.now(timezone.utc)
        text_sha256 = hashlib.sha256(note.encode()).hexdigest()
        event_id = hashlib.sha256(
            f"{note}:{timestamp.isoformat()}".encode()
        ).hexdigest()[:16]

        return cls(
            event_id=event_id,
            timestamp_utc=timestamp.isoformat(),
            kind="note",
            category=category,
            tags=tags,
            text=note,
            sha256=text_sha256,
            provenance={"source": "claude_session"},
            session_id=session_id,
            ttl_days=ttl_days,
        )


class EvidenceBus:
    """
    Single write path that fans out to all evidence stores.

    Stores:
    1. notes/YYYY-MM-DD/note_<id>.json - JSON receipts
    2. notes/notes.sqlite + FTS5 - Fast search
    3. EchoMemory (if available) - Crash-resilient state
    """

    def __init__(
        self,
        project_root: Optional[Path] = None,
        echo_memory_db: Optional[Path] = None,
    ):
        self.project_root = Path(project_root) if project_root else PROJECT_ROOT
        self._notes_db_conn = None
        self._echo_memory = None
        self._echo_memory_db = echo_memory_db

    def _get_notes_db(self):
        """Get or create notes SQLite database with FTS5."""
        if self._notes_db_conn is not None:
            return self._notes_db_conn

        notes_dir = self.project_root / "notes"
        notes_dir.mkdir(exist_ok=True)

        db_path = notes_dir / "notes.sqlite"
        self._notes_db_conn = sqlite3.connect(str(db_path), check_same_thread=False)

        # Create tables if not exist
        cur = self._notes_db_conn.cursor()
        cur.execute("""
            CREATE TABLE IF NOT EXISTS notes (
                note_id TEXT PRIMARY KEY,
                note TEXT NOT NULL,
                category TEXT NOT NULL,
                tags TEXT,
                session_id TEXT,
                ttl_days INTEGER,
                created_at TEXT NOT NULL,
                expires_at TEXT,
                receipt_path TEXT,
                sha256 TEXT
            )
        """)

        # Create FTS5 virtual table for full-text search
        cur.execute("""
            CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
                note_id,
                note,
                category,
                tags,
                content=notes,
                content_rowid=rowid
            )
        """)

        # Triggers for FTS sync
        cur.execute("""
            CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
                INSERT INTO notes_fts(rowid, note_id, note, category, tags)
                VALUES (new.rowid, new.note_id, new.note, new.category, new.tags);
            END
        """)

        cur.execute("""
            CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
                INSERT INTO notes_fts(notes_fts, rowid, note_id, note, category, tags)
                VALUES('delete', old.rowid, old.note_id, old.note, old.category, old.tags);
                INSERT INTO notes_fts(rowid, note_id, note, category, tags)
                VALUES (new.rowid, new.note_id, new.note, new.category, new.tags);
            END
        """)

        cur.execute("""
            CREATE TRIGGER IF NOT EXISTS notes_ad AFTER DELETE ON notes BEGIN
                INSERT INTO notes_fts(notes_fts, rowid, note_id, note, category, tags)
                VALUES('delete', old.rowid, old.note_id, old.note, old.category, old.tags);
            END
        """)

        self._notes_db_conn.commit()
        return self._notes_db_conn

    def _get_echo_memory(self):
        """Get or initialize EchoMemory."""
        if self._echo_memory is not None:
            return self._echo_memory

        try:
            # Try helix-cdc local echo_memory first
            echo_memory_path = self.project_root / "echo_memory.py"
            if echo_memory_path.exists():
                import importlib.util
                spec = importlib.util.spec_from_file_location("echo_memory", echo_memory_path)
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                if self._echo_memory_db:
                    self._echo_memory = module.EchoMemory(self._echo_memory_db)
                else:
                    self._echo_memory = module.EchoMemory()
            else:
                # Fallback to installed module
                from echo_memory import EchoMemory
                if self._echo_memory_db:
                    self._echo_memory = EchoMemory(self._echo_memory_db)
                else:
                    self._echo_memory = EchoMemory()
            return self._echo_memory
        except Exception as e:
            print(f"[EvidenceBus] EchoMemory init failed: {e}")
            return None

    def _write_json_receipt(self, event: EvidenceEvent) -> Optional[str]:
        """Write JSON receipt file."""
        try:
            timestamp = datetime.fromisoformat(event.timestamp_utc.replace("Z", "+00:00"))
            date_str = timestamp.strftime("%Y-%m-%d")
            notes_dir = self.project_root / "notes" / date_str
            notes_dir.mkdir(parents=True, exist_ok=True)

            # Calculate expiration if TTL set
            expires_at = None
            if event.ttl_days:
                from datetime import timedelta
                expires_at = (timestamp + timedelta(days=event.ttl_days)).isoformat()

            receipt = {
                "type": f"echo_{event.kind}",
                "event_id": event.event_id,
                "timestamp_utc": event.timestamp_utc,
                "content": {
                    "text": event.text,
                    "category": event.category,
                    "tags": event.tags,
                },
                "provenance": event.provenance,
                "ttl_days": event.ttl_days,
                "expires_at": expires_at,
                "sha256": event.sha256,
            }

            receipt_path = notes_dir / f"{event.kind}_{event.event_id}.json"
            with open(receipt_path, "w") as f:
                json.dump(receipt, f, indent=2)

            return str(receipt_path.relative_to(self.project_root))
        except Exception as e:
            print(f"[EvidenceBus] JSON receipt failed: {e}")
            return None

    def _insert_notes_db(self, event: EvidenceEvent) -> bool:
        """Insert into notes.sqlite + FTS5."""
        try:
            conn = self._get_notes_db()
            cur = conn.cursor()

            timestamp = datetime.fromisoformat(event.timestamp_utc.replace("Z", "+00:00"))
            expires_at = None
            if event.ttl_days:
                from datetime import timedelta
                expires_at = (timestamp + timedelta(days=event.ttl_days)).isoformat()

            date_str = timestamp.strftime("%Y-%m-%d")
            receipt_path = f"notes/{date_str}/{event.kind}_{event.event_id}.json"

            cur.execute("""
                INSERT INTO notes (note_id, note, category, tags, session_id,
                                  ttl_days, created_at, expires_at, receipt_path, sha256)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """, (
                event.event_id,
                event.text,
                event.category,
                ",".join(event.tags),
                event.session_id,
                event.ttl_days,
                timestamp.isoformat(),
                expires_at,
                receipt_path,
                event.sha256,
            ))
            conn.commit()
            return True
        except sqlite3.IntegrityError:
            # Already exists
            return False
        except Exception as e:
            print(f"[EvidenceBus] notes.sqlite insert failed: {e}")
            return False

    def _checkpoint_echo_memory(self, event: EvidenceEvent) -> Optional[str]:
        """Checkpoint to EchoMemory."""
        echo_mem = self._get_echo_memory()
        if not echo_mem:
            return None

        try:
            key = f"{event.kind}:{event.event_id}"
            echo_mem.checkpoint(
                key=key,
                value={
                    "text": event.text,
                    "note": event.text,  # Alias for compatibility
                    "category": event.category,
                    "tags": event.tags,
                    "sha256": event.sha256,
                    "timestamp": event.timestamp_utc,
                    "session_id": event.session_id,
                }
            )

            # Also add as conversation message for Se tracking
            if event.session_id:
                echo_mem.add_message(
                    session_id=event.session_id,
                    role="claude",
                    content=event.text,
                    metadata={"category": event.category, "tags": event.tags}
                )

            return key
        except Exception as e:
            print(f"[EvidenceBus] EchoMemory checkpoint failed: {e}")
            return None

    def _write_bridge_receipt(
        self,
        event: EvidenceEvent,
        receipts: Dict[str, Any]
    ) -> Optional[str]:
        """Write bridge receipt linking all stores."""
        try:
            echo_mem = self._get_echo_memory()
            timestamp = datetime.fromisoformat(event.timestamp_utc.replace("Z", "+00:00"))
            date_str = timestamp.strftime("%Y-%m-%d")
            notes_dir = self.project_root / "notes" / date_str

            bridge_receipt = {
                "type": "echo_bridge",
                "event_id": event.event_id,
                "kind": event.kind,
                "stores": {
                    "json_receipt": receipts.get("json"),
                    "notes_db": str(self.project_root / "notes" / "notes.sqlite"),
                    "echo_memory_key": receipts.get("echo_memory"),
                    "echo_memory_db": str(echo_mem.db_path) if echo_mem and hasattr(echo_mem, 'db_path') else None,
                },
                "sha256": event.sha256,
                "timestamp_utc": event.timestamp_utc,
            }

            bridge_path = notes_dir / f"bridge_{event.event_id}.json"
            with open(bridge_path, "w") as f:
                json.dump(bridge_receipt, f, indent=2)

            return str(bridge_path.relative_to(self.project_root))
        except Exception as e:
            print(f"[EvidenceBus] Bridge receipt failed: {e}")
            return None

    def emit(self, event: EvidenceEvent) -> Dict[str, Optional[str]]:
        """
        Fan out event to all stores.

        Returns dict of receipt paths/keys for each store.
        """
        receipts = {}

        # 1. JSON receipt
        receipts["json"] = self._write_json_receipt(event)

        # 2. notes.sqlite + FTS5
        receipts["notes_db"] = "notes/notes.sqlite" if self._insert_notes_db(event) else None

        # 3. EchoMemory checkpoint
        receipts["echo_memory"] = self._checkpoint_echo_memory(event)

        # 4. Bridge receipt
        receipts["bridge"] = self._write_bridge_receipt(event, receipts)

        return receipts


# CLI for testing
if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="EvidenceBus CLI")
    parser.add_argument("--emit", action="store_true", help="Emit a test event")
    parser.add_argument("--note", type=str, default="Test note from CLI", help="Note text")
    parser.add_argument("--category", type=str, default="learning", help="Category")
    parser.add_argument("--tags", type=str, default="test,cli", help="Comma-separated tags")

    args = parser.parse_args()

    if args.emit:
        bus = EvidenceBus()
        event = EvidenceEvent.from_note(
            note=args.note,
            category=args.category,
            tags=args.tags.split(","),
        )
        receipts = bus.emit(event)
        print(f"Emitted event: {event.event_id}")
        print(json.dumps(receipts, indent=2))
