"""
Phase 4B: Structural Causal Models

Upgrade from flat edge list to true causal DAGs.
Handles DAG enforcement (cycle detection) and latent confounder generation.
"""
from typing import Tuple, List
from pulse.core.brain_db import get_conn
from pulse.core import event_bus

class CausalViolationError(Exception):
    pass

def enforce_dag(from_node: str, to_node: str, edge_type: str = "CAUSES") -> bool:
    """
    Checks if inserting (from_node -> to_node) creates a cycle.
    Raises CausalViolationError if cycle is detected.
    """
    if edge_type not in ["CAUSES", "PREVENTS"]:
        return True # Only causal edges need DAG enforcement
        
    conn = get_conn()
    
    # Fast cycle detection using Recursive CTE
    # We want to see if `to_node` can reach `from_node` (which would mean adding from->to creates a cycle)
    query = """
    WITH RECURSIVE
      search_path(node_id, path) AS (
        SELECT 
            ? as node_id, 
            ? as path
            
        UNION ALL
        
        SELECT 
            e.to_node,
            w.path || '->' || e.to_node
        FROM search_path w
        JOIN graph_edges e ON w.node_id = e.from_node
        WHERE e.edge_type IN ('CAUSES', 'PREVENTS')
          AND INSTR(w.path, e.to_node) = 0
      )
    SELECT 1 FROM search_path WHERE node_id = ? LIMIT 1;
    """
    with conn:
        res = conn.execute(query, (to_node, str(to_node), from_node)).fetchone()
        if res:
            raise CausalViolationError(f"Causal cycle detected: {to_node} already causes {from_node}.")
            
    return True

def add_causal_edge(from_node: str, to_node: str, causal_weight: float):
    """
    Adds a causal edge with a signed strength [-1.0, 1.0].
    """
    causal_weight = max(-1.0, min(1.0, causal_weight))
    edge_type = "CAUSES" if causal_weight >= 0 else "PREVENTS"
    enforce_dag(from_node, to_node, edge_type)
    
    conn = get_conn()
    with conn:
        conn.execute("""
            INSERT INTO graph_edges (from_node, to_node, edge_type, confidence, metadata)
            VALUES (?, ?, ?, ?, ?)
            ON CONFLICT(from_node, edge_type, to_node) DO UPDATE SET 
            confidence = MAX(confidence, excluded.confidence),
            metadata = excluded.metadata
        """, (from_node, to_node, edge_type, abs(causal_weight), f'{{"causal_weight": {causal_weight}}}'))

def detect_confounders() -> List[Tuple[str, str]]:
    """
    Scan for 2 nodes sharing 3+ common causes.
    Injects a Latent_Confounder node if found.
    Returns list of (NodeA, NodeB) pairs that were confounded.
    """
    conn = get_conn()
    query = """
    SELECT e1.to_node as node_a, e2.to_node as node_b, COUNT(e1.from_node) as shared_causes
    FROM graph_edges e1
    JOIN graph_edges e2 ON e1.from_node = e2.from_node AND e1.to_node < e2.to_node
    WHERE e1.edge_type IN ('CAUSES', 'PREVENTS') AND e2.edge_type IN ('CAUSES', 'PREVENTS')
    GROUP BY e1.to_node, e2.to_node
    HAVING shared_causes >= 3
    """
    
    with conn:
        res = conn.execute(query).fetchall()
        confounded_pairs = []
        for row in res[:50]:  # Cap to 50 confounders per run
            na, nb = row["node_a"], row["node_b"]

            # Create latent confounder
            latent_id = f"LATENT_CONFOUNDER_{na}_{nb}"

            # We don't overwrite if it already exists
            exists = conn.execute("SELECT 1 FROM graph_nodes WHERE id = ?", (latent_id,)).fetchone()
            if not exists:
                conn.execute("INSERT INTO graph_nodes (id, type, text) VALUES (?, 'LATENT', ?)",
                            (latent_id, f"Unobserved common cause of {na} and {nb}"))
                conn.execute("INSERT INTO graph_edges (from_node, to_node, edge_type, confidence) VALUES (?, ?, 'CAUSES', 0.5)",
                            (latent_id, na))
                conn.execute("INSERT INTO graph_edges (from_node, to_node, edge_type, confidence) VALUES (?, ?, 'CAUSES', 0.5)",
                            (latent_id, nb))

                confounded_pairs.append((na, nb))
                event_bus.publish("causal.confounder_detected", "causal_model", {"node_a": na, "node_b": nb, "latent_id": latent_id})

        return confounded_pairs
