"""
FGIP Causality Engine - Main API for the knowledge graph.

Append-only operations. Every edge starts as CLAIMED, becomes EVIDENCED when
a source artifact is attached with SHA256 verification.
"""

import hashlib
import json
import sqlite3
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple

from .models import (
    EdgeStatus,
    Sensitivity,
    AssertionLevel,
    Visibility,
    Node,
    Edge,
    Source,
    Claim,
    EdgeAttr,
    EdgeSource,
    EdgeStatusLog,
    SourceType,
    ClaimType,
)
from .schema import init_schema, get_schema_version, SCHEMA_VERSION
from .sources import hash_file, store_source, fetch_url, verify_source


class CausalityEngine:
    """
    Main API for the FGIP Causality Engine.

    All operations are append-only. Status changes are logged.
    """

    def __init__(self, db_path: Path, sources_dir: Path, receipts_dir: Optional[Path] = None):
        """
        Initialize the Causality Engine.

        Args:
            db_path: Path to SQLite database file
            sources_dir: Directory for source artifacts
            receipts_dir: Directory for operation receipts (optional)
        """
        self.db_path = Path(db_path)
        self.sources_dir = Path(sources_dir)
        self.receipts_dir = Path(receipts_dir) if receipts_dir else None

        # Ensure directories exist
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self.sources_dir.mkdir(parents=True, exist_ok=True)
        if self.receipts_dir:
            self.receipts_dir.mkdir(parents=True, exist_ok=True)

        # Connect to database
        self._conn = sqlite3.connect(str(self.db_path))
        self._conn.row_factory = sqlite3.Row

    def close(self):
        """Close the database connection."""
        if self._conn:
            self._conn.close()
            self._conn = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    # ========================================
    # Database Initialization
    # ========================================

    def init_db(self) -> dict:
        """
        Initialize the database schema.

        Returns:
            Receipt dict with initialization details
        """
        receipt = init_schema(self._conn)

        # Write receipt to file if receipts_dir is set
        if self.receipts_dir:
            self._write_receipt(receipt)

        return receipt

    def get_schema_version(self) -> Optional[str]:
        """Get the current schema version."""
        return get_schema_version(self._conn)

    # ========================================
    # Node Operations
    # ========================================

    def add_node(
        self,
        node_id: str,
        node_type: str,
        name: str,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> str:
        """
        Add a node to the graph.

        Args:
            node_id: Unique node identifier (e.g., "ORG:us_chamber")
            node_type: Node type (e.g., "ORGANIZATION", "PERSON")
            name: Human-readable name
            metadata: Optional metadata dict

        Returns:
            node_id
        """
        cursor = self._conn.cursor()

        # Create node object for normalization
        node = Node(node_id=node_id, node_type=node_type, name=name, metadata=metadata or {})

        cursor.execute("""
            INSERT INTO nodes (node_id, node_type, name, normalized_name, metadata, created_at, updated_at)
            VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
        """, (
            node.node_id,
            node.node_type,
            node.name,
            node.normalized_name,
            json.dumps(node.metadata) if node.metadata else None,
        ))

        self._conn.commit()

        # Create receipt
        receipt = self._create_receipt("add_node", "node", node_id, {
            "node_type": node_type,
            "name": name,
        })
        self._store_receipt(receipt)

        return node_id

    def get_node(self, node_id: str) -> Optional[Node]:
        """Get a node by ID."""
        cursor = self._conn.cursor()
        cursor.execute("SELECT * FROM nodes WHERE node_id = ?", (node_id,))
        row = cursor.fetchone()

        if not row:
            return None

        return Node(
            node_id=row['node_id'],
            node_type=row['node_type'],
            name=row['name'],
            normalized_name=row['normalized_name'],
            metadata=json.loads(row['metadata']) if row['metadata'] else {},
            created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None,
            updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None,
        )

    def list_nodes(self, node_type: Optional[str] = None) -> List[Node]:
        """List all nodes, optionally filtered by type."""
        cursor = self._conn.cursor()

        if node_type:
            cursor.execute("SELECT * FROM nodes WHERE node_type = ?", (node_type,))
        else:
            cursor.execute("SELECT * FROM nodes")

        nodes = []
        for row in cursor.fetchall():
            nodes.append(Node(
                node_id=row['node_id'],
                node_type=row['node_type'],
                name=row['name'],
                normalized_name=row['normalized_name'],
                metadata=json.loads(row['metadata']) if row['metadata'] else {},
                created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None,
                updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None,
            ))

        return nodes

    # ========================================
    # Edge Operations
    # ========================================

    def add_edge(
        self,
        edge_type: str,
        from_node: str,
        to_node: str,
        edge_id: Optional[str] = None,
        status: EdgeStatus = EdgeStatus.CLAIMED,
        sensitivity: Sensitivity = Sensitivity.LOW,
        assertion_level: AssertionLevel = AssertionLevel.INFERENCE,
        visibility: Visibility = Visibility.PRIVATE,
        confidence: Optional[float] = None,
        date_occurred_start: Optional[datetime] = None,
        date_occurred_end: Optional[datetime] = None,
        notes: Optional[str] = None,
        attrs: Optional[Dict[str, Any]] = None,
    ) -> str:
        """
        Add an edge to the graph.

        Without a source, edge status is CLAIMED.

        Args:
            edge_type: Relationship type (e.g., "LOBBIED", "FUNDED")
            from_node: Source node ID
            to_node: Target node ID
            edge_id: Optional edge ID (auto-generated if not provided)
            status: Initial status (default: CLAIMED)
            sensitivity: Sensitivity level (default: LOW)
            assertion_level: FACT, INFERENCE, or HYPOTHESIS (default: INFERENCE)
            visibility: PRIVATE or PUBLIC (default: PRIVATE)
            confidence: Confidence score 0-1 (optional)
            date_occurred_start: Start date of relationship (optional)
            date_occurred_end: End date of relationship (optional)
            notes: Free-text notes (optional)
            attrs: Key-value attributes (optional)

        Returns:
            edge_id
        """
        # Lint notes for catnip phrases
        if notes:
            from .narrative import lint_notes
            is_clean, violations = lint_notes(notes)
            if not is_clean:
                # Auto-sanitize or warn
                from .narrative import sanitize_text
                notes, _ = sanitize_text(notes)

        cursor = self._conn.cursor()

        # Generate edge_id if not provided
        if edge_id is None:
            edge_id = f"edge_{uuid.uuid4().hex[:16]}"

        # Insert edge
        cursor.execute("""
            INSERT INTO edges (
                edge_id, edge_type, from_node_id, to_node_id,
                status, sensitivity, assertion_level, visibility, confidence,
                date_occurred_start, date_occurred_end, notes,
                created_at, updated_at
            )
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
        """, (
            edge_id,
            edge_type,
            from_node,
            to_node,
            status.value,
            sensitivity.value,
            assertion_level.value,
            visibility.value,
            confidence,
            date_occurred_start.isoformat() if date_occurred_start else None,
            date_occurred_end.isoformat() if date_occurred_end else None,
            notes,
        ))

        # Insert attributes
        if attrs:
            for key, value in attrs.items():
                value_type = "string"
                if isinstance(value, int):
                    value_type = "int"
                elif isinstance(value, float):
                    value_type = "float"
                elif isinstance(value, (dict, list)):
                    value = json.dumps(value)
                    value_type = "json"

                cursor.execute("""
                    INSERT INTO edge_attrs (edge_id, key, value, value_type)
                    VALUES (?, ?, ?, ?)
                """, (edge_id, key, str(value), value_type))

        self._conn.commit()

        # Create receipt
        receipt = self._create_receipt("add_edge", "edge", edge_id, {
            "edge_type": edge_type,
            "from_node": from_node,
            "to_node": to_node,
            "status": status.value,
        })
        self._store_receipt(receipt)

        return edge_id

    def get_edge(self, edge_id: str) -> Optional[Edge]:
        """Get an edge by ID with its attributes."""
        cursor = self._conn.cursor()
        cursor.execute("SELECT * FROM edges WHERE edge_id = ?", (edge_id,))
        row = cursor.fetchone()

        if not row:
            return None

        # Get attributes
        cursor.execute("SELECT * FROM edge_attrs WHERE edge_id = ?", (edge_id,))
        attrs = [
            EdgeAttr(
                edge_id=r['edge_id'],
                key=r['key'],
                value=r['value'],
                value_type=r['value_type'],
            )
            for r in cursor.fetchall()
        ]

        # Get source IDs
        cursor.execute("SELECT source_id FROM edge_sources WHERE edge_id = ?", (edge_id,))
        source_ids = [r['source_id'] for r in cursor.fetchall()]

        # Parse assertion_level and visibility (with defaults for legacy data)
        assertion_level_str = row['assertion_level'] if 'assertion_level' in row.keys() else 'INFERENCE'
        visibility_str = row['visibility'] if 'visibility' in row.keys() else 'PRIVATE'

        return Edge(
            edge_id=row['edge_id'],
            edge_type=row['edge_type'],
            from_node_id=row['from_node_id'],
            to_node_id=row['to_node_id'],
            status=EdgeStatus(row['status']),
            sensitivity=Sensitivity(row['sensitivity']),
            assertion_level=AssertionLevel(assertion_level_str) if assertion_level_str else AssertionLevel.INFERENCE,
            visibility=Visibility(visibility_str) if visibility_str else Visibility.PRIVATE,
            confidence=row['confidence'],
            date_occurred_start=datetime.fromisoformat(row['date_occurred_start']) if row['date_occurred_start'] else None,
            date_occurred_end=datetime.fromisoformat(row['date_occurred_end']) if row['date_occurred_end'] else None,
            notes=row['notes'],
            created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None,
            updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None,
            attrs=attrs,
            source_ids=source_ids,
        )

    def list_edges(
        self,
        edge_type: Optional[str] = None,
        from_node: Optional[str] = None,
        to_node: Optional[str] = None,
        status: Optional[EdgeStatus] = None,
    ) -> List[Edge]:
        """List edges with optional filters."""
        cursor = self._conn.cursor()

        query = "SELECT * FROM edges WHERE 1=1"
        params = []

        if edge_type:
            query += " AND edge_type = ?"
            params.append(edge_type)
        if from_node:
            query += " AND from_node_id = ?"
            params.append(from_node)
        if to_node:
            query += " AND to_node_id = ?"
            params.append(to_node)
        if status:
            query += " AND status = ?"
            params.append(status.value)

        cursor.execute(query, params)

        edges = []
        for row in cursor.fetchall():
            edge = self.get_edge(row['edge_id'])
            if edge:
                edges.append(edge)

        return edges

    def update_edge_status(
        self,
        edge_id: str,
        new_status: EdgeStatus,
        reason: Optional[str] = None,
        actor: Optional[str] = None,
    ) -> bool:
        """
        Update edge status with logging (append-only).

        Args:
            edge_id: Edge to update
            new_status: New status
            reason: Reason for change
            actor: Who made the change

        Returns:
            True if updated, False if edge not found
        """
        cursor = self._conn.cursor()

        # Get current status
        cursor.execute("SELECT status FROM edges WHERE edge_id = ?", (edge_id,))
        row = cursor.fetchone()

        if not row:
            return False

        old_status = row['status']

        # Log status change (append-only)
        log_id = f"log_{uuid.uuid4().hex[:16]}"
        cursor.execute("""
            INSERT INTO edge_status_log (log_id, edge_id, old_status, new_status, reason, actor, timestamp)
            VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
        """, (log_id, edge_id, old_status, new_status.value, reason, actor))

        # Update edge
        cursor.execute("""
            UPDATE edges SET status = ?, updated_at = datetime('now')
            WHERE edge_id = ?
        """, (new_status.value, edge_id))

        self._conn.commit()

        # Create receipt
        receipt = self._create_receipt("update_edge_status", "edge", edge_id, {
            "old_status": old_status,
            "new_status": new_status.value,
            "reason": reason,
            "actor": actor,
        })
        self._store_receipt(receipt)

        return True

    # ========================================
    # Source Operations
    # ========================================

    def add_source(
        self,
        source_type: SourceType = SourceType.OTHER,
        title: Optional[str] = None,
        author: Optional[str] = None,
        publisher: Optional[str] = None,
        date_published: Optional[datetime] = None,
        url: Optional[str] = None,
        file_path: Optional[Path] = None,
        tier: int = 2,
        notes: Optional[str] = None,
    ) -> Tuple[str, Optional[str]]:
        """
        Add a source to the database.

        If file_path is provided, stores the file by SHA256.
        If url is provided, fetches and stores the content.

        Args:
            source_type: Type of source
            title: Source title
            author: Author name
            publisher: Publisher name
            date_published: Publication date
            url: Source URL (will be fetched)
            file_path: Local file path (will be stored)
            tier: Source tier (0=primary, 1=secondary, 2=tertiary)
            notes: Notes about the source

        Returns:
            Tuple of (source_id, sha256)
        """
        cursor = self._conn.cursor()

        source_id = None
        sha256 = None
        stored_path = None

        # Handle file storage
        if file_path:
            file_path = Path(file_path)
            source_id, sha256 = store_source(file_path, self.sources_dir)
            stored_path = f"{sha256[:2]}/{sha256}{file_path.suffix}"

        # Handle URL fetch
        elif url:
            try:
                source_id, sha256, stored_path = fetch_url(url, self.sources_dir)
            except Exception as e:
                # Create source without content
                source_id = f"src_{uuid.uuid4().hex[:16]}"
                notes = f"{notes or ''}\n[Fetch failed: {e}]".strip()

        else:
            source_id = f"src_{uuid.uuid4().hex[:16]}"

        # Insert source
        cursor.execute("""
            INSERT INTO sources (
                source_id, source_type, title, author, publisher,
                date_published, date_accessed, url, file_path, sha256,
                tier, notes, created_at
            )
            VALUES (?, ?, ?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?, datetime('now'))
        """, (
            source_id,
            source_type.value,
            title,
            author,
            publisher,
            date_published.isoformat() if date_published else None,
            url,
            stored_path,
            sha256,
            tier,
            notes,
        ))

        self._conn.commit()

        # Create receipt
        receipt = self._create_receipt("add_source", "source", source_id, {
            "source_type": source_type.value,
            "sha256": sha256,
            "url": url,
        })
        self._store_receipt(receipt)

        return source_id, sha256

    def get_source(self, source_id: str) -> Optional[Source]:
        """Get a source by ID."""
        cursor = self._conn.cursor()
        cursor.execute("SELECT * FROM sources WHERE source_id = ?", (source_id,))
        row = cursor.fetchone()

        if not row:
            return None

        return Source(
            source_id=row['source_id'],
            source_type=SourceType(row['source_type']),
            title=row['title'],
            author=row['author'],
            publisher=row['publisher'],
            date_published=datetime.fromisoformat(row['date_published']) if row['date_published'] else None,
            date_accessed=datetime.fromisoformat(row['date_accessed']) if row['date_accessed'] else None,
            url=row['url'],
            file_path=row['file_path'],
            sha256=row['sha256'],
            tier=row['tier'],
            notes=row['notes'],
            created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None,
        )

    def attach_source(
        self,
        edge_id: str,
        source_id: Optional[str] = None,
        url: Optional[str] = None,
        file: Optional[Path] = None,
        claim_excerpt: Optional[str] = None,
        page_ref: Optional[str] = None,
        promote_to_evidenced: bool = True,
    ) -> dict:
        """
        Attach a source to an edge.

        If promote_to_evidenced=True and the source has a SHA256,
        the edge status is promoted to EVIDENCED.

        Args:
            edge_id: Edge to attach source to
            source_id: Existing source ID (optional)
            url: URL to fetch as new source (optional)
            file: File path to store as new source (optional)
            claim_excerpt: Quote from source supporting edge
            page_ref: Page number or section reference
            promote_to_evidenced: Auto-promote to EVIDENCED if source has SHA256

        Returns:
            Receipt dict with operation details
        """
        cursor = self._conn.cursor()

        # Create source if needed
        if source_id is None:
            if url:
                source_id, sha256 = self.add_source(url=url)
            elif file:
                source_id, sha256 = self.add_source(file_path=file)
            else:
                raise ValueError("Must provide source_id, url, or file")
        else:
            # Get existing source
            source = self.get_source(source_id)
            sha256 = source.sha256 if source else None

        # Link edge to source
        cursor.execute("""
            INSERT OR REPLACE INTO edge_sources (edge_id, source_id, claim_excerpt, page_ref, added_at)
            VALUES (?, ?, ?, ?, datetime('now'))
        """, (edge_id, source_id, claim_excerpt, page_ref))

        self._conn.commit()

        # Promote to EVIDENCED if source has SHA256
        promoted = False
        if promote_to_evidenced and sha256:
            edge = self.get_edge(edge_id)
            if edge and edge.status == EdgeStatus.CLAIMED:
                self.update_edge_status(
                    edge_id,
                    EdgeStatus.EVIDENCED,
                    reason=f"Source attached with SHA256: {sha256[:16]}...",
                    actor="CausalityEngine",
                )
                promoted = True

        # Create receipt
        receipt = self._create_receipt("attach_source", "edge", edge_id, {
            "source_id": source_id,
            "sha256": sha256,
            "promoted_to_evidenced": promoted,
            "claim_excerpt": claim_excerpt[:100] if claim_excerpt else None,
        })
        self._store_receipt(receipt)

        return receipt

    # ========================================
    # Claim Operations
    # ========================================

    def add_claim(
        self,
        claim_text: str,
        claim_type: ClaimType = ClaimType.OTHER,
        required_tier: int = 2,
        edge_ids: Optional[List[str]] = None,
    ) -> str:
        """
        Add a high-level claim that may span multiple edges.

        Args:
            claim_text: The claim statement
            claim_type: Type of claim
            required_tier: Minimum source tier needed
            edge_ids: List of edges supporting this claim

        Returns:
            claim_id
        """
        cursor = self._conn.cursor()

        claim_id = f"claim_{uuid.uuid4().hex[:16]}"

        cursor.execute("""
            INSERT INTO claims (claim_id, claim_text, claim_type, required_tier, status, created_at)
            VALUES (?, ?, ?, ?, 'CLAIMED', datetime('now'))
        """, (claim_id, claim_text, claim_type.value, required_tier))

        # Link edges
        if edge_ids:
            for edge_id in edge_ids:
                cursor.execute("""
                    INSERT INTO claim_edges (claim_id, edge_id)
                    VALUES (?, ?)
                """, (claim_id, edge_id))

        self._conn.commit()

        # Create receipt
        receipt = self._create_receipt("add_claim", "claim", claim_id, {
            "claim_type": claim_type.value,
            "edge_count": len(edge_ids) if edge_ids else 0,
        })
        self._store_receipt(receipt)

        return claim_id

    def get_claim(self, claim_id: str) -> Optional[Claim]:
        """Get a claim by ID."""
        cursor = self._conn.cursor()
        cursor.execute("SELECT * FROM claims WHERE claim_id = ?", (claim_id,))
        row = cursor.fetchone()

        if not row:
            return None

        # Get linked edges
        cursor.execute("SELECT edge_id FROM claim_edges WHERE claim_id = ?", (claim_id,))
        edge_ids = [r['edge_id'] for r in cursor.fetchall()]

        return Claim(
            claim_id=row['claim_id'],
            claim_text=row['claim_text'],
            claim_type=ClaimType(row['claim_type']),
            required_tier=row['required_tier'],
            status=EdgeStatus(row['status']),
            created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None,
            edge_ids=edge_ids,
        )

    # ========================================
    # Receipt Management
    # ========================================

    def _create_receipt(
        self,
        operation: str,
        target_type: str,
        target_id: str,
        details: dict,
    ) -> dict:
        """Create an operation receipt."""
        receipt_id = f"rcpt_{uuid.uuid4().hex[:16]}"
        timestamp = datetime.utcnow().isoformat()

        details_json = json.dumps(details, sort_keys=True)
        sha256 = hashlib.sha256(details_json.encode()).hexdigest()

        return {
            "receipt_id": receipt_id,
            "operation": operation,
            "target_type": target_type,
            "target_id": target_id,
            "details": details,
            "sha256": sha256,
            "timestamp": timestamp,
        }

    def _store_receipt(self, receipt: dict):
        """Store receipt in database and optionally to file."""
        cursor = self._conn.cursor()

        cursor.execute("""
            INSERT INTO operation_receipts (
                receipt_id, operation, target_type, target_id, details, sha256, timestamp
            )
            VALUES (?, ?, ?, ?, ?, ?, ?)
        """, (
            receipt['receipt_id'],
            receipt['operation'],
            receipt['target_type'],
            receipt['target_id'],
            json.dumps(receipt['details']),
            receipt['sha256'],
            receipt['timestamp'],
        ))

        self._conn.commit()

    def _write_receipt(self, receipt: dict):
        """Write receipt to file in receipts_dir."""
        if not self.receipts_dir:
            return

        filename = f"{receipt['receipt_id']}.json"
        path = self.receipts_dir / filename

        with open(path, 'w') as f:
            json.dump(receipt, f, indent=2)

    # ========================================
    # Statistics
    # ========================================

    def get_stats(self) -> dict:
        """Get database statistics."""
        cursor = self._conn.cursor()

        stats = {}

        # Node counts
        cursor.execute("SELECT COUNT(*) as count FROM nodes")
        stats['total_nodes'] = cursor.fetchone()['count']

        cursor.execute("SELECT node_type, COUNT(*) as count FROM nodes GROUP BY node_type")
        stats['nodes_by_type'] = {row['node_type']: row['count'] for row in cursor.fetchall()}

        # Edge counts
        cursor.execute("SELECT COUNT(*) as count FROM edges")
        stats['total_edges'] = cursor.fetchone()['count']

        cursor.execute("SELECT status, COUNT(*) as count FROM edges GROUP BY status")
        stats['edges_by_status'] = {row['status']: row['count'] for row in cursor.fetchall()}

        cursor.execute("SELECT edge_type, COUNT(*) as count FROM edges GROUP BY edge_type")
        stats['edges_by_type'] = {row['edge_type']: row['count'] for row in cursor.fetchall()}

        # Source counts
        cursor.execute("SELECT COUNT(*) as count FROM sources")
        stats['total_sources'] = cursor.fetchone()['count']

        cursor.execute("SELECT COUNT(*) as count FROM sources WHERE sha256 IS NOT NULL")
        stats['sources_with_sha256'] = cursor.fetchone()['count']

        # Claim counts
        cursor.execute("SELECT COUNT(*) as count FROM claims")
        stats['total_claims'] = cursor.fetchone()['count']

        return stats
