"""E2E Assessment Orchestrator — extended pipeline for full ERP assessments.

Extends PlatformOrchestrator with 12 additional phases (Tiers 1-4):
  Tier 1-2 (phases 10-15):
  - bus_matrix: Generate Kimball bus matrix from relationships + classification
  - dm_spec_generate: Auto-generate DM specs from discovery metadata
  - dm_load: Load dimensional model using specs
  - quality_from_classification: Generate quality expectations from DataShield
  - graph_sync: Sync classified tables + relationships to GraphRAG
  Tier 3 — Enterprise DW (phases 16-18):
  - enterprise_grain_analysis: Cross-system grain detection and comparison
  - enterprise_dim_master: Conformed dimension mastering with surrogate keys
  - enterprise_consolidation: Bus matrix, DDL, and deploy script generation
  Tier 4 — Report Parity (phases 19-21):
  - report_parity_compile: Parse reports into structured ReportSpec objects
  - report_parity_validate: Progressive gate-based parity validation
  - report_parity_certificate: Generate pass/fail parity certificates

Phases run in order: the original 10 + 12 new = 22 total.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any, Dict, List, Optional

from src.persistence.adapter import PersistenceAdapter
from src.persistence.file_adapter import FilePersistenceAdapter
from src.workflows.orchestrator import (
    OrchestratorConfig,
    PlatformOrchestrator,
    RunSummary,
    PHASE_ORDER,
)

logger = logging.getLogger(__name__)


# Extended phase order for E2E assessments
E2E_PHASE_ORDER = list(PHASE_ORDER) + [
    "bus_matrix",
    "dm_spec_generate",
    "dm_load",
    "quality_from_classification",
    "graph_sync",
    "enterprise_grain_analysis",
    "enterprise_dim_master",
    "enterprise_consolidation",
    "report_parity_compile",
    "report_parity_validate",
    "report_parity_certificate",
]


class E2EAssessmentOrchestrator(PlatformOrchestrator):
    """Extended orchestrator with bus matrix, DM loading, quality, graph,
    enterprise DW consolidation (Tier 3), and report parity (Tier 4) phases.

    Inherits all 10 built-in phases and adds 12 more for full E2E assessments.
    """

    def __init__(
        self,
        config: OrchestratorConfig,
        persistence: Optional[PersistenceAdapter] = None,
        dm_spec_path: Optional[str] = None,
        dest_db: Optional[str] = None,
        dest_schema: Optional[str] = None,
        source_connection: Optional[str] = None,
        dest_connection: Optional[str] = None,
        enterprise_systems: Optional[List[Dict[str, Any]]] = None,
        mastering_strategy: str = "first_match",
        report_specs_json: Optional[str] = None,
        parity_actual_values_json: Optional[str] = None,
    ):
        super().__init__(config=config, persistence=persistence)
        self.dm_spec_path = dm_spec_path
        self.dest_db = dest_db
        self.dest_schema = dest_schema
        self.source_connection = source_connection
        self.dest_connection = dest_connection
        self.enterprise_systems = enterprise_systems or []
        self.mastering_strategy = mastering_strategy
        self.report_specs_json = report_specs_json
        self.parity_actual_values_json = parity_actual_values_json

    def phase_bus_matrix(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 10: Generate Kimball bus matrix.

        Only requires classification to be non-empty — relationships are
        optional (BusMatrixGenerator handles empty rels gracefully).
        """
        from src.data_modeling.bus_matrix import BusMatrixGenerator

        rels = ctx.get("load_metadata", {}).get("relationships", [])
        classification = ctx.get("detect_dimensions", {}).get("classification", {})

        if not classification:
            return {"skipped": True, "reason": "no classification data from detect_dimensions"}

        gen = BusMatrixGenerator()
        result = gen.generate(rels, classification)
        result["markdown"] = gen.to_markdown(result)
        return result

    def phase_dm_spec_generate(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 11: Auto-generate DM specs (or load from template)."""
        from src.data_modeling.erp_configs.templates import load_dm_template
        from src.data_modeling.spec_generator import DMSpecGenerator

        # Try template first
        template_specs = load_dm_template(self.config.erp)
        if template_specs:
            return {
                "source": "template",
                "erp": self.config.erp,
                "specs_count": len(template_specs),
                "specs": [s.to_dict() for s in template_specs],
            }

        # Auto-generate from discovery
        rels = ctx.get("load_metadata", {}).get("relationships", [])
        classification = ctx.get("detect_dimensions", {}).get("classification", {})

        if not classification:
            return {"skipped": True, "reason": "no classification data"}

        gen = DMSpecGenerator()
        specs = gen.generate(
            relationships=rels,
            classification=classification,
            erp_type=self.config.erp.upper(),
            run_id=self._summary.run_id if self._summary else "unknown",
        )
        return {
            "source": "auto_generated",
            "specs_count": len(specs),
            "specs": [s.to_dict() for s in specs],
        }

    def phase_dm_load(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 12: Load dimensional model from specs."""
        from src.data_modeling.dm_loader import GenericDMLoader, TableSpec

        spec_data = ctx.get("dm_spec_generate", {})
        if spec_data.get("skipped"):
            return {"skipped": True, "reason": "no specs available"}

        raw_specs = spec_data.get("specs", [])
        if not raw_specs:
            return {"skipped": True, "reason": "empty specs"}

        if not self.dest_db or not self.dest_schema:
            return {"skipped": True, "reason": "dest_db and dest_schema required for DM load"}

        # Get persistent connections from pool (avoids repeated SSO prompts)
        sf_source = self._sf_source_conn
        sf_dest = None
        if self.dest_connection:
            from src.data_modeling.snowflake_pool import sf_pool
            sf_dest = sf_pool.get(
                self.dest_connection,
                database=self.dest_db,
                schema=self.dest_schema,
            )

        loader = GenericDMLoader(
            source_db=self.config.database,
            source_schema=self.config.schema,
            dest_db=self.dest_db,
            dest_schema=self.dest_schema,
            source_connection=self.source_connection or self.config.snowflake_connection,
            dest_connection=self.dest_connection,
            sf_source_conn=sf_source,
            sf_dest_conn=sf_dest,
        )

        # Ensure dest schema exists before loading
        try:
            dest_conn = loader._connect_dest()
            dest_conn.cursor().execute(
                f'CREATE SCHEMA IF NOT EXISTS "{self.dest_db}"."{self.dest_schema}"'
            )
        except Exception as exc:
            logger.warning("Could not pre-create dest schema: %s", exc)

        # Use sequential loading when sharing connections (thread-safety)
        specs = [TableSpec.from_dict(s) for s in raw_specs]
        use_parallel = sf_source is None and sf_dest is None

        # Allow callers to configure per-type row limits and fact date filter
        dm_opts = getattr(self, "_dm_load_options", {})
        return loader.load(
            specs=specs,
            parallel=use_parallel,
            dim_limit=dm_opts.get("dim_limit", 200_000),
            txn_limit=dm_opts.get("txn_limit", 1_000_000),
            txn_where=dm_opts.get("txn_where", ""),
        )

    def phase_quality_from_classification(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 13: Generate quality expectations from DataShield classifications.

        Prefers per-column classifications from ``ai_classify`` for richer
        expectations.  Falls back to table-level classification from
        ``detect_dimensions`` if ai_classify data is unavailable.
        """
        try:
            from src.data_quality.classification_bridge import ClassificationExpectationBridge
        except ImportError:
            return {"skipped": True, "reason": "classification_bridge not available"}

        ai_cls = ctx.get("ai_classify", {}).get("classifications", {})
        bridge = ClassificationExpectationBridge()
        suites = []

        if ai_cls:
            # Rich per-column classifications from DataShield
            for table, rules in ai_cls.items():
                col_map = {}
                for rule in rules:
                    col_name = rule.get("column_name", "")
                    cls_val = rule.get("classification", "safe")
                    if cls_val != "safe":  # Skip safe — no expectations needed
                        col_map[col_name] = cls_val
                if col_map:
                    suite = bridge.generate_suite(
                        suite_name=f"auto_{table}",
                        classifications=col_map,
                        table_name=table,
                        database=self.config.database,
                        schema_name=self.config.schema,
                    )
                    suites.append({
                        "table": table,
                        "suite_name": suite.name,
                        "expectations": len(suite.expectations),
                    })
        else:
            # Fallback: table-level classification from detect_dimensions
            classification = ctx.get("detect_dimensions", {}).get("classification", {})
            if not classification:
                return {"skipped": True, "reason": "no classification data"}
            for table, cls_type in classification.items():
                col_map = {}
                if cls_type == "dimension":
                    col_map[f"{table}_KEY"] = "identifier"
                elif cls_type == "fact":
                    col_map[f"{table}_AMOUNT"] = "measure"
                if col_map:
                    suite = bridge.generate_suite(
                        suite_name=f"auto_{table}",
                        classifications=col_map,
                        table_name=table,
                        database=self.config.database,
                        schema_name=self.config.schema,
                    )
                    suites.append({
                        "table": table,
                        "suite_name": suite.name,
                        "expectations": len(suite.expectations),
                    })

        return {"suites_generated": len(suites), "suites": suites}

    def phase_graph_sync(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 14: Sync classified tables and relationships to GraphRAG (Rec #12)."""
        try:
            from src.knowledge_base import KBIngestionPipeline
            from src.knowledge_base.types import GraphNode, GraphEdge
        except ImportError:
            return {"skipped": True, "reason": "knowledge_base not available"}

        classification = ctx.get("detect_dimensions", {}).get("classification", {})
        rels = ctx.get("load_metadata", {}).get("relationships", [])

        if not classification:
            return {"skipped": True, "reason": "no classification data"}

        nodes: List[GraphNode] = []
        edges: List[GraphEdge] = []

        for table, cls_type in classification.items():
            nodes.append(GraphNode(
                id=f"TABLE:{table}",
                type=cls_type.capitalize(),
                properties={"name": table, "classification": cls_type},
            ))

        for rel in rels:
            src = rel.get("source_table", "")
            tgt = rel.get("target_table", "")
            if src and tgt:
                edges.append(GraphEdge(
                    source=f"TABLE:{src}",
                    target=f"TABLE:{tgt}",
                    type="FOREIGN_KEY",
                    properties={
                        "source_column": rel.get("source_column", ""),
                        "target_column": rel.get("target_column", ""),
                        "method": rel.get("method", ""),
                        "confidence": rel.get("confidence", 0),
                    },
                ))

        pipeline = KBIngestionPipeline()
        count = pipeline.ingest_graph(nodes, edges)
        return {"nodes": len(nodes), "edges": len(edges), "ingested": count}

    # ------------------------------------------------------------------
    # Tier 3 — Enterprise DW phases (16-18)
    # ------------------------------------------------------------------

    def phase_enterprise_grain_analysis(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 16: Cross-system grain detection and comparison."""
        from src.enterprise_dw.grain_analyzer import GrainAnalyzer

        if self.enterprise_systems:
            # Multi-system path
            analyzer = GrainAnalyzer()
            for system in self.enterprise_systems:
                analyzer.register_system(
                    system_name=system["name"],
                    tables=system.get("tables", []),
                )
            comparisons = analyzer.compare_all()
            all_specs = []
            for sys_name, specs in analyzer._specs.items():
                all_specs.extend([s.model_dump() for s in specs])
            return {
                "grain_specs": all_specs,
                "comparisons": [c.model_dump() for c in comparisons],
                "total_specs": len(all_specs),
                "total_comparisons": len(comparisons),
                "mode": "multi_system",
            }

        # Single-system fallback
        dm_load = ctx.get("dm_load", {})
        if dm_load.get("skipped"):
            return {"skipped": True, "reason": "no enterprise_systems and dm_load was skipped"}

        classification = ctx.get("detect_dimensions", {}).get("classification", {})
        if not classification:
            return {"skipped": True, "reason": "no classification data"}

        fact_tables = [t for t, c in classification.items() if c == "fact"]
        if not fact_tables:
            return {"skipped": True, "reason": "no fact tables in classification"}

        sample_tables = ctx.get("load_metadata", {}).get("sample_tables", {})
        analyzer = GrainAnalyzer()
        tables_for_reg = []
        for ft in fact_tables:
            columns = list(sample_tables.get(ft, {}).keys()) if sample_tables else []
            tables_for_reg.append({"table_name": ft, "columns": columns})
        specs = analyzer.register_system(system_name="primary", tables=tables_for_reg)
        return {
            "grain_specs": [s.model_dump() for s in specs],
            "comparisons": [],
            "total_specs": len(specs),
            "total_comparisons": 0,
            "mode": "single_system",
        }

    def phase_enterprise_dim_master(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 17: Conformed dimension mastering with surrogate keys."""
        from src.enterprise_dw.conformed_dim_master import ConformedDimMaster
        from src.enterprise_dw.contracts import MasteringStrategy

        if self.enterprise_systems:
            # Multi-system path
            master = ConformedDimMaster()
            for system in self.enterprise_systems:
                master.register_system_dimensions(
                    system_name=system["name"],
                    dimensions=system.get("dimensions", []),
                )
            conformed = master.detect_conformed()

            try:
                strategy = MasteringStrategy(self.mastering_strategy)
            except ValueError:
                strategy = MasteringStrategy.FIRST_MATCH

            key_maps = []
            for cd in conformed:
                system_data = {}
                source_tables = [sd.get("table", sd.get("table_name", "")) for sd in cd.source_dims]
                for system in self.enterprise_systems:
                    for dim in system.get("dimensions", []):
                        if dim.get("table_name") in source_tables:
                            system_data[system["name"]] = dim.get("members", [])
                km = master.master_dimension(
                    conformed_dim=cd,
                    system_data=system_data,
                    strategy=strategy,
                )
                key_maps.append(km.model_dump())

            return {
                "conformed_dimensions": [cd.model_dump() for cd in conformed],
                "key_maps": key_maps,
                "total_conformed": len(conformed),
                "mastering_strategy": self.mastering_strategy,
                "mode": "multi_system",
            }

        # Single-system fallback
        classification = ctx.get("detect_dimensions", {}).get("classification", {})
        if not classification:
            return {"skipped": True, "reason": "no classification data and no enterprise_systems"}

        dim_tables = [t for t, c in classification.items() if c == "dimension"]
        return {
            "conformed_dimensions": [],
            "key_maps": [],
            "total_conformed": 0,
            "mastering_strategy": self.mastering_strategy,
            "mode": "single_system",
            "note": f"conformed dims require multi-system; found {len(dim_tables)} dimension(s)",
        }

    def phase_enterprise_consolidation(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 18: Enterprise bus matrix, DDL, and deploy script generation."""
        from src.enterprise_dw.contracts import ConformedDimension, GrainComparison
        from src.enterprise_dw.fact_harmonizer import FactHarmonizer
        from src.enterprise_dw.enterprise_bus_matrix import EnterpriseBusMatrixGenerator
        from src.enterprise_dw.ddl_generator import EnterpriseDDLGenerator

        grain_ctx = ctx.get("enterprise_grain_analysis", {})
        dim_ctx = ctx.get("enterprise_dim_master", {})

        if grain_ctx.get("skipped") and dim_ctx.get("skipped"):
            return {"skipped": True, "reason": "both upstream phases were skipped"}

        # Reconstruct Pydantic models from serialized dicts
        comparisons = []
        for comp_dict in grain_ctx.get("comparisons", []):
            try:
                comparisons.append(GrainComparison(**comp_dict))
            except Exception:
                pass

        conformed_dims = []
        for cd_dict in dim_ctx.get("conformed_dimensions", []):
            try:
                conformed_dims.append(ConformedDimension(**cd_dict))
            except Exception:
                pass

        # Fact harmonization
        harmonizer = FactHarmonizer()
        alignments = []
        for comp in comparisons:
            cols_a = comp.grain_a.grain_columns
            cols_b = comp.grain_b.grain_columns
            alignment = harmonizer.align_facts(comp, cols_a, cols_b)
            alignment.union_sql = harmonizer.generate_union_sql(
                alignment,
                target_table=f"ENT_{comp.table_a}",
            )
            alignments.append(alignment)

        # Enterprise bus matrix
        bus_gen = EnterpriseBusMatrixGenerator()
        for comp in comparisons:
            bus_gen.add_fact(
                table_name=comp.table_a,
                source_systems=[comp.system_a, comp.system_b],
                grain=comp.grain_a.grain_level,
                dimension_columns=comp.shared_dimensions,
            )
        for cd in conformed_dims:
            bus_gen.add_conformed_dimension(cd)
        for alignment in alignments:
            bus_gen.add_alignment(alignment)
        bus_matrix = bus_gen.build()
        bus_matrix_md = bus_gen.to_markdown()

        # DDL generation
        ddl_gen = EnterpriseDDLGenerator()
        schema = ddl_gen.generate_all_ddl(
            facts=alignments,
            dimensions=conformed_dims,
        )
        deploy_script = ddl_gen.generate_deploy_script(schema)

        return {
            "enterprise_bus_matrix": bus_matrix.model_dump(),
            "bus_matrix_markdown": bus_matrix_md,
            "conformance_report": {
                "overall_conformance": bus_matrix.overall_conformance,
                "all_systems": bus_matrix.all_systems,
                "all_dimensions": bus_matrix.all_dimensions,
            },
            "enterprise_schema": schema.model_dump(),
            "deploy_script": deploy_script,
            "alignments_count": len(alignments),
            "conformed_dimensions_count": len(conformed_dims),
        }

    # ------------------------------------------------------------------
    # Tier 4 — Report Parity phases (19-21)
    # ------------------------------------------------------------------

    def phase_report_parity_compile(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 19: Parse reports into structured ReportSpec objects."""
        from src.report_parity.report_spec_compiler import ReportSpecCompiler

        compiler = ReportSpecCompiler()

        # Path 1: Explicit config JSON
        if self.report_specs_json:
            import json
            try:
                raw = json.loads(self.report_specs_json)
            except (json.JSONDecodeError, TypeError):
                return {"skipped": True, "reason": "invalid report_specs_json"}

            specs_list = raw if isinstance(raw, list) else [raw]
            compiled = []
            for item in specs_list:
                if "kpis" in item:
                    spec = compiler.compile_from_blce(
                        report_analysis=item,
                        expected_values=item.get("expected_values", {}),
                    )
                else:
                    spec = compiler.compile(
                        report_name=item.get("report_name", "Unknown"),
                        source_system=item.get("source_system", ""),
                        line_items=item.get("line_items", []),
                        grain_columns=item.get("grain_columns"),
                        temporal_column=item.get("temporal_column", ""),
                        temporal_grain=item.get("temporal_grain", "monthly"),
                        global_filters=item.get("filters"),
                        metadata=item.get("metadata"),
                    )
                compiled.append(spec.model_dump())
            return {
                "specs": compiled,
                "specs_count": len(compiled),
                "source": "config",
            }

        # Path 2: BLCE report_analyses from phase 6 (batch bridge)
        report_analyses = ctx.get("report_analyses", [])
        if report_analyses:
            from src.report_parity.blce_bridge import BLCEParityBridge
            bridge = BLCEParityBridge()
            specs = bridge.extract_from_blce_context(ctx)
            if specs:
                return {
                    "specs": [s.model_dump() for s in specs],
                    "specs_count": len(specs),
                    "source": "blce_bridge",
                }

        # Path 3: Legacy e2e_chain fallback (single report via bridge)
        blce_ctx = ctx.get("e2e_chain", {})
        if blce_ctx:
            report_analysis = blce_ctx.get("report_analysis")
            if report_analysis:
                from src.report_parity.blce_bridge import BLCEParityBridge
                bridge = BLCEParityBridge()
                spec = bridge.convert_report_analysis(report_analysis)
                return {
                    "specs": [spec.model_dump()],
                    "specs_count": 1,
                    "source": "blce_context",
                }

        return {"skipped": True, "reason": "no report_specs_json and no BLCE context"}

    def phase_report_parity_validate(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 20: Progressive gate-based parity validation."""
        from src.report_parity.progressive_runner import ProgressiveRunner
        from src.report_parity.contracts import ReportSpec

        compile_ctx = ctx.get("report_parity_compile", {})
        if compile_ctx.get("skipped"):
            return {"skipped": True, "reason": "compile phase was skipped"}

        raw_specs = compile_ctx.get("specs", [])
        if not raw_specs:
            return {"skipped": True, "reason": "no compiled specs"}

        # Load actual values
        import json
        actual_values = None
        if self.parity_actual_values_json:
            try:
                actual_values = json.loads(self.parity_actual_values_json)
            except (json.JSONDecodeError, TypeError):
                actual_values = None

        runner = ProgressiveRunner(tolerance_pct=0.01)
        states = []
        all_certified = True
        for raw_spec in raw_specs:
            spec = ReportSpec(**raw_spec)
            state = runner.run_progressive(spec=spec, all_actual_values=actual_values)
            states.append(state.model_dump())
            if state.overall_status != "certified":
                all_certified = False

        return {
            "validation_states": states,
            "specs_validated": len(states),
            "overall_certified": all_certified,
        }

    def phase_report_parity_certificate(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 21: Generate pass/fail parity certificates with evidence."""
        from src.report_parity.parity_certificate import ParityCertificate
        from src.report_parity.contracts import ProgressiveValidationState

        validate_ctx = ctx.get("report_parity_validate", {})
        if validate_ctx.get("skipped"):
            return {"skipped": True, "reason": "validate phase was skipped"}

        raw_states = validate_ctx.get("validation_states", [])
        if not raw_states:
            return {"skipped": True, "reason": "no validation states"}

        cert_gen = ParityCertificate()
        certificates = []
        all_certified = True
        for raw_state in raw_states:
            state = ProgressiveValidationState(**raw_state)
            cert = cert_gen.generate(state)
            cert["markdown"] = cert_gen.to_markdown(cert)
            certificates.append(cert)
            if cert.get("status") != "CERTIFIED":
                all_certified = False

        return {
            "certificates": certificates,
            "certificates_count": len(certificates),
            "all_certified": all_certified,
        }

    def phase_artifact_bundle(self, ctx: Dict[str, Any]) -> Dict[str, Any]:
        """Phase 9 (override): Generate rich HTML report using ReportGenerator."""
        try:
            from src.artifacts.report_generator import ReportGenerator
            from src.artifacts.bundle import ArtifactBundle

            run_id = self._summary.run_id if self._summary else "unknown"
            output_dir = Path(self.config.output_dir) / "artifacts"

            # Build rich report
            run_result = {
                "run_id": run_id,
                "status": self._summary.status if self._summary else "unknown",
                "phases": [
                    {"name": p.name, "status": p.status, "duration_seconds": p.duration_seconds, "error": p.error}
                    for p in (self._summary.phases if self._summary else [])
                ],
                "total_duration_seconds": self._summary.total_duration_seconds if self._summary else 0,
                "tables": ctx.get("load_metadata", {}).get("tables", []),
                "relationships": ctx.get("load_metadata", {}).get("relationships", []),
                "classification": ctx.get("detect_dimensions", {}).get("classification", {}),
                "bus_matrix": ctx.get("bus_matrix"),
            }
            report_path = ReportGenerator.from_run_result(run_result, output_dir)

            # Also create bundle with JSON artifacts
            bundle = ArtifactBundle(run_id=run_id, output_dir=output_dir)
            if self._summary:
                bundle.add("run_summary", self._summary.to_dict(), fmt="json")
            bundle.generate_manifest()

            return {"report": str(report_path), "artifacts": len(bundle.artifacts)}
        except Exception as exc:
            return {"error": str(exc)}


class E2EAssessmentTemplate:
    """Simplified wrapper for E2E assessment workflows.

    Example::

        template = E2EAssessmentTemplate(
            source_config={"source_type": "snowflake", "database": "ENERTIA_RAW", "schema": "ENERTIA_DBO", "tables": "*"},
            dest_config={"dest_db": "AKHAN", "dest_schema": "PUBLIC", "output_dir": "data/e2e_runs"},
            options={"erp": "enertia"},
        )
        summary = template.run(skip_phases=["dm_load"])
    """

    PHASE_NAMES = E2E_PHASE_ORDER

    def __init__(
        self,
        source_config: Dict[str, Any],
        dest_config: Optional[Dict[str, Any]] = None,
        options: Optional[Dict[str, Any]] = None,
        persistence: Optional[PersistenceAdapter] = None,
    ):
        dest = dest_config or {}
        opts = options or {}

        self._orch_config = OrchestratorConfig(
            source_type=source_config.get("source_type", "snowflake"),
            database=source_config.get("database", ""),
            schema=source_config.get("schema", ""),
            tables=source_config.get("tables", ""),
            csv_folder=source_config.get("csv_folder"),
            connection_string=source_config.get("connection_string"),
            snowflake_connection=source_config.get("snowflake_connection"),
            erp=opts.get("erp", "enertia"),
            sample_limit=opts.get("sample_limit", 200),
            min_overlap=opts.get("min_overlap", 0.5),
            dimension_threshold=opts.get("dimension_threshold", 3),
            focus_patterns=opts.get("focus_patterns"),
            dbt_project_name=dest.get("dbt_project_name"),
            dbt_run=dest.get("dbt_run", False),
            dbt_docs=dest.get("dbt_docs", False),
            output_dir=dest.get("output_dir", "data/workflow_runs"),
        )

        self._dest_db = dest.get("dest_db")
        self._dest_schema = dest.get("dest_schema")
        self._dm_spec_path = dest.get("dm_spec_path")
        self._dest_connection = dest.get("dest_connection")

        # Tier 3/4 options
        self._enterprise_systems = opts.get("enterprise_systems")
        self._mastering_strategy = opts.get("mastering_strategy", "first_match")
        self._report_specs_json = opts.get("report_specs_json")
        self._parity_actual_values_json = opts.get("parity_actual_values_json")

        # DM load options (row limits and optional fact date filter)
        self._dm_load_options = opts.get("dm_load_options", {})

        self._persistence = persistence or FilePersistenceAdapter(
            base_dir=str(Path(self._orch_config.output_dir) / "runs")
        )

    def run(
        self,
        skip_phases: Optional[List[str]] = None,
        only_phases: Optional[List[str]] = None,
        trust_attestation: Optional[Dict[str, Any]] = None,
        trust_lane: str = "client_cortex_raw",
        checkpoint_dir: Optional[str] = None,
        resume_from: Optional[str] = None,
    ) -> RunSummary:
        """Execute the E2E assessment workflow.

        Args:
            skip_phases: Phase names to skip.
            only_phases: If set, run only these phases.
            checkpoint_dir: Save phase checkpoints for resume support.
            resume_from: Resume from a previous checkpoint directory.
        """
        orch = E2EAssessmentOrchestrator(
            config=self._orch_config,
            persistence=self._persistence,
            dm_spec_path=self._dm_spec_path,
            dest_db=self._dest_db,
            dest_schema=self._dest_schema,
            source_connection=self._orch_config.snowflake_connection,
            dest_connection=self._dest_connection,
            enterprise_systems=self._enterprise_systems,
            mastering_strategy=self._mastering_strategy,
            report_specs_json=self._report_specs_json,
            parity_actual_values_json=self._parity_actual_values_json,
        )
        orch.context["trust_attestation"] = trust_attestation
        orch.context["trust_lane"] = trust_lane

        # Pass DM load options (row limits, date filter) to the orchestrator
        if self._dm_load_options:
            orch._dm_load_options = self._dm_load_options

        phases = only_phases or list(E2E_PHASE_ORDER)
        skip = set(skip_phases or [])
        return orch.run(
            phases=phases,
            skip=list(skip),
            checkpoint_dir=checkpoint_dir,
            resume_from=resume_from,
        )
