"""BLCE Parallel Pipeline — maps orchestrator phases into dependency levels.

Converts the 22 sequential orchestrator phases into a directed acyclic graph
and executes independent phases in parallel via ``AgentRuntime``.

Phase dependency map::

    Level 0: e2e_chain, intake, catalog_baseline
    Level 1: consultant_intake, parse_sources
    Level 2: report_processing, reconciliation, normalize, hierarchy_attach, evidence_sample,
             bus_matrix, quality_recommend, ai_enrich
    Level 3: cross_reference
    Level 4: governance
    Level 5: model_generation, persist, skill_generation
    Level 6: agent_swarm, auto_build
    Level 7: artifact_bundle, deployment
"""
from __future__ import annotations

import logging
from typing import Any, Dict, List, Optional, Set

from .agent_runtime import AgentRuntime, AgentTask
from .contracts import RuntimeResult
from .message_bus import MessageBus

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Phase dependency graph
# ---------------------------------------------------------------------------

PHASE_DEPENDENCIES: Dict[str, List[str]] = {
    "e2e_chain":         [],
    "intake":            [],
    "catalog_baseline":  [],
    "consultant_intake": ["e2e_chain"],
    "parse_sources":     ["intake"],
    "report_processing": ["intake"],
    "reconciliation":    ["report_processing"],
    "normalize":         ["parse_sources"],
    "hierarchy_attach":  ["parse_sources"],
    "cross_reference":   ["normalize"],
    "evidence_sample":   ["parse_sources"],
    "governance":        ["cross_reference", "evidence_sample"],
    "model_generation":  ["normalize", "cross_reference",
                          "consultant_intake", "report_processing"],
    "persist":           ["governance"],
    "bus_matrix":        ["parse_sources"],
    "quality_recommend": ["parse_sources"],
    "skill_generation":  ["governance"],
    "ai_enrich":         ["parse_sources"],
    "agent_swarm":       ["ai_enrich"],
    "auto_build":        ["model_generation", "quality_recommend"],
    "artifact_bundle":   ["persist", "bus_matrix", "quality_recommend",
                          "skill_generation", "ai_enrich", "agent_swarm",
                          "auto_build"],
    "deployment":        ["auto_build"],
}


# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------

class ParallelPipelineRunner:
    """Wraps an orchestrator to execute its phases in parallel where safe.

    Parameters
    ----------
    orchestrator
        A ``BLCEOrchestrator`` instance whose ``phase_*`` methods will be
        called.  The orchestrator's shared ``context`` dict provides
        thread-safety by convention: each phase writes to its own key.
    max_workers : int
        Thread pool size.
    timeout : int
        Per-phase timeout in seconds.
    """

    def __init__(
        self,
        orchestrator,
        max_workers: int = 4,
        timeout: int = 300,
    ):
        self.orchestrator = orchestrator
        self.max_workers = max_workers
        self.timeout = timeout
        self.bus = MessageBus()

    # -- task graph construction --------------------------------------------

    def build_task_graph(
        self,
        phases: List[str],
        skip_set: Optional[Set[str]] = None,
    ) -> AgentRuntime:
        """Convert phase list + dependency map into an ``AgentRuntime``."""
        skip = skip_set or set()
        rt = AgentRuntime(
            max_workers=self.max_workers,
            timeout=self.timeout,
            bus=self.bus,
        )

        active_phases = [p for p in phases if p not in skip]

        for phase_name in active_phases:
            method = self._resolve_phase_method(phase_name)
            if method is None:
                logger.warning("No method for phase '%s', skipping", phase_name)
                continue

            # Filter dependencies to only those that are active
            deps = [
                d for d in PHASE_DEPENDENCIES.get(phase_name, [])
                if d in active_phases
            ]

            rt.add_task(AgentTask(
                task_id=phase_name,
                name=phase_name,
                callable=method,
                depends_on=deps,
            ))

        return rt

    # -- execution ----------------------------------------------------------

    def run(self, context: Dict[str, Any]) -> RuntimeResult:
        """Build the task graph and execute all phases.

        Each phase callable receives *context* as a keyword argument
        (matching ``phase_*(context)`` signature on the orchestrator).
        """
        from .orchestrator import BLCE_FULL_PHASE_ORDER

        phases = list(BLCE_FULL_PHASE_ORDER)
        skip_set = set(getattr(self.orchestrator, "_parallel_skip", []))

        rt = self.build_task_graph(phases, skip_set)

        # Inject context kwarg into every task
        for task in rt.tasks.values():
            task.kwargs["context"] = context

        self.bus.publish(
            "phase.started",
            sender="ParallelPipelineRunner",
            payload={"phases": [t.task_id for t in rt.tasks.values()]},
        )

        result = rt.run()

        self.bus.publish(
            "phase.completed",
            sender="ParallelPipelineRunner",
            payload={
                "completed": result.tasks_completed,
                "failed": result.tasks_failed,
                "duration": result.total_duration,
            },
        )

        return result

    # -- helpers ------------------------------------------------------------

    def _resolve_phase_method(self, phase_name: str):
        """Look up ``orchestrator.phase_{name}``."""
        method_name = f"phase_{phase_name}"
        return getattr(self.orchestrator, method_name, None)

    @staticmethod
    def get_phase_dependencies() -> Dict[str, List[str]]:
        """Return a copy of the phase dependency map."""
        return {k: list(v) for k, v in PHASE_DEPENDENCIES.items()}

    @staticmethod
    def resolve_levels(
        phases: List[str],
        skip_set: Optional[Set[str]] = None,
    ) -> List[List[str]]:
        """Resolve topological levels for the given phases (utility).

        This is a static helper for inspection / MCP tools.
        """
        skip = skip_set or set()
        active = [p for p in phases if p not in skip]
        active_set = set(active)

        # Kahn's algorithm
        in_degree = {p: 0 for p in active}
        dependents: Dict[str, List[str]] = {p: [] for p in active}

        for p in active:
            for dep in PHASE_DEPENDENCIES.get(p, []):
                if dep in active_set:
                    in_degree[p] += 1
                    dependents[dep].append(p)

        from collections import deque
        queue: deque[str] = deque(
            p for p, deg in in_degree.items() if deg == 0
        )
        levels: List[List[str]] = []
        processed = 0

        while queue:
            level = sorted(queue)
            queue.clear()
            levels.append(level)
            processed += len(level)
            for p in level:
                for dep_p in dependents[p]:
                    in_degree[dep_p] -= 1
                    if in_degree[dep_p] == 0:
                        queue.append(dep_p)

        return levels


