"""
FGIP Graph Queries - Causality tracing, cycle detection, contradiction checks.

All queries return auditable results with source coverage scores.
"""

import sqlite3
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Dict, Any, Optional, Set, Tuple

from .models import EdgeStatus, Sensitivity


@dataclass
class PathResult:
    """Result of a causality trace query."""
    path: List[str]                    # Node IDs in order
    edges: List[str]                   # Edge IDs in order
    total_length: int
    source_coverage: float             # Fraction of edges with sources
    evidenced_count: int               # Edges with EVIDENCED status
    claimed_count: int                 # Edges with CLAIMED status
    weakest_confidence: Optional[float] = None
    edge_details: List[Dict] = field(default_factory=list)


@dataclass
class OwnershipLoopResult:
    """Result of an ownership loop detection."""
    has_cycle: bool
    cycle_path: List[str] = field(default_factory=list)
    cycle_edges: List[str] = field(default_factory=list)
    cycle_length: int = 0


@dataclass
class ContradictionResult:
    """Result of a contradiction check."""
    entity: str
    contradictions: List[Dict] = field(default_factory=list)
    warning_count: int = 0
    error_count: int = 0


@dataclass
class CorrectionScoreResult:
    """Result of a correction score computation."""
    entity: str
    score: float                       # 0.0 - 1.0
    total_edges: int
    evidenced_edges: int
    claimed_edges: int
    disputed_edges: int
    retracted_edges: int
    source_coverage: float
    tier_breakdown: Dict[int, int] = field(default_factory=dict)
    audit_trail: List[Dict] = field(default_factory=list)


def trace_causality(
    conn: sqlite3.Connection,
    start_node: str,
    end_node: str,
    max_depth: int = 10,
    edge_types: Optional[List[str]] = None,
    require_evidenced: bool = False,
) -> List[PathResult]:
    """
    Find causal paths between two nodes using BFS.

    Args:
        conn: SQLite connection
        start_node: Starting node ID
        end_node: Target node ID
        max_depth: Maximum path length
        edge_types: Optional list of edge types to follow
        require_evidenced: Only follow EVIDENCED edges

    Returns:
        List of PathResult objects, sorted by source coverage
    """
    cursor = conn.cursor()
    cursor.row_factory = sqlite3.Row

    # BFS to find all paths
    paths = []
    queue = deque([(start_node, [start_node], [], 0)])  # (current, path, edges, depth)
    visited_paths = set()

    while queue:
        current, path, edges, depth = queue.popleft()

        if depth >= max_depth:
            continue

        # Build query for outgoing edges
        query = "SELECT * FROM edges WHERE from_node_id = ?"
        params = [current]

        if edge_types:
            placeholders = ','.join('?' * len(edge_types))
            query += f" AND edge_type IN ({placeholders})"
            params.extend(edge_types)

        if require_evidenced:
            query += " AND status = 'EVIDENCED'"

        cursor.execute(query, params)

        for row in cursor.fetchall():
            next_node = row['to_node_id']
            edge_id = row['edge_id']

            # Avoid cycles within a single path
            if next_node in path:
                continue

            new_path = path + [next_node]
            new_edges = edges + [edge_id]

            # Path signature to avoid duplicates
            path_sig = tuple(new_edges)
            if path_sig in visited_paths:
                continue
            visited_paths.add(path_sig)

            if next_node == end_node:
                # Found a path - compute coverage
                result = _compute_path_coverage(cursor, new_path, new_edges)
                paths.append(result)
            else:
                queue.append((next_node, new_path, new_edges, depth + 1))

    # Sort by source coverage (descending)
    paths.sort(key=lambda p: p.source_coverage, reverse=True)

    return paths


def _compute_path_coverage(
    cursor: sqlite3.Cursor,
    path: List[str],
    edges: List[str],
) -> PathResult:
    """Compute source coverage and confidence for a path."""
    evidenced = 0
    claimed = 0
    with_sources = 0
    min_confidence = None
    edge_details = []

    for edge_id in edges:
        cursor.execute("SELECT * FROM edges WHERE edge_id = ?", (edge_id,))
        edge_row = cursor.fetchone()

        if edge_row['status'] == 'EVIDENCED':
            evidenced += 1
        elif edge_row['status'] == 'CLAIMED':
            claimed += 1

        # Check for sources
        cursor.execute("SELECT COUNT(*) as count FROM edge_sources WHERE edge_id = ?", (edge_id,))
        source_count = cursor.fetchone()['count']
        if source_count > 0:
            with_sources += 1

        # Track minimum confidence
        if edge_row['confidence'] is not None:
            if min_confidence is None or edge_row['confidence'] < min_confidence:
                min_confidence = edge_row['confidence']

        edge_details.append({
            "edge_id": edge_id,
            "edge_type": edge_row['edge_type'],
            "status": edge_row['status'],
            "source_count": source_count,
            "confidence": edge_row['confidence'],
        })

    total = len(edges)
    coverage = with_sources / total if total > 0 else 0.0

    return PathResult(
        path=path,
        edges=edges,
        total_length=total,
        source_coverage=coverage,
        evidenced_count=evidenced,
        claimed_count=claimed,
        weakest_confidence=min_confidence,
        edge_details=edge_details,
    )


def ownership_loop(
    conn: sqlite3.Connection,
    entity: str,
    ownership_types: Optional[List[str]] = None,
) -> OwnershipLoopResult:
    """
    Detect ownership cycles starting from an entity.

    Follows OWNS_SHARES, INVESTED_IN, CONTROLS edges by default.

    Args:
        conn: SQLite connection
        entity: Starting node ID
        ownership_types: Edge types to follow (default: ownership-related)

    Returns:
        OwnershipLoopResult
    """
    if ownership_types is None:
        ownership_types = ['OWNS_SHARES', 'INVESTED_IN', 'CONTROLS', 'SUBSIDIARY_OF']

    cursor = conn.cursor()
    cursor.row_factory = sqlite3.Row

    # DFS for cycle detection
    visited = set()
    path = []
    path_edges = []

    def dfs(node: str) -> Optional[Tuple[List[str], List[str]]]:
        if node in visited:
            # Found cycle - find where it starts
            try:
                cycle_start = path.index(node)
                return (path[cycle_start:] + [node], path_edges[cycle_start:])
            except ValueError:
                return None

        visited.add(node)
        path.append(node)

        # Query outgoing ownership edges
        placeholders = ','.join('?' * len(ownership_types))
        cursor.execute(f"""
            SELECT edge_id, to_node_id FROM edges
            WHERE from_node_id = ? AND edge_type IN ({placeholders})
            AND status != 'RETRACTED'
        """, [node] + ownership_types)

        for row in cursor.fetchall():
            path_edges.append(row['edge_id'])
            result = dfs(row['to_node_id'])
            if result:
                return result
            path_edges.pop()

        path.pop()
        return None

    cycle = dfs(entity)

    if cycle:
        cycle_path, cycle_edges = cycle
        return OwnershipLoopResult(
            has_cycle=True,
            cycle_path=cycle_path,
            cycle_edges=cycle_edges,
            cycle_length=len(cycle_edges),
        )
    else:
        return OwnershipLoopResult(has_cycle=False)


def contradiction_check(
    conn: sqlite3.Connection,
    entity: Optional[str] = None,
) -> List[ContradictionResult]:
    """
    Check for contradictions in the graph.

    Rules checked:
    1. Entity cannot own >100% of another entity (sum of OWNS_SHARES)
    2. Entity cannot be both SUBSIDIARY_OF and PARENT_OF same entity
    3. DISPUTED edges should have corresponding EVIDENCED edges
    4. Temporal contradictions (edge dates don't make sense)

    Args:
        conn: SQLite connection
        entity: Optional entity to check (None = check all)

    Returns:
        List of ContradictionResult objects
    """
    cursor = conn.cursor()
    cursor.row_factory = sqlite3.Row

    results = []

    # Get entities to check
    if entity:
        entities = [entity]
    else:
        cursor.execute("SELECT DISTINCT node_id FROM nodes")
        entities = [row['node_id'] for row in cursor.fetchall()]

    for ent in entities:
        contradictions = []

        # Rule 1: Ownership >100%
        cursor.execute("""
            SELECT to_node_id, SUM(CAST(value AS REAL)) as total_pct
            FROM edges e
            JOIN edge_attrs a ON e.edge_id = a.edge_id
            WHERE e.from_node_id = ? AND e.edge_type = 'OWNS_SHARES'
            AND a.key = 'percentage' AND e.status != 'RETRACTED'
            GROUP BY to_node_id
            HAVING total_pct > 100
        """, (ent,))

        for row in cursor.fetchall():
            contradictions.append({
                "type": "OWNERSHIP_EXCEEDS_100",
                "severity": "ERROR",
                "target": row['to_node_id'],
                "total_percentage": row['total_pct'],
                "message": f"Ownership of {row['to_node_id']} exceeds 100% ({row['total_pct']}%)",
            })

        # Rule 2: Bidirectional parent/subsidiary
        cursor.execute("""
            SELECT e1.to_node_id
            FROM edges e1
            JOIN edges e2 ON e1.to_node_id = e2.from_node_id
            WHERE e1.from_node_id = ?
            AND e1.edge_type = 'SUBSIDIARY_OF'
            AND e2.edge_type = 'SUBSIDIARY_OF'
            AND e2.to_node_id = ?
            AND e1.status != 'RETRACTED'
            AND e2.status != 'RETRACTED'
        """, (ent, ent))

        for row in cursor.fetchall():
            contradictions.append({
                "type": "CIRCULAR_SUBSIDIARY",
                "severity": "ERROR",
                "target": row['to_node_id'],
                "message": f"Circular subsidiary relationship with {row['to_node_id']}",
            })

        # Rule 3: DISPUTED without corresponding evidence
        cursor.execute("""
            SELECT e.edge_id, e.edge_type, e.to_node_id
            FROM edges e
            WHERE e.from_node_id = ?
            AND e.status = 'DISPUTED'
            AND NOT EXISTS (
                SELECT 1 FROM edge_sources es WHERE es.edge_id = e.edge_id
            )
        """, (ent,))

        for row in cursor.fetchall():
            contradictions.append({
                "type": "DISPUTED_WITHOUT_EVIDENCE",
                "severity": "WARNING",
                "edge_id": row['edge_id'],
                "message": f"DISPUTED edge {row['edge_type']}->{row['to_node_id']} has no sources",
            })

        if contradictions:
            results.append(ContradictionResult(
                entity=ent,
                contradictions=contradictions,
                warning_count=sum(1 for c in contradictions if c['severity'] == 'WARNING'),
                error_count=sum(1 for c in contradictions if c['severity'] == 'ERROR'),
            ))

    return results


def correction_score(
    conn: sqlite3.Connection,
    entity: str,
    include_outgoing: bool = True,
    include_incoming: bool = True,
) -> CorrectionScoreResult:
    """
    Compute an auditable correction score for an entity.

    The score reflects how well-sourced and verified the entity's
    relationships are. Higher = more trustworthy.

    Score components:
    - 50% edge status (EVIDENCED = 1.0, CLAIMED = 0.5, DISPUTED = 0.2, RETRACTED = 0)
    - 30% source coverage (fraction of edges with attached sources)
    - 20% source tier (Tier 0 = 1.0, Tier 1 = 0.8, Tier 2 = 0.5)

    Args:
        conn: SQLite connection
        entity: Node ID to score
        include_outgoing: Include edges from this entity
        include_incoming: Include edges to this entity

    Returns:
        CorrectionScoreResult with full breakdown
    """
    cursor = conn.cursor()
    cursor.row_factory = sqlite3.Row

    # Collect all relevant edges
    edges = []

    if include_outgoing:
        cursor.execute("""
            SELECT edge_id, edge_type, from_node_id, to_node_id, status
            FROM edges WHERE from_node_id = ?
        """, (entity,))
        edges.extend(cursor.fetchall())

    if include_incoming:
        cursor.execute("""
            SELECT edge_id, edge_type, from_node_id, to_node_id, status
            FROM edges WHERE to_node_id = ?
        """, (entity,))
        edges.extend(cursor.fetchall())

    # Remove duplicates
    seen_ids = set()
    unique_edges = []
    for e in edges:
        if e['edge_id'] not in seen_ids:
            seen_ids.add(e['edge_id'])
            unique_edges.append(e)
    edges = unique_edges

    if not edges:
        return CorrectionScoreResult(
            entity=entity,
            score=0.0,
            total_edges=0,
            evidenced_edges=0,
            claimed_edges=0,
            disputed_edges=0,
            retracted_edges=0,
            source_coverage=0.0,
        )

    # Count by status
    status_counts = {'EVIDENCED': 0, 'CLAIMED': 0, 'DISPUTED': 0, 'RETRACTED': 0}
    edges_with_sources = 0
    tier_counts = {0: 0, 1: 0, 2: 0}
    audit_trail = []

    status_weights = {'EVIDENCED': 1.0, 'CLAIMED': 0.5, 'DISPUTED': 0.2, 'RETRACTED': 0.0}
    tier_weights = {0: 1.0, 1: 0.8, 2: 0.5}

    status_sum = 0.0
    tier_sum = 0.0
    tier_count = 0

    for edge in edges:
        status = edge['status']
        status_counts[status] = status_counts.get(status, 0) + 1
        status_sum += status_weights.get(status, 0.0)

        # Check sources
        cursor.execute("""
            SELECT s.tier FROM edge_sources es
            JOIN sources s ON es.source_id = s.source_id
            WHERE es.edge_id = ?
        """, (edge['edge_id'],))

        sources = cursor.fetchall()
        if sources:
            edges_with_sources += 1
            for src in sources:
                tier = src['tier']
                tier_counts[tier] = tier_counts.get(tier, 0) + 1
                tier_sum += tier_weights.get(tier, 0.5)
                tier_count += 1

        audit_trail.append({
            "edge_id": edge['edge_id'],
            "edge_type": edge['edge_type'],
            "direction": "outgoing" if edge['from_node_id'] == entity else "incoming",
            "status": status,
            "source_count": len(sources),
        })

    total = len(edges)

    # Compute score components
    status_score = status_sum / total
    source_coverage = edges_with_sources / total
    tier_score = tier_sum / tier_count if tier_count > 0 else 0.5

    # Weighted final score
    final_score = (0.5 * status_score) + (0.3 * source_coverage) + (0.2 * tier_score)

    return CorrectionScoreResult(
        entity=entity,
        score=round(final_score, 4),
        total_edges=total,
        evidenced_edges=status_counts['EVIDENCED'],
        claimed_edges=status_counts['CLAIMED'],
        disputed_edges=status_counts['DISPUTED'],
        retracted_edges=status_counts['RETRACTED'],
        source_coverage=round(source_coverage, 4),
        tier_breakdown=tier_counts,
        audit_trail=audit_trail,
    )


def get_edge_sources(
    conn: sqlite3.Connection,
    edge_id: str,
) -> List[Dict]:
    """Get all sources attached to an edge with details."""
    cursor = conn.cursor()
    cursor.row_factory = sqlite3.Row

    cursor.execute("""
        SELECT s.*, es.claim_excerpt, es.page_ref, es.added_at
        FROM edge_sources es
        JOIN sources s ON es.source_id = s.source_id
        WHERE es.edge_id = ?
    """, (edge_id,))

    return [dict(row) for row in cursor.fetchall()]


def get_node_edges(
    conn: sqlite3.Connection,
    node_id: str,
    direction: str = "both",
) -> Dict[str, List[Dict]]:
    """
    Get all edges connected to a node.

    Args:
        conn: SQLite connection
        node_id: Node ID
        direction: "outgoing", "incoming", or "both"

    Returns:
        Dict with "outgoing" and/or "incoming" lists
    """
    cursor = conn.cursor()
    cursor.row_factory = sqlite3.Row

    result = {}

    if direction in ("outgoing", "both"):
        cursor.execute("""
            SELECT * FROM edges WHERE from_node_id = ?
        """, (node_id,))
        result["outgoing"] = [dict(row) for row in cursor.fetchall()]

    if direction in ("incoming", "both"):
        cursor.execute("""
            SELECT * FROM edges WHERE to_node_id = ?
        """, (node_id,))
        result["incoming"] = [dict(row) for row in cursor.fetchall()]

    return result
