"""Validation pipeline: load data, discover structure, compare to ground truth."""

from __future__ import annotations

import csv
import json
from pathlib import Path

from orxaq.fabric.validation.metrics import ValidationMetrics, compute_metrics
from orxaq.intelligence.consensus import ConsensusEngine
from orxaq.intelligence.pc_algorithm import PCAlgorithm


class ValidationPipeline:
    """End-to-end validation: data -> discovery -> comparison -> metrics.

    Loads a dataset and ground truth DAG, runs causal discovery,
    and computes comparison metrics (SHD, F1, orientation accuracy).
    """

    def __init__(
        self,
        data_path: str | Path | None = None,
        ground_truth_path: str | Path | None = None,
        algorithm: str = "pc",
    ) -> None:
        self._data_path = Path(data_path) if data_path else None
        self._gt_path = Path(ground_truth_path) if ground_truth_path else None
        self._algorithm = algorithm
        self._data: list[dict[str, float]] = []
        self._true_edges: list[tuple[str, str]] = []
        self._predicted_edges: list[tuple[str, str]] = []
        self._metrics: ValidationMetrics | None = None
        self._variables: list[str] = []

    def load_data(self, data: list[dict[str, float]] | None = None) -> None:
        """Load data from path or directly."""
        if data is not None:
            self._data = data
            if data:
                self._variables = list(data[0].keys())
            return

        if self._data_path is None:
            msg = "No data path or data provided"
            raise ValueError(msg)

        self._data = []
        with open(self._data_path) as f:
            reader = csv.DictReader(f)
            for row in reader:
                self._data.append({k: float(v) for k, v in row.items()})
        if self._data:
            self._variables = list(self._data[0].keys())

    def load_ground_truth(self, edges: list[tuple[str, str]] | None = None) -> None:
        """Load ground truth edges from path or directly."""
        if edges is not None:
            self._true_edges = edges
            return

        if self._gt_path is None:
            msg = "No ground truth path or edges provided"
            raise ValueError(msg)

        with open(self._gt_path) as f:
            dag = json.load(f)
        self._true_edges = [(e["source"], e["target"]) for e in dag["edges"]]

    def discover(self, variables: list[str] | None = None) -> list[tuple[str, str]]:
        """Run causal discovery on loaded data."""
        if not self._data:
            msg = "No data loaded — call load_data() first"
            raise ValueError(msg)

        use_vars = variables or self._variables[:5]

        if self._algorithm == "consensus":
            filtered = [{v: row[v] for v in use_vars} for row in self._data]
            engine = ConsensusEngine()
            consensus_result = engine.discover(filtered, use_vars)
            self._predicted_edges = [(e.source, e.target) for e in consensus_result.edges]
        else:
            # PCAlgorithm expects list[dict], filter to requested variables.
            filtered = [{v: row[v] for v in use_vars} for row in self._data]
            pc = PCAlgorithm()
            pc_result = pc.fit(filtered, use_vars)
            self._predicted_edges = list(pc_result.skeleton)

        return self._predicted_edges

    def evaluate(self) -> ValidationMetrics:
        """Compare discovered edges to ground truth."""
        # Filter true edges to only those involving discovered variables.
        discovered_vars = set()
        for s, t in self._predicted_edges:
            discovered_vars.add(s)
            discovered_vars.add(t)
        for s, t in self._true_edges:
            discovered_vars.add(s)
            discovered_vars.add(t)

        relevant_true = [
            (s, t)
            for s, t in self._true_edges
            if s in self._variables[:5] and t in self._variables[:5]
        ]

        self._metrics = compute_metrics(relevant_true, self._predicted_edges)
        return self._metrics

    def run(
        self,
        data: list[dict[str, float]] | None = None,
        true_edges: list[tuple[str, str]] | None = None,
        variables: list[str] | None = None,
    ) -> ValidationMetrics:
        """Run full pipeline: load -> discover -> evaluate."""
        self.load_data(data)
        self.load_ground_truth(true_edges)
        self.discover(variables)
        return self.evaluate()

    @property
    def metrics(self) -> ValidationMetrics | None:
        """Return last computed metrics."""
        return self._metrics

    @property
    def predicted_edges(self) -> list[tuple[str, str]]:
        """Return discovered edges."""
        return self._predicted_edges

    @property
    def true_edges(self) -> list[tuple[str, str]]:
        """Return ground truth edges."""
        return self._true_edges
