"""MDX logic extractor using regex-based parsing.

Extracts business logic from Analysis Services MDX queries:
- Calculated members (WITH MEMBER) as measures
- WHERE slicer / sub-select filters
- ON ROWS / ON COLUMNS as grain / dimension axes
- Cube and dimension references as dependencies
"""
from __future__ import annotations

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

from ..constants import (
    FilterOperator,
    MeasureAggregation,
    SourceType,
)
from ..contracts import (
    Dependency,
    FilterPredicate,
    GrainColumn,
    LogicArtifact,
    LogicObjects,
    Measure,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# MDX aggregation function mapping
# ---------------------------------------------------------------------------

_MDX_AGG_MAP: Dict[str, MeasureAggregation] = {
    "SUM": MeasureAggregation.SUM,
    "AGGREGATE": MeasureAggregation.SUM,
    "COUNT": MeasureAggregation.COUNT,
    "DISTINCTCOUNT": MeasureAggregation.COUNT_DISTINCT,
    "AVG": MeasureAggregation.AVG,
    "MIN": MeasureAggregation.MIN,
    "MAX": MeasureAggregation.MAX,
    "IIF": MeasureAggregation.CUSTOM,
    "CASE": MeasureAggregation.CUSTOM,
    "DIVIDE": MeasureAggregation.CUSTOM,
}

# ---------------------------------------------------------------------------
# Regex patterns for MDX parsing
# ---------------------------------------------------------------------------

# WITH MEMBER [Measures].[Name] AS <expression>
_WITH_MEMBER_PATTERN = re.compile(
    r"WITH\s+MEMBER\s+(\[Measures\]\.\[([^\]]+)\])\s+AS\s+(.+?)(?=\s+MEMBER\s|\s+SELECT\s|$)",
    re.IGNORECASE | re.DOTALL,
)

# Single MEMBER definition (for multiple WITH clauses)
_MEMBER_PATTERN = re.compile(
    r"MEMBER\s+(\[Measures\]\.\[([^\]]+)\])\s+AS\s+(.+?)(?=\s+MEMBER\s|\s+SET\s|\s+SELECT\s|$)",
    re.IGNORECASE | re.DOTALL,
)

# WITH SET definitions
_SET_PATTERN = re.compile(
    r"SET\s+(\[([^\]]+)\])\s+AS\s+(.+?)(?=\s+MEMBER\s|\s+SET\s|\s+SELECT\s|$)",
    re.IGNORECASE | re.DOTALL,
)

# MDX aggregation call
_MDX_AGG_CALL = re.compile(
    r"\b(SUM|AGGREGATE|COUNT|DISTINCTCOUNT|AVG|MIN|MAX|IIF|CASE|DIVIDE)\s*\(",
    re.IGNORECASE,
)

# SELECT ... ON COLUMNS/ROWS pattern
_ON_AXIS_PATTERN = re.compile(
    r"\{?\s*(.+?)\s*\}?\s+ON\s+(COLUMNS|ROWS|PAGES|\d)",
    re.IGNORECASE | re.DOTALL,
)

# FROM [CubeName] or FROM (sub-select)
_FROM_CUBE_PATTERN = re.compile(
    r"FROM\s+\[([^\]]+)\]",
    re.IGNORECASE,
)

# FROM (SELECT ... ) sub-select
_FROM_SUBSELECT_PATTERN = re.compile(
    r"FROM\s*\(\s*SELECT\s+(.+?)\s+ON\s+\d\s+FROM\s+\[([^\]]+)\]",
    re.IGNORECASE | re.DOTALL,
)

# WHERE clause / slicer
_WHERE_PATTERN = re.compile(
    r"WHERE\s*\(\s*(.+?)\s*\)\s*$",
    re.IGNORECASE | re.DOTALL,
)

# Dimension hierarchy reference: [Dimension].[Hierarchy].[Level]
_DIM_HIERARCHY_PATTERN = re.compile(
    r"\[([^\]]+)\]\.\[([^\]]+)\](?:\.\[([^\]]+)\])?(?:\.\[([^\]]+)\])?",
)

# Measure reference: [Measures].[MeasureName]
_MEASURE_REF_PATTERN = re.compile(
    r"\[Measures\]\.\[([^\]]+)\]",
    re.IGNORECASE,
)

# .Members, .Children, .CurrentMember
_MEMBER_FUNC_PATTERN = re.compile(
    r"\[([^\]]+)\]\.\[([^\]]+)\]\.(?:Members|Children|CurrentMember|AllMembers)",
    re.IGNORECASE,
)

# CROSSJOIN, NONEMPTYCROSSJOIN, NONEMPTY
_CROSSJOIN_PATTERN = re.compile(
    r"(?:CROSSJOIN|NONEMPTYCROSSJOIN|NONEMPTY)\s*\(\s*(.+?)\s*\)",
    re.IGNORECASE | re.DOTALL,
)

# FILTER function
_MDX_FILTER_PATTERN = re.compile(
    r"FILTER\s*\(\s*(.+?),\s*(.+?)\)",
    re.IGNORECASE | re.DOTALL,
)

# Comparison in MDX: dimension.member > value
_MDX_COMPARISON_PATTERN = re.compile(
    r"(\[.+?\](?:\.\[.+?\])*)\s*(>|<|>=|<=|<>|=)\s*(.+?)(?:\s*[,)]|$)",
)

_MDX_OP_MAP = {
    "=": FilterOperator.EQ,
    "<>": FilterOperator.NEQ,
    ">": FilterOperator.GT,
    ">=": FilterOperator.GTE,
    "<": FilterOperator.LT,
    "<=": FilterOperator.LTE,
}


class MDXLogicExtractor:
    """Extract business logic from Analysis Services MDX queries.

    Handles:
    - WITH MEMBER calculated members as measure definitions
    - ON COLUMNS/ROWS as axis definitions (grain)
    - FROM [Cube] as cube dependency
    - WHERE clause as slicer/filter
    - Dimension hierarchy references
    - FILTER(), CROSSJOIN(), NONEMPTY() functions
    """

    def extract(
        self,
        source: str,
        source_path: str = "",
        source_name: str = "",
    ) -> LogicArtifact:
        """Parse MDX source and extract all business logic elements."""
        if not source or not source.strip():
            return LogicArtifact(
                source_type=SourceType.MDX,
                source_path=source_path,
                source_name=source_name,
                raw_source="",
                confidence=0.0,
                explanation="Empty MDX source",
            )

        try:
            measures = self._extract_measures(source)
            filters = self._extract_filters(source)
            grain_columns = self._extract_grain(source)
            dependencies = self._extract_dependencies(source)
            confidence = self._compute_confidence(measures, filters, grain_columns, dependencies)

            grain_desc = ""
            if grain_columns:
                grain_desc = ", ".join(g.column for g in grain_columns)

            return LogicArtifact(
                source_type=SourceType.MDX,
                source_path=source_path,
                source_name=source_name,
                raw_source=source[:2000],
                grain=grain_desc,
                confidence=confidence,
                objects=LogicObjects(
                    measures=measures,
                    filters=filters,
                    grain_columns=grain_columns,
                    dependencies=dependencies,
                ),
                explanation=self._build_explanation(measures, filters, grain_columns, dependencies),
            )
        except Exception as e:
            logger.warning("MDX parse error for %s: %s", source_path or source_name, e)
            return LogicArtifact(
                source_type=SourceType.MDX,
                source_path=source_path,
                source_name=source_name,
                raw_source=source[:2000],
                confidence=0.0,
                explanation=f"Parse error: {e}",
            )

    def extract_file(self, file_path: str) -> LogicArtifact:
        """Extract logic from an MDX file on disk."""
        from pathlib import Path

        path = Path(file_path)
        if not path.exists():
            return LogicArtifact(
                source_type=SourceType.MDX,
                source_path=file_path,
                confidence=0.0,
                explanation=f"File not found: {file_path}",
            )

        source = path.read_text(encoding="utf-8", errors="replace")
        return self.extract(source, source_path=file_path, source_name=path.stem)

    # ------------------------------------------------------------------
    # Measure extraction
    # ------------------------------------------------------------------

    def _extract_measures(self, source: str) -> List[Measure]:
        """Extract calculated members and measure references from MDX."""
        measures: List[Measure] = []
        seen_names: Set[str] = set()

        # 1. WITH MEMBER definitions
        for match in _MEMBER_PATTERN.finditer(source):
            name = match.group(2).strip()
            expr = match.group(3).strip()

            if name.lower() in seen_names:
                continue

            agg = MeasureAggregation.CUSTOM
            agg_match = _MDX_AGG_CALL.search(expr)
            if agg_match:
                func_name = agg_match.group(1).upper()
                agg = _MDX_AGG_MAP.get(func_name, MeasureAggregation.CUSTOM)

            # Extract referenced measures and columns
            refs = _MEASURE_REF_PATTERN.findall(expr)
            cols = [r for r in refs if r.lower() != name.lower()]

            measures.append(Measure(
                name=name,
                expression=expr[:500],
                aggregation=agg,
                source_columns=cols,
                confidence=0.85,
            ))
            seen_names.add(name.lower())

        # 2. Measure references in SELECT (not already found as calculated)
        for match in _MEASURE_REF_PATTERN.finditer(source):
            name = match.group(1).strip()
            if name.lower() in seen_names:
                continue

            measures.append(Measure(
                name=name,
                expression=f"[Measures].[{name}]",
                aggregation=MeasureAggregation.CUSTOM,
                confidence=0.7,
            ))
            seen_names.add(name.lower())

        return measures

    # ------------------------------------------------------------------
    # Filter extraction
    # ------------------------------------------------------------------

    def _extract_filters(self, source: str) -> List[FilterPredicate]:
        """Extract filters from WHERE clause and FILTER() function."""
        filters: List[FilterPredicate] = []
        seen: Set[str] = set()

        # 1. WHERE slicer
        for match in _WHERE_PATTERN.finditer(source):
            slicer = match.group(1).strip()
            # Parse dimension member references as filters
            for dim_match in _DIM_HIERARCHY_PATTERN.finditer(slicer):
                dim = dim_match.group(1)
                hierarchy = dim_match.group(2)
                member = dim_match.group(3) or dim_match.group(4) or ""

                if dim.lower() == "measures":
                    continue

                key = f"{dim}.{hierarchy}.{member}".lower()
                if key not in seen:
                    col = f"{dim}.{hierarchy}"
                    filters.append(FilterPredicate(
                        column=col,
                        operator=FilterOperator.EQ,
                        value=member if member else hierarchy,
                        source_clause=slicer[:200],
                    ))
                    seen.add(key)

        # 2. FILTER() function
        for match in _MDX_FILTER_PATTERN.finditer(source):
            condition = match.group(2).strip()
            for cmp in _MDX_COMPARISON_PATTERN.finditer(condition):
                ref = cmp.group(1).strip()
                op_str = cmp.group(2).strip()
                val = cmp.group(3).strip()
                key = f"{ref}|{op_str}|{val}".lower()
                if key not in seen:
                    filters.append(FilterPredicate(
                        column=ref,
                        operator=_MDX_OP_MAP.get(op_str, FilterOperator.EQ),
                        value=val,
                        source_clause=condition[:200],
                    ))
                    seen.add(key)

        # 3. Sub-select filters
        for match in _FROM_SUBSELECT_PATTERN.finditer(source):
            sub_expr = match.group(1).strip()
            for dim_match in _DIM_HIERARCHY_PATTERN.finditer(sub_expr):
                dim = dim_match.group(1)
                hierarchy = dim_match.group(2)
                if dim.lower() == "measures":
                    continue
                key = f"sub.{dim}.{hierarchy}".lower()
                if key not in seen:
                    filters.append(FilterPredicate(
                        column=f"{dim}.{hierarchy}",
                        operator=FilterOperator.IN,
                        value="(sub-select members)",
                        source_clause=sub_expr[:200],
                    ))
                    seen.add(key)

        return filters

    # ------------------------------------------------------------------
    # Grain extraction
    # ------------------------------------------------------------------

    def _extract_grain(self, source: str) -> List[GrainColumn]:
        """Extract grain from ON ROWS/COLUMNS axis definitions."""
        grain_columns: List[GrainColumn] = []
        seen: Set[str] = set()

        for match in _ON_AXIS_PATTERN.finditer(source):
            axis_expr = match.group(1).strip()
            axis_name = match.group(2).strip().upper()

            # Skip COLUMNS axis if it only has measures
            if axis_name in ("COLUMNS", "0"):
                # Check if it's only measures
                non_measure_dims = [
                    m for m in _DIM_HIERARCHY_PATTERN.finditer(axis_expr)
                    if m.group(1).lower() != "measures"
                ]
                if not non_measure_dims:
                    continue

            # Extract dimension hierarchy references
            for dim_match in _DIM_HIERARCHY_PATTERN.finditer(axis_expr):
                dim = dim_match.group(1).strip()
                hierarchy = dim_match.group(2).strip()

                if dim.lower() == "measures":
                    continue

                key = f"{dim}.{hierarchy}".lower()
                if key not in seen:
                    grain_columns.append(GrainColumn(
                        column=hierarchy,
                        table=dim,
                    ))
                    seen.add(key)

        return grain_columns

    # ------------------------------------------------------------------
    # Dependency extraction
    # ------------------------------------------------------------------

    def _extract_dependencies(self, source: str) -> List[Dependency]:
        """Extract cube and dimension references as dependencies."""
        deps: List[Dependency] = []
        seen: Set[str] = set()

        # 1. FROM [CubeName]
        for match in _FROM_CUBE_PATTERN.finditer(source):
            cube = match.group(1).strip()
            if cube.lower() not in seen:
                deps.append(Dependency(
                    name=cube,
                    dep_type="cube",
                ))
                seen.add(cube.lower())

        # 2. All dimension references
        for match in _DIM_HIERARCHY_PATTERN.finditer(source):
            dim = match.group(1).strip()
            if dim.lower() in ("measures",) or dim.lower() in seen:
                continue
            deps.append(Dependency(
                name=dim,
                dep_type="dimension",
            ))
            seen.add(dim.lower())

        return deps

    # ------------------------------------------------------------------
    # Confidence
    # ------------------------------------------------------------------

    def _compute_confidence(
        self,
        measures: List[Measure],
        filters: List[FilterPredicate],
        grain: List[GrainColumn],
        deps: List[Dependency],
    ) -> float:
        score = 0.0
        if measures:
            score += 0.30
            # Bonus for calculated members (explicit definitions)
            calculated = sum(1 for m in measures if m.confidence >= 0.8)
            if calculated > 0:
                score += 0.10
        if filters:
            score += 0.15
        if grain:
            score += 0.20
        if deps:
            score += 0.15
        return min(round(score, 2), 1.0)

    def _build_explanation(
        self,
        measures: List[Measure],
        filters: List[FilterPredicate],
        grain: List[GrainColumn],
        deps: List[Dependency],
    ) -> str:
        parts = []
        if measures:
            names = [m.name for m in measures[:5]]
            parts.append(f"{len(measures)} measure(s): {', '.join(names)}")
        if filters:
            parts.append(f"{len(filters)} filter(s)")
        if grain:
            cols = [f"{g.table}.{g.column}" if g.table else g.column for g in grain[:5]]
            parts.append(f"grain: {', '.join(cols)}")
        if deps:
            cubes = [d.name for d in deps if d.dep_type == "cube"]
            dims = [d.name for d in deps if d.dep_type == "dimension"]
            if cubes:
                parts.append(f"cube: {cubes[0]}")
            if dims:
                parts.append(f"{len(dims)} dimension(s)")
        return "; ".join(parts) if parts else "No business logic elements detected"
