"""Mart Pipeline Generator.

Generates the 4-object DDL pipeline from a MartConfig:

  VW_1  — Translation View (CASE on ID_SOURCE, hierarchy joins)
  DT_2  — Granularity Table (UNPIVOT, dynamic columns, exclusions)
  DT_3A — Pre-Aggregation Fact (UNION ALL branches, SUM, sign change)
  DT_3  — Data Mart (formula cascade, surrogate keys, level backfill)
"""

from __future__ import annotations

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

from .formula_engine import FormulaPrecedenceEngine
from .types import (
    MartConfig,
    MartPipeline,
    MartValidationResult,
    ObjectType,
    PipelineLayer,
    PipelineObject,
    QualityIssueLevel,
    ValidationMessage,
)

logger = logging.getLogger(__name__)


class MartPipelineGenerator:
    """Generates SQL DDL for each layer of the mart pipeline."""

    def __init__(self, formula_engine: Optional[FormulaPrecedenceEngine] = None) -> None:
        self._formula_engine = formula_engine or FormulaPrecedenceEngine()

    # ------------------------------------------------------------------
    # VW_1 — Translation View
    # ------------------------------------------------------------------

    def generate_vw1(self, config: MartConfig) -> PipelineObject:
        """Generate VW_1 Translation View.

        Creates a view that:
        - Uses CASE statements to map ID_SOURCE values to physical columns
        - Joins hierarchy dimensions to source data
        - Applies column-level translations

        Args:
            config: Mart configuration.

        Returns:
            PipelineObject with the VIEW DDL.
        """
        schema = config.schema_name
        name = f"VW_1_{config.project_name.upper()}"
        fqn = f"{schema}.{name}"

        # Build CASE expressions from column_map
        case_lines: List[str] = []
        for m in config.column_map:
            case_lines.append(
                f"        CASE WHEN ID_SOURCE = '{m.id_source}' "
                f"THEN {m.physical_column} END AS {m.id_source}"
            )

        if not case_lines:
            case_block = "        src.*"
        else:
            case_block = ",\n".join(case_lines)

        # Join to hierarchy table
        join_clause = ""
        if config.hierarchy_table:
            join_clause = (
                f"\n    LEFT JOIN {schema}.{config.hierarchy_table} h\n"
                f"        ON src.HIERARCHY_KEY = h.HIERARCHY_KEY"
            )

        ddl = (
            f"CREATE OR REPLACE VIEW {fqn} AS\n"
            f"SELECT\n"
            f"        src.ID_SOURCE,\n"
            f"{case_block},\n"
            f"        h.*\n"
            f"    FROM {schema}.{config.mapping_table} src{join_clause};\n"
        )

        return PipelineObject(
            object_type=ObjectType.VIEW,
            object_name=fqn,
            layer=PipelineLayer.VW_1,
            ddl=ddl,
            depends_on=[
                f"{schema}.{config.mapping_table}",
                f"{schema}.{config.hierarchy_table}",
            ],
        )

    # ------------------------------------------------------------------
    # DT_2 — Granularity Table
    # ------------------------------------------------------------------

    def generate_dt2(self, config: MartConfig) -> PipelineObject:
        """Generate DT_2 Granularity Table.

        Creates a table that:
        - UNPIVOTs measure columns into rows (ID_SOURCE + VALUE)
        - Applies dynamic column mapping for ID_SOURCE resolution
        - Optionally excludes rows via NOT IN subquery
        - Optionally applies multi-round group filter precedence

        Args:
            config: Mart configuration.

        Returns:
            PipelineObject with the TABLE DDL.
        """
        schema = config.schema_name
        vw1_name = f"VW_1_{config.project_name.upper()}"
        name = f"DT_2_{config.project_name.upper()}"
        fqn = f"{schema}.{name}"

        # UNPIVOT columns
        measure_cols = [m.physical_column for m in config.column_map]
        if measure_cols:
            unpivot_list = ", ".join(measure_cols)
            unpivot_clause = (
                f"    UNPIVOT (\n"
                f"        MEASURE_VALUE FOR ID_SOURCE IN ({unpivot_list})\n"
                f"    ) unpvt"
            )
        else:
            unpivot_clause = "    -- No column mappings defined; passthrough\n    src"

        # Exclusion clause
        exclusion_clause = ""
        if config.has_exclusions:
            exclusion_clause = (
                f"\n    WHERE ID_SOURCE NOT IN (\n"
                f"        SELECT EXCLUDED_SOURCE FROM {schema}.EXCLUSION_LIST\n"
                f"        WHERE PROJECT = '{config.project_name}'\n"
                f"    )"
            )

        # Group filter precedence
        filter_clause = ""
        if config.has_group_filter_precedence:
            filter_clause = (
                f"\n    QUALIFY ROW_NUMBER() OVER (\n"
                f"        PARTITION BY ID_SOURCE, HIERARCHY_KEY\n"
                f"        ORDER BY FILTER_PRECEDENCE ASC\n"
                f"    ) = 1"
            )

        ddl = (
            f"CREATE OR REPLACE TABLE {fqn} AS\n"
            f"SELECT *\n"
            f"FROM {schema}.{vw1_name}\n"
            f"{unpivot_clause}{exclusion_clause}{filter_clause};\n"
        )

        return PipelineObject(
            object_type=ObjectType.TABLE,
            object_name=fqn,
            layer=PipelineLayer.DT_2,
            ddl=ddl,
            depends_on=[f"{schema}.{vw1_name}"],
        )

    # ------------------------------------------------------------------
    # DT_3A — Pre-Aggregation Fact
    # ------------------------------------------------------------------

    def generate_dt3a(self, config: MartConfig) -> PipelineObject:
        """Generate DT_3A Pre-Aggregation Fact.

        Creates a table that:
        - UNION ALL branches from join_patterns
        - Filters by ACCOUNT_SEGMENT
        - Applies SIGN_CHANGE_FLAG if enabled
        - Aggregates with SUM

        Args:
            config: Mart configuration.

        Returns:
            PipelineObject with the TABLE DDL.
        """
        schema = config.schema_name
        dt2_name = f"DT_2_{config.project_name.upper()}"
        name = f"DT_3A_{config.project_name.upper()}"
        fqn = f"{schema}.{name}"

        # Build UNION ALL branches
        branches: List[str] = []
        for jp in config.join_patterns:
            join_key_cols = ", ".join(jp.join_keys) if jp.join_keys else "HIERARCHY_KEY"
            fact_agg = ", ".join(
                f"SUM({fk}) AS {fk}" for fk in jp.fact_keys
            ) if jp.fact_keys else f"SUM({config.measure_prefix}VALUE) AS MEASURE_VALUE"

            source = jp.source_table or f"{schema}.{dt2_name}"

            where_parts = [f"ACCOUNT_SEGMENT = '{config.account_segment}'"]
            if jp.filter:
                where_parts.append(jp.filter)

            where_clause = " AND ".join(where_parts)

            sign_col = ""
            if config.has_sign_change:
                sign_col = ",\n            SIGN_CHANGE_FLAG"

            branch = (
                f"    -- Branch: {jp.name}\n"
                f"    SELECT\n"
                f"            '{jp.name}' AS SOURCE_BRANCH,\n"
                f"            {join_key_cols},\n"
                f"            {fact_agg}{sign_col}\n"
                f"        FROM {source}\n"
                f"        WHERE {where_clause}\n"
                f"        GROUP BY {join_key_cols}"
                + (", SIGN_CHANGE_FLAG" if config.has_sign_change else "")
            )
            branches.append(branch)

        if not branches:
            # Fallback: simple aggregation from DT_2
            branches.append(
                f"    SELECT\n"
                f"            'default' AS SOURCE_BRANCH,\n"
                f"            HIERARCHY_KEY,\n"
                f"            SUM(MEASURE_VALUE) AS MEASURE_VALUE\n"
                f"        FROM {schema}.{dt2_name}\n"
                f"        WHERE ACCOUNT_SEGMENT = '{config.account_segment}'\n"
                f"        GROUP BY HIERARCHY_KEY"
            )

        union_body = "\n    UNION ALL\n".join(branches)

        ddl = (
            f"CREATE OR REPLACE TABLE {fqn} AS\n"
            f"{union_body};\n"
        )

        return PipelineObject(
            object_type=ObjectType.TABLE,
            object_name=fqn,
            layer=PipelineLayer.DT_3A,
            ddl=ddl,
            depends_on=[f"{schema}.{dt2_name}"],
        )

    # ------------------------------------------------------------------
    # DT_3 — Data Mart
    # ------------------------------------------------------------------

    def generate_dt3(
        self,
        config: MartConfig,
        hierarchy_data: Optional[Dict[str, Any]] = None,
    ) -> PipelineObject:
        """Generate DT_3 Data Mart.

        Creates a table that:
        - Applies formula precedence cascade (5 levels of CTEs)
        - Generates DENSE_RANK surrogate keys
        - Backfills hierarchy level columns

        Args:
            config: Mart configuration.
            hierarchy_data: Optional hierarchy data for formula extraction.

        Returns:
            PipelineObject with the TABLE DDL.
        """
        schema = config.schema_name
        dt3a_name = f"DT_3A_{config.project_name.upper()}"
        name = f"DT_3_{config.project_name.upper()}"
        fqn = f"{schema}.{name}"

        # Formula cascade
        cascade_sql = ""
        if hierarchy_data and hierarchy_data.get("formulas"):
            try:
                self._formula_engine.extract_formulas(config.project_name, hierarchy_data)
                cascade_sql = self._formula_engine.generate_cascade_cte(
                    config.project_name,
                    base_table=f"{schema}.{dt3a_name}",
                )
            except Exception as e:
                logger.warning("Formula cascade generation failed: %s", e)
                cascade_sql = ""

        if cascade_sql:
            # Wrap cascade in CREATE TABLE
            ddl = (
                f"CREATE OR REPLACE TABLE {fqn} AS\n"
                f"{cascade_sql}\n"
                f";\n"
            )
        else:
            # Simple pass-through with surrogate keys and level backfill
            ddl = (
                f"CREATE OR REPLACE TABLE {fqn} AS\n"
                f"WITH RANKED AS (\n"
                f"    SELECT\n"
                f"        *,\n"
                f"        DENSE_RANK() OVER (ORDER BY HIERARCHY_KEY) AS SURROGATE_KEY\n"
                f"    FROM {schema}.{dt3a_name}\n"
                f")\n"
                f"SELECT\n"
                f"    r.*,\n"
                f"    h.LEVEL_1,\n"
                f"    h.LEVEL_2,\n"
                f"    h.LEVEL_3,\n"
                f"    h.LEVEL_4,\n"
                f"    h.LEVEL_5\n"
                f"FROM RANKED r\n"
                f"LEFT JOIN {schema}.{config.hierarchy_table} h\n"
                f"    ON r.HIERARCHY_KEY = h.HIERARCHY_KEY;\n"
            )

        return PipelineObject(
            object_type=ObjectType.TABLE,
            object_name=fqn,
            layer=PipelineLayer.DT_3,
            ddl=ddl,
            depends_on=[f"{schema}.{dt3a_name}", f"{schema}.{config.hierarchy_table}"],
        )

    # ------------------------------------------------------------------
    # Full Pipeline
    # ------------------------------------------------------------------

    def generate_full_pipeline(
        self,
        config: MartConfig,
        hierarchy_data: Optional[Dict[str, Any]] = None,
    ) -> MartPipeline:
        """Generate all 4 pipeline objects in dependency order.

        Args:
            config: Mart configuration.
            hierarchy_data: Optional hierarchy data for DT_3 formulas.

        Returns:
            MartPipeline with all objects.
        """
        objects = [
            self.generate_vw1(config),
            self.generate_dt2(config),
            self.generate_dt3a(config),
            self.generate_dt3(config, hierarchy_data),
        ]

        notes = [
            f"Generated 4-object pipeline for {config.project_name}",
            f"Report type: {config.report_type.value}",
            f"Account segment: {config.account_segment}",
            f"Join patterns: {len(config.join_patterns)}",
            f"Column mappings: {len(config.column_map)}",
        ]
        if config.has_sign_change:
            notes.append("SIGN_CHANGE_FLAG enabled")
        if config.has_exclusions:
            notes.append("Exclusion filtering enabled")
        if config.has_group_filter_precedence:
            notes.append("Group filter precedence enabled")

        pipeline = MartPipeline(
            project_name=config.project_name,
            objects=objects,
            generation_notes=notes,
        )
        logger.info("Generated full pipeline: %s (%d objects)", config.project_name, len(objects))
        return pipeline

    # ------------------------------------------------------------------
    # dbt Output
    # ------------------------------------------------------------------

    def generate_dbt_models(self, config: MartConfig) -> Dict[str, str]:
        """Generate pipeline as dbt model SQL files.

        Returns:
            Dict mapping model filename to SQL content.
        """
        pipeline = self.generate_full_pipeline(config)
        models: Dict[str, str] = {}

        for obj in pipeline.objects:
            # Convert DDL to dbt model (remove CREATE TABLE/VIEW wrapper)
            sql = obj.ddl
            # Strip CREATE ... AS prefix for dbt
            for prefix in ("CREATE OR REPLACE TABLE ", "CREATE OR REPLACE VIEW "):
                if sql.upper().startswith(prefix.upper()):
                    # Find the AS keyword
                    as_idx = sql.upper().find(" AS\n")
                    if as_idx > 0:
                        sql = sql[as_idx + 4:]  # Skip " AS\n"
                    break

            # Remove trailing semicolons
            sql = sql.rstrip().rstrip(";")

            filename = f"{obj.layer.value}_{config.project_name.lower()}.sql"
            header = (
                f"-- dbt model: {obj.object_name}\n"
                f"-- layer: {obj.layer.value}\n"
                f"-- generated by DataBridge Mart Factory\n\n"
                f"{{{{ config(materialized='{_dbt_materialization(obj)}') }}}}\n\n"
            )
            models[filename] = header + sql + "\n"

        return models

    # ------------------------------------------------------------------
    # Validation
    # ------------------------------------------------------------------

    def validate_pipeline(self, pipeline: MartPipeline) -> MartValidationResult:
        """Validate a generated pipeline for completeness and correctness.

        Args:
            pipeline: The pipeline to validate.

        Returns:
            MartValidationResult.
        """
        messages: List[ValidationMessage] = []

        def _check(name: str, passed: bool, msg: str, sev: QualityIssueLevel = QualityIssueLevel.ERROR):
            messages.append(ValidationMessage(check=name, passed=passed, message=msg, severity=sev))

        # Check all 4 layers present
        layers_present = {o.layer for o in pipeline.objects}
        for layer in PipelineLayer:
            _check(
                f"layer_{layer.value}_present",
                layer in layers_present,
                f"Missing pipeline layer: {layer.value}" if layer not in layers_present else "OK",
            )

        # Check DDL non-empty
        for obj in pipeline.objects:
            _check(
                f"{obj.layer.value}_has_ddl",
                bool(obj.ddl.strip()),
                f"{obj.layer.value} DDL is empty" if not obj.ddl.strip() else "OK",
            )

        # Check dependency chain
        object_names = {o.object_name for o in pipeline.objects}
        for obj in pipeline.objects:
            for dep in obj.depends_on:
                # Dependencies can be external tables, so just warn
                if dep not in object_names:
                    _check(
                        f"{obj.layer.value}_dep_{dep}",
                        True,
                        f"External dependency: {dep}",
                        QualityIssueLevel.INFO,
                    )

        checked = len(messages)
        passed = sum(1 for m in messages if m.passed)
        valid = all(m.passed for m in messages if m.severity == QualityIssueLevel.ERROR)

        return MartValidationResult(
            valid=valid, messages=messages, checked=checked, passed=passed,
        )


def _dbt_materialization(obj: PipelineObject) -> str:
    """Return dbt materialization type."""
    if obj.object_type == ObjectType.VIEW:
        return "view"
    return "table"
