from __future__ import annotations

import json
import urllib.error
import urllib.parse
import urllib.request
from typing import Any

from lark_oapi.api.sheets.v3 import (
    CreateSpreadsheetRequest,
    GetSpreadsheetRequest,
    QuerySpreadsheetSheetRequest,
    Spreadsheet,
)

from ._client import get_client, make_option
from ._scopes import InsufficientScopeError, MODULE_SCOPES

_FEISHU_OPEN_API = "https://open.feishu.cn"


def _http(method: str, path: str, token: str, *, body: Any = None, params: dict | None = None) -> dict:
    url = _FEISHU_OPEN_API + path
    if params:
        url += "?" + urllib.parse.urlencode(params)
    data = json.dumps(body).encode() if body is not None else None
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json; charset=utf-8",
    }
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req) as resp:
            result: dict = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        raw = e.read().decode(errors="replace")
        raise RuntimeError(f"HTTP {e.code} {path}: {raw}") from e
    if result.get("code", 0) != 0:
        code = result.get("code")
        msg = result.get("msg", "")
        if code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("sheets", []), module="sheets")
        raise RuntimeError(f"飞书 API 错误 code={code}: {msg}")
    return result


def parse_composite_token(raw: str) -> tuple[str, str]:
    """拆分 docx block_type=30 的复合 token ``{spreadsheet_token}_{sheet_id}``。

    Returns (spreadsheet_token, sheet_id)。
    Raises ValueError if raw contains no underscore separator.
    """
    if "_" not in raw:
        raise ValueError(
            f"无法解析复合 token {raw!r}：预期格式为 {{spreadsheet_token}}_{{sheet_id}}"
        )
    spreadsheet_token, sheet_id = raw.rsplit("_", 1)
    return spreadsheet_token, sheet_id


def get_spreadsheet(token: str, spreadsheet_token: str) -> dict:
    client = get_client()
    request = (
        GetSpreadsheetRequest.builder()
        .spreadsheet_token(spreadsheet_token)
        .build()
    )
    response = client.sheets.v3.spreadsheet.get(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("sheets", []), module="sheets")
        raise RuntimeError(f"获取电子表格失败: code={response.code}, msg={response.msg}")
    sht = response.data.spreadsheet
    return {
        "spreadsheet_token": getattr(sht, "spreadsheet_token", ""),
        "title": getattr(sht, "title", ""),
        "owner_id": getattr(sht, "owner_id", ""),
        "url": getattr(sht, "url", ""),
    }


def list_sheets(token: str, spreadsheet_token: str) -> list[dict]:
    client = get_client()
    request = QuerySpreadsheetSheetRequest.builder().spreadsheet_token(spreadsheet_token).build()
    response = client.sheets.v3.spreadsheet_sheet.query(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("sheets", []), module="sheets")
        raise RuntimeError(f"获取工作表列表失败: code={response.code}, msg={response.msg}")
    result = []
    for s in response.data.sheets or []:
        gp = getattr(s, "grid_properties", None)
        result.append({
            "sheet_id": getattr(s, "sheet_id", ""),
            "title": getattr(s, "title", ""),
            "index": getattr(s, "index", 0),
            "hidden": getattr(s, "hidden", False),
            "row_count": getattr(gp, "row_count", 0) if gp else 0,
            "column_count": getattr(gp, "column_count", 0) if gp else 0,
        })
    return result


def create_spreadsheet(token: str, title: str, *, folder_token: str = "") -> dict:
    client = get_client()
    sht_builder = Spreadsheet.builder().title(title)
    if folder_token:
        sht_builder = sht_builder.folder_token(folder_token)
    request = CreateSpreadsheetRequest.builder().request_body(sht_builder.build()).build()
    response = client.sheets.v3.spreadsheet.create(request, make_option(token))
    if not response.success():
        if response.code == 99991679:
            raise InsufficientScopeError(MODULE_SCOPES.get("sheets", []), module="sheets")
        raise RuntimeError(f"创建电子表格失败: code={response.code}, msg={response.msg}")
    sht = response.data.spreadsheet
    return {
        "spreadsheet_token": getattr(sht, "spreadsheet_token", ""),
        "title": getattr(sht, "title", ""),
        "url": getattr(sht, "url", ""),
    }


def _cell_to_value(cell: Any, *, raw: bool = False) -> Any:
    """Convert a raw Feishu cell value to a typed, serialisable dict.

    Return shapes:
      {"type": "empty",  "value": None}
      {"type": "number", "value": int|float}
      {"type": "text",   "value": str}
      {"type": "rich",   "text": str, "segments": list}  — plain text join + original segments
    """
    if cell is None:
        return {"type": "empty", "value": None}
    if isinstance(cell, (int, float)):
        return {"type": "number", "value": cell}
    if isinstance(cell, str):
        return {"type": "text", "value": cell}
    if isinstance(cell, list):
        plain = "".join(seg.get("text", "") for seg in cell if isinstance(seg, dict))
        if raw:
            return {"type": "rich", "text": plain, "segments": cell}
        return {"type": "rich", "text": plain, "segments": cell}
    return {"type": "text", "value": str(cell)}


def _rows_to_typed(rows: list[list[Any]], *, raw: bool = False) -> list[list[dict]]:
    return [[_cell_to_value(cell, raw=raw) for cell in row] for row in rows]


def read_values(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
    range_: str = "",
    *,
    raw: bool = False,
) -> dict:
    full_range = f"{sheet_id}!{range_}" if range_ else sheet_id
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values/{full_range}"
    result = _http("GET", path, token, params={"renderType": "FormattedValue"})
    vr = result.get("data", {}).get("valueRange", {})
    raw_rows: list[list[Any]] = vr.get("values") or []
    return {
        "spreadsheet_token": spreadsheet_token,
        "sheet_id": sheet_id,
        "range": vr.get("range", full_range),
        "rows": _rows_to_typed(raw_rows, raw=raw),
    }


def write_values(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
    range_: str,
    values: list[list[Any]],
) -> dict:
    full_range = f"{sheet_id}!{range_}"
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values"
    body = {"valueRange": {"range": full_range, "values": values}}
    result = _http("PUT", path, token, body=body)
    updates = result.get("data", {})
    return {
        "updated_range": updates.get("updatedRange", full_range),
        "updated_rows": updates.get("updatedRows", 0),
        "updated_columns": updates.get("updatedColumns", 0),
        "updated_cells": updates.get("updatedCells", 0),
    }


def append_values(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
    values: list[list[Any]],
    *,
    insert_data_option: str = "OVERWRITE",
) -> dict:
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_append"
    body = {
        "valueRange": {"range": sheet_id, "values": values},
        "insertDataOption": insert_data_option,
    }
    result = _http("POST", path, token, body=body)
    data = result.get("data", {})
    return {
        "table_range": data.get("tableRange", ""),
        "updates": data.get("updates", {}),
    }


def clear_values(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
    range_: str,
) -> dict:
    full_range = f"{sheet_id}!{range_}"
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values/{full_range}"
    result = _http("DELETE", path, token)
    return {
        "spreadsheet_token": spreadsheet_token,
        "cleared_range": full_range,
    }


def batch_read_values(
    token: str,
    spreadsheet_token: str,
    ranges: list[str],
    *,
    raw: bool = False,
) -> list[dict]:
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_get"
    result = _http("GET", path, token, params={
        "ranges": ",".join(ranges),
        "renderType": "FormattedValue",
    })
    value_ranges = result.get("data", {}).get("valueRanges") or []
    output = []
    for vr in value_ranges:
        raw_rows: list[list[Any]] = vr.get("values") or []
        range_str: str = vr.get("range", "")
        sheet_id = range_str.split("!")[0] if "!" in range_str else range_str
        output.append({
            "spreadsheet_token": spreadsheet_token,
            "sheet_id": sheet_id,
            "range": range_str,
            "rows": _rows_to_typed(raw_rows, raw=raw),
        })
    return output


def batch_write_values(
    token: str,
    spreadsheet_token: str,
    range_values: list[dict],
) -> dict:
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_update"
    body = {"valueRanges": [{"range": rv["range"], "values": rv["values"]} for rv in range_values]}
    result = _http("POST", path, token, body=body)
    data = result.get("data", {})
    return {
        "spreadsheet_token": spreadsheet_token,
        "total_updated_cells": data.get("totalUpdatedCells", 0),
        "total_updated_rows": data.get("totalUpdatedRows", 0),
        "total_updated_columns": data.get("totalUpdatedColumns", 0),
        "responses": data.get("responses", []),
    }


def merge_cells(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
    range_: str,
    *,
    merge_type: str = "MERGE_ALL",
) -> dict:
    full_range = f"{sheet_id}!{range_}"
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/merge_cells"
    body = {"range": full_range, "mergeType": merge_type}
    _http("POST", path, token, body=body)
    return {"spreadsheet_token": spreadsheet_token, "merged_range": full_range, "merge_type": merge_type}


def unmerge_cells(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
    range_: str,
) -> dict:
    full_range = f"{sheet_id}!{range_}"
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/unmerge_cells"
    body = {"range": full_range}
    _http("POST", path, token, body=body)
    return {"spreadsheet_token": spreadsheet_token, "unmerged_range": full_range}


def add_sheet(
    token: str,
    spreadsheet_token: str,
    title: str,
    *,
    index: int | None = None,
) -> dict:
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
    props: dict[str, Any] = {"title": title}
    if index is not None:
        props["index"] = index
    body = {"requests": [{"addSheet": {"properties": props}}]}
    result = _http("POST", path, token, body=body)
    replies = result.get("data", {}).get("replies", [])
    if replies:
        props_out = replies[0].get("addSheet", {}).get("properties", {})
        return {
            "sheet_id": props_out.get("sheetId", ""),
            "title": props_out.get("title", title),
            "index": props_out.get("index", 0),
        }
    return {"sheet_id": "", "title": title, "index": 0}


def delete_sheet(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
) -> dict:
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
    body = {"requests": [{"deleteSheet": {"sheetId": sheet_id}}]}
    _http("POST", path, token, body=body)
    return {"spreadsheet_token": spreadsheet_token, "deleted_sheet_id": sheet_id}


def copy_sheet(
    token: str,
    spreadsheet_token: str,
    sheet_id: str,
    *,
    dest_title: str | None = None,
) -> dict:
    path = f"/open-apis/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
    copy_to: dict[str, Any] = {"sheetId": sheet_id}
    if dest_title:
        copy_to["title"] = dest_title
    body = {"requests": [{"copySheet": {"source": {"sheetId": sheet_id}, "destination": copy_to}}]}
    result = _http("POST", path, token, body=body)
    replies = result.get("data", {}).get("replies", [])
    if replies:
        props_out = replies[0].get("copySheet", {}).get("properties", {})
        return {
            "sheet_id": props_out.get("sheetId", ""),
            "title": props_out.get("title", ""),
            "index": props_out.get("index", 0),
        }
    return {"sheet_id": "", "title": dest_title or "", "index": 0}
