"""Formula Precedence Engine.

Manages the 5-level precedence cascade used by DT_3 to compute
calculated rows in the correct dependency order.

Levels:
  P1 — Base aggregations (SUM of source data)
  P2 — Simple calculated rows (e.g. Gross Profit = Revenue - COGS)
  P3 — Ratios and percentages
  P4 — Variance calculations
  P5 — Complex derived metrics
"""

from __future__ import annotations

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

from .types import FormulaCascade, FormulaPrecedence

logger = logging.getLogger(__name__)


class FormulaPrecedenceEngine:
    """Extracts, orders, and generates SQL for formula cascades."""

    def __init__(self) -> None:
        self._cascades: Dict[str, FormulaCascade] = {}

    # ------------------------------------------------------------------
    # Extraction
    # ------------------------------------------------------------------

    def extract_formulas(
        self,
        project_name: str,
        hierarchy_data: Dict[str, Any],
    ) -> FormulaCascade:
        """Parse hierarchy data for formula definitions.

        Looks for ``formulas`` key containing dicts with keys:
        name, operation, operands, level (optional).

        Args:
            project_name: Cascade identifier.
            hierarchy_data: Dict with ``formulas`` list.

        Returns:
            FormulaCascade with extracted formulas.
        """
        raw_formulas = hierarchy_data.get("formulas", [])
        formulas: List[FormulaPrecedence] = []

        for f in raw_formulas:
            name = f.get("name", "")
            operation = f.get("operation", "SUM").upper()
            operands = f.get("operands", [])
            level = f.get("level", None)

            # Determine precedence from operation type
            precedence = self._infer_precedence(operation, operands, formulas)

            logic = self._build_logic_expr(operation, operands)
            param_ref = [op for op in operands if isinstance(op, str)]

            formulas.append(FormulaPrecedence(
                precedence_level=precedence if level is None else min(max(level, 1), 5),
                formula_group=name,
                logic=logic,
                param_ref=param_ref,
                output_column=f.get("output_column", name),
            ))

        cascade = FormulaCascade(
            project_name=project_name,
            formulas=formulas,
        )
        self._cascades[project_name] = cascade
        logger.info("Extracted %d formulas for %s", len(formulas), project_name)
        return cascade

    # ------------------------------------------------------------------
    # Precedence chain
    # ------------------------------------------------------------------

    def build_precedence_chain(self, project_name: str) -> Dict[int, List[FormulaPrecedence]]:
        """Group formulas by precedence level 1-5.

        Returns:
            Dict mapping level number to list of formulas at that level.

        Raises:
            KeyError: If cascade not found.
        """
        cascade = self._require(project_name)
        chain: Dict[int, List[FormulaPrecedence]] = {i: [] for i in range(1, 6)}
        for f in cascade.formulas:
            chain[f.precedence_level].append(f)
        return chain

    # ------------------------------------------------------------------
    # SQL Generation
    # ------------------------------------------------------------------

    def generate_calculation_sql(
        self,
        formula: FormulaPrecedence,
        source_alias: str = "src",
    ) -> str:
        """Generate SQL expression for a single formula.

        Args:
            formula: The formula to convert.
            source_alias: Table alias for column references.

        Returns:
            SQL expression string.
        """
        logic = formula.logic
        # Replace references with qualified column names
        for ref in formula.param_ref:
            safe_ref = re.escape(ref)
            logic = re.sub(
                rf"\b{safe_ref}\b",
                f"{source_alias}.{_sql_safe(ref)}",
                logic,
            )
        return logic

    def generate_cascade_cte(
        self,
        project_name: str,
        base_table: str = "DT_3A",
        target_table: str = "DT_3",
    ) -> str:
        """Generate the full 5-level cascade as CTEs.

        Structure:
          P1_BASE — base aggregations from DT_3A
          P2_CALC — simple calculated rows via UNION ALL
          P3_CALC — ratios via UNION ALL
          P4_CALC — variances via UNION ALL
          P5_CALC — complex metrics via UNION ALL
          Final SELECT combines all levels.

        Args:
            project_name: Cascade identifier.
            base_table: Source table for P1.
            target_table: Target table name.

        Returns:
            Complete SQL string with CTEs.
        """
        chain = self.build_precedence_chain(project_name)
        cascade = self._require(project_name)
        cte_parts: List[str] = []

        # P1: Base aggregation CTE
        cte_parts.append(
            f"P1_BASE AS (\n"
            f"    SELECT *\n"
            f"    FROM {base_table}\n"
            f")"
        )

        prev_cte = "P1_BASE"
        for level in range(2, 6):
            formulas = chain[level]
            if not formulas:
                # Empty level: pass through
                cte_parts.append(
                    f"P{level}_CALC AS (\n"
                    f"    SELECT * FROM {prev_cte}\n"
                    f")"
                )
            else:
                union_parts = [f"    SELECT * FROM {prev_cte}"]
                for f in formulas:
                    expr = self.generate_calculation_sql(f, prev_cte)
                    union_parts.append(
                        f"    UNION ALL\n"
                        f"    SELECT\n"
                        f"        '{f.formula_group}' AS FORMULA_GROUP,\n"
                        f"        {f.precedence_level} AS PRECEDENCE_LEVEL,\n"
                        f"        ({expr}) AS CALCULATED_VALUE\n"
                        f"    FROM {prev_cte}"
                    )
                cte_parts.append(
                    f"P{level}_CALC AS (\n"
                    + "\n".join(union_parts)
                    + "\n)"
                )
            prev_cte = f"P{level}_CALC"

        # Assemble
        cte_sql = "WITH\n" + ",\n".join(cte_parts)
        final_sql = (
            f"{cte_sql}\n"
            f"SELECT *\n"
            f"FROM {prev_cte}"
        )

        cascade.cascade_sql = final_sql
        return final_sql

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

    def validate_dependencies(self, project_name: str) -> List[str]:
        """Check that all formula dependencies are satisfiable.

        Returns:
            List of error messages (empty if valid).
        """
        cascade = self._require(project_name)
        errors: List[str] = []
        known_groups: Set[str] = set()

        chain = self.build_precedence_chain(project_name)
        for level in range(1, 6):
            for f in chain[level]:
                for ref in f.param_ref:
                    if ref not in known_groups and level > 1:
                        errors.append(
                            f"Formula '{f.formula_group}' (P{level}) references "
                            f"'{ref}' which is not defined at a lower precedence level"
                        )
                known_groups.add(f.formula_group)

        if errors:
            logger.warning("Dependency issues in %s: %s", project_name, errors)
        return errors

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

    def get_cascade(self, project_name: str) -> Optional[FormulaCascade]:
        """Retrieve a cascade by project name."""
        return self._cascades.get(project_name)

    def _require(self, project_name: str) -> FormulaCascade:
        cascade = self._cascades.get(project_name)
        if cascade is None:
            raise KeyError(f"Formula cascade not found: {project_name}")
        return cascade

    @staticmethod
    def _infer_precedence(
        operation: str,
        operands: List[Any],
        existing: List[FormulaPrecedence],
    ) -> int:
        """Infer precedence level from operation type."""
        op = operation.upper()
        if op in ("SUM", "COUNT", "AVG", "MIN", "MAX"):
            return 1
        if op in ("SUBTRACT", "ADD", "MULTIPLY"):
            return 2
        if op in ("DIVIDE", "RATIO", "PERCENTAGE"):
            return 3
        if op in ("VARIANCE", "DELTA", "CHANGE"):
            return 4
        return 5

    @staticmethod
    def _build_logic_expr(operation: str, operands: List[Any]) -> str:
        """Build a human-readable logic expression."""
        op = operation.upper()
        if not operands:
            return f"{op}()"

        if op == "SUM":
            return f"SUM({', '.join(str(o) for o in operands)})"
        if op == "SUBTRACT" and len(operands) >= 2:
            return f"{operands[0]} - {operands[1]}"
        if op == "ADD":
            return " + ".join(str(o) for o in operands)
        if op == "MULTIPLY" and len(operands) >= 2:
            return " * ".join(str(o) for o in operands)
        if op == "DIVIDE" and len(operands) >= 2:
            return f"{operands[0]} / NULLIF({operands[1]}, 0)"
        if op in ("RATIO", "PERCENTAGE") and len(operands) >= 2:
            return f"({operands[0]} / NULLIF({operands[1]}, 0)) * 100"
        if op in ("VARIANCE", "DELTA") and len(operands) >= 2:
            return f"{operands[0]} - {operands[1]}"

        return f"{op}({', '.join(str(o) for o in operands)})"


def _sql_safe(name: str) -> str:
    """Sanitize a name for SQL identifier use."""
    return re.sub(r"[^A-Za-z0-9_]", "_", name)
