"""BLCE Report Processor — extract KPIs, dimensions, and filters from reports.

Supports Excel (.xlsx) and PDF (.pdf) files via existing BLCE parsers,
then scores against known report patterns.
"""
from __future__ import annotations

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

from .constants import ReportPatternType
from .contracts import (
    ExtractedDimension,
    ExtractedFilter,
    ExtractedKPI,
    LogicArtifact,
    ReportAnalysis,
)
from .report_patterns import (
    DIMENSION_KEYWORD_MAP,
    MEASURE_KEYWORD_MAP,
    REPORT_PATTERNS,
)

logger = logging.getLogger(__name__)


class ReportProcessor:
    """Extract KPIs, dimensions, and filters from Excel/PDF reports."""

    def __init__(
        self,
        *,
        excel_wizard_enabled: bool = False,
        excel_wizard_use_copilot: bool = True,
        excel_wizard_gateway_url: str = "",
        excel_wizard_copilot_mode: str = "guarded",
        excel_wizard_high_confidence_threshold: float = 0.80,
        excel_wizard_review_threshold: float = 0.55,
        excel_wizard_max_chunks: int = 2000,
    ):
        self.excel_wizard_enabled = excel_wizard_enabled
        self.excel_wizard_use_copilot = excel_wizard_use_copilot
        self._wizard = None
        if excel_wizard_enabled:
            from .excel_wizard import ExcelLogicWizard
            self._wizard = ExcelLogicWizard(
                gateway_url=excel_wizard_gateway_url,
                high_confidence_threshold=excel_wizard_high_confidence_threshold,
                review_threshold=excel_wizard_review_threshold,
                copilot_mode=excel_wizard_copilot_mode,
                max_chunks=excel_wizard_max_chunks,
            )

    def process_report(self, file_path: str) -> ReportAnalysis:
        """Dispatch to Excel/PDF parser, then extract KPIs/dims/filters.

        Args:
            file_path: Path to an .xlsx or .pdf file.

        Returns:
            ReportAnalysis with extracted elements.
        """
        ext = os.path.splitext(file_path)[1].lower()

        if ext in (".xlsx", ".xls", ".xlsm"):
            artifact = self._extract_from_excel(file_path)
        elif ext == ".pdf":
            artifact = self._extract_from_pdf(file_path)
        else:
            logger.warning("Unsupported file type: %s", ext)
            return ReportAnalysis(file_path=file_path)

        kpis = self._extract_kpis(artifact)
        dimensions = self._extract_dimensions(artifact)
        filters = self._extract_filters(artifact)
        report_type = self._match_report_pattern(kpis, dimensions)

        return ReportAnalysis(
            file_path=file_path,
            report_type=report_type,
            kpis=kpis,
            dimensions=dimensions,
            filters=filters,
        )

    # ------------------------------------------------------------------
    # Parser delegates
    # ------------------------------------------------------------------

    def _extract_from_excel(self, file_path: str) -> LogicArtifact:
        """Use ExcelLogicExtractor to parse workbook."""
        try:
            from .parsers.excel_parser import ExcelLogicExtractor

            extractor = ExcelLogicExtractor()
            return extractor.extract_file(file_path)
        except Exception as exc:
            logger.warning("Excel parse failed: %s", exc)
            return LogicArtifact(source_path=file_path)

    def process_report_with_wizard(
        self,
        file_path: str,
        *,
        prompt_hint: str = "",
    ) -> ReportAnalysis:
        """Process report and optionally enrich with Excel wizard suggestions."""
        base = self.process_report(file_path)
        ext = os.path.splitext(file_path)[1].lower()
        if ext not in (".xlsx", ".xls", ".xlsm"):
            return base
        if not self._wizard:
            return base

        try:
            wiz = self._wizard.analyze_workbook(
                file_path,
                prompt_hint=prompt_hint,
                use_copilot=self.excel_wizard_use_copilot,
            )
            base.excel_wizard_id = wiz.analysis_id
            base.wizard_suggestions = wiz.suggestions
            base.wizard_confidence = wiz.confidence
            # Keep deterministic and wizard suggestions separate; model phase
            # applies confidence/review gate before consuming suggestions.
            return base
        except Exception as exc:
            logger.warning("Excel wizard enrichment failed: %s", exc)
            return base

    def _extract_from_pdf(self, file_path: str) -> LogicArtifact:
        """Use PDFLogicExtractor to parse PDF."""
        try:
            from .parsers.pdf_parser import PDFLogicExtractor

            extractor = PDFLogicExtractor()
            return extractor.extract_file(file_path)
        except Exception as exc:
            logger.warning("PDF parse failed: %s", exc)
            return LogicArtifact(source_path=file_path)

    # ------------------------------------------------------------------
    # Extraction helpers
    # ------------------------------------------------------------------

    def _extract_kpis(self, artifact: LogicArtifact) -> List[ExtractedKPI]:
        """Convert measures from artifact into ExtractedKPI objects."""
        kpis: List[ExtractedKPI] = []
        for m in artifact.objects.measures:
            name = m.alias or m.name
            if not name:
                continue

            canonical = self._canonicalize_measure(name)
            kpis.append(
                ExtractedKPI(
                    name=canonical or name,
                    business_name=name,
                    formula=m.expression,
                    source=artifact.source_path,
                    source_type="report",
                    confidence=max(m.confidence, 0.5),
                )
            )
        return kpis

    def _extract_dimensions(
        self, artifact: LogicArtifact
    ) -> List[ExtractedDimension]:
        """Convert grain columns and dependencies into ExtractedDimension."""
        dims: List[ExtractedDimension] = []
        seen: set = set()

        for gc in artifact.objects.grain_columns:
            col_lower = gc.column.lower()
            dim_name = self._canonicalize_dimension(col_lower)
            if dim_name and dim_name not in seen:
                seen.add(dim_name)
                dims.append(
                    ExtractedDimension(
                        name=dim_name,
                        business_name=gc.column,
                        source_tables=[gc.table] if gc.table else [],
                        key_columns=[gc.column],
                        extracted_from="report",
                        confidence=0.7,
                    )
                )

        for dep in artifact.objects.dependencies:
            dep_lower = dep.name.lower()
            if "dim" in dep_lower and dep.name not in seen:
                seen.add(dep.name)
                dims.append(
                    ExtractedDimension(
                        name=dep.name,
                        business_name=dep.name.replace("_", " ").title(),
                        source_tables=[dep.name],
                        extracted_from="report",
                        confidence=0.6,
                    )
                )

        return dims

    def _extract_filters(self, artifact: LogicArtifact) -> List[ExtractedFilter]:
        """Convert filter predicates into ExtractedFilter objects."""
        filters: List[ExtractedFilter] = []
        for fp in artifact.objects.filters:
            if not fp.column and not fp.source_clause:
                continue
            filters.append(
                ExtractedFilter(
                    description=fp.source_clause or f"{fp.column} {fp.operator.value} {fp.value}",
                    sql_expression=fp.source_clause,
                    applies_to=[fp.column] if fp.column else [],
                    source=artifact.source_path,
                    confidence=0.6,
                )
            )
        return filters

    # ------------------------------------------------------------------
    # Pattern matching
    # ------------------------------------------------------------------

    def _match_report_pattern(
        self,
        kpis: List[ExtractedKPI],
        dimensions: List[ExtractedDimension],
    ) -> ReportPatternType:
        """Score extracted data against known report patterns."""
        kpi_names = {k.name.lower() for k in kpis}
        dim_names = {d.name.lower() for d in dimensions}

        best_type = ReportPatternType.CUSTOM
        best_score = 0.0

        for pattern_key, pattern in REPORT_PATTERNS.items():
            expected_m = pattern.get("expected_measures", [])
            expected_d = pattern.get("expected_dimensions", [])

            m_hits = sum(1 for m in expected_m if m in kpi_names)
            d_hits = sum(1 for d in expected_d if d in dim_names)

            total = len(expected_m) + len(expected_d)
            if total == 0:
                continue

            score = (m_hits + d_hits) / total
            if score > best_score:
                best_score = score
                try:
                    best_type = ReportPatternType(pattern_key)
                except ValueError:
                    best_type = ReportPatternType.CUSTOM

        if best_score < 0.3:
            return ReportPatternType.CUSTOM

        return best_type

    # ------------------------------------------------------------------
    # Canonicalization
    # ------------------------------------------------------------------

    @staticmethod
    def _canonicalize_measure(name: str) -> str:
        """Map a measure name to canonical form."""
        lower = name.lower().strip()
        for keyword, canonical in MEASURE_KEYWORD_MAP.items():
            if keyword in lower:
                return canonical
        return ""

    @staticmethod
    def _canonicalize_dimension(col_name: str) -> str:
        """Map a column/grain name to a dimension name."""
        lower = col_name.lower().strip()
        for keyword, dim in DIMENSION_KEYWORD_MAP.items():
            if keyword in lower:
                return dim
        return ""
