"""BLCE workflow controller — chains BLCE phases into a pipeline.

Follows the PlatformOrchestrator phase pattern: each phase is a method
that receives accumulated context and returns a result dict.  The
controller does NOT replace the orchestrator — it wraps BLCE-specific
phase logic and can be called standalone or wired into the orchestrator.
"""
from __future__ import annotations

import logging
import time
import uuid
from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional

from .config import BLCEConfig
from .contracts import BLCERunSummary, LogicArtifact

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Phase definitions
# ---------------------------------------------------------------------------

BLCE_PHASE_ORDER = [
    "parse_sources",
    "normalize",
    "cross_reference",
    "evidence_sample",
    "governance",
    "skill_generation",
]


# ---------------------------------------------------------------------------
# Phase result (mirrors orchestrator.PhaseResult)
# ---------------------------------------------------------------------------

class BLCEPhaseResult:
    """Result of a single BLCE phase."""
    __slots__ = ("name", "status", "started_at", "ended_at",
                 "duration_seconds", "output", "error")

    def __init__(self, name: str):
        self.name = name
        self.status = "pending"
        self.started_at: Optional[str] = None
        self.ended_at: Optional[str] = None
        self.duration_seconds: float = 0.0
        self.output: Dict[str, Any] = {}
        self.error: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "status": self.status,
            "started_at": self.started_at,
            "ended_at": self.ended_at,
            "duration_seconds": self.duration_seconds,
            "error": self.error,
            "output_keys": list(self.output.keys()),
        }


# ---------------------------------------------------------------------------
# Workflow controller
# ---------------------------------------------------------------------------

class BLCEWorkflowController:
    """Chain BLCE phases into a managed pipeline.

    Phases:
    1. parse_sources — SQL/Python/Excel parsing into LogicArtifacts
    2. normalize — measure/filter/grain normalization
    3. cross_reference — cross-system entity mapping
    4. evidence_sample — build evidence sampling queries
    5. governance — CORE/CUSTOM/CANDIDATE classification
    6. skill_generation — auto-generate skills for CORE artifacts
    """

    def __init__(self, config: Optional[BLCEConfig] = None):
        self.config = config or BLCEConfig()
        self._runs: Dict[str, Dict[str, Any]] = {}

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def run_pipeline(
        self,
        *,
        phases: Optional[List[str]] = None,
        skip: Optional[List[str]] = None,
        artifacts: Optional[List[LogicArtifact]] = None,
        client_id: str = "",
    ) -> BLCERunSummary:
        """Execute BLCE phases sequentially.

        Args:
            phases: Explicit phase list (default: all 6).
            skip: Phase names to skip.
            artifacts: Pre-parsed artifacts (skips parse phase).
            client_id: Client identifier for governance.

        Returns:
            BLCERunSummary with per-phase results.
        """
        run_id = f"blce_{uuid.uuid4().hex[:8]}"
        phase_list = phases or list(BLCE_PHASE_ORDER)
        skip_set = set(skip or self.config.skip_phases or [])

        summary = BLCERunSummary(
            run_id=run_id,
            client_id=client_id or self.config.client_id,
            started_at=datetime.now(timezone.utc).isoformat(),
        )

        # Pipeline context — accumulated across phases
        context: Dict[str, Any] = {
            "run_id": run_id,
            "artifacts": [a.model_dump() for a in artifacts] if artifacts else [],
            "normalized_measures": [],
            "grain_contracts": [],
            "cross_references": [],
            "evidence_samples": [],
            "governance_decisions": [],
            "skills": [],
            "client_id": client_id or self.config.client_id,
            "config": {
                "sql_dialect": self.config.sql_dialect,
                "evidence_sample_months": self.config.evidence_sample_months,
                "evidence_sample_max_rows": self.config.evidence_sample_max_rows,
            },
        }

        phase_results: List[BLCEPhaseResult] = []
        t0 = time.time()

        for phase_name in phase_list:
            if phase_name in skip_set:
                pr = BLCEPhaseResult(phase_name)
                pr.status = "skipped"
                phase_results.append(pr)
                summary.phases_skipped.append(phase_name)
                continue

            pr = self.run_phase(phase_name, context)
            phase_results.append(pr)

            if pr.status == "error":
                summary.errors.append(f"{phase_name}: {pr.error}")
                # Continue with remaining phases — don't abort
                logger.warning("Phase %s failed: %s", phase_name, pr.error)
            else:
                summary.phases_completed.append(phase_name)

        # Finalize summary
        elapsed = time.time() - t0
        summary.completed_at = datetime.now(timezone.utc).isoformat()
        summary.duration_seconds = round(elapsed, 1)
        summary.artifacts_extracted = len(context.get("artifacts", []))
        summary.measures_found = len(context.get("normalized_measures", []))
        summary.cross_references = len(context.get("cross_references", []))
        summary.source_files = self._count_sources(context)

        # Store the run
        self._runs[run_id] = {
            "summary": summary.model_dump(),
            "phases": [pr.to_dict() for pr in phase_results],
            "context": {
                "artifact_count": len(context.get("artifacts", [])),
                "measure_count": len(context.get("normalized_measures", [])),
                "xref_count": len(context.get("cross_references", [])),
                "decision_count": len(context.get("governance_decisions", [])),
                "skill_count": len(context.get("skills", [])),
            },
        }

        return summary

    def run_phase(
        self,
        phase_name: str,
        context: Dict[str, Any],
    ) -> BLCEPhaseResult:
        """Execute a single BLCE phase.

        Dispatches to the appropriate phase method.
        """
        pr = BLCEPhaseResult(phase_name)
        pr.started_at = datetime.now(timezone.utc).isoformat()
        pr.status = "running"

        method_name = f"phase_{phase_name}"
        method = getattr(self, method_name, None)

        if method is None:
            pr.status = "error"
            pr.error = f"Unknown phase: {phase_name}"
            pr.ended_at = datetime.now(timezone.utc).isoformat()
            return pr

        t0 = time.time()
        try:
            result = method(context)
            pr.output = result
            pr.status = "completed"
        except Exception as exc:
            pr.status = "error"
            pr.error = str(exc)
            logger.exception("Phase %s failed", phase_name)

        pr.duration_seconds = round(time.time() - t0, 2)
        pr.ended_at = datetime.now(timezone.utc).isoformat()
        return pr

    def get_status(self, run_id: str) -> Dict[str, Any]:
        """Get the status of a pipeline run."""
        if run_id not in self._runs:
            return {"error": f"Run {run_id} not found."}
        return self._runs[run_id]

    def list_runs(self) -> List[Dict[str, Any]]:
        """List all pipeline runs in this session."""
        return [
            {
                "run_id": rid,
                "status": data["summary"].get("completed_at", "running"),
                "phases_completed": len(data["summary"].get("phases_completed", [])),
                "duration_seconds": data["summary"].get("duration_seconds", 0),
            }
            for rid, data in self._runs.items()
        ]

    # ------------------------------------------------------------------
    # Phase implementations
    # ------------------------------------------------------------------

    def phase_parse_sources(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 1: Parse SQL/Python/Excel/DAX/MDX/PDF sources into LogicArtifacts."""
        from .parsers import (
            SQLLogicExtractor, PythonLogicExtractor, ExcelLogicExtractor,
            DAXLogicExtractor, MDXLogicExtractor, PDFLogicExtractor,
        )

        artifacts = []
        errors = []

        sql_ext = SQLLogicExtractor(dialect=self.config.sql_dialect)
        py_ext = PythonLogicExtractor()
        xl_ext = ExcelLogicExtractor()
        dax_ext = DAXLogicExtractor()
        mdx_ext = MDXLogicExtractor()
        pdf_ext = PDFLogicExtractor()

        # Parse SQL files/strings
        for sql_path in self.config.sql_paths:
            try:
                art = sql_ext.extract_file(sql_path) if sql_path.endswith(".sql") else sql_ext.extract(sql_path)
                artifacts.append(art.model_dump())
            except Exception as exc:
                errors.append(f"SQL parse error ({sql_path[:50]}): {exc}")

        # Parse Python files
        for py_path in self.config.python_paths:
            try:
                art = py_ext.extract_file(py_path)
                artifacts.append(art.model_dump())
            except Exception as exc:
                errors.append(f"Python parse error ({py_path}): {exc}")

        # Parse Excel files
        for xl_path in self.config.excel_paths:
            try:
                art = xl_ext.extract_file(xl_path)
                artifacts.append(art.model_dump())
            except Exception as exc:
                errors.append(f"Excel parse error ({xl_path}): {exc}")

        # Parse DAX files
        for dax_path in self.config.dax_paths:
            try:
                art = dax_ext.extract_file(dax_path)
                artifacts.append(art.model_dump())
            except Exception as exc:
                errors.append(f"DAX parse error ({dax_path}): {exc}")

        # Parse MDX files
        for mdx_path in self.config.mdx_paths:
            try:
                art = mdx_ext.extract_file(mdx_path)
                artifacts.append(art.model_dump())
            except Exception as exc:
                errors.append(f"MDX parse error ({mdx_path}): {exc}")

        # Parse PDF files
        for pdf_path in self.config.pdf_paths:
            try:
                art = pdf_ext.extract_file(pdf_path)
                artifacts.append(art.model_dump())
            except Exception as exc:
                errors.append(f"PDF parse error ({pdf_path}): {exc}")

        # If artifacts were pre-loaded in context, keep them
        existing = context.get("artifacts", [])
        artifacts = existing + artifacts
        context["artifacts"] = artifacts

        return {
            "parsed": len(artifacts) - len(existing),
            "total": len(artifacts),
            "errors": errors,
        }

    def phase_normalize(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 2: Normalize measures, filters, and grain contracts."""
        from .normalizers import MeasureNormalizer, FilterNormalizer, GrainContractBuilder

        arts = [LogicArtifact(**a) for a in context.get("artifacts", [])]
        if not arts:
            return {"status": "skipped", "reason": "No artifacts to normalize"}

        mn = MeasureNormalizer()
        fn = FilterNormalizer()
        gb = GrainContractBuilder()

        normalized = mn.normalize(arts)
        common_filters = fn.find_common_filters(arts)
        contracts = gb.build_contracts(arts)

        context["normalized_measures"] = [m.model_dump() for m in normalized]
        context["grain_contracts"] = [c.model_dump() for c in contracts]

        return {
            "measures_normalized": len(normalized),
            "common_filters": len(common_filters),
            "grain_contracts": len(contracts),
        }

    def phase_cross_reference(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 3: Discover cross-system entity mappings."""
        from .crossref import CrossReferenceDiscovery

        arts = [LogicArtifact(**a) for a in context.get("artifacts", [])]
        if len(arts) < 2:
            return {"status": "skipped", "reason": "Need 2+ artifacts for cross-reference"}

        disc = CrossReferenceDiscovery()
        model = disc.discover(arts)
        context["cross_references"] = [m.model_dump() for m in model.mappings]

        return {
            "mappings": len(model.mappings),
            "domains": model.domains,
            "source_systems": model.source_systems,
        }

    def phase_evidence_sample(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 4: Build evidence sampling queries."""
        from .evidence import EvidenceSampler

        arts = [LogicArtifact(**a) for a in context.get("artifacts", [])]
        if not arts:
            return {"status": "skipped", "reason": "No artifacts"}

        sampler = EvidenceSampler(
            max_rows=self.config.evidence_sample_max_rows,
            sample_months=self.config.evidence_sample_months,
        )

        samples = []
        for art in arts:
            sample = sampler.collect_sample(art)
            samples.append(sample.model_dump())

        context["evidence_samples"] = samples
        return {"samples_built": len(samples)}

    def phase_governance(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 5: Classify artifacts as CORE/CUSTOM/CANDIDATE."""
        from .governance import GovernanceEngine

        arts = [LogicArtifact(**a) for a in context.get("artifacts", [])]
        if not arts:
            return {"status": "skipped", "reason": "No artifacts"}

        engine = GovernanceEngine()
        decisions = engine.classify_batch(arts)
        report = engine.generate_report(decisions)

        context["governance_decisions"] = [d.model_dump() for d in decisions]
        context["governance_report"] = report

        return {
            "classified": len(decisions),
            "core": report.get("core", 0),
            "custom": report.get("custom", 0),
            "candidate": report.get("candidate", 0),
        }

    def phase_skill_generation(self, context: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 6: Generate skill prompts for CORE artifacts."""
        from .contracts import (
            GovernanceDecision as GD,
            MeetingNotesAnalysis,
            ProposedModel,
            ReportAnalysis,
        )
        from .skill_generator import SkillGenerator

        arts = [LogicArtifact(**a) for a in context.get("artifacts", [])]
        decisions = [GD(**d) for d in context.get("governance_decisions", [])]

        if not arts or not decisions:
            return {"status": "skipped", "reason": "No artifacts or decisions"}

        # Reconstruct enriched context for P2.3 skill enhancement
        report_analyses = [
            ReportAnalysis(**ra) for ra in context.get("report_analyses", [])
        ]
        notes_analyses = [
            MeetingNotesAnalysis(**na) for na in context.get("notes_analyses", [])
        ]
        proposed_model = (
            ProposedModel(**context["proposed_model"])
            if context.get("proposed_model") else None
        )

        gen = SkillGenerator(skills_dir=self.config.output_dir + "/skills")
        skills = gen.generate_batch(
            arts, decisions,
            client_id=context.get("client_id", ""),
            report_analyses=report_analyses or None,
            notes_analyses=notes_analyses or None,
            proposed_model=proposed_model,
        )

        context["skills"] = [s.model_dump() for s in skills]
        return {
            "skills_generated": len(skills),
            "skill_names": [s.skill_name for s in skills],
        }

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    def _count_sources(self, context: Dict[str, Any]) -> int:
        """Count distinct source files from config."""
        return (
            len(self.config.sql_paths)
            + len(self.config.python_paths)
            + len(self.config.excel_paths)
            + len(self.config.csv_paths)
            + len(self.config.dax_paths)
            + len(self.config.mdx_paths)
            + len(self.config.pdf_paths)
        )

