from __future__ import annotations

import math
import re
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Sequence

EXCEL_EXTENSIONS = {".xlsx", ".xls", ".xlsm", ".xlsb"}
MODEL_EXTENSIONS = {
    ".pkl",
    ".pickle",
    ".joblib",
    ".pt",
    ".pth",
    ".onnx",
    ".xgb",
    ".cbm",
    ".model",
    ".sav",
    ".bin",
    ".json",
    ".yaml",
    ".yml",
}

EXCEL_SECTION_KEYS = (
    "factor_definitions",
    "dropdown_options",
    "factor_values",
    "calculation_logic",
)

EXCEL_SHEET_KIND_ALIASES = {
    "factor_definitions": {
        "factor_definitions",
        "factor_definition",
        "factors",
        "factor_meta",
        "factor_setup",
        "因子定义",
        "因子",
    },
    "dropdown_options": {
        "dropdown_options",
        "factor_options",
        "options",
        "option_lists",
        "option_values",
        "下拉选项",
        "选项",
    },
    "factor_values": {
        "factor_values",
        "factor_value",
        "values",
        "rating_table",
        "value_table",
        "因子取值",
        "取值",
    },
    "calculation_logic": {
        "calculation_logic",
        "logic",
        "rules",
        "formula",
        "formulas",
        "calc",
        "计算逻辑",
        "规则",
        "公式",
    },
}

EXCEL_COLUMN_ALIASES = {
    "factor_name": {"factor_name", "factor", "name", "field", "因子", "因子名"},
    "data_type": {"data_type", "datatype", "type", "类型"},
    "default_value": {"default_value", "default", "缺省值", "默认值"},
    "description": {"description", "desc", "说明", "备注"},
    "required": {"required", "is_required", "mandatory", "必填"},
    "option_source": {"option_source", "options_source", "option_ref", "下拉来源"},
    "value_source": {"value_source", "values_source", "取值来源"},
    "option_value": {"option_value", "value", "option", "选项值", "选项"},
    "option_label": {"option_label", "label", "display", "显示名", "显示"},
    "option_order": {"option_order", "order", "排序", "顺序"},
    "factor_value": {"factor_value", "value", "因子值"},
    "score": {"score", "rate", "factor", "系数", "分值"},
    "weight": {"weight", "权重"},
    "condition": {"condition", "when", "条件"},
    "target": {"target", "output", "输出", "目标"},
    "expression": {"expression", "formula", "logic", "rule", "表达式", "公式"},
    "step": {"step", "stage", "步骤"},
    "priority": {"priority", "order", "优先级"},
    "note": {"note", "notes", "备注"},
}

FORMULA_CELL_REF_PATTERN = re.compile(
    r"(?:(?:'[^']+'|[A-Za-z_][A-Za-z0-9_.]*)!)?\$?[A-Za-z]{1,3}\$?\d+(?::\$?[A-Za-z]{1,3}\$?\d+)?",
    re.IGNORECASE,
)
FORMULA_NAME_TOKEN_PATTERN = re.compile(r"\b[A-Za-z_][A-Za-z0-9_.]*\b")
NUMERIC_LABEL_PATTERN = re.compile(r"^[+-]?(?:(?:\d+(?:\.\d+)?)|(?:\.\d+))(?:%?)$")
CELL_REF_LABEL_PATTERN = re.compile(r"^[A-Za-z]{1,3}\d+$", re.IGNORECASE)


class ExcelModelManagerMixin:
    @staticmethod
    def _normalize_excel_token(value: str) -> str:
        raw = str(value or "").strip().lower()
        return re.sub(r"[\s_\-]+", "", raw)

    @classmethod
    def _canonical_column_name(cls, header: Any) -> str:
        token = cls._normalize_excel_token(str(header or ""))
        if not token:
            return ""
        for canonical, alias_set in EXCEL_COLUMN_ALIASES.items():
            if token in {cls._normalize_excel_token(alias) for alias in alias_set}:
                return canonical
        return token

    @staticmethod
    def _is_placeholder_excel_column_name(name: Any) -> bool:
        text = str(name or "").strip().lower()
        return (not text) or text.startswith("unnamed:")

    @classmethod
    def _classify_sheet_name(cls, sheet_name: str) -> Optional[str]:
        token = cls._normalize_excel_token(sheet_name)
        if not token:
            return None
        for section, aliases in EXCEL_SHEET_KIND_ALIASES.items():
            alias_tokens = {cls._normalize_excel_token(alias) for alias in aliases}
            if token in alias_tokens:
                return section
        return None

    @staticmethod
    def _is_missing_excel_value(value: Any) -> bool:
        if value is None:
            return True
        if isinstance(value, str):
            return not value.strip()
        if isinstance(value, float):
            return math.isnan(value)
        return False

    @staticmethod
    def _json_safe_value(value: Any) -> Any:
        if value is None:
            return None
        if isinstance(value, (str, bool, int)):
            return value
        if isinstance(value, float):
            if math.isnan(value) or math.isinf(value):
                return None
            return value
        if hasattr(value, "isoformat"):
            try:
                return value.isoformat()
            except Exception:
                pass
        return str(value)

    @staticmethod
    def _normalize_label_text(value: Any) -> str:
        text = str(value or "").strip()
        if not text:
            return ""
        return re.sub(r"\s+", " ", text).strip()

    @classmethod
    def _is_meaningful_label_text(cls, value: Any) -> bool:
        text = cls._normalize_label_text(value)
        if not text:
            return False
        if text.startswith("="):
            return False
        if NUMERIC_LABEL_PATTERN.match(text):
            return False
        if CELL_REF_LABEL_PATTERN.match(text):
            return False
        if re.fullmatch(r"[^\w\u4e00-\u9fff]+", text):
            return False
        if text.lower() in {"true", "false", "none", "null", "nan", "n/a", "na"}:
            return False
        return True

    @classmethod
    def _humanize_identifier_label(cls, value: str) -> str:
        text = cls._normalize_label_text(value)
        if not text:
            return ""
        text = re.sub(r"[_\.]+", " ", text)
        text = re.sub(r"(?<=[a-z])(?=[A-Z])", " ", text)
        text = re.sub(r"\s+", " ", text).strip()
        return text

    @classmethod
    def _extract_table_rows_from_sheet(
        cls,
        sheet: Any,
        *,
        max_columns: int = 80,
        max_rows: int = 2000,
    ) -> Dict[str, Any]:
        header_row_idx: Optional[int] = None
        max_col = min(int(getattr(sheet, "max_column", 0) or 0), max_columns)
        max_row = min(int(getattr(sheet, "max_row", 0) or 0), max_rows)
        if max_col <= 0 or max_row <= 0:
            return {"headers": [], "rows": []}

        for row_idx in range(1, min(max_row, 40) + 1):
            non_empty = 0
            for col_idx in range(1, max_col + 1):
                cell = sheet.cell(row=row_idx, column=col_idx)
                if not cls._is_missing_excel_value(cell.value):
                    non_empty += 1
            if non_empty >= 2:
                header_row_idx = row_idx
                break

        if header_row_idx is None:
            return {"headers": [], "rows": []}

        header_columns: List[Dict[str, Any]] = []
        used_keys: set[str] = set()
        for col_idx in range(1, max_col + 1):
            raw_value = sheet.cell(row=header_row_idx, column=col_idx).value
            if cls._is_missing_excel_value(raw_value):
                continue
            raw_label = str(raw_value).strip()
            key = cls._canonical_column_name(raw_label) or f"column_{col_idx}"
            dedup_key = key
            suffix = 1
            while dedup_key in used_keys:
                dedup_key = f"{key}_{suffix}"
                suffix += 1
            used_keys.add(dedup_key)
            header_columns.append({"column": col_idx, "raw": raw_label, "key": dedup_key})

        if not header_columns:
            return {"headers": [], "rows": []}

        rows: List[Dict[str, Any]] = []
        empty_run = 0
        for row_idx in range(header_row_idx + 1, max_row + 1):
            row_payload: Dict[str, Any] = {}
            for header in header_columns:
                cell = sheet.cell(row=row_idx, column=int(header["column"]))
                value = cell.value
                if cell.data_type == "f" and value is not None:
                    value_text = str(value)
                    value = value_text if value_text.startswith("=") else f"={value_text}"
                normalized = cls._json_safe_value(value)
                if cls._is_missing_excel_value(normalized):
                    continue
                row_payload[str(header["key"])] = normalized

            if not row_payload:
                empty_run += 1
                if empty_run >= 30:
                    break
                continue
            empty_run = 0
            rows.append(row_payload)
            if len(rows) >= max_rows:
                break

        return {
            "headers": [str(item["key"]) for item in header_columns],
            "rows": rows,
            "header_row": header_row_idx,
        }

    @classmethod
    def _infer_sheet_kind_from_headers(cls, headers: Sequence[str]) -> Optional[str]:
        header_set = {str(item) for item in headers}
        if "factor_name" in header_set and {"data_type", "default_value", "required"} & header_set:
            return "factor_definitions"
        if "factor_name" in header_set and {"option_value", "option_label"} & header_set:
            return "dropdown_options"
        if "factor_name" in header_set and {"factor_value", "score", "weight"} & header_set:
            return "factor_values"
        if {"expression", "target"} & header_set:
            return "calculation_logic"
        return None

    @classmethod
    def _extract_range_anchor_context(cls, sheet: Any, reference: str) -> Dict[str, Any]:
        text = str(reference or "").strip()
        if not text:
            return {}
        token = text.split()[0].split(",")[0].strip()
        if not token:
            return {}
        if "!" in token:
            token = token.split("!", 1)[1]
        token = token.split(":", 1)[0].replace("$", "").strip()
        if not token:
            return {}
        try:
            from openpyxl.utils.cell import coordinate_to_tuple

            row_idx, col_idx = coordinate_to_tuple(token)
        except Exception:
            return {}

        if row_idx <= 0 or col_idx <= 0:
            return {}

        anchor_cell = sheet.cell(row=row_idx, column=col_idx)
        anchor_value = cls._json_safe_value(anchor_cell.value)

        def _nearest_text(row_step: int, col_step: int) -> Optional[str]:
            for step in range(1, 11):
                r = row_idx + row_step * step
                c = col_idx + col_step * step
                if r <= 0 or c <= 0:
                    break
                try:
                    value = sheet.cell(row=r, column=c).value
                except Exception:
                    break
                if cls._is_missing_excel_value(value):
                    continue
                text_value = cls._normalize_label_text(value)
                if cls._is_meaningful_label_text(text_value):
                    return text_value
            return None

        neighbor_labels: Dict[str, Any] = {}
        for direction, row_step, col_step in (
            ("up", -1, 0),
            ("down", 1, 0),
            ("left", 0, -1),
            ("right", 0, 1),
        ):
            label = _nearest_text(row_step=row_step, col_step=col_step)
            if label:
                neighbor_labels[direction] = label

        payload: Dict[str, Any] = {"anchor_cell": token}
        if not cls._is_missing_excel_value(anchor_value):
            payload["anchor_value"] = anchor_value
        if neighbor_labels:
            payload["neighbor_labels"] = neighbor_labels
        return payload

    @classmethod
    def _parse_inline_option_formula(cls, formula: str) -> List[str]:
        text = str(formula or "").strip()
        if not text:
            return []
        if text.startswith("="):
            text = text[1:].strip()
        if text.startswith('"') and text.endswith('"') and len(text) >= 2:
            text = text[1:-1]
        if "," not in text:
            return []
        parts = [item.strip() for item in text.split(",")]
        return [item for item in parts if item]

    @classmethod
    def _extract_sheet_validations(cls, sheet: Any) -> List[Dict[str, Any]]:
        entries: List[Dict[str, Any]] = []
        dv_container = getattr(sheet, "data_validations", None)
        validations = getattr(dv_container, "dataValidation", None)
        if not validations:
            return entries
        for item in validations:
            if str(getattr(item, "type", "")).lower() != "list":
                continue
            formula1 = str(getattr(item, "formula1", "") or "")
            inline_options = cls._parse_inline_option_formula(formula1)
            target_range = str(getattr(item, "sqref", ""))
            anchor_context = cls._extract_range_anchor_context(sheet, target_range)
            entries.append(
                {
                    "sheet": str(getattr(sheet, "title", "")),
                    "type": "list",
                    "target_range": target_range,
                    "formula": formula1,
                    "inline_options": inline_options,
                    "allow_blank": bool(getattr(item, "allow_blank", False)),
                    **anchor_context,
                }
            )
        return entries

    @classmethod
    def _extract_formula_cells(
        cls,
        sheet: Any,
        *,
        limit: int = 200,
    ) -> List[Dict[str, Any]]:
        out: List[Dict[str, Any]] = []
        max_col = min(int(getattr(sheet, "max_column", 0) or 0), 80)
        max_row = min(int(getattr(sheet, "max_row", 0) or 0), 2000)
        if max_col <= 0 or max_row <= 0:
            return out
        for row_idx in range(1, max_row + 1):
            for col_idx in range(1, max_col + 1):
                cell = sheet.cell(row=row_idx, column=col_idx)
                if cell.data_type != "f" or cell.value is None:
                    continue
                formula_text = str(cell.value)
                out.append(
                    {
                        "sheet": str(getattr(sheet, "title", "")),
                        "cell": str(getattr(cell, "coordinate", "")),
                        "formula": formula_text if formula_text.startswith("=") else f"={formula_text}",
                    }
                )
                if len(out) >= limit:
                    return out
        return out

    @classmethod
    def _extract_workbook_defined_names(cls, workbook: Any, *, limit: int = 300) -> List[Dict[str, Any]]:
        entries: List[Dict[str, Any]] = []
        defined_names = getattr(workbook, "defined_names", None)
        if not defined_names:
            return entries

        worksheets = {str(getattr(sheet, "title", "")): sheet for sheet in getattr(workbook, "worksheets", [])}
        count = 0
        for key in list(defined_names.keys()):
            if count >= limit:
                break
            try:
                named_ref = defined_names[key]
            except Exception:
                continue

            name = str(getattr(named_ref, "name", key) or key)
            attr_text = str(getattr(named_ref, "attr_text", "") or "")
            try:
                destinations = list(named_ref.destinations)
            except Exception:
                destinations = []

            if destinations:
                for sheet_name, reference in destinations:
                    if count >= limit:
                        break
                    sheet_name_text = str(sheet_name)
                    ref_text = str(reference)
                    payload: Dict[str, Any] = {
                        "name": name,
                        "sheet": sheet_name_text,
                        "reference": ref_text,
                    }
                    sheet = worksheets.get(sheet_name_text)
                    if sheet is not None:
                        payload.update(cls._extract_range_anchor_context(sheet, ref_text))
                    entries.append(payload)
                    count += 1
            elif attr_text:
                entries.append({"name": name, "formula": attr_text})
                count += 1
        return entries

    @staticmethod
    def _normalize_sheet_token(token: str, *, default_sheet: str) -> str:
        text = str(token or "").strip()
        if not text:
            return default_sheet
        if text.startswith("'") and text.endswith("'") and len(text) >= 2:
            text = text[1:-1].replace("''", "'")
        return text or default_sheet

    @classmethod
    def _parse_formula_dependencies(
        cls,
        *,
        formula: str,
        current_sheet: str,
        named_reference_names: Sequence[str],
    ) -> Dict[str, Any]:
        formula_text = str(formula or "")
        name_set = {str(name).lower() for name in named_reference_names if str(name).strip()}

        cell_refs: List[Dict[str, Any]] = []
        seen_cell_keys: set[str] = set()
        for raw_token in FORMULA_CELL_REF_PATTERN.findall(formula_text):
            token = str(raw_token).strip()
            sheet_name = current_sheet
            ref_text = token
            if "!" in token:
                sheet_token, ref_text = token.split("!", 1)
                sheet_name = cls._normalize_sheet_token(sheet_token, default_sheet=current_sheet)
            ref_text = ref_text.replace("$", "").strip()
            if not ref_text:
                continue
            if ":" in ref_text:
                anchor_cell = ref_text.split(":", 1)[0].strip().upper()
                range_text = ref_text.upper()
            else:
                anchor_cell = ref_text.upper()
                range_text = None
            if not re.match(r"^[A-Z]{1,3}\d+$", anchor_cell):
                continue

            dedup_key = f"{sheet_name}::{anchor_cell}::{range_text or ''}"
            if dedup_key in seen_cell_keys:
                continue
            seen_cell_keys.add(dedup_key)
            payload: Dict[str, Any] = {"sheet": sheet_name, "cell": anchor_cell}
            if range_text:
                payload["range"] = range_text
            cell_refs.append(payload)

        named_refs: List[str] = []
        seen_named: set[str] = set()
        for token in FORMULA_NAME_TOKEN_PATTERN.findall(formula_text):
            token_text = str(token).strip()
            token_lower = token_text.lower()
            if token_lower not in name_set or token_lower in seen_named:
                continue
            named_refs.append(token_text)
            seen_named.add(token_lower)

        return {
            "cell_refs": cell_refs,
            "named_refs": named_refs,
        }

    @classmethod
    def _build_input_cells_from_dependencies(
        cls,
        *,
        workbook: Any,
        formula_cells: Sequence[Dict[str, Any]],
        named_references: Sequence[Dict[str, Any]],
        dropdown_validations: Sequence[Dict[str, Any]],
        max_items: int = 500,
    ) -> List[Dict[str, Any]]:
        sheets = {str(getattr(sheet, "title", "")): sheet for sheet in getattr(workbook, "worksheets", [])}
        named_by_cell: Dict[str, List[Dict[str, Any]]] = {}
        for item in named_references:
            sheet_name = str(item.get("sheet", "")).strip()
            cell_name = str(item.get("anchor_cell", "")).strip().upper()
            if not sheet_name or not cell_name:
                continue
            key = f"{sheet_name}::{cell_name}"
            named_by_cell.setdefault(key, []).append(dict(item))
        named_to_cell_keys: Dict[str, List[str]] = {}
        for cell_key, refs in named_by_cell.items():
            for ref in refs:
                name = str(ref.get("name", "")).strip().lower()
                if not name:
                    continue
                named_to_cell_keys.setdefault(name, []).append(cell_key)

        validation_by_cell: Dict[str, List[Dict[str, Any]]] = {}
        for item in dropdown_validations:
            sheet_name = str(item.get("sheet", "")).strip()
            cell_name = str(item.get("anchor_cell", "")).strip().upper()
            if not sheet_name or not cell_name:
                continue
            key = f"{sheet_name}::{cell_name}"
            validation_by_cell.setdefault(key, []).append(dict(item))

        referenced_by_cell: Dict[str, List[Dict[str, Any]]] = {}
        for formula in formula_cells:
            formula_sheet = str(formula.get("sheet", "")).strip()
            formula_cell = str(formula.get("cell", "")).strip().upper()
            for ref in formula.get("dependencies", {}).get("cell_refs", []):
                ref_sheet = str(ref.get("sheet", "")).strip()
                ref_cell = str(ref.get("cell", "")).strip().upper()
                if not ref_sheet or not ref_cell:
                    continue
                key = f"{ref_sheet}::{ref_cell}"
                referenced_by_cell.setdefault(key, []).append(
                    {"sheet": formula_sheet, "cell": formula_cell, "formula": str(formula.get("formula", ""))}
                )
            for named_ref in formula.get("dependencies", {}).get("named_refs", []):
                named_key = str(named_ref).strip().lower()
                for mapped_cell_key in named_to_cell_keys.get(named_key, []):
                    referenced_by_cell.setdefault(mapped_cell_key, []).append(
                        {"sheet": formula_sheet, "cell": formula_cell, "formula": str(formula.get("formula", ""))}
                    )

        candidate_keys = set(named_by_cell.keys()) | set(validation_by_cell.keys()) | set(referenced_by_cell.keys())
        entries: List[Dict[str, Any]] = []
        for key in sorted(candidate_keys):
            if len(entries) >= max_items:
                break
            sheet_name, cell_name = key.split("::", 1)
            sheet = sheets.get(sheet_name)
            if sheet is None:
                continue
            try:
                cell_obj = sheet[cell_name]
            except Exception:
                continue
            if str(getattr(cell_obj, "data_type", "")) == "f":
                continue

            payload: Dict[str, Any] = {
                "sheet": sheet_name,
                "cell": cell_name,
                "value": cls._json_safe_value(getattr(cell_obj, "value", None)),
                "defined_names": [str(item.get("name", "")) for item in named_by_cell.get(key, []) if item.get("name")],
                "referenced_by": referenced_by_cell.get(key, []),
            }

            context = cls._extract_range_anchor_context(sheet, cell_name)
            if context.get("neighbor_labels"):
                payload["neighbor_labels"] = dict(context["neighbor_labels"])

            validations = validation_by_cell.get(key, [])
            if validations:
                payload["validation"] = {
                    "formula": str(validations[0].get("formula", "")),
                    "inline_options": list(validations[0].get("inline_options", [])),
                    "allow_blank": bool(validations[0].get("allow_blank", False)),
                }

            entries.append(payload)
        return entries

    @staticmethod
    def _make_sheet_cell_key(sheet: str, cell: str) -> str:
        return f"{str(sheet).strip()}::{str(cell).strip().upper()}"

    @staticmethod
    def _split_sheet_cell_key(key: str) -> tuple[str, str]:
        sheet, cell = str(key).split("::", 1)
        return sheet, cell

    @classmethod
    def _build_calculation_graph(
        cls,
        *,
        formula_cells: Sequence[Dict[str, Any]],
        named_references: Sequence[Dict[str, Any]],
        input_cells: Sequence[Dict[str, Any]],
        max_nodes: int = 2000,
        max_edges: int = 8000,
        max_paths: int = 4000,
        max_depth: int = 30,
    ) -> Dict[str, Any]:
        formula_by_key: Dict[str, Dict[str, Any]] = {}
        for item in formula_cells:
            sheet_name = str(item.get("sheet", "")).strip()
            cell_name = str(item.get("cell", "")).strip().upper()
            if not sheet_name or not cell_name:
                continue
            formula_by_key[cls._make_sheet_cell_key(sheet_name, cell_name)] = dict(item)

        input_by_key: Dict[str, Dict[str, Any]] = {}
        for item in input_cells:
            sheet_name = str(item.get("sheet", "")).strip()
            cell_name = str(item.get("cell", "")).strip().upper()
            if not sheet_name or not cell_name:
                continue
            input_by_key[cls._make_sheet_cell_key(sheet_name, cell_name)] = dict(item)

        named_to_cell_keys: Dict[str, List[str]] = {}
        for item in named_references:
            name = str(item.get("name", "")).strip()
            sheet_name = str(item.get("sheet", "")).strip()
            cell_name = str(item.get("anchor_cell", "")).strip().upper()
            if not name or not sheet_name or not cell_name:
                continue
            named_to_cell_keys.setdefault(name.lower(), []).append(
                cls._make_sheet_cell_key(sheet_name, cell_name)
            )

        formula_referenced_by_formula: set[str] = set()
        for formula_item in formula_by_key.values():
            deps = formula_item.get("dependencies", {})
            for ref in deps.get("cell_refs", []):
                ref_sheet = str(ref.get("sheet", "")).strip()
                ref_cell = str(ref.get("cell", "")).strip().upper()
                if not ref_sheet or not ref_cell:
                    continue
                dep_key = cls._make_sheet_cell_key(ref_sheet, ref_cell)
                if dep_key in formula_by_key:
                    formula_referenced_by_formula.add(dep_key)
            for named_ref in deps.get("named_refs", []):
                for dep_key in named_to_cell_keys.get(str(named_ref).strip().lower(), []):
                    if dep_key in formula_by_key:
                        formula_referenced_by_formula.add(dep_key)

        output_keys = [key for key in formula_by_key.keys() if key not in formula_referenced_by_formula]
        if not output_keys:
            output_keys = list(formula_by_key.keys())

        nodes: Dict[str, Dict[str, Any]] = {}
        edges: List[Dict[str, Any]] = []
        edge_keys: set[str] = set()
        paths: List[Dict[str, Any]] = []

        def _upsert_cell_node(cell_key: str) -> Optional[str]:
            if cell_key in nodes:
                return cell_key
            if len(nodes) >= max_nodes:
                return None
            sheet_name, cell_name = cls._split_sheet_cell_key(cell_key)
            if cell_key in formula_by_key:
                item = formula_by_key[cell_key]
                nodes[cell_key] = {
                    "id": cell_key,
                    "kind": "formula_cell",
                    "sheet": sheet_name,
                    "cell": cell_name,
                    "formula": str(item.get("formula", "")),
                }
            elif cell_key in input_by_key:
                item = input_by_key[cell_key]
                nodes[cell_key] = {
                    "id": cell_key,
                    "kind": "input_cell",
                    "sheet": sheet_name,
                    "cell": cell_name,
                    "value": item.get("value"),
                    "defined_names": list(item.get("defined_names", [])),
                    "neighbor_labels": dict(item.get("neighbor_labels", {})),
                    "validation": dict(item.get("validation", {})) if item.get("validation") else None,
                }
            else:
                nodes[cell_key] = {
                    "id": cell_key,
                    "kind": "referenced_cell",
                    "sheet": sheet_name,
                    "cell": cell_name,
                }
            return cell_key

        def _upsert_unresolved_named_node(name: str) -> Optional[str]:
            node_id = f"name::{name}"
            if node_id in nodes:
                return node_id
            if len(nodes) >= max_nodes:
                return None
            nodes[node_id] = {"id": node_id, "kind": "unresolved_named_ref", "name": name}
            return node_id

        def _add_edge(from_id: str, to_id: str, *, via: str, token: Optional[str] = None) -> bool:
            edge_key = f"{from_id}||{to_id}||{via}||{token or ''}"
            if edge_key in edge_keys:
                return True
            if len(edges) >= max_edges:
                return False
            payload: Dict[str, Any] = {"from": from_id, "to": to_id, "via": via}
            if token:
                payload["token"] = token
            edges.append(payload)
            edge_keys.add(edge_key)
            return True

        def _record_path(output_key: str, path_nodes: List[str], *, leaf_kind: str) -> None:
            if len(paths) >= max_paths:
                return
            paths.append(
                {
                    "output": output_key,
                    "path": list(path_nodes),
                    "leaf_kind": leaf_kind,
                }
            )

        def _walk(current_key: str, output_key: str, path_nodes: List[str], visiting: set[str], depth: int) -> None:
            if depth > max_depth:
                _record_path(output_key, path_nodes, leaf_kind="max_depth_reached")
                return
            item = formula_by_key.get(current_key)
            if item is None:
                _record_path(output_key, path_nodes, leaf_kind="non_formula_leaf")
                return

            deps = item.get("dependencies", {})
            expanded = False

            for ref in deps.get("cell_refs", []):
                ref_sheet = str(ref.get("sheet", "")).strip()
                ref_cell = str(ref.get("cell", "")).strip().upper()
                if not ref_sheet or not ref_cell:
                    continue
                dep_key = cls._make_sheet_cell_key(ref_sheet, ref_cell)
                dep_node_id = _upsert_cell_node(dep_key)
                if dep_node_id is None:
                    continue
                if not _add_edge(current_key, dep_node_id, via="cell_ref", token=str(ref.get("range", "")) or None):
                    continue
                expanded = True
                if dep_key in formula_by_key and dep_key not in visiting:
                    _walk(
                        dep_key,
                        output_key,
                        path_nodes + [dep_node_id],
                        visiting | {dep_key},
                        depth + 1,
                    )
                else:
                    _record_path(output_key, path_nodes + [dep_node_id], leaf_kind=nodes[dep_node_id]["kind"])

            for named_ref in deps.get("named_refs", []):
                named_text = str(named_ref).strip()
                if not named_text:
                    continue
                target_keys = named_to_cell_keys.get(named_text.lower(), [])
                if target_keys:
                    for dep_key in target_keys:
                        dep_node_id = _upsert_cell_node(dep_key)
                        if dep_node_id is None:
                            continue
                        if not _add_edge(current_key, dep_node_id, via="named_ref", token=named_text):
                            continue
                        expanded = True
                        if dep_key in formula_by_key and dep_key not in visiting:
                            _walk(
                                dep_key,
                                output_key,
                                path_nodes + [dep_node_id],
                                visiting | {dep_key},
                                depth + 1,
                            )
                        else:
                            _record_path(output_key, path_nodes + [dep_node_id], leaf_kind=nodes[dep_node_id]["kind"])
                else:
                    unresolved_id = _upsert_unresolved_named_node(named_text)
                    if unresolved_id is None:
                        continue
                    if _add_edge(current_key, unresolved_id, via="named_ref_unresolved", token=named_text):
                        expanded = True
                        _record_path(output_key, path_nodes + [unresolved_id], leaf_kind="unresolved_named_ref")

            if not expanded:
                _record_path(output_key, path_nodes, leaf_kind="formula_without_dependencies")

        output_payload: List[Dict[str, Any]] = []
        for output_key in output_keys:
            node_id = _upsert_cell_node(output_key)
            if node_id is None:
                continue
            sheet_name, cell_name = cls._split_sheet_cell_key(output_key)
            output_payload.append(
                {
                    "id": output_key,
                    "sheet": sheet_name,
                    "cell": cell_name,
                    "formula": str(formula_by_key.get(output_key, {}).get("formula", "")),
                }
            )
            _walk(output_key, output_key, [output_key], {output_key}, depth=0)

        return {
            "output_cells": output_payload,
            "nodes": list(nodes.values()),
            "edges": edges,
            "paths": paths,
        }

    @classmethod
    def _extract_excel_config_auto_import(cls, path: Path) -> Dict[str, Any]:
        result: Dict[str, Any] = {
            "parser_version": "excel_config_v1",
            "has_structured_config": False,
            "sheet_names": [],
            "sections": {key: [] for key in EXCEL_SECTION_KEYS},
            "sheet_kind_map": {},
            "dropdown_validations": [],
            "formula_cells": [],
            "named_references": [],
            "input_cells": [],
            "calculation_graph": {
                "output_cells": [],
                "nodes": [],
                "edges": [],
                "paths": [],
            },
            "warnings": [],
        }
        try:
            import openpyxl
        except Exception as exc:
            result["warnings"].append(f"openpyxl unavailable: {exc}")
            return result

        try:
            workbook = openpyxl.load_workbook(path, data_only=False, read_only=False)
        except Exception as exc:
            result["warnings"].append(f"openpyxl parse failed: {exc}")
            return result

        remaining_formula_slots = 200
        for sheet in workbook.worksheets:
            sheet_name = str(sheet.title)
            result["sheet_names"].append(sheet_name)

            table_data = cls._extract_table_rows_from_sheet(sheet)
            headers = [str(item) for item in table_data.get("headers", [])]
            rows = list(table_data.get("rows", []))

            kind = cls._classify_sheet_name(sheet_name) or cls._infer_sheet_kind_from_headers(headers)
            if kind in EXCEL_SECTION_KEYS:
                result["sheet_kind_map"][sheet_name] = kind
                if rows:
                    result["sections"][kind].extend(rows)

            validations = cls._extract_sheet_validations(sheet)
            if validations:
                result["dropdown_validations"].extend(validations)

            if remaining_formula_slots > 0:
                formula_cells = cls._extract_formula_cells(sheet, limit=remaining_formula_slots)
                if formula_cells:
                    result["formula_cells"].extend(formula_cells)
                    remaining_formula_slots -= len(formula_cells)

        named_references = cls._extract_workbook_defined_names(workbook)
        if named_references:
            result["named_references"] = named_references

        named_reference_names = [str(item.get("name", "")) for item in result["named_references"] if item.get("name")]
        annotated_formula_cells: List[Dict[str, Any]] = []
        for item in result["formula_cells"]:
            sheet_name = str(item.get("sheet", ""))
            formula = str(item.get("formula", ""))
            dependencies = cls._parse_formula_dependencies(
                formula=formula,
                current_sheet=sheet_name,
                named_reference_names=named_reference_names,
            )
            enriched = dict(item)
            enriched["dependencies"] = dependencies
            annotated_formula_cells.append(enriched)
        result["formula_cells"] = annotated_formula_cells

        input_cells = cls._build_input_cells_from_dependencies(
            workbook=workbook,
            formula_cells=result["formula_cells"],
            named_references=result["named_references"],
            dropdown_validations=result["dropdown_validations"],
        )
        if input_cells:
            result["input_cells"] = input_cells

        result["calculation_graph"] = cls._build_calculation_graph(
            formula_cells=result["formula_cells"],
            named_references=result["named_references"],
            input_cells=result["input_cells"],
        )

        if not result["sections"]["calculation_logic"] and result["formula_cells"]:
            result["sections"]["calculation_logic"] = [
                {
                    "sheet": row.get("sheet"),
                    "target": row.get("cell"),
                    "expression": row.get("formula"),
                    "dependencies": row.get("dependencies", {}),
                }
                for row in result["formula_cells"]
            ]

        result["has_structured_config"] = (
            any(bool(result["sections"][key]) for key in EXCEL_SECTION_KEYS)
            or bool(result["dropdown_validations"])
            or bool(result["formula_cells"])
            or bool(result["named_references"])
            or bool(result["input_cells"])
            or bool(result.get("calculation_graph", {}).get("edges"))
        )
        try:
            workbook.close()
        except Exception:
            pass
        return result

    @classmethod
    def _inspect_excel(cls, path: Path) -> Dict[str, Any]:
        excel_auto_import = cls._extract_excel_config_auto_import(path)
        try:
            import pandas as pd
        except Exception as exc:
            return {
                "parse_error": f"pandas unavailable: {exc}",
                "config_auto_import": excel_auto_import,
                "sheet_count": len(excel_auto_import.get("sheet_names", [])),
                "sheets": [],
            }

        openpyxl_workbook = None
        try:
            import openpyxl

            openpyxl_workbook = openpyxl.load_workbook(path, data_only=False, read_only=False)
        except Exception:
            openpyxl_workbook = None

        try:
            workbook = pd.ExcelFile(path)
        except Exception as exc:
            return {
                "parse_error": str(exc),
                "config_auto_import": excel_auto_import,
                "sheet_count": len(excel_auto_import.get("sheet_names", [])),
                "sheets": [],
            }

        sheet_infos: List[Dict[str, Any]] = []
        try:
            for sheet_name in workbook.sheet_names:
                sheet_name_text = str(sheet_name)
                sheet_entry: Dict[str, Any] = {"sheet": sheet_name_text}
                detected_headers: List[str] = []
                detected_rows_sampled = 0

                if openpyxl_workbook is not None and sheet_name_text in set(openpyxl_workbook.sheetnames):
                    try:
                        table_preview = cls._extract_table_rows_from_sheet(openpyxl_workbook[sheet_name_text], max_rows=200)
                        detected_headers = [str(col) for col in table_preview.get("headers", [])[:30]]
                        detected_rows_sampled = int(len(table_preview.get("rows", [])))
                    except Exception:
                        detected_headers = []
                        detected_rows_sampled = 0

                try:
                    frame = pd.read_excel(workbook, sheet_name=sheet_name_text, nrows=200)
                    pandas_columns = [str(col) for col in frame.columns[:30]]
                    unnamed_count = sum(
                        1 for col in pandas_columns if cls._is_placeholder_excel_column_name(col)
                    )
                    unnamed_ratio = (unnamed_count / len(pandas_columns)) if pandas_columns else 1.0
                    use_detected_headers = bool(detected_headers) and unnamed_ratio >= 0.5

                    sheet_entry["rows_sampled"] = int(len(frame))
                    sheet_entry["columns"] = detected_headers if use_detected_headers else pandas_columns
                    if use_detected_headers:
                        sheet_entry["columns_source"] = "openpyxl_detected_header"
                except Exception as exc:
                    if detected_headers:
                        sheet_entry["rows_sampled"] = int(detected_rows_sampled)
                        sheet_entry["columns"] = detected_headers
                        sheet_entry["columns_source"] = "openpyxl_detected_header"
                        sheet_entry["parse_warning"] = f"pandas parse failed: {exc}"
                    else:
                        sheet_entry["parse_error"] = str(exc)

                sheet_infos.append(sheet_entry)
        finally:
            if openpyxl_workbook is not None:
                try:
                    openpyxl_workbook.close()
                except Exception:
                    pass
            try:
                workbook.close()
            except Exception:
                pass
        return {
            "sheet_count": len(workbook.sheet_names),
            "sheets": sheet_infos,
            "config_auto_import": excel_auto_import,
        }

    @staticmethod
    def _coerce_calc_input_value(value: Any) -> Any:
        if value is None:
            return None
        if isinstance(value, (int, float, bool)):
            return value
        text = str(value).strip()
        if not text:
            return None
        lower = text.lower()
        if lower in {"true", "yes", "on"}:
            return True
        if lower in {"false", "no", "off"}:
            return False
        if re.match(r"^[+-]?\d+$", text):
            try:
                return int(text)
            except Exception:
                pass
        if re.match(r"^[+-]?\d*\.\d+$", text):
            try:
                return float(text)
            except Exception:
                pass
        return text

    @classmethod
    def _extract_excel_artifacts_from_version(
        cls,
        *,
        model_name: str,
        version: str,
        record: Dict[str, Any],
    ) -> List[Dict[str, Any]]:
        manifest = list(record.get("artifact_manifest", []))
        out: List[Dict[str, Any]] = []
        for item in manifest:
            relative_path = str(item.get("relative_path", "")).strip()
            if not relative_path:
                continue
            excel_profile = item.get("excel_profile", {})
            if not isinstance(excel_profile, dict):
                continue
            config = excel_profile.get("config_auto_import", {})
            if not isinstance(config, dict):
                continue
            input_cells = list(config.get("input_cells", []))
            formula_cells = list(config.get("formula_cells", []))
            if not input_cells and not formula_cells:
                continue
            out.append(
                {
                    "model_name": model_name,
                    "version": version,
                    "artifact_name": str(item.get("stored_name") or item.get("original_name") or Path(relative_path).name),
                    "artifact_path": relative_path,
                    "size_bytes": int(item.get("size_bytes", 0) or 0),
                    "input_count": len(input_cells),
                    "formula_count": len(formula_cells),
                    "sheet_count": len(config.get("sheet_names", [])),
                    "config_auto_import": config,
                }
            )
        return out

    @classmethod
    def _input_label_from_metadata(cls, item: Dict[str, Any]) -> str:
        neighbor_labels = item.get("neighbor_labels", {})
        if isinstance(neighbor_labels, dict):
            # Spreadsheet templates usually place semantic labels on the left/up cells.
            for key in ("left", "up", "right", "down"):
                text = cls._normalize_label_text(neighbor_labels.get(key, ""))
                if cls._is_meaningful_label_text(text):
                    return text

        defined_names = [str(name).strip() for name in item.get("defined_names", []) if str(name).strip()]
        for name in defined_names:
            humanized = cls._humanize_identifier_label(name)
            if cls._is_meaningful_label_text(humanized):
                return humanized

        sheet = str(item.get("sheet", "")).strip()
        cell = str(item.get("cell", "")).strip().upper()
        return f"{sheet} {cell}".strip()

    @classmethod
    def _sheet_cell_key_from_formula_token(
        cls,
        *,
        token: str,
        current_sheet: str,
    ) -> tuple[Optional[str], bool]:
        raw = str(token or "").strip()
        if not raw:
            return None, False
        sheet_name = current_sheet
        ref_text = raw
        if "!" in raw:
            sheet_token, ref_text = raw.split("!", 1)
            sheet_name = cls._normalize_sheet_token(sheet_token, default_sheet=current_sheet)
        ref_text = ref_text.replace("$", "").strip()
        if not ref_text:
            return None, False
        if ":" in ref_text:
            return None, True
        ref_text = ref_text.upper()
        if not re.match(r"^[A-Z]{1,3}\d+$", ref_text):
            return None, False
        return cls._make_sheet_cell_key(sheet_name, ref_text), False

    @classmethod
    def _build_named_reference_cell_map(
        cls,
        named_references: Sequence[Dict[str, Any]],
    ) -> Dict[str, str]:
        mapping: Dict[str, str] = {}
        for item in named_references:
            name = str(item.get("name", "")).strip()
            sheet = str(item.get("sheet", "")).strip()
            cell = str(item.get("anchor_cell", "")).strip().upper()
            if not name or not sheet or not cell:
                continue
            mapping[name.lower()] = cls._make_sheet_cell_key(sheet, cell)
        return mapping

    @staticmethod
    def _excel_eval_functions() -> Dict[str, Callable[..., Any]]:
        def _flatten_args(args: Sequence[Any]) -> List[Any]:
            out: List[Any] = []
            for value in args:
                if isinstance(value, (list, tuple)):
                    out.extend(_flatten_args(list(value)))
                else:
                    out.append(value)
            return out

        def _num(value: Any) -> float:
            if value is None:
                return 0.0
            if isinstance(value, bool):
                return 1.0 if value else 0.0
            if isinstance(value, (int, float)):
                return float(value)
            text = str(value).strip()
            if not text:
                return 0.0
            return float(text)

        def _if_fn(cond: Any, left: Any, right: Any) -> Any:
            return left if bool(cond) else right

        def _iferror_fn(value: Any, fallback: Any) -> Any:
            return value if value is not None else fallback

        def _sum_fn(*args: Any) -> float:
            return float(sum(_num(item) for item in _flatten_args(args)))

        def _min_fn(*args: Any) -> Any:
            values = [item for item in _flatten_args(args) if item is not None and str(item).strip() != ""]
            return min(values) if values else None

        def _max_fn(*args: Any) -> Any:
            values = [item for item in _flatten_args(args) if item is not None and str(item).strip() != ""]
            return max(values) if values else None

        return {
            "__if__": _if_fn,
            "__iferror__": _iferror_fn,
            "__and__": lambda *args: all(bool(item) for item in args),
            "__or__": lambda *args: any(bool(item) for item in args),
            "__not__": lambda value: not bool(value),
            "__sum__": _sum_fn,
            "__min__": _min_fn,
            "__max__": _max_fn,
            "__abs__": lambda value: abs(_num(value)),
            "__round__": lambda value, digits=0: round(_num(value), int(_num(digits))),
            "__int__": lambda value: int(_num(value)),
            "__float__": lambda value: float(_num(value)),
        }

    @classmethod
    def _eval_excel_formula_text(
        cls,
        *,
        formula: str,
        current_sheet: str,
        resolve_cell_value: Callable[[str], Any],
        resolve_named_value: Callable[[str], Any],
    ) -> Any:
        formula_text = str(formula or "").strip()
        if formula_text.startswith("="):
            expr = formula_text[1:].strip()
        else:
            expr = formula_text
        if not expr:
            return None

        eval_locals: Dict[str, Any] = {}
        placeholder_idx = 0

        def _put_placeholder(value: Any) -> str:
            nonlocal placeholder_idx
            name = f"__v{placeholder_idx}"
            placeholder_idx += 1
            eval_locals[name] = value
            return name

        for token in sorted(set(FORMULA_CELL_REF_PATTERN.findall(expr)), key=len, reverse=True):
            cell_key, is_range = cls._sheet_cell_key_from_formula_token(token=token, current_sheet=current_sheet)
            if is_range:
                raise ValueError(f"Range references are not supported yet: {token}")
            if not cell_key:
                continue
            placeholder = _put_placeholder(resolve_cell_value(cell_key))
            expr = expr.replace(token, placeholder)

        named_tokens = sorted(set(FORMULA_NAME_TOKEN_PATTERN.findall(expr)), key=len, reverse=True)
        for token in named_tokens:
            token_text = str(token).strip()
            if not token_text:
                continue
            token_upper = token_text.upper()
            if token_upper in {
                "IF",
                "IFERROR",
                "AND",
                "OR",
                "NOT",
                "SUM",
                "MIN",
                "MAX",
                "ABS",
                "ROUND",
                "INT",
                "TRUE",
                "FALSE",
            }:
                continue
            try:
                value = resolve_named_value(token_text)
            except Exception:
                continue
            placeholder = _put_placeholder(value)
            expr = re.sub(
                rf"(?<![A-Za-z0-9_.]){re.escape(token_text)}(?![A-Za-z0-9_.])",
                placeholder,
                expr,
                flags=re.IGNORECASE,
            )

        func_map = {
            "IFERROR": "__iferror__",
            "IF": "__if__",
            "AND": "__and__",
            "OR": "__or__",
            "NOT": "__not__",
            "SUM": "__sum__",
            "MIN": "__min__",
            "MAX": "__max__",
            "ABS": "__abs__",
            "ROUND": "__round__",
            "INT": "__int__",
            "FLOAT": "__float__",
        }
        for name, target in func_map.items():
            expr = re.sub(rf"\b{name}\s*\(", f"{target}(", expr, flags=re.IGNORECASE)

        expr = re.sub(r"\bTRUE\b", "True", expr, flags=re.IGNORECASE)
        expr = re.sub(r"\bFALSE\b", "False", expr, flags=re.IGNORECASE)
        expr = expr.replace("^", "**")
        expr = expr.replace("<>", "!=")
        expr = re.sub(r"(?<![<>=!])=(?!=)", "==", expr)
        expr = expr.replace("&", "+")
        expr = re.sub(r"(\d+(?:\.\d+)?)%", r"(\1/100)", expr)

        safe_locals = {**cls._excel_eval_functions(), **eval_locals}
        try:
            return eval(expr, {"__builtins__": {}}, safe_locals)
        except Exception as exc:
            raise ValueError(f"Formula evaluation failed for '{formula_text}': {exc}") from exc

    def list_excel_calculator_artifacts(self, model_name: str, version: str) -> List[Dict[str, Any]]:
        model_name = (model_name or "").strip()
        version = (version or "").strip()
        if not model_name:
            raise ValueError("model_name is required")
        if not version:
            raise ValueError("version is required")
        record = self.registry.get(model_name, version=version)
        artifacts = self._extract_excel_artifacts_from_version(
            model_name=model_name,
            version=version,
            record=record,
        )
        return [
            {
                "model_name": item["model_name"],
                "version": item["version"],
                "artifact_name": item["artifact_name"],
                "artifact_path": item["artifact_path"],
                "size_bytes": item["size_bytes"],
                "input_count": item["input_count"],
                "formula_count": item["formula_count"],
                "sheet_count": item["sheet_count"],
            }
            for item in artifacts
        ]

    def get_excel_calculator_schema(
        self,
        *,
        model_name: str,
        version: str,
        artifact_path: str,
    ) -> Dict[str, Any]:
        model_name = (model_name or "").strip()
        version = (version or "").strip()
        artifact_path = (artifact_path or "").strip()
        if not model_name:
            raise ValueError("model_name is required")
        if not version:
            raise ValueError("version is required")
        if not artifact_path:
            raise ValueError("artifact_path is required")

        record = self.registry.get(model_name, version=version)
        artifacts = self._extract_excel_artifacts_from_version(
            model_name=model_name,
            version=version,
            record=record,
        )
        selected = next((item for item in artifacts if item["artifact_path"] == artifact_path), None)
        if selected is None:
            raise ValueError(f"Excel artifact not found for {model_name}:{version} -> {artifact_path}")

        config = selected["config_auto_import"]
        input_fields: List[Dict[str, Any]] = []
        for item in config.get("input_cells", []):
            sheet = str(item.get("sheet", "")).strip()
            cell = str(item.get("cell", "")).strip().upper()
            if not sheet or not cell:
                continue
            key = self._make_sheet_cell_key(sheet, cell)
            validation = item.get("validation", {}) if isinstance(item.get("validation"), dict) else {}
            options = [str(opt) for opt in validation.get("inline_options", []) if str(opt).strip()]
            input_fields.append(
                {
                    "key": key,
                    "sheet": sheet,
                    "cell": cell,
                    "label": self._input_label_from_metadata(item),
                    "default_value": item.get("value"),
                    "options": options,
                    "defined_names": [str(name) for name in item.get("defined_names", []) if str(name).strip()],
                    "neighbor_labels": dict(item.get("neighbor_labels", {}))
                    if isinstance(item.get("neighbor_labels"), dict)
                    else {},
                }
            )

        output_cells = []
        calc_graph = config.get("calculation_graph", {})
        if isinstance(calc_graph, dict):
            output_cells = [
                {
                    "id": str(item.get("id", "")).strip(),
                    "sheet": str(item.get("sheet", "")).strip(),
                    "cell": str(item.get("cell", "")).strip().upper(),
                    "formula": str(item.get("formula", "")),
                }
                for item in calc_graph.get("output_cells", [])
                if str(item.get("sheet", "")).strip() and str(item.get("cell", "")).strip()
            ]
        if not output_cells:
            output_cells = [
                {
                    "id": self._make_sheet_cell_key(str(item.get("sheet", "")), str(item.get("cell", "")).upper()),
                    "sheet": str(item.get("sheet", "")).strip(),
                    "cell": str(item.get("cell", "")).strip().upper(),
                    "formula": str(item.get("formula", "")),
                }
                for item in config.get("formula_cells", [])[:30]
                if str(item.get("sheet", "")).strip() and str(item.get("cell", "")).strip()
            ]

        raw_graph = config.get("calculation_graph", {})
        if not isinstance(raw_graph, dict):
            raw_graph = {}
        graph_nodes = list(raw_graph.get("nodes", [])) if isinstance(raw_graph.get("nodes", []), list) else []
        graph_edges = list(raw_graph.get("edges", [])) if isinstance(raw_graph.get("edges", []), list) else []
        if (not graph_nodes and not graph_edges) and (
            isinstance(config.get("formula_cells"), list) or isinstance(config.get("input_cells"), list)
        ):
            rebuilt_graph = self._build_calculation_graph(
                formula_cells=list(config.get("formula_cells", [])),
                named_references=list(config.get("named_references", [])),
                input_cells=list(config.get("input_cells", [])),
            )
            graph_nodes = list(rebuilt_graph.get("nodes", [])) if isinstance(rebuilt_graph, dict) else []
            graph_edges = list(rebuilt_graph.get("edges", [])) if isinstance(rebuilt_graph, dict) else []
            if not output_cells:
                output_cells = list(rebuilt_graph.get("output_cells", [])) if isinstance(rebuilt_graph, dict) else []

        calculation_graph = {
            "nodes": graph_nodes,
            "edges": graph_edges,
            "output_cells": output_cells,
        }

        return {
            "model_name": model_name,
            "version": version,
            "artifact_name": selected["artifact_name"],
            "artifact_path": selected["artifact_path"],
            "input_fields": input_fields,
            "output_cells": output_cells,
            "calculation_graph": calculation_graph,
        }

    def run_excel_single_calculation(
        self,
        *,
        model_name: str,
        version: str,
        artifact_path: str,
        inputs: Dict[str, Any],
    ) -> Dict[str, Any]:
        model_name = (model_name or "").strip()
        version = (version or "").strip()
        artifact_path = (artifact_path or "").strip()
        if not model_name:
            raise ValueError("model_name is required")
        if not version:
            raise ValueError("version is required")
        if not artifact_path:
            raise ValueError("artifact_path is required")

        record = self.registry.get(model_name, version=version)
        artifacts = self._extract_excel_artifacts_from_version(
            model_name=model_name,
            version=version,
            record=record,
        )
        selected = next((item for item in artifacts if item["artifact_path"] == artifact_path), None)
        if selected is None:
            raise ValueError(f"Excel artifact not found for {model_name}:{version} -> {artifact_path}")

        excel_path = (self.base_dir / artifact_path).resolve()
        if not excel_path.exists():
            raise FileNotFoundError(f"Artifact file not found: {excel_path}")

        config = selected["config_auto_import"]
        input_cells = list(config.get("input_cells", []))
        formula_cells = list(config.get("formula_cells", []))
        named_refs = list(config.get("named_references", []))

        input_value_map: Dict[str, Any] = {}
        for item in input_cells:
            sheet = str(item.get("sheet", "")).strip()
            cell = str(item.get("cell", "")).strip().upper()
            if not sheet or not cell:
                continue
            key = self._make_sheet_cell_key(sheet, cell)
            input_value_map[key] = item.get("value")

        named_to_cell = self._build_named_reference_cell_map(named_refs)
        normalized_inputs: Dict[str, Any] = {}
        for key, raw_value in dict(inputs or {}).items():
            token = str(key or "").strip()
            if not token:
                continue
            coerced = self._coerce_calc_input_value(raw_value)
            if token in input_value_map:
                input_value_map[token] = coerced
                normalized_inputs[token] = coerced
                continue
            mapped_cell = named_to_cell.get(token.lower())
            if mapped_cell:
                input_value_map[mapped_cell] = coerced
                normalized_inputs[mapped_cell] = coerced

        formula_by_key: Dict[str, Dict[str, Any]] = {}
        for item in formula_cells:
            sheet = str(item.get("sheet", "")).strip()
            cell = str(item.get("cell", "")).strip().upper()
            if not sheet or not cell:
                continue
            formula_by_key[self._make_sheet_cell_key(sheet, cell)] = dict(item)

        try:
            import openpyxl
        except Exception as exc:
            raise RuntimeError(f"openpyxl is required for single calculation: {exc}") from exc

        workbook = openpyxl.load_workbook(excel_path, data_only=False, read_only=False)
        sheet_map = {str(sheet.title): sheet for sheet in workbook.worksheets}

        for cell_key, value in input_value_map.items():
            sheet_name, cell_name = self._split_sheet_cell_key(cell_key)
            sheet = sheet_map.get(sheet_name)
            if sheet is None:
                continue
            try:
                sheet[cell_name].value = value
            except Exception:
                continue

        cache: Dict[str, Any] = {}
        in_stack: set[str] = set()
        errors: List[Dict[str, Any]] = []

        def resolve_cell_value(cell_key: str) -> Any:
            if cell_key in cache:
                return cache[cell_key]
            if cell_key in in_stack:
                raise ValueError(f"Circular reference detected: {cell_key}")
            in_stack.add(cell_key)
            try:
                if cell_key in input_value_map:
                    value = input_value_map.get(cell_key)
                    cache[cell_key] = value
                    return value
                formula_item = formula_by_key.get(cell_key)
                if formula_item is not None:
                    sheet_name, _cell_name = self._split_sheet_cell_key(cell_key)
                    formula_text = str(formula_item.get("formula", ""))
                    value = self._eval_excel_formula_text(
                        formula=formula_text,
                        current_sheet=sheet_name,
                        resolve_cell_value=resolve_cell_value,
                        resolve_named_value=resolve_named_value,
                    )
                    cache[cell_key] = value
                    return value
                sheet_name, cell_name = self._split_sheet_cell_key(cell_key)
                sheet = sheet_map.get(sheet_name)
                if sheet is None:
                    cache[cell_key] = None
                    return None
                value = getattr(sheet[cell_name], "value", None)
                cache[cell_key] = value
                return value
            finally:
                in_stack.discard(cell_key)

        def resolve_named_value(name: str) -> Any:
            key = named_to_cell.get(str(name).strip().lower())
            if not key:
                raise ValueError(f"Named reference not found: {name}")
            return resolve_cell_value(key)

        output_cells = []
        calc_graph = config.get("calculation_graph", {})
        if isinstance(calc_graph, dict):
            output_cells = [
                {
                    "id": str(item.get("id", "")).strip(),
                    "sheet": str(item.get("sheet", "")).strip(),
                    "cell": str(item.get("cell", "")).strip().upper(),
                    "formula": str(item.get("formula", "")),
                }
                for item in calc_graph.get("output_cells", [])
                if str(item.get("sheet", "")).strip() and str(item.get("cell", "")).strip()
            ]
        if not output_cells:
            output_cells = [
                {
                    "id": self._make_sheet_cell_key(str(item.get("sheet", "")), str(item.get("cell", "")).upper()),
                    "sheet": str(item.get("sheet", "")).strip(),
                    "cell": str(item.get("cell", "")).strip().upper(),
                    "formula": str(item.get("formula", "")),
                }
                for item in formula_cells[:30]
                if str(item.get("sheet", "")).strip() and str(item.get("cell", "")).strip()
            ]

        results: List[Dict[str, Any]] = []
        for item in output_cells:
            sheet = str(item.get("sheet", "")).strip()
            cell = str(item.get("cell", "")).strip().upper()
            key = self._make_sheet_cell_key(sheet, cell)
            try:
                value = resolve_cell_value(key)
                results.append(
                    {
                        "id": key,
                        "sheet": sheet,
                        "cell": cell,
                        "value": self._json_safe_value(value),
                        "formula": str(item.get("formula", "")),
                    }
                )
            except Exception as exc:
                errors.append({"id": key, "error": str(exc), "formula": str(item.get("formula", ""))})

        try:
            workbook.close()
        except Exception:
            pass

        return {
            "model_name": model_name,
            "version": version,
            "artifact_name": selected["artifact_name"],
            "artifact_path": artifact_path,
            "input_values": {key: self._json_safe_value(value) for key, value in input_value_map.items()},
            "applied_inputs": {key: self._json_safe_value(value) for key, value in normalized_inputs.items()},
            "output_values": results,
            "errors": errors,
            "engine": "excel_formula_subset_v1",
        }

