"""XLSX 读写的单一实现（openpyxl）：多工作表、公式字面量读写、与 doc_extract 共用的表格导出。"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Callable

from openpyxl import Workbook, load_workbook

MAX_SHEETS = 20
MAX_ROWS_PER_SHEET = 5000
MAX_COLS = 256
# 结构化读取时的上限（防工具 JSON 过大）
MAX_READ_ROWS_PER_SHEET = 2000
MAX_READ_COLS_PER_SHEET = 128
MAX_PATCH_CELLS = 8000


def _policy_touch(path: Path, policy_check: Callable[[Path], None]) -> Path:
    path = path.expanduser().resolve()
    policy_check(path)
    return path


def _coerce_cell_for_write(val: Any) -> Any:
    """将工具 JSON 中的标量/小对象转为 openpyxl 可接受的单元格值（含公式）。"""
    if val is None:
        return None
    if isinstance(val, dict):
        if "formula" in val and val["formula"] is not None:
            return str(val["formula"])
        if "value" in val:
            return _coerce_cell_for_write(val["value"])
        return None
    if isinstance(val, str):
        s = val
        if s.strip().startswith("="):
            return s
    return val


def _append_row(ws: Any, row: list[Any] | tuple[Any, ...]) -> str | None:
    out: list[Any] = []
    for cell in row:
        if not isinstance(cell, (list, tuple)):
            out.append(_coerce_cell_for_write(cell))
            continue
        if len(cell) != 2 or str(cell[0]).lower() != "formula":
            return 'tuple cells must be ["formula", "=..."] for formula cells'
        out.append(str(cell[1]))
    ws.append(out)
    return None


def xlsx_write_workbook(
    path: Path,
    sheets: list[dict[str, Any]],
    *,
    policy_check: Callable[[Path], None],
    append: bool = False,
) -> dict[str, Any]:
    """
    写入或追加 Excel。

    ``sheets`` 每项: ``name``, 可选 ``headers``, ``rows`` (list[list])。
    单元格可为标量；若 ``str`` 以 ``=`` 开头则写入公式；
    或为 ``{"formula": "=SUM(A1:A2)"}`` / ``{"value": ...}``。
    亦支持两元组 ``["formula", "=A1+B1"]``（与 JSON 数组兼容）。
    """
    if not isinstance(sheets, list) or not sheets:
        return {"ok": False, "error": "sheets must be non-empty list"}
    if len(sheets) > MAX_SHEETS:
        return {"ok": False, "error": f"max {MAX_SHEETS} sheets"}

    out_path = _policy_touch(path, policy_check)
    out_path.parent.mkdir(parents=True, exist_ok=True)

    if append and out_path.is_file():
        wb = load_workbook(out_path)
    else:
        wb = Workbook()

    used_default_sheet = False

    for spec in sheets:
        if not isinstance(spec, dict):
            return {"ok": False, "error": "each sheet must be object"}
        name = spec.get("name") or "Sheet"
        if not isinstance(name, str):
            return {"ok": False, "error": "sheet name must be string"}
        name = name[:31] if len(name) > 31 else name
        headers = spec.get("headers")
        rows = spec.get("rows") or []
        if headers is not None and not isinstance(headers, (list, tuple)):
            return {"ok": False, "error": "headers must be list"}
        if not isinstance(rows, list):
            return {"ok": False, "error": "rows must be list"}
        if len(rows) > MAX_ROWS_PER_SHEET:
            return {"ok": False, "error": f"max {MAX_ROWS_PER_SHEET} rows per sheet"}

        if name in wb.sheetnames:
            ws = wb[name]
            mr = ws.max_row or 0
            if mr:
                ws.delete_rows(1, mr)
        elif (
            not used_default_sheet
            and len(wb.sheetnames) == 1
            and wb.active.title == "Sheet"
        ):
            ws = wb.active
            ws.title = name
            used_default_sheet = True
        else:
            ws = wb.create_sheet(title=name)

        if headers:
            if len(headers) > MAX_COLS:
                return {"ok": False, "error": f"max {MAX_COLS} columns"}
            err = _append_row(ws, list(headers))
            if err:
                return {"ok": False, "error": err}
        for row in rows:
            if not isinstance(row, (list, tuple)):
                return {"ok": False, "error": "each row must be list"}
            if len(row) > MAX_COLS:
                return {"ok": False, "error": f"max {MAX_COLS} columns"}
            err = _append_row(ws, list(row))
            if err:
                return {"ok": False, "error": err}

    wb.save(out_path)
    return {"ok": True, "path": str(out_path), "sheet_names": wb.sheetnames}


def _serialize_cell_value(
    cell: Any, *, data_only: bool
) -> dict[str, Any]:
    """将 openpyxl ReadOnlyCell 或普通 Cell 变为 JSON 友好结构。"""
    out: dict[str, Any] = {}
    v = cell.value
    if data_only:
        if v is None:
            out["v"] = None
        elif isinstance(v, float):
            out["v"] = v
        else:
            out["v"] = v
        return out
    # data_only False: 区分公式与常量
    dt = getattr(cell, "data_type", None)
    if dt == "f" or (isinstance(v, str) and str(v).strip().startswith("=")):
        out["formula"] = v
    else:
        out["v"] = v
    return out


def xlsx_read_workbook(
    path: Path,
    *,
    policy_check: Callable[[Path], None],
    sheet_names: list[str] | None = None,
    value_mode: str = "cached",
    max_rows_per_sheet: int = MAX_READ_ROWS_PER_SHEET,
    max_cols_per_sheet: int = MAX_READ_COLS_PER_SHEET,
    row_start: int = 1,
) -> dict[str, Any]:
    """
    结构化读取 .xlsx（多表）。**openpyxl 不计算公式**：
    ``value_mode=cached`` 时返回 Excel 文件中**已保存的缓存结果**（未在 Excel 中计算过可能为 null）；
    ``value_mode=raw`` 返回公式字符串（``formula``）与常量（``v``）。
    ``value_mode=both`` 打开两次工作簿以合并公式字面量 + 缓存值（``cached`` 字段）。
    """
    p = _policy_touch(path, policy_check)
    if not p.is_file() or p.suffix.lower() != ".xlsx":
        return {"ok": False, "error": "path must be an existing .xlsx file"}
    mode = (value_mode or "cached").strip().lower()
    if mode not in ("cached", "raw", "both"):
        return {"ok": False, "error": "value_mode must be cached, raw, or both"}

    mr_cap = max(1, min(int(max_rows_per_sheet), MAX_READ_ROWS_PER_SHEET))
    mc_cap = max(1, min(int(max_cols_per_sheet), MAX_READ_COLS_PER_SHEET))
    r0 = max(1, int(row_start))

    def _read_grid(
        data_only: bool,
    ) -> dict[str, list[list[dict[str, Any]]]]:
        grids: dict[str, list[list[dict[str, Any]]]] = {}
        wb = load_workbook(p, read_only=True, data_only=data_only)
        try:
            names = list(wb.sheetnames)
            pick = names
            if sheet_names:
                wanted = [n for n in sheet_names if n in names]
                if not wanted:
                    pick = names
                else:
                    pick = wanted
            for sn in pick[:MAX_SHEETS]:
                ws = wb[sn]
                grid: list[list[dict[str, Any]]] = []
                last_r = r0 + mr_cap - 1
                last_c = mc_cap
                for row in ws.iter_rows(
                    min_row=r0,
                    max_row=last_r,
                    min_col=1,
                    max_col=last_c,
                    values_only=False,
                ):
                    line: list[dict[str, Any]] = []
                    for cell in row:
                        line.append(_serialize_cell_value(cell, data_only=data_only))
                    grid.append(line)
                grids[sn] = grid
        finally:
            wb.close()
        return grids

    if mode == "cached":
        grids = _read_grid(True)
        return {
            "ok": True,
            "path": str(p),
            "value_mode": "cached",
            "note": "Values are Excel cached results (data_only). Null may mean empty or never calculated in Excel.",
            "sheets": [
                {
                    "name": name,
                    "row_start": r0,
                    "max_rows_returned": len(rows),
                    "max_cols_returned": len(rows[0]) if rows else 0,
                    "rows": rows,
                }
                for name, rows in grids.items()
            ],
        }
    if mode == "raw":
        grids = _read_grid(False)
        return {
            "ok": True,
            "path": str(p),
            "value_mode": "raw",
            "note": "Formula cells use `formula`; literals use `v`. Not evaluated by openpyxl.",
            "sheets": [
                {
                    "name": name,
                    "row_start": r0,
                    "max_rows_returned": len(rows),
                    "max_cols_returned": len(rows[0]) if rows else 0,
                    "rows": rows,
                }
                for name, rows in grids.items()
            ],
        }

    raw_g = _read_grid(False)
    cache_g = _read_grid(True)
    merged: dict[str, list[list[dict[str, Any]]]] = {}
    for name in raw_g:
        rg = raw_g[name]
        cg = cache_g.get(name, [])
        mgrid: list[list[dict[str, Any]]] = []
        for ri, rrow in enumerate(rg):
            nrow: list[dict[str, Any]] = []
            crow = cg[ri] if ri < len(cg) else []
            for ci, ccell in enumerate(rrow):
                cell = dict(ccell)
                if "formula" in cell:
                    if ci < len(crow) and "v" in crow[ci]:
                        cell["cached"] = crow[ci].get("v")
                nrow.append(cell)
            mgrid.append(nrow)
        merged[name] = mgrid
    return {
        "ok": True,
        "path": str(p),
        "value_mode": "both",
        "note": "Formula cells include `cached` from the file when Excel saved a value.",
        "sheets": [
            {
                "name": name,
                "row_start": r0,
                "max_rows_returned": len(rows),
                "max_cols_returned": len(rows[0]) if rows else 0,
                "rows": rows,
            }
            for name, rows in merged.items()
        ],
    }


def xlsx_patch_workbook(
    path: Path,
    cells: list[dict[str, Any]],
    *,
    policy_check: Callable[[Path], None],
    create_sheets: bool = False,
) -> dict[str, Any]:
    """
    按 1-based ``row`` / ``col`` 修补现有工作簿；``value`` 可为标量、以 ``=`` 开头的公式串、
    ``{"formula": "..."}`` / ``{"value": ...}``。可选 ``create_sheets`` 在缺少工作表名时创建。
    """
    p = _policy_touch(path, policy_check)
    if not p.is_file():
        return {"ok": False, "error": "path must be an existing file"}
    if p.suffix.lower() != ".xlsx":
        return {"ok": False, "error": "path must be .xlsx"}
    if not isinstance(cells, list) or not cells:
        return {"ok": False, "error": "cells must be non-empty list"}
    if len(cells) > MAX_PATCH_CELLS:
        return {"ok": False, "error": f"max {MAX_PATCH_CELLS} cells per call"}

    wb = load_workbook(p)
    try:
        known = set(wb.sheetnames)
        for i, spec in enumerate(cells):
            if not isinstance(spec, dict):
                return {"ok": False, "error": f"cells[{i}] must be object"}
            sn = spec.get("sheet")
            if not sn or not isinstance(sn, str):
                return {"ok": False, "error": f"cells[{i}].sheet must be string"}
            sn = sn[:31] if len(sn) > 31 else sn
            if sn not in known:
                if not create_sheets:
                    return {"ok": False, "error": f"unknown sheet: {sn!r}"}
                wb.create_sheet(title=sn)
                known.add(sn)
            try:
                row = int(spec["row"])
                col = int(spec["col"])
            except (KeyError, TypeError, ValueError):
                return {"ok": False, "error": f"cells[{i}] needs integer row and col (1-based)"}
            if row < 1 or col < 1:
                return {"ok": False, "error": f"cells[{i}] row/col must be >= 1"}
            val = spec.get("value")
            coerced = _coerce_cell_for_write(val)
            wb[sn].cell(row=row, column=col).value = coerced
        names = list(wb.sheetnames)
        wb.save(p)
        return {"ok": True, "path": str(p), "sheet_names": names}
    finally:
        wb.close()


def xlsx_extract_markdown_tables(
    src: Path,
    sheets: list[str],
    *,
    stem: str,
    notes: list[str],
) -> str:
    """供 doc_extract 使用：多表 → Markdown 伪表格（data_only 缓存值；openpyxl 不计算公式）。"""
    wb = load_workbook(src, read_only=True, data_only=True)
    try:
        parts: list[str] = [f"# {stem}\n"]
        for sheet in sheets:
            if sheet not in wb.sheetnames:
                notes.append(f"xlsx sheet {sheet!r} missing, skip")
                continue
            ws = wb[sheet]
            parts.append(f"\n## {sheet}\n")
            rows_out: list[str] = []
            for row in ws.iter_rows(values_only=True):
                cells = ["" if c is None else str(c) for c in row]
                if any(x.strip() for x in cells):
                    rows_out.append("| " + " | ".join(cells) + " |")
            if rows_out:
                parts.append("\n".join(rows_out))
        return "\n".join(parts)
    finally:
        wb.close()
