"""Real-time soliton scoring for the DAG editor."""

from __future__ import annotations

import math

from orxaq.kernel.world_dag import WorldDAG


class SolitonScorer:
    """Compute soliton scores for a World DAG.

    The soliton score measures causal coherence of the graph:
    - Connectivity: fraction of nodes reachable from roots
    - Balance: how evenly distributed the in/out degrees are
    - Depth: ratio of actual path depth to maximum possible
    - Density: edge density relative to a sparse DAG
    """

    def __init__(self, dag: WorldDAG) -> None:
        self.dag = dag

    def overall_score(self) -> float:
        """Compute aggregate soliton score in [0, 1]."""
        if self.dag.node_count() == 0:
            return 0.0

        scores = self.component_scores()
        weights = {
            "connectivity": 0.35,
            "balance": 0.25,
            "depth": 0.20,
            "density": 0.20,
        }
        total = sum(scores.get(k, 0.0) * w for k, w in weights.items())
        return round(min(1.0, max(0.0, total)), 4)

    def component_scores(self) -> dict[str, float]:
        """Compute individual score components."""
        if self.dag.node_count() == 0:
            return {"connectivity": 0.0, "balance": 0.0, "depth": 0.0, "density": 0.0}

        return {
            "connectivity": self._connectivity_score(),
            "balance": self._balance_score(),
            "depth": self._depth_score(),
            "density": self._density_score(),
        }

    def node_scores(self) -> dict[str, float]:
        """Per-node soliton scores based on local structure."""
        scores: dict[str, float] = {}
        for eid in self.dag._entities:
            parents = len(self.dag.parents(eid))
            children = len(self.dag.children(eid))
            degree = parents + children
            if degree == 0:
                scores[eid] = 0.1
            else:
                connectivity = min(1.0, degree / 3.0)
                balance = 1.0 - abs(parents - children) / (degree + 1)
                scores[eid] = round(0.6 * connectivity + 0.4 * balance, 4)
        return scores

    def edge_impact(self, source: str, target: str) -> dict:
        """Estimate impact of adding/removing an edge."""
        current = self.overall_score()
        edge_id = f"{source}\u2192{target}"
        exists = edge_id in self.dag._edges
        return {
            "current_score": current,
            "edge_exists": exists,
            "source": source,
            "target": target,
        }

    def _connectivity_score(self) -> float:
        n = self.dag.node_count()
        if n <= 1:
            return 1.0

        roots = [eid for eid in self.dag._entities if not self.dag.parents(eid)]
        if not roots:
            return 0.5

        reachable: set[str] = set(roots)
        for root in roots:
            reachable.update(self.dag.descendants(root))

        return len(reachable) / n

    def _balance_score(self) -> float:
        n = self.dag.node_count()
        if n <= 1:
            return 1.0

        in_degrees = []
        out_degrees = []
        for eid in self.dag._entities:
            in_degrees.append(len(self.dag.parents(eid)))
            out_degrees.append(len(self.dag.children(eid)))

        def _gini(values: list[int]) -> float:
            if not values or sum(values) == 0:
                return 0.0
            sorted_v = sorted(values)
            n = len(sorted_v)
            total = sum(sorted_v)
            weighted_sum = sum((i + 1) * v for i, v in enumerate(sorted_v))
            return (2.0 * weighted_sum) / (n * total) - (n + 1.0) / n

        gini = (_gini(in_degrees) + _gini(out_degrees)) / 2.0
        return max(0.0, 1.0 - gini)

    def _depth_score(self) -> float:
        n = self.dag.node_count()
        if n <= 1:
            return 1.0

        topo = self.dag.topological_sort()
        if not topo:
            return 0.0

        dist: dict[str, int] = {eid: 0 for eid in topo}
        for node in topo:
            for child in self.dag.children(node):
                if dist.get(child, 0) < dist[node] + 1:
                    dist[child] = dist[node] + 1

        max_depth = max(dist.values()) if dist else 0
        ideal = max(1.0, math.log2(n))
        return min(1.0, max_depth / ideal)

    def _density_score(self) -> float:
        n = self.dag.node_count()
        e = self.dag.edge_count()
        if n <= 1:
            return 0.0

        ideal = 1.5 * n
        if e == 0:
            return 0.0
        ratio = e / ideal
        return min(1.0, 2.0 * ratio / (1.0 + ratio))
