"""Excel logic extractor using openpyxl.

Extracts business logic from:
- Named ranges → business measure definitions
- Formulas → calculation logic (SUMIFS, VLOOKUP, IF chains)
- Cell references across sheets → dependency graph
"""
from __future__ import annotations

import logging
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple

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

logger = logging.getLogger(__name__)

# Regex patterns for Excel formula decomposition
_SUMIFS_PATTERN = re.compile(
    r"SUMIFS?\s*\(\s*([^,]+),\s*([^)]+)\)",
    re.IGNORECASE,
)
_COUNTIFS_PATTERN = re.compile(
    r"COUNTIFS?\s*\(\s*([^)]+)\)",
    re.IGNORECASE,
)
_AVERAGEIFS_PATTERN = re.compile(
    r"AVERAGEIFS?\s*\(\s*([^,]+),\s*([^)]+)\)",
    re.IGNORECASE,
)
_VLOOKUP_PATTERN = re.compile(
    r"VLOOKUP\s*\(\s*([^,]+),\s*([^,]+),\s*(\d+)",
    re.IGNORECASE,
)
_IF_PATTERN = re.compile(
    r"IF\s*\(\s*([^,]+),\s*([^,]+),\s*([^)]+)\)",
    re.IGNORECASE,
)
_SHEET_REF_PATTERN = re.compile(
    r"(?:'([^']+)'|(\w+))!([A-Z]+\d+(?::[A-Z]+\d+)?)",
)
_SIMPLE_AGG_PATTERN = re.compile(
    r"(SUM|COUNT|AVERAGE|MIN|MAX|COUNTA)\s*\(\s*([^)]+)\)",
    re.IGNORECASE,
)

# Map Excel functions to our aggregation types
_EXCEL_AGG_MAP = {
    "SUM": MeasureAggregation.SUM,
    "SUMIF": MeasureAggregation.SUM,
    "SUMIFS": MeasureAggregation.SUM,
    "COUNT": MeasureAggregation.COUNT,
    "COUNTIF": MeasureAggregation.COUNT,
    "COUNTIFS": MeasureAggregation.COUNT,
    "COUNTA": MeasureAggregation.COUNT,
    "AVERAGE": MeasureAggregation.AVG,
    "AVERAGEIF": MeasureAggregation.AVG,
    "AVERAGEIFS": MeasureAggregation.AVG,
    "MIN": MeasureAggregation.MIN,
    "MAX": MeasureAggregation.MAX,
}


class ExcelLogicExtractor:
    """Extract business logic from Excel workbooks."""

    def extract_file(self, file_path: str) -> LogicArtifact:
        """Extract logic from an Excel file on disk."""
        try:
            import openpyxl
        except ImportError:
            logger.warning("openpyxl not installed — cannot parse Excel files")
            return LogicArtifact(
                source_type=SourceType.EXCEL,
                source_path=file_path,
                source_name=Path(file_path).name,
                raw_source="",
                confidence=0.0,
                explanation="openpyxl not installed",
            )

        try:
            wb = openpyxl.load_workbook(file_path, data_only=False)
        except Exception as e:
            logger.warning("Excel load error for %s: %s", file_path, e)
            return LogicArtifact(
                source_type=SourceType.EXCEL,
                source_path=file_path,
                source_name=Path(file_path).name,
                raw_source="",
                confidence=0.0,
                explanation=f"Load error: {e}",
            )

        measures: List[Measure] = []
        filters: List[FilterPredicate] = []
        dependencies: List[Dependency] = []
        grain_columns: List[GrainColumn] = []
        formulas_found: List[str] = []
        named_ranges_found: List[str] = []

        # --- Named ranges ---
        for name_key, defn in wb.defined_names.items():
            named_ranges_found.append(name_key)
            try:
                destinations = list(defn.destinations)
                for sheet_title, coord in destinations:
                    dependencies.append(Dependency(
                        name=f"{sheet_title}!{coord}",
                        dep_type="named_range",
                    ))
            except Exception:
                pass  # Some defined names aren't resolvable

        # --- Scan sheets for formulas ---
        for ws in wb.worksheets:
            for row in ws.iter_rows():
                for cell in row:
                    if cell.value and isinstance(cell.value, str) and cell.value.startswith("="):
                        formula = cell.value[1:]  # Strip leading =
                        formulas_found.append(formula)

                        # Extract measures from aggregation formulas
                        measures.extend(self._extract_measures_from_formula(
                            formula, ws.title, cell.coordinate,
                        ))

                        # Extract filters from conditional formulas
                        filters.extend(self._extract_filters_from_formula(
                            formula, ws.title, cell.coordinate,
                        ))

                        # Extract cross-sheet dependencies
                        dependencies.extend(self._extract_sheet_refs(
                            formula, ws.title,
                        ))

        wb.close()

        # Compute confidence
        has_content = bool(measures or filters or dependencies or named_ranges_found)
        confidence = 0.5 if has_content else 0.1

        if measures:
            confidence += 0.2
        if named_ranges_found:
            confidence += 0.1
        confidence = min(confidence, 1.0)

        return LogicArtifact(
            source_type=SourceType.EXCEL,
            source_path=file_path,
            source_name=Path(file_path).name,
            raw_source=f"Formulas: {len(formulas_found)}, Named ranges: {len(named_ranges_found)}",
            confidence=confidence,
            objects=LogicObjects(
                measures=measures,
                filters=filters,
                dependencies=dependencies,
                grain_columns=grain_columns,
            ),
            metadata={
                "formula_count": len(formulas_found),
                "named_range_count": len(named_ranges_found),
                "sheet_count": len(wb.sheetnames) if hasattr(wb, "sheetnames") else 0,
                "named_ranges": named_ranges_found[:20],
            },
        )

    def extract(self, formulas: List[str], source_path: str = "", source_name: str = "") -> LogicArtifact:
        """Extract logic from a list of formula strings (for testing or inline use)."""
        measures: List[Measure] = []
        filters: List[FilterPredicate] = []
        dependencies: List[Dependency] = []

        for formula in formulas:
            measures.extend(self._extract_measures_from_formula(formula, "Sheet1", ""))
            filters.extend(self._extract_filters_from_formula(formula, "Sheet1", ""))
            dependencies.extend(self._extract_sheet_refs(formula, "Sheet1"))

        has_content = bool(measures or filters or dependencies)
        confidence = 0.5 if has_content else 0.1
        if measures:
            confidence += 0.2
        confidence = min(confidence, 1.0)

        return LogicArtifact(
            source_type=SourceType.EXCEL,
            source_path=source_path,
            source_name=source_name or source_path,
            raw_source=f"Formulas: {len(formulas)}",
            confidence=confidence,
            objects=LogicObjects(
                measures=measures,
                filters=filters,
                dependencies=dependencies,
            ),
            metadata={"formula_count": len(formulas)},
        )

    # ---- Formula decomposition ----

    def _extract_measures_from_formula(
        self, formula: str, sheet: str, cell: str,
    ) -> List[Measure]:
        """Extract measures from SUMIFS, COUNTIFS, AVERAGE, etc."""
        measures: List[Measure] = []

        # SUMIFS / SUMIF
        for m in _SUMIFS_PATTERN.finditer(formula):
            sum_range = m.group(1).strip()
            criteria = m.group(2).strip()
            measures.append(Measure(
                name=f"SUMIFS({sum_range})",
                expression=m.group(0),
                aggregation=MeasureAggregation.SUM,
                source_columns=[self._clean_range_ref(sum_range)],
                confidence=0.7,
            ))

        # COUNTIFS / COUNTIF
        for m in _COUNTIFS_PATTERN.finditer(formula):
            measures.append(Measure(
                name=f"COUNTIFS({m.group(1).strip()[:40]})",
                expression=m.group(0),
                aggregation=MeasureAggregation.COUNT,
                confidence=0.6,
            ))

        # AVERAGEIFS / AVERAGEIF
        for m in _AVERAGEIFS_PATTERN.finditer(formula):
            avg_range = m.group(1).strip()
            measures.append(Measure(
                name=f"AVERAGEIFS({avg_range})",
                expression=m.group(0),
                aggregation=MeasureAggregation.AVG,
                source_columns=[self._clean_range_ref(avg_range)],
                confidence=0.7,
            ))

        # Simple aggregations (SUM, COUNT, AVERAGE, MIN, MAX)
        if not measures:
            for m in _SIMPLE_AGG_PATTERN.finditer(formula):
                func_name = m.group(1).upper()
                agg_range = m.group(2).strip()
                agg_type = _EXCEL_AGG_MAP.get(func_name, MeasureAggregation.CUSTOM)
                measures.append(Measure(
                    name=f"{func_name}({agg_range})",
                    expression=m.group(0),
                    aggregation=agg_type,
                    source_columns=[self._clean_range_ref(agg_range)],
                    confidence=0.6,
                ))

        # VLOOKUP — treated as a dependency/measure bridge
        for m in _VLOOKUP_PATTERN.finditer(formula):
            lookup_val = m.group(1).strip()
            table_range = m.group(2).strip()
            col_idx = m.group(3)
            measures.append(Measure(
                name=f"VLOOKUP({lookup_val},{col_idx})",
                expression=m.group(0),
                aggregation=MeasureAggregation.CUSTOM,
                source_columns=[self._clean_range_ref(table_range)],
                confidence=0.5,
            ))

        return measures

    def _extract_filters_from_formula(
        self, formula: str, sheet: str, cell: str,
    ) -> List[FilterPredicate]:
        """Extract filter-like conditions from IF/SUMIFS criteria."""
        filters: List[FilterPredicate] = []

        # IF conditions
        for m in _IF_PATTERN.finditer(formula):
            condition = m.group(1).strip()
            filters.append(FilterPredicate(
                column="",
                source_clause=f"IF({condition})",
                confidence=0.4,
            ))

        # SUMIFS criteria pairs (criteria_range, criteria)
        for m in _SUMIFS_PATTERN.finditer(formula):
            criteria_str = m.group(2).strip()
            # Criteria come in pairs: range, value, range, value, ...
            parts = [p.strip() for p in criteria_str.split(",")]
            for i in range(0, len(parts) - 1, 2):
                crit_range = parts[i]
                crit_value = parts[i + 1] if i + 1 < len(parts) else ""
                if crit_range and crit_value:
                    filters.append(FilterPredicate(
                        column=self._clean_range_ref(crit_range),
                        source_clause=f"{crit_range}={crit_value}",
                        confidence=0.5,
                    ))

        return filters

    def _extract_sheet_refs(self, formula: str, current_sheet: str) -> List[Dependency]:
        """Extract cross-sheet references as dependencies."""
        deps: List[Dependency] = []
        seen = set()

        for m in _SHEET_REF_PATTERN.finditer(formula):
            ref_sheet = m.group(1) or m.group(2)
            ref_range = m.group(3)
            if ref_sheet and ref_sheet != current_sheet:
                key = f"{ref_sheet}!{ref_range}"
                if key not in seen:
                    deps.append(Dependency(
                        name=ref_sheet,
                        dep_type="sheet_ref",
                    ))
                    seen.add(key)

        return deps

    # ---- Utilities ----

    def _clean_range_ref(self, ref: str) -> str:
        """Clean an Excel range reference to a simple column name."""
        # Remove sheet prefix
        if "!" in ref:
            ref = ref.split("!", 1)[1]
        # Remove $ signs
        ref = ref.replace("$", "")
        return ref
