"""Excel Logic Wizard for complex workbook understanding and BLCE suggestions."""
from __future__ import annotations

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

from .copilot_gateway_client import CopilotGatewayClient
from .contracts import (
    ExcelConfigCandidate,
    ExcelFormulaNode,
    ExcelHierarchyCandidate,
    ExcelNamedRangeIntent,
    ExcelPivotIntent,
    ExcelWizardAnalysis,
    ExcelWizardEvidence,
    ExcelWizardSuggestion,
    ExtractedDimension,
    ExtractedFilter,
    ExtractedKPI,
    ReportAnalysis,
)
from .excel_wizard_rag import ExcelWorkbookRAG, WorkbookChunk

logger = logging.getLogger(__name__)


class ExcelLogicWizard:
    """Extract and suggest business modeling signals from complex Excel reports."""

    def __init__(
        self,
        *,
        gateway_url: str = "",
        high_confidence_threshold: float = 0.80,
        review_threshold: float = 0.55,
        copilot_mode: str = "guarded",
        max_chunks: int = 2000,
    ):
        self.gateway_client = CopilotGatewayClient(gateway_url=gateway_url)
        self.high_confidence_threshold = high_confidence_threshold
        self.review_threshold = review_threshold
        self.copilot_mode = copilot_mode
        self.max_chunks = max_chunks
        self._analysis_store: Dict[str, ExcelWizardAnalysis] = {}
        self._rag_store: Dict[str, ExcelWorkbookRAG] = {}

    def analyze_workbook(
        self,
        file_path: str,
        *,
        prompt_hint: str = "",
        use_copilot: bool = True,
    ) -> ExcelWizardAnalysis:
        try:
            import openpyxl
        except ImportError:
            return ExcelWizardAnalysis(
                file_path=file_path,
                workbook_name=Path(file_path).name,
                confidence=0.0,
                warnings=["openpyxl_not_installed"],
            )

        try:
            wb = openpyxl.load_workbook(file_path, data_only=False)
        except Exception as exc:
            return ExcelWizardAnalysis(
                file_path=file_path,
                workbook_name=Path(file_path).name,
                confidence=0.0,
                warnings=[f"workbook_load_error:{exc}"],
            )

        formula_nodes: List[ExcelFormulaNode] = []
        named_ranges: List[ExcelNamedRangeIntent] = []
        pivots: List[ExcelPivotIntent] = []
        hierarchy_candidates: List[ExcelHierarchyCandidate] = []
        config_candidates: List[ExcelConfigCandidate] = []
        suggestions: List[ExcelWizardSuggestion] = []
        chunks: List[WorkbookChunk] = []

        # Named ranges
        for name_key, defn in wb.defined_names.items():
            destinations: List[str] = []
            try:
                destinations = [f"{s}!{c}" for s, c in defn.destinations]
            except Exception:
                pass
            inferred = self._infer_named_range_role(name_key)
            nr = ExcelNamedRangeIntent(
                name=name_key,
                destinations=destinations,
                inferred_role=inferred,
                confidence=0.65 if inferred else 0.45,
            )
            named_ranges.append(nr)
            chunks.append(
                WorkbookChunk(
                    chunk_id=f"nr:{name_key}",
                    chunk_type="named_range",
                    text=f"named_range {name_key} role {inferred} destinations {' '.join(destinations)}",
                    metadata={"name": name_key},
                )
            )

            if inferred in ("hierarchy", "rollup"):
                hierarchy_candidates.append(
                    ExcelHierarchyCandidate(
                        name=name_key,
                        levels=self._extract_hierarchy_levels(name_key),
                        rationale="Named range suggests hierarchy semantics",
                        confidence=0.62,
                    )
                )
            if inferred in ("mapping", "business_rule", "rollup"):
                config_candidates.append(
                    ExcelConfigCandidate(
                        config_type="semantic_rule",
                        name=name_key,
                        payload={"destinations": destinations},
                        rationale="Named range appears reusable as configuration",
                        confidence=0.6,
                    )
                )

        # Sheets/formulas/pivots
        for ws in wb.worksheets:
            chunks.append(
                WorkbookChunk(
                    chunk_id=f"sheet:{ws.title}",
                    chunk_type="sheet",
                    text=f"worksheet {ws.title}",
                    metadata={"sheet": ws.title},
                )
            )
            for row in ws.iter_rows():
                for cell in row:
                    if not (isinstance(cell.value, str) and cell.value.startswith("=")):
                        continue
                    formula = cell.value[1:]
                    deps = self._extract_formula_dependencies(formula)
                    node = ExcelFormulaNode(
                        sheet_name=ws.title,
                        cell_ref=cell.coordinate,
                        formula=formula,
                        dependencies=deps,
                    )
                    formula_nodes.append(node)
                    chunks.append(
                        WorkbookChunk(
                            chunk_id=f"formula:{ws.title}:{cell.coordinate}",
                            chunk_type="formula",
                            text=f"sheet {ws.title} cell {cell.coordinate} formula {formula}",
                            metadata={"sheet": ws.title, "cell": cell.coordinate},
                        )
                    )
                    suggestions.extend(
                        self._suggest_from_formula(ws.title, cell.coordinate, formula)
                    )

            pivot_objs = getattr(ws, "_pivots", None) or []
            for idx, _ in enumerate(pivot_objs):
                piv = ExcelPivotIntent(
                    sheet_name=ws.title,
                    pivot_name=f"{ws.title}_pivot_{idx + 1}",
                    confidence=0.55,
                )
                pivots.append(piv)
                suggestions.append(
                    ExcelWizardSuggestion(
                        suggestion_type="hierarchy_candidate",
                        title=f"Pivot hierarchy on {ws.title}",
                        description="Pivot table indicates multi-level aggregation behavior.",
                        proposed_payload={"sheet_name": ws.title, "pivot_name": piv.pivot_name},
                        confidence=0.56,
                        source="deterministic",
                        evidence_refs=[ExcelWizardEvidence(sheet_name=ws.title, pivot_name=piv.pivot_name)],
                    )
                )
                chunks.append(
                    WorkbookChunk(
                        chunk_id=f"pivot:{ws.title}:{idx + 1}",
                        chunk_type="pivot",
                        text=f"pivot {piv.pivot_name} in sheet {ws.title}",
                        metadata={"sheet": ws.title, "pivot": piv.pivot_name},
                    )
                )

        wb.close()

        complexity = self._compute_complexity(formula_nodes, named_ranges, pivots)
        base_conf = self._compute_base_confidence(formula_nodes, named_ranges, suggestions)
        warnings: List[str] = []

        # Optional Copilot enrichment via gateway
        enriched_suggestions: List[ExcelWizardSuggestion] = []
        if use_copilot and self.copilot_mode == "guarded" and (
            complexity >= 0.5 or base_conf < self.high_confidence_threshold
        ):
            copilot_resp = self.gateway_client.enrich_workbook_context(
                file_path=file_path,
                prompt_hint=prompt_hint,
                workbook_context={
                    "formula_count": len(formula_nodes),
                    "named_range_count": len(named_ranges),
                    "pivot_count": len(pivots),
                    "sample_formulas": [f.formula for f in formula_nodes[:20]],
                },
            )
            if copilot_resp.get("status") == "ok":
                enriched_suggestions = self._parse_copilot_suggestions(copilot_resp.get("data", {}))
            else:
                warnings.append(copilot_resp.get("error", "copilot_unavailable"))

        all_suggestions = self._merge_and_rank_suggestions(suggestions, enriched_suggestions)
        all_suggestions = all_suggestions[:300]
        rag = ExcelWorkbookRAG()
        rag.add_chunks(chunks[: self.max_chunks])

        analysis = ExcelWizardAnalysis(
            file_path=file_path,
            workbook_name=Path(file_path).name,
            formula_nodes=formula_nodes,
            named_ranges=named_ranges,
            pivots=pivots,
            hierarchy_candidates=hierarchy_candidates,
            config_candidates=config_candidates,
            suggestions=all_suggestions,
            complexity_score=complexity,
            confidence=max(base_conf, self._avg_confidence(all_suggestions)),
            warnings=warnings,
            metadata={
                "generated_at": datetime.now(timezone.utc).isoformat(),
                "chunk_count": rag.chunk_count(),
                "copilot_enriched": bool(enriched_suggestions),
            },
        )

        self._analysis_store[analysis.analysis_id] = analysis
        self._rag_store[analysis.analysis_id] = rag
        return analysis

    def get_analysis(self, analysis_id: str) -> Optional[ExcelWizardAnalysis]:
        return self._analysis_store.get(analysis_id)

    def list_suggestions(
        self, analysis_id: str, *, min_confidence: float = 0.0, status: str = "all"
    ) -> List[ExcelWizardSuggestion]:
        analysis = self.get_analysis(analysis_id)
        if not analysis:
            return []
        out = []
        for s in analysis.suggestions:
            if s.confidence < min_confidence:
                continue
            if status != "all" and s.status != status:
                continue
            out.append(s)
        return out

    def review_suggestion(
        self,
        analysis_id: str,
        suggestion_id: str,
        *,
        action: str,
        reviewer: str = "",
        comments: str = "",
    ) -> Optional[ExcelWizardSuggestion]:
        analysis = self.get_analysis(analysis_id)
        if not analysis:
            return None
        action = action.lower().strip()
        status_map = {"approve": "approved", "reject": "rejected", "defer": "deferred"}
        if action not in status_map:
            return None
        for s in analysis.suggestions:
            if s.suggestion_id != suggestion_id:
                continue
            s.status = status_map[action]
            s.reviewer = reviewer
            s.review_comment = comments
            s.reviewed_at = datetime.now(timezone.utc).isoformat()
            return s
        return None

    def query_analysis(self, analysis_id: str, question: str) -> Dict[str, Any]:
        analysis = self.get_analysis(analysis_id)
        rag = self._rag_store.get(analysis_id)
        if not analysis or not rag:
            return {"error": f"analysis_not_found:{analysis_id}"}

        hits = rag.retrieve(question, top_k=8)
        citations = []
        lines = []
        for chunk, score in hits:
            md = chunk.metadata
            cite = {
                "chunk_id": chunk.chunk_id,
                "score": round(score, 3),
                "sheet_name": md.get("sheet", ""),
                "cell_ref": md.get("cell", ""),
            }
            citations.append(cite)
            lines.append(f"{chunk.chunk_type}: {chunk.text[:200]}")

        if not lines:
            answer = "No relevant workbook evidence found for this question."
        else:
            answer = "Relevant workbook evidence:\n" + "\n".join(lines[:5])

        return {"analysis_id": analysis_id, "answer": answer, "citations": citations}

    def to_report_analysis(self, wizard_analysis: ExcelWizardAnalysis) -> ReportAnalysis:
        """Convert wizard outputs into ReportAnalysis-compatible objects."""
        kpis: List[ExtractedKPI] = []
        dims: List[ExtractedDimension] = []
        flts: List[ExtractedFilter] = []

        for s in wizard_analysis.suggestions:
            if s.status == "rejected":
                continue
            if s.suggestion_type == "kpi_definition":
                kpis.append(
                    ExtractedKPI(
                        name=s.proposed_payload.get("name", s.title),
                        business_name=s.title,
                        formula=s.proposed_payload.get("formula", ""),
                        source=wizard_analysis.file_path,
                        source_type="report",
                        confidence=s.confidence,
                    )
                )
            elif s.suggestion_type in ("dimension_candidate", "hierarchy_candidate"):
                dims.append(
                    ExtractedDimension(
                        name=s.proposed_payload.get("name", s.title),
                        business_name=s.title,
                        hierarchy_levels=s.proposed_payload.get("levels", []),
                        key_columns=s.proposed_payload.get("key_columns", []),
                        extracted_from="report",
                        confidence=s.confidence,
                    )
                )
            elif s.suggestion_type == "filter_rule":
                flts.append(
                    ExtractedFilter(
                        description=s.description or s.title,
                        sql_expression=s.proposed_payload.get("sql_expression", ""),
                        applies_to=s.proposed_payload.get("applies_to", []),
                        source=wizard_analysis.file_path,
                        confidence=s.confidence,
                    )
                )

        return ReportAnalysis(
            file_path=wizard_analysis.file_path,
            kpis=kpis,
            dimensions=dims,
            filters=flts,
            excel_wizard_id=wizard_analysis.analysis_id,
            wizard_suggestions=wizard_analysis.suggestions,
            wizard_confidence=wizard_analysis.confidence,
        )

    # ----------------------------- internals -----------------------------

    @staticmethod
    def _extract_formula_dependencies(formula: str) -> List[str]:
        return list(dict.fromkeys(re.findall(r"(?:'[^']+'|[A-Za-z0-9_]+)![A-Z]+\d+", formula)))

    @staticmethod
    def _infer_named_range_role(name_key: str) -> str:
        name = (name_key or "").lower()
        if any(k in name for k in ("hier", "level", "rollup")):
            return "hierarchy"
        if any(k in name for k in ("map", "lookup", "xref")):
            return "mapping"
        if any(k in name for k in ("rule", "calc", "metric")):
            return "business_rule"
        if any(k in name for k in ("group", "agg", "bucket")):
            return "rollup"
        return ""

    @staticmethod
    def _extract_hierarchy_levels(name_key: str) -> List[str]:
        levels = re.findall(r"level[_\s]?(\d+)", (name_key or "").lower())
        return [f"LEVEL_{lv}" for lv in levels]

    def _suggest_from_formula(self, sheet: str, cell: str, formula: str) -> List[ExcelWizardSuggestion]:
        f = formula.upper()
        suggestions: List[ExcelWizardSuggestion] = []
        ev = [ExcelWizardEvidence(sheet_name=sheet, cell_ref=cell, formula=formula)]

        sum_match = re.search(r"SUMIFS?\(([^)]+)\)", formula, flags=re.IGNORECASE)
        if sum_match:
            suggestions.append(
                ExcelWizardSuggestion(
                    suggestion_type="kpi_definition",
                    title=f"KPI from {sheet}!{cell}",
                    description="Aggregation formula likely defines a KPI.",
                    proposed_payload={
                        "name": f"{sheet}_{cell}_kpi".lower(),
                        "formula": formula,
                        "sheet": sheet,
                    },
                    confidence=0.72,
                    source="deterministic",
                    evidence_refs=ev,
                )
            )

        if "IF(" in f or "COUNTIF" in f or "SUMIF" in f:
            suggestions.append(
                ExcelWizardSuggestion(
                    suggestion_type="filter_rule",
                    title=f"Filter logic from {sheet}!{cell}",
                    description="Conditional formula likely encodes a business filter.",
                    proposed_payload={"sql_expression": formula, "applies_to": [sheet]},
                    confidence=0.64,
                    source="deterministic",
                    evidence_refs=ev,
                )
            )

        if any(tok in f for tok in ("VLOOKUP(", "XLOOKUP(", "INDEX(", "MATCH(")):
            suggestions.append(
                ExcelWizardSuggestion(
                    suggestion_type="configuration_candidate",
                    title=f"Lookup mapping in {sheet}!{cell}",
                    description="Lookup formula may represent mapping configuration.",
                    proposed_payload={"name": f"{sheet}_lookup_map", "formula": formula},
                    confidence=0.61,
                    source="deterministic",
                    evidence_refs=ev,
                )
            )

        # Basic hierarchy inference from formula tokens like region/area/well
        hierarchy_tokens = [t for t in ("company", "region", "district", "field", "well") if t in f.lower()]
        if len(hierarchy_tokens) >= 2:
            suggestions.append(
                ExcelWizardSuggestion(
                    suggestion_type="hierarchy_candidate",
                    title=f"Hierarchy inferred from {sheet}!{cell}",
                    description="Formula references multiple hierarchy-like levels.",
                    proposed_payload={"name": f"{sheet}_hierarchy", "levels": hierarchy_tokens},
                    confidence=0.58,
                    source="deterministic",
                    evidence_refs=ev,
                )
            )

        return suggestions

    @staticmethod
    def _compute_complexity(
        formula_nodes: List[ExcelFormulaNode],
        named_ranges: List[ExcelNamedRangeIntent],
        pivots: List[ExcelPivotIntent],
    ) -> float:
        score = (
            min(len(formula_nodes) / 200.0, 1.0) * 0.6
            + min(len(named_ranges) / 50.0, 1.0) * 0.2
            + min(len(pivots) / 20.0, 1.0) * 0.2
        )
        return round(min(score, 1.0), 3)

    @staticmethod
    def _compute_base_confidence(
        formula_nodes: List[ExcelFormulaNode],
        named_ranges: List[ExcelNamedRangeIntent],
        suggestions: List[ExcelWizardSuggestion],
    ) -> float:
        if not formula_nodes and not named_ranges:
            return 0.2
        conf = 0.45
        if formula_nodes:
            conf += 0.2
        if named_ranges:
            conf += 0.1
        if suggestions:
            conf += min(len(suggestions) / 50.0, 0.2)
        return round(min(conf, 0.95), 3)

    @staticmethod
    def _avg_confidence(suggestions: List[ExcelWizardSuggestion]) -> float:
        if not suggestions:
            return 0.0
        return round(sum(s.confidence for s in suggestions) / len(suggestions), 3)

    @staticmethod
    def _parse_copilot_suggestions(data: Dict[str, Any]) -> List[ExcelWizardSuggestion]:
        out: List[ExcelWizardSuggestion] = []
        for item in data.get("suggestions", []):
            out.append(
                ExcelWizardSuggestion(
                    suggestion_type=item.get("suggestion_type", "configuration_candidate"),
                    title=item.get("title", "Copilot suggestion"),
                    description=item.get("description", ""),
                    proposed_payload=item.get("proposed_payload", {}),
                    confidence=float(item.get("confidence", 0.6)),
                    source="copilot",
                    evidence_refs=[
                        ExcelWizardEvidence(
                            sheet_name=ev.get("sheet_name", ""),
                            cell_ref=ev.get("cell_ref", ""),
                            range_ref=ev.get("range_ref", ""),
                            formula=ev.get("formula", ""),
                            named_range=ev.get("named_range", ""),
                            pivot_name=ev.get("pivot_name", ""),
                            explanation=ev.get("explanation", ""),
                        )
                        for ev in item.get("evidence_refs", [])
                    ],
                )
            )
        return out

    def _merge_and_rank_suggestions(
        self,
        deterministic: List[ExcelWizardSuggestion],
        copilot: List[ExcelWizardSuggestion],
    ) -> List[ExcelWizardSuggestion]:
        merged: Dict[Tuple[str, str], ExcelWizardSuggestion] = {}

        for s in deterministic + copilot:
            key = (s.suggestion_type, (s.title or "").lower())
            if key not in merged:
                merged[key] = s
                continue

            existing = merged[key]
            # Merge confidence and provenance when both systems found same idea.
            if s.confidence > existing.confidence:
                existing.confidence = s.confidence
                existing.description = s.description or existing.description
                existing.proposed_payload = s.proposed_payload or existing.proposed_payload
            existing.evidence_refs.extend(s.evidence_refs)
            if existing.source != s.source:
                existing.source = "hybrid"

        ranked = sorted(merged.values(), key=lambda s: s.confidence, reverse=True)
        return ranked

