#!/usr/bin/env python3
"""Update slide 1 of the administrative district service monthly PPTX.

取数逻辑：
- `/tender/overall?year={year}&month={month}&scope=行政区&scope_value={district}`
  更新累计招标金额、累计招标次数和同比指标。
- `/tender/industry?year={year}&month={month}&scope=行政区&scope_value={district}`
  更新增长最强行业、规模最大行业和经营主线。
- `/tender/major-buyers?year={year}&month={month}&scope=行政区&scope_value={district}`
  更新政府侧、企业侧 Top5 客户。
- `/tender/class?year={year}&month={month}&scope=行政区&scope_value={district}`
  更新政府侧、企业侧累计招标规模、个数和占比。
"""

from __future__ import annotations

import argparse
import json
import re
import tempfile
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from zipfile import ZipFile


PAGE_NUMBER = 1
SLIDE_XML = "ppt/slides/slide1.xml"
DATA_SOURCES = [
    "/tender/overall",
    "/tender/industry",
    "/tender/major-buyers",
    "/tender/class",
    "/tender/time",
    "/tender/customer-139",
]


OOXML_NS = {
    "a": "http://schemas.openxmlformats.org/drawingml/2006/main",
    "p": "http://schemas.openxmlformats.org/presentationml/2006/main",
}

for _prefix, _uri in OOXML_NS.items():
    ET.register_namespace(_prefix, _uri)


class P1UpdateError(ValueError):
    """Raised when P1 cannot be updated from complete, trusted data."""


@dataclass(frozen=True)
class P1UpdateModel:
    replacements: dict[str, str]
    quarter_table_rows: list[list[str]]
    sequential_replacements: dict[str, list[str]]


FOLLOWUP_PROMPT_SUFFIX = "请基于数据进行概括总结分析，凸显数据特征"


def _format_decimal(value: Decimal) -> str:
    return format(value.normalize(), "f")


def _parse_decimal(value: Any, field_path: str) -> Decimal:
    if value is None:
        raise P1UpdateError(f"P1 更新失败：{field_path} 为空，无法更新第一页。")
    raw = str(value).strip()
    if not raw or raw == "/":
        raise P1UpdateError(f"P1 更新失败：{field_path} 不是可计算数字，无法更新第一页。")
    raw = raw.replace(",", "").removesuffix("%")
    try:
        return Decimal(raw)
    except InvalidOperation as exc:
        raise P1UpdateError(f"P1 更新失败：{field_path}={value!r} 不是数字。") from exc


def _required_text(row: dict[str, Any], key: str, field_path: str) -> str:
    value = row.get(key)
    text = "" if value is None else str(value).strip()
    if not text:
        raise P1UpdateError(f"P1 更新失败：{field_path} 为空，无法更新第一页。")
    return text


def _required_dict(response: dict[str, Any], endpoint: str, key: str) -> dict[str, Any]:
    value = response.get(key)
    if not isinstance(value, dict):
        raise P1UpdateError(f"P1 更新失败：{endpoint} 响应缺少 {key}。")
    return value


def _required_rows(response: dict[str, Any], endpoint: str, key: str) -> list[dict[str, Any]]:
    value = response.get(key)
    if not isinstance(value, list) or not value:
        raise P1UpdateError(f"P1 更新失败：{endpoint} 响应缺少 {key}。")
    rows = [row for row in value if isinstance(row, dict)]
    if len(rows) != len(value):
        raise P1UpdateError(f"P1 更新失败：{endpoint}.{key} 中存在非对象行。")
    return rows


def _format_growth(value: Any, field_path: str) -> str:
    number = _parse_decimal(value, field_path)
    sign = "+" if number > 0 else ""
    return f"{sign}{_format_decimal(number)}%"


def _format_growth_symbol(value: Any, field_path: str) -> str:
    raw = "" if value is None else str(value).strip()
    if not raw or raw == "/":
        return "/"
    return _format_growth(value, field_path)


def _format_optional_growth(value: Any, field_path: str) -> str:
    raw = "" if value is None else str(value).strip()
    if not raw or raw == "/":
        return "/"
    return _format_growth(value, field_path)


def _format_amount_yi(value: Any, field_path: str) -> str:
    return _format_decimal(_parse_decimal(value, field_path))


def _format_amount_wan_from_yi(value: Any, field_path: str) -> str:
    return _format_decimal((_parse_decimal(value, field_path) * Decimal("10000")).quantize(Decimal("0.01")))


def _top_rows_by_amount(rows: list[dict[str, Any]], count: int) -> list[dict[str, Any]]:
    if len(rows) < count:
        raise P1UpdateError(f"P1 更新失败：/tender/industry.累计数据 少于 {count} 行。")
    return sorted(
        rows,
        key=lambda row: _parse_decimal(row.get("金额（亿元）"), "/tender/industry.累计数据.金额（亿元）"),
        reverse=True,
    )[:count]


def _top_rows_by_growth(rows: list[dict[str, Any]], count: int) -> list[dict[str, Any]]:
    candidates: list[tuple[Decimal, dict[str, Any]]] = []
    for row in rows:
        raw_growth = row.get("金额同比增幅")
        if raw_growth is None or str(raw_growth).strip() == "/":
            continue
        candidates.append(
            (
                _parse_decimal(raw_growth, "/tender/industry.累计数据.金额同比增幅"),
                row,
            )
        )
    if len(candidates) < count:
        raise P1UpdateError(f"P1 更新失败：可排序的行业金额同比增幅少于 {count} 行。")
    return [row for _, row in sorted(candidates, key=lambda item: item[0], reverse=True)[:count]]


def _top_buyers(response: dict[str, Any], section: str, count: int) -> list[dict[str, Any]]:
    rows = _required_rows(response, "/tender/major-buyers", section)
    if len(rows) < count:
        raise P1UpdateError(f"P1 更新失败：/tender/major-buyers.{section} 少于 {count} 行。")
    return rows[:count]


def _side_class(response: dict[str, Any], key: str) -> dict[str, Any]:
    return _required_dict(response, "/tender/class", key)


def _industry_amount_label(row: dict[str, Any]) -> str:
    industry = _required_text(row, "行业", "/tender/industry.累计数据.行业")
    amount = _format_amount_yi(row.get("金额（亿元）"), f"/tender/industry.累计数据.{industry}.金额（亿元）")
    return f"{industry} {amount}亿元"


def _industry_growth_label(row: dict[str, Any]) -> str:
    industry = _required_text(row, "行业", "/tender/industry.累计数据.行业")
    growth = _format_growth(row.get("金额同比增幅"), f"/tender/industry.累计数据.{industry}.金额同比增幅")
    return f"{industry} {growth}"


def _planning_line(row: dict[str, Any], growth_mode: bool) -> str:
    industry = _required_text(row, "行业", "/tender/industry.累计数据.行业")
    amount = _format_amount_yi(row.get("金额（亿元）"), f"/tender/industry.累计数据.{industry}.金额（亿元）")
    count = int(_parse_decimal(row.get("个数"), f"/tender/industry.累计数据.{industry}.个数"))
    growth = _format_optional_growth(row.get("金额同比增幅"), f"/tender/industry.累计数据.{industry}.金额同比增幅")
    if growth_mode:
        return _wrap_followup_prompt(f"{industry}累计招标金额{amount}亿元、招标次数{count}次，金额同比{growth}")
    return _wrap_followup_prompt(f"{industry}累计招标金额{amount}亿元、招标次数{count}次")


def _industry_line(row: dict[str, Any]) -> str:
    industry = _required_text(row, "行业", "/tender/industry.累计数据.行业")
    amount = _format_amount_yi(row.get("金额（亿元）"), f"/tender/industry.累计数据.{industry}.金额（亿元）")
    count = int(_parse_decimal(row.get("个数"), f"/tender/industry.累计数据.{industry}.个数"))
    count_growth = _format_optional_growth(row.get("个数同比增幅"), f"/tender/industry.累计数据.{industry}.个数同比增幅")
    amount_growth = _format_optional_growth(row.get("金额同比增幅"), f"/tender/industry.累计数据.{industry}.金额同比增幅")
    return _wrap_followup_prompt(
        f"{industry}累计招标金额{amount}亿元、招标次数{count}次，个数同比{count_growth}、金额同比{amount_growth}"
    )


def _buyer_line(row: dict[str, Any], label: str) -> str:
    unit = _required_text(row, "招标单位", f"/tender/major-buyers.{label}.招标单位")
    count = int(_parse_decimal(row.get("招标次数"), f"/tender/major-buyers.{label}.{unit}.招标次数"))
    amount = _parse_decimal(row.get("招标预算金额（万元）"), f"/tender/major-buyers.{label}.{unit}.招标预算金额（万元）")
    return _wrap_followup_prompt(
        f"{unit}招标{count}次、预算{_format_decimal(amount)}万元，侧别{label.removesuffix('招标单位')}"
    )


def _wrap_followup_prompt(fact_text: str) -> str:
    return f"{{{{{fact_text}；{FOLLOWUP_PROMPT_SUFFIX}}}}}"


def _quarter_parts(label: str) -> tuple[str, str]:
    match = re.fullmatch(r"\s*(\d{4})年([1-4])季度\s*", label)
    if not match:
        raise P1UpdateError(f"P1 更新失败：/tender/time.季度数据.季度={label!r} 格式不正确。")
    return f"{match.group(1)}年", f"Q{match.group(2)}"


def _quarter_table_rows(response: dict[str, Any]) -> list[list[str]]:
    rows = _required_rows(response, "/tender/time", "季度数据")
    if len(rows) < 5:
        raise P1UpdateError("P1 更新失败：/tender/time.季度数据 少于 5 行。")
    selected = rows[-5:]
    years: list[str] = []
    quarters: list[str] = []
    government_amounts: list[str] = []
    enterprise_amounts: list[str] = []
    for row in selected:
        label = _required_text(row, "季度", "/tender/time.季度数据.季度")
        year_label, quarter_label = _quarter_parts(label)
        years.append(year_label)
        quarters.append(quarter_label)
        government_amounts.append(
            f"{_parse_decimal(row.get('政府侧招采金额（亿元）'), f'/tender/time.季度数据.{label}.政府侧招采金额（亿元）').quantize(Decimal('0.01'))}亿"
        )
        enterprise_amounts.append(
            f"{_parse_decimal(row.get('企业侧招采金额（亿元）'), f'/tender/time.季度数据.{label}.企业侧招采金额（亿元）').quantize(Decimal('0.01'))}亿"
        )

    year_row = ["时间类型"]
    previous_year = ""
    for year_label in years:
        year_row.append(year_label if year_label != previous_year else "")
        previous_year = year_label

    return [
        year_row,
        ["", *quarters],
        ["政府", *government_amounts],
        ["企业", *enterprise_amounts],
    ]


def _customer_139_summary_replacements(response: dict[str, Any]) -> dict[str, str]:
    monthly = response.get("月度数据")
    if not isinstance(monthly, dict):
        raise P1UpdateError("P1 更新失败：/tender/customer-139 缺少 月度数据。")
    count = int(_parse_decimal(monthly.get("招标次数"), "/tender/customer-139.月度数据.招标次数"))
    amount_yi = _parse_decimal(
        monthly.get("招标预算金额（亿元）"),
        "/tender/customer-139.月度数据.招标预算金额（亿元）",
    ).quantize(Decimal("0.01"))
    return {
        "{{规模万元}} 万": f"{amount_yi}亿元",
        "{{招标个数}}": str(count),
    }


def build_p1_update_model(
    overall_response: dict[str, Any],
    industry_response: dict[str, Any],
    major_buyers_response: dict[str, Any],
    class_response: dict[str, Any],
    time_response: dict[str, Any],
    customer_139_response: dict[str, Any],
    district: str,
) -> P1UpdateModel:
    cumulative = _required_dict(overall_response, "/tender/overall", "累计数据")
    industry_rows = _required_rows(industry_response, "/tender/industry", "累计数据")
    amount_top5 = _top_rows_by_amount(industry_rows, 5)
    growth_top5 = _top_rows_by_growth(industry_rows, 5)
    government_buyers = _top_buyers(major_buyers_response, "政府侧招标单位", 5)
    enterprise_buyers = _top_buyers(major_buyers_response, "企业侧招标单位", 5)
    government_side = _side_class(class_response, "政府侧累计数据")
    enterprise_side = _side_class(class_response, "企业侧累计数据")

    replacements = {
        "{{某某行政区}}": district,
        "{{累计金额}}": _format_amount_yi(cumulative.get("招标预算金额（亿元）"), "/tender/overall.累计数据.招标预算金额（亿元）"),
        "{{金额同比}}": _format_growth_symbol(cumulative.get("招标金额同比增幅"), "/tender/overall.累计数据.招标金额同比增幅"),
        "{{累计次数}}": str(int(_parse_decimal(cumulative.get("招标次数"), "/tender/overall.累计数据.招标次数"))),
        "{{次数同比}}": _format_growth_symbol(cumulative.get("招标次数同比增幅"), "/tender/overall.累计数据.招标次数同比增幅"),
        "{{规划1及分析结果，最后修订}}": _planning_line(amount_top5[0], growth_mode=False),
        "{{规划2及分析结果，最后修订}}": _planning_line(growth_top5[0], growth_mode=True),
        "{{行业1及分析结果}}": _industry_line(amount_top5[0]),
        "{{行业2及分析结果}}": _industry_line(amount_top5[1]),
        "{{客群1及分析结果}}": _buyer_line(government_buyers[0], "政府侧招标单位"),
        "{{客群2及分析结果}}": _buyer_line(enterprise_buyers[0], "企业侧招标单位"),
        "{{政府侧招标规模万元}}": _format_amount_wan_from_yi(
            government_side.get("招标预算金额（亿元）"),
            "/tender/class.政府侧累计数据.招标预算金额（亿元）",
        ),
        "{{政府侧招标金额占比}}": _required_text(
            government_side,
            "招标金额占比",
            "/tender/class.政府侧累计数据.招标金额占比",
        ),
        "{{政府侧招标个数}}": str(
            int(_parse_decimal(government_side.get("招标次数"), "/tender/class.政府侧累计数据.招标次数"))
        ),
        "{{政府侧招标次数占比}}": _required_text(
            government_side,
            "招标次数占比",
            "/tender/class.政府侧累计数据.招标次数占比",
        ),
        "{{企业侧招标规模万元}}": _format_amount_wan_from_yi(
            enterprise_side.get("招标预算金额（亿元）"),
            "/tender/class.企业侧累计数据.招标预算金额（亿元）",
        ),
        "{{企业侧招标金额占比}}": _required_text(
            enterprise_side,
            "招标金额占比",
            "/tender/class.企业侧累计数据.招标金额占比",
        ),
        "{{企业侧招标个数}}": str(
            int(_parse_decimal(enterprise_side.get("招标次数"), "/tender/class.企业侧累计数据.招标次数"))
        ),
        "{{企业侧招标次数占比}}": _required_text(
            enterprise_side,
            "招标次数占比",
            "/tender/class.企业侧累计数据.招标次数占比",
        ),
    }
    replacements.update(_customer_139_summary_replacements(customer_139_response))
    for index, row in enumerate(amount_top5, start=1):
        replacements[f"{{{{行业{index}金额}}}}"] = _industry_amount_label(row)
    for index, row in enumerate(growth_top5, start=1):
        replacements[f"{{{{行业{index}增幅}}}}"] = _industry_growth_label(row)
    for index, row in enumerate(government_buyers, start=1):
        replacements[f"{{{{政府侧客户 {index}}}}}"] = _required_text(
            row,
            "招标单位",
            f"/tender/major-buyers.政府侧招标单位[{index}].招标单位",
        )
    for index, row in enumerate(enterprise_buyers, start=1):
        replacements[f"{{{{企业侧客户 {index}}}}}"] = _required_text(
            row,
            "招标单位",
            f"/tender/major-buyers.企业侧招标单位[{index}].招标单位",
        )

    return P1UpdateModel(
        replacements=replacements,
        quarter_table_rows=_quarter_table_rows(time_response),
        sequential_replacements={
            "{{环比增幅%}}": [
                _format_growth_symbol(
                    government_side.get("招标预算金额环比增幅"),
                    "/tender/class.政府侧累计数据.招标预算金额环比增幅",
                ),
                _format_growth_symbol(
                    government_side.get("招标次数环比增幅"),
                    "/tender/class.政府侧累计数据.招标次数环比增幅",
                ),
                _format_growth_symbol(
                    enterprise_side.get("招标预算金额环比增幅"),
                    "/tender/class.企业侧累计数据.招标预算金额环比增幅",
                ),
                _format_growth_symbol(
                    enterprise_side.get("招标次数环比增幅"),
                    "/tender/class.企业侧累计数据.招标次数环比增幅",
                ),
            ],
        },
    )


def fetch_json(api_base: str, endpoint: str, params: dict[str, Any]) -> dict[str, Any]:
    url = f"{api_base.rstrip('/')}{endpoint}?{urlencode(params)}"
    request = Request(url)
    try:
        with urlopen(request, timeout=120) as response:
            payload = response.read().decode("utf-8")
            status = response.status
    except HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="replace")
        raise P1UpdateError(f"P1 更新失败：{endpoint} HTTP {exc.code}: {detail}") from exc
    except URLError as exc:
        raise P1UpdateError(f"P1 更新失败：无法连接 FastAPI {url}: {exc.reason}") from exc
    if status != 200:
        raise P1UpdateError(f"P1 更新失败：{endpoint} HTTP {status}。")
    try:
        data = json.loads(payload)
    except json.JSONDecodeError as exc:
        raise P1UpdateError(f"P1 更新失败：{endpoint} 返回值不是 JSON。") from exc
    if not isinstance(data, dict):
        raise P1UpdateError(f"P1 更新失败：{endpoint} 返回 JSON 根节点不是对象。")
    return data


def fetch_p1_responses(
    api_base: str,
    year: int,
    month: str,
    district: str,
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any], dict[str, Any]]:
    district = district.strip()
    if not district:
        raise P1UpdateError("P1 更新失败：行政区参数 --district 不能为空。")
    params = {"year": year, "month": month, "scope": "行政区", "scope_value": district}
    return (
        fetch_json(api_base, "/tender/overall", params),
        fetch_json(api_base, "/tender/industry", params),
        fetch_json(api_base, "/tender/major-buyers", params),
        fetch_json(api_base, "/tender/class", params),
        fetch_json(api_base, "/tender/time", params),
        fetch_json(api_base, "/tender/customer-139", params),
    )


def _replace_slide_text(
    root: ET.Element,
    replacements: dict[str, str],
    sequential_replacements: dict[str, list[str]],
) -> None:
    replaced_counts = {placeholder: 0 for placeholder in replacements}
    sequential_counts = {placeholder: 0 for placeholder in sequential_replacements}
    for paragraph in root.findall(".//a:p", OOXML_NS):
        text_nodes = paragraph.findall(".//a:t", OOXML_NS)
        if not text_nodes:
            continue
        original = "".join(node.text or "" for node in text_nodes)
        updated = original
        for placeholder, value in replacements.items():
            if placeholder in updated:
                replaced_counts[placeholder] += updated.count(placeholder)
                updated = updated.replace(placeholder, value)
        for placeholder, values in sequential_replacements.items():
            while placeholder in updated:
                index = sequential_counts[placeholder]
                if index >= len(values):
                    raise P1UpdateError(f"P1 更新失败：模板第 1 页占位符 {placeholder} 出现次数多于设计值。")
                updated = updated.replace(placeholder, values[index], 1)
                sequential_counts[placeholder] += 1
        if updated != original:
            text_nodes[0].text = updated
            for node in text_nodes[1:]:
                node.text = ""
    missing = sorted(placeholder for placeholder, count in replaced_counts.items() if count < 1)
    sequential_missing = sorted(
        placeholder
        for placeholder, values in sequential_replacements.items()
        if sequential_counts[placeholder] < len(values)
    )
    if missing or sequential_missing:
        raise P1UpdateError(f"P1 更新失败：模板第 1 页缺少占位符：{', '.join(missing + sequential_missing)}。")


def _set_text_nodes(parent: ET.Element, value: str) -> None:
    text_nodes = parent.findall(".//a:t", OOXML_NS)
    if not text_nodes:
        tx_body = parent.find("./a:txBody", OOXML_NS)
        if tx_body is None:
            tx_body = ET.SubElement(parent, f"{{{OOXML_NS['a']}}}txBody")
            ET.SubElement(tx_body, f"{{{OOXML_NS['a']}}}bodyPr")
            ET.SubElement(tx_body, f"{{{OOXML_NS['a']}}}lstStyle")
        paragraph = tx_body.find("./a:p", OOXML_NS)
        if paragraph is None:
            paragraph = ET.SubElement(tx_body, f"{{{OOXML_NS['a']}}}p")
        run = ET.SubElement(paragraph, f"{{{OOXML_NS['a']}}}r")
        text_node = ET.SubElement(run, f"{{{OOXML_NS['a']}}}t")
        text_nodes = [text_node]
    text_nodes[0].text = value
    for node in text_nodes[1:]:
        node.text = ""


def _update_quarter_table(root: ET.Element, rows: list[list[str]]) -> None:
    table = root.find(".//a:tbl", OOXML_NS)
    if table is None:
        raise P1UpdateError("P1 更新失败：第 1 页未找到季度表。")
    xml_rows = table.findall("./a:tr", OOXML_NS)
    if len(xml_rows) < len(rows):
        raise P1UpdateError("P1 更新失败：第 1 页季度表行数不足。")
    for row_index, row_values in enumerate(rows):
        cells = xml_rows[row_index].findall("./a:tc", OOXML_NS)
        if len(cells) < len(row_values):
            raise P1UpdateError("P1 更新失败：第 1 页季度表列数不足。")
        for cell, value in zip(cells, row_values, strict=True):
            _set_text_nodes(cell, value)


def update_p1_pptx(pptx_path: Path, output_path: Path, model: P1UpdateModel) -> None:
    if not pptx_path.exists():
        raise P1UpdateError(f"P1 更新失败：输入 PPTX 不存在：{pptx_path}")
    output_path.parent.mkdir(parents=True, exist_ok=True)

    required_parts = {"ppt/slides/slide1.xml"}
    with ZipFile(pptx_path, "r") as source:
        missing = sorted(required_parts - set(source.namelist()))
        if missing:
            raise P1UpdateError(f"P1 更新失败：PPTX 缺少必要部件：{', '.join(missing)}。")

        slide_root = ET.fromstring(source.read("ppt/slides/slide1.xml"))
        _replace_slide_text(slide_root, model.replacements, model.sequential_replacements)
        _update_quarter_table(slide_root, model.quarter_table_rows)
        updates = {
            "ppt/slides/slide1.xml": ET.tostring(slide_root, encoding="utf-8", xml_declaration=True),
        }

        if pptx_path.resolve() == output_path.resolve():
            with tempfile.NamedTemporaryFile(
                prefix="p1-update-",
                suffix=".pptx",
                dir=output_path.parent,
                delete=False,
            ) as tmp:
                actual_output = Path(tmp.name)
        else:
            actual_output = output_path
        try:
            with ZipFile(actual_output, "w") as target:
                for info in source.infolist():
                    target.writestr(info, updates.get(info.filename, source.read(info.filename)))
            if actual_output != output_path:
                actual_output.replace(output_path)
        except Exception:
            if actual_output != output_path and actual_output.exists():
                actual_output.unlink()
            raise


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Update page 1 of the administrative district service monthly PPTX.")
    parser.add_argument("--pptx", required=True, type=Path, help="Input PPTX template path.")
    parser.add_argument("--output", required=True, type=Path, help="Output PPTX path.")
    parser.add_argument("--year", required=True, type=int, help="Report year, for example 2026.")
    parser.add_argument("--month", required=True, help="Report month, for example 4月.")
    parser.add_argument("--api-base", required=True, help="FastAPI base URL.")
    parser.add_argument("--district", required=True, help="行政区名称，例如 浦东新区。")
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    responses = fetch_p1_responses(args.api_base, args.year, args.month, args.district)
    model = build_p1_update_model(*responses, args.district.strip())
    update_p1_pptx(args.pptx, args.output, model)


if __name__ == "__main__":
    main()
