"""MS Graph API client for Excel workbook analysis.

Supports two modes:
1. Cloud (drive_item_id set): calls Microsoft Graph API endpoints
2. Local fallback (file_path only): uses openpyxl to parse .xlsx directly
"""
from __future__ import annotations

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

from .contracts import (
    ExcelFormula,
    LogicArtifact,
    Measure,
    NamedRange,
    PivotTableConfig,
    WorkbookExtraction,
)


class GraphCopilotClient:
    """MS Graph API client for Excel workbook analysis."""

    def __init__(
        self,
        access_token: str = "",
        tenant_id: str = "",
        client_id: str = "",
    ):
        self.access_token = access_token or os.getenv("MSGRAPH_ACCESS_TOKEN", "")
        self.tenant_id = tenant_id or os.getenv("MSGRAPH_TENANT_ID", "")
        self.client_id = client_id or os.getenv("MSGRAPH_CLIENT_ID", "")
        self._base_url = "https://graph.microsoft.com/v1.0"

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def extract_workbook(
        self,
        file_path: str,
        drive_item_id: str = "",
    ) -> WorkbookExtraction:
        """Extract formulas, named ranges, and structure from an Excel workbook.

        Two modes:
        1. Cloud (drive_item_id set): Call Graph API endpoints
        2. Local fallback (file_path only): Use openpyxl to parse .xlsx directly
        """
        if drive_item_id and self.access_token:
            extraction = self._extract_via_graph_api(drive_item_id)
        else:
            extraction = self._extract_local(file_path)

        extraction.file_path = file_path

        # Translate formulas to BLCE Measures
        if extraction.formulas:
            extraction.measures = self._translate_formulas_to_measures(
                extraction.formulas
            )

        # Build a LogicArtifact summarising the workbook
        if extraction.formulas or extraction.named_ranges:
            from .contracts import LogicObjects

            extraction.artifact = LogicArtifact(
                source_type="excel",
                source_path=file_path,
                source_name=os.path.basename(file_path) if file_path else "",
                grain=f"{len(extraction.sheets)} sheets",
                confidence=0.7 if extraction.formulas else 0.3,
                objects=LogicObjects(measures=extraction.measures),
                explanation=(
                    f"Extracted {len(extraction.formulas)} formulas, "
                    f"{len(extraction.named_ranges)} named ranges from "
                    f"{len(extraction.sheets)} sheets."
                ),
            )

        return extraction

    # ------------------------------------------------------------------
    # Graph API path
    # ------------------------------------------------------------------

    def _extract_via_graph_api(self, drive_item_id: str) -> WorkbookExtraction:
        """Graph API path: GET /me/drive/items/{id}/workbook/worksheets, names, tables."""
        import urllib.request
        import json

        headers = {
            "Authorization": f"Bearer {self.access_token}",
            "Content-Type": "application/json",
        }
        extraction = WorkbookExtraction(source="graph_api")

        # 1. Get worksheets
        sheets_data = self._graph_get(
            f"/me/drive/items/{drive_item_id}/workbook/worksheets",
            headers,
        )
        for ws in sheets_data.get("value", []):
            extraction.sheets.append(ws.get("name", ""))

        # 2. Get named ranges
        names_data = self._graph_get(
            f"/me/drive/items/{drive_item_id}/workbook/names",
            headers,
        )
        for nr in names_data.get("value", []):
            extraction.named_ranges.append(
                NamedRange(
                    name=nr.get("name", ""),
                    refers_to=nr.get("value", ""),
                    scope="workbook",
                )
            )

        # 3. Get formulas from each sheet's used range
        for sheet_name in extraction.sheets:
            range_data = self._graph_get(
                f"/me/drive/items/{drive_item_id}/workbook/worksheets"
                f"/{_quote(sheet_name)}/usedRange",
                headers,
            )
            formulas_grid = range_data.get("formulas", [])
            for r_idx, row in enumerate(formulas_grid):
                for c_idx, cell_val in enumerate(row):
                    if isinstance(cell_val, str) and cell_val.startswith("="):
                        col_letter = _col_letter(c_idx)
                        extraction.formulas.append(
                            ExcelFormula(
                                cell_address=f"{col_letter}{r_idx + 1}",
                                formula=cell_val[1:],
                                sheet_name=sheet_name,
                            )
                        )

        # 4. Get tables (pivot table proxies)
        tables_data = self._graph_get(
            f"/me/drive/items/{drive_item_id}/workbook/tables",
            headers,
        )
        for tbl in tables_data.get("value", []):
            extraction.pivot_tables.append(
                PivotTableConfig(
                    name=tbl.get("name", ""),
                    source_range=tbl.get("range", {}).get("address", ""),
                )
            )

        return extraction

    def _graph_get(self, path: str, headers: Dict[str, str]) -> Dict[str, Any]:
        """Execute a GET request against the Graph API."""
        import urllib.request
        import json

        url = f"{self._base_url}{path}"
        req = urllib.request.Request(url, headers=headers)
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                return json.loads(resp.read().decode())
        except Exception:
            return {}

    # ------------------------------------------------------------------
    # Local fallback
    # ------------------------------------------------------------------

    def _extract_local(self, file_path: str) -> WorkbookExtraction:
        """Local fallback: openpyxl formula/named-range extraction."""
        try:
            import openpyxl
        except ImportError:
            return WorkbookExtraction(
                file_path=file_path,
                source="local",
            )

        if not file_path or not os.path.isfile(file_path):
            return WorkbookExtraction(file_path=file_path or "", source="local")

        wb = openpyxl.load_workbook(file_path, data_only=False)
        extraction = WorkbookExtraction(source="local")
        extraction.sheets = wb.sheetnames[:]

        # Extract defined names
        for name_key in wb.defined_names.definedName:
            destinations = list(name_key.destinations)
            refers_to = ""
            if destinations:
                sheet_title, coord = destinations[0]
                refers_to = f"'{sheet_title}'!{coord}" if sheet_title else coord
            extraction.named_ranges.append(
                NamedRange(
                    name=name_key.name,
                    refers_to=refers_to or str(name_key.value),
                    scope="workbook",
                )
            )

        # Extract formulas from all cells
        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("=")
                    ):
                        extraction.formulas.append(
                            ExcelFormula(
                                cell_address=cell.coordinate,
                                formula=cell.value[1:],
                                sheet_name=ws.title,
                            )
                        )

        wb.close()
        return extraction

    # ------------------------------------------------------------------
    # Formula-to-Measure translation
    # ------------------------------------------------------------------

    _AGG_PATTERN = re.compile(
        r"(SUMIFS?|SUM|COUNTIFS?|COUNT|AVERAGEIFS?|AVERAGE|MIN|MAX)\s*\(",
        re.IGNORECASE,
    )
    _AGG_MAP = {
        "SUM": "SUM",
        "SUMIF": "SUM",
        "SUMIFS": "SUM",
        "COUNT": "COUNT",
        "COUNTIF": "COUNT",
        "COUNTIFS": "COUNT",
        "AVERAGE": "AVG",
        "AVERAGEIF": "AVG",
        "AVERAGEIFS": "AVG",
        "MIN": "MIN",
        "MAX": "MAX",
    }

    def _translate_formulas_to_measures(
        self, formulas: List[ExcelFormula]
    ) -> List[Measure]:
        """Convert Excel formulas (SUMIFS, COUNTIFS, etc.) to BLCE Measures.

        Reuses logic patterns from ExcelLogicExtractor in parsers/excel_parser.py.
        """
        measures: List[Measure] = []
        seen: set = set()

        for ef in formulas:
            for match in self._AGG_PATTERN.finditer(ef.formula):
                func_name = match.group(1).upper()
                agg = self._AGG_MAP.get(func_name, "custom")
                measure_name = f"{func_name}_{ef.sheet_name}_{ef.cell_address}"

                if measure_name in seen:
                    continue
                seen.add(measure_name)

                measures.append(
                    Measure(
                        name=measure_name,
                        expression=ef.formula,
                        aggregation=agg,
                        source_table=ef.sheet_name,
                        alias=ef.cell_address,
                        confidence=0.6,
                    )
                )

        return measures


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

def _col_letter(index: int) -> str:
    """Convert 0-based column index to Excel column letter(s)."""
    result = ""
    while True:
        result = chr(65 + index % 26) + result
        index = index // 26 - 1
        if index < 0:
            break
    return result


def _quote(sheet_name: str) -> str:
    """URL-encode a sheet name for Graph API paths."""
    import urllib.parse
    return urllib.parse.quote(sheet_name, safe="")
