#!/usr/bin/env python3
"""Export projects above a large-amount threshold for manual review."""

from __future__ import annotations

import argparse
import html
import re
import zipfile
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from typing import Any
from xml.etree import ElementTree as ET


NS_MAIN = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
NS_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"


def cell_text(cell: ET.Element) -> str:
    if cell.attrib.get("t") == "inlineStr":
        return "".join(node.text or "" for node in cell.findall(f".//{{{NS_MAIN}}}t"))
    value = cell.find(f"{{{NS_MAIN}}}v")
    return value.text if value is not None and value.text is not None else ""


def read_sheet_rows(xlsx_path: Path, sheet_name: str) -> list[dict[str, str]]:
    with zipfile.ZipFile(xlsx_path) as zf:
        workbook = ET.fromstring(zf.read("xl/workbook.xml"))
        rels_root = ET.fromstring(zf.read("xl/_rels/workbook.xml.rels"))
        rels = {item.attrib["Id"]: item.attrib["Target"] for item in rels_root}

        target = None
        sheets_node = workbook.find(f"{{{NS_MAIN}}}sheets")
        if sheets_node is None:
            raise ValueError("Workbook has no sheets node.")
        for sheet in sheets_node:
            if sheet.attrib.get("name") == sheet_name:
                rel_id = sheet.attrib[f"{{{NS_REL}}}id"]
                target = rels[rel_id]
                break
        if target is None:
            raise ValueError(f"Sheet not found: {sheet_name}")

        worksheet = ET.fromstring(zf.read(f"xl/{target}"))
        raw_rows: list[list[str]] = []
        for row in worksheet.findall(f".//{{{NS_MAIN}}}sheetData/{{{NS_MAIN}}}row"):
            raw_rows.append([cell_text(cell) for cell in row.findall(f"{{{NS_MAIN}}}c")])

    if not raw_rows:
        return []
    headers = raw_rows[0]
    rows = []
    for raw in raw_rows[1:]:
        if not any(value.strip() for value in raw):
            continue
        rows.append({header: raw[idx] if idx < len(raw) else "" for idx, header in enumerate(headers)})
    return rows


def parse_amount(value: Any) -> float | None:
    text = str(value or "").strip().replace(",", "")
    for token in ("元", "￥", "¥"):
        text = text.replace(token, "")
    if not text:
        return None
    try:
        return float(text)
    except ValueError:
        return None


def money_yuan(value: float | None) -> str:
    if value is None:
        return "未披露"
    if abs(value) >= 100000000:
        return f"{value / 100000000:.2f} 亿元"
    if abs(value) >= 10000:
        return f"{value / 10000:.2f} 万元"
    return f"{value:,.0f} 元"


def normalize_title(title: str) -> str:
    return re.sub(r"\d+", "#", title or "")


def build_review_rows(rows: list[dict[str, str]], threshold: float) -> list[dict[str, str]]:
    selected = []
    for row in rows:
        amount = parse_amount(row.get("金额数值") or row.get("中标金额（元）"))
        if amount is None or amount <= threshold:
            continue
        review = dict(row)
        review["审核金额"] = f"{amount:.2f}"
        review["金额展示"] = money_yuan(amount)
        review["审核要点"] = "超过 1 亿元；核对公告金额单位、合同范围与是否为总包/食材劳务合并口径。"
        selected.append(review)

    groups: dict[tuple[str, str, str, str], list[dict[str, str]]] = defaultdict(list)
    for row in selected:
        groups[
            (
                row.get("发布时间") or row.get("信息发布时间", ""),
                row.get("招标单位", ""),
                row.get("中标单位", ""),
                row.get("审核金额", ""),
            )
        ].append(row)

    for group in groups.values():
        if len(group) < 2:
            continue
        normalized_titles = {normalize_title(item.get("项目名称", "")) for item in group}
        if len(normalized_titles) <= 2:
            for item in group:
                item["审核要点"] += " 同日同采购方/供应商/金额出现多条相似记录，重点排查重复公告、标段拆分或金额录入问题。"

    return sorted(selected, key=lambda item: float(item["审核金额"]), reverse=True)


def esc(value: Any) -> str:
    return html.escape(str(value or ""), quote=True)


def render_html(rows: list[dict[str, str]], source: Path, threshold: float) -> str:
    total_amount = sum(float(row["审核金额"]) for row in rows)
    generated_at = datetime.now().strftime("%Y-%m-%d %H:%M")
    threshold_label = money_yuan(threshold)
    table_rows = []
    for idx, row in enumerate(rows, start=1):
        url = row.get("官网查看地址", "")
        table_rows.append(
            f"""
            <tr>
              <td>{idx}</td>
              <td><strong>{esc(row.get("项目名称"))}</strong><span>{esc(row.get("项目编号")) or "项目编号未披露"}</span></td>
              <td>{esc(row.get("发布时间") or row.get("信息发布时间"))}</td>
              <td>{esc(row.get("发布省份"))}/{esc(row.get("发布市级"))}</td>
              <td>{esc(row.get("金额展示"))}<span>{esc(row.get("审核金额"))} 元</span></td>
              <td>{esc(row.get("招标单位"))}</td>
              <td>{esc(row.get("中标单位"))}</td>
              <td>{esc(row.get("审核要点"))}</td>
              <td>{f'<a href="{esc(url)}">查看公告</a>' if url else "未披露"}</td>
            </tr>
            """
        )

    empty_state = ""
    if not rows:
        empty_state = '<p class="empty">未发现超过阈值的项目。</p>'

    return f"""<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>超一亿元项目审核清单</title>
  <style>
    :root {{
      --ink: #172033;
      --muted: #667085;
      --line: #d8dee8;
      --paper: #f6f8fb;
      --panel: #ffffff;
      --accent: #b42318;
      --accent-soft: #fff1f0;
    }}
    * {{ box-sizing: border-box; }}
    body {{
      margin: 0;
      background: var(--paper);
      color: var(--ink);
      font: 14px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
    }}
    main {{
      width: min(1180px, calc(100vw - 40px));
      margin: 0 auto;
      padding: 40px 0 56px;
    }}
    header {{
      display: grid;
      grid-template-columns: minmax(0, 1fr) auto;
      gap: 28px;
      align-items: end;
      border-bottom: 2px solid var(--ink);
      padding-bottom: 24px;
    }}
    h1 {{
      margin: 0 0 10px;
      font-size: clamp(28px, 4vw, 44px);
      line-height: 1.12;
      letter-spacing: 0;
    }}
    .meta, .source {{
      color: var(--muted);
      margin: 0;
    }}
    .badge {{
      display: inline-block;
      margin-bottom: 14px;
      padding: 4px 10px;
      border: 1px solid var(--accent);
      color: var(--accent);
      background: var(--accent-soft);
      font-weight: 700;
    }}
    .kpis {{
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 14px;
      margin: 22px 0;
    }}
    .kpi {{
      background: var(--panel);
      border: 1px solid var(--line);
      padding: 18px;
    }}
    .kpi span {{
      display: block;
      color: var(--muted);
      font-size: 13px;
      margin-bottom: 6px;
    }}
    .kpi strong {{
      display: block;
      font-size: 24px;
      line-height: 1.2;
    }}
    section {{
      background: var(--panel);
      border: 1px solid var(--line);
      overflow: auto;
    }}
    table {{
      width: 100%;
      min-width: 1120px;
      border-collapse: collapse;
    }}
    th, td {{
      border-bottom: 1px solid var(--line);
      padding: 12px 10px;
      vertical-align: top;
      text-align: left;
    }}
    th {{
      position: sticky;
      top: 0;
      z-index: 1;
      background: #eef2f7;
      color: #344054;
      font-size: 12px;
      letter-spacing: .04em;
      text-transform: uppercase;
      white-space: nowrap;
    }}
    td:nth-child(1), td:nth-child(3), td:nth-child(4) {{ white-space: nowrap; }}
    td:nth-child(5) {{
      color: var(--accent);
      font-weight: 800;
      white-space: nowrap;
    }}
    td span {{
      display: block;
      margin-top: 4px;
      color: var(--muted);
      font-size: 12px;
      font-weight: 400;
    }}
    a {{
      color: #175cd3;
      font-weight: 700;
      text-decoration: none;
      border-bottom: 1px solid currentColor;
    }}
    .notes {{
      display: grid;
      gap: 10px;
      margin-top: 18px;
      color: #344054;
    }}
    .notes p {{
      margin: 0;
      padding-left: 12px;
      border-left: 3px solid var(--accent);
    }}
    .empty {{
      padding: 22px;
      margin: 0;
      color: var(--muted);
    }}
    @media (max-width: 760px) {{
      main {{ width: min(100% - 24px, 1180px); padding-top: 24px; }}
      header, .kpis {{ grid-template-columns: 1fr; }}
    }}
    @media print {{
      body {{ background: #fff; }}
      main {{ width: 100%; padding: 0; }}
      section {{ overflow: visible; }}
      table {{ min-width: 0; font-size: 11px; }}
      a {{ color: inherit; border-bottom: 0; }}
    }}
  </style>
</head>
<body>
  <main>
    <header>
      <div>
        <span class="badge">人工复核清单</span>
        <h1>超一亿元项目审核清单</h1>
        <p class="meta">筛选口径：中标金额（元）/金额数值 &gt; {esc(threshold_label)}；来源表：原始清洗数据；生成时间：{esc(generated_at)}</p>
      </div>
      <p class="source">来源工作簿<br>{esc(source.name)}</p>
    </header>

    <div class="kpis" aria-label="汇总指标">
      <div class="kpi"><span>待审核项目数</span><strong>{len(rows)} 条</strong></div>
      <div class="kpi"><span>超阈值金额合计</span><strong>{esc(money_yuan(total_amount))}</strong></div>
      <div class="kpi"><span>单项金额阈值</span><strong>{esc(threshold_label)}</strong></div>
    </div>

    <section>
      {empty_state}
      <table>
        <thead>
          <tr>
            <th>#</th>
            <th>项目 / 编号</th>
            <th>发布时间</th>
            <th>地区</th>
            <th>中标金额</th>
            <th>招标单位</th>
            <th>中标单位</th>
            <th>审核要点</th>
            <th>证据链接</th>
          </tr>
        </thead>
        <tbody>
          {''.join(table_rows)}
        </tbody>
      </table>
    </section>

    <div class="notes">
      <p>两条记录同日、同采购方、同供应商、同金额，且项目编号仅尾号不同；审核时优先确认是否为重复公告、两个标段，或金额单位录入为元/万元时发生误读。</p>
      <p>本清单不删除原始数据，只作为人工复核入口；最终保留、合并或修正金额应以官方公告详情和合同口径为准。</p>
    </div>
  </main>
</body>
</html>
"""


def xml_escape(value: Any) -> str:
    return (
        str(value or "")
        .replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace('"', "&quot;")
    )


def col_letter(n: int) -> str:
    result = ""
    while n:
        n, rem = divmod(n - 1, 26)
        result = chr(65 + rem) + result
    return result


def cell_xml(ref: str, value: Any, style: int = 0) -> str:
    if value is None or value == "":
        return f'<c r="{ref}" s="{style}"/>'
    number = parse_amount(value)
    if number is not None and str(value).strip() == str(value).strip().replace(",", ""):
        return f'<c r="{ref}" s="{style}"><v>{number}</v></c>'
    return f'<c r="{ref}" t="inlineStr" s="{style}"><is><t>{xml_escape(value)}</t></is></c>'


def row_xml(row_num: int, values: list[Any], styles: list[int] | None = None) -> str:
    styles = styles or [0] * len(values)
    return "<row>" + "".join(
        cell_xml(f"{col_letter(idx)}{row_num}", value, styles[idx - 1] if idx - 1 < len(styles) else 0)
        for idx, value in enumerate(values, start=1)
    ) + "</row>"


def write_xlsx(rows: list[dict[str, str]], output: Path) -> None:
    headers = [
        "序号",
        "项目名称",
        "项目编号",
        "发布时间",
        "省份",
        "城市",
        "中标金额（元）",
        "金额展示",
        "招标单位",
        "中标单位",
        "审核要点",
        "官网查看地址",
    ]
    data = []
    for idx, row in enumerate(rows, start=1):
        data.append(
            [
                idx,
                row.get("项目名称", ""),
                row.get("项目编号", ""),
                row.get("发布时间") or row.get("信息发布时间", ""),
                row.get("发布省份", ""),
                row.get("发布市级", ""),
                row.get("审核金额", ""),
                row.get("金额展示", ""),
                row.get("招标单位", ""),
                row.get("中标单位", ""),
                row.get("审核要点", ""),
                row.get("官网查看地址", ""),
            ]
        )

    widths = [8, 56, 22, 14, 10, 12, 18, 14, 28, 30, 58, 64]
    cols = "".join(
        f'<col min="{idx}" max="{idx}" width="{width}" customWidth="1"/>'
        for idx, width in enumerate(widths, start=1)
    )
    sheet_rows = [row_xml(1, headers, [1] * len(headers))]
    sheet_rows.extend(row_xml(idx, row, [0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0]) for idx, row in enumerate(data, start=2))
    last_row = max(len(data) + 1, 1)
    worksheet = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="{NS_MAIN}" xmlns:r="{NS_REL}">
  <dimension ref="A1:L{last_row}"/>
  <sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>
  <sheetFormatPr defaultRowHeight="20"/>
  <cols>{cols}</cols>
  <sheetData>{''.join(sheet_rows)}</sheetData>
  <autoFilter ref="A1:L{last_row}"/>
  <pageMargins left="0.5" right="0.5" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
</worksheet>
"""
    styles = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="{NS_MAIN}">
  <fonts count="2"><font><sz val="11"/><name val="Arial"/></font><font><b/><sz val="11"/><color rgb="FFFFFFFF"/><name val="Arial"/></font></fonts>
  <fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FF172033"/><bgColor indexed="64"/></patternFill></fill></fills>
  <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>
  <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
  <cellXfs count="3">
    <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
    <xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFill="1" applyFont="1"/>
    <xf numFmtId="3" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
  </cellXfs>
  <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>
"""
    content_types = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
  <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
  <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
</Types>
"""
    workbook = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="{NS_MAIN}" xmlns:r="{NS_REL}"><sheets><sheet name="超一亿元审核清单" sheetId="1" r:id="rId1"/></sheets></workbook>
"""
    root_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
</Relationships>
"""
    workbook_rels = """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>
"""
    output.parent.mkdir(parents=True, exist_ok=True)
    with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("[Content_Types].xml", content_types)
        zf.writestr("_rels/.rels", root_rels)
        zf.writestr("xl/workbook.xml", workbook)
        zf.writestr("xl/_rels/workbook.xml.rels", workbook_rels)
        zf.writestr("xl/styles.xml", styles)
        zf.writestr("xl/worksheets/sheet1.xml", worksheet)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--source", default="docs/deliverables/2026-h1-yifangbao-bid-analysis-workbook.xlsx")
    parser.add_argument("--threshold", type=float, default=100000000)
    parser.add_argument("--html", default="docs/reports/2026-h1-yifangbao-large-amount-review.html")
    parser.add_argument("--xlsx", default="docs/deliverables/2026-h1-yifangbao-large-amount-review.xlsx")
    args = parser.parse_args()

    source = Path(args.source)
    rows = read_sheet_rows(source, "原始清洗数据")
    selected = build_review_rows(rows, args.threshold)
    Path(args.html).write_text(render_html(selected, source, args.threshold), encoding="utf-8")
    write_xlsx(selected, Path(args.xlsx))
    print(f"source_rows={len(rows)} selected_rows={len(selected)} threshold={args.threshold:.0f}")
    print(f"html={args.html}")
    print(f"xlsx={args.xlsx}")


if __name__ == "__main__":
    main()
