#!/usr/bin/env python3
"""Build Word packages for tender source materials of the three detailed projects."""

from __future__ import annotations

import re
import ssl
import subprocess
import tempfile
import urllib.error
import urllib.request
from datetime import datetime
from html import unescape
from html.parser import HTMLParser
from pathlib import Path
from typing import Any

from docx import Document
from docx.enum.text import WD_BREAK
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter


ROOT = Path(__file__).resolve().parents[1]
BASE_DIR = ROOT / "docs" / "deliverables" / "20260615-full-candidate-tender-details"
OUT_DIR = BASE_DIR / "招标材料Word版"
RAW_DIR = OUT_DIR / "_html-originals"
CAPTURED_AT = datetime.now().strftime("%Y-%m-%d %H:%M")

FONT = "Microsoft YaHei"
BLUE = "1F4E79"
LIGHT_BLUE = "D9EAF7"
WHITE = "FFFFFF"
BORDER = Border(
    left=Side(style="thin", color="B7C9DA"),
    right=Side(style="thin", color="B7C9DA"),
    top=Side(style="thin", color="B7C9DA"),
    bottom=Side(style="thin", color="B7C9DA"),
)


PROJECTS: list[dict[str, Any]] = [
    {
        "slug": "01-南京地铁5号线工程新线食堂智能结算设备采购",
        "title": "南京地铁5号线工程新线食堂智能结算设备采购",
        "excel": BASE_DIR / "01-南京地铁5号线工程新线食堂智能结算设备采购.xlsx",
        "materials": [
            {
                "name": "招标公告",
                "url": "https://njggzy.nanjing.gov.cn/njweb/gchw/070001/20241009/643b0162-4a8c-40ad-9d3e-d2cadbfe538d.html",
            },
            {
                "name": "南京地铁官网招标公告",
                "url": "https://www.njmetro.com.cn/njdtweb/portal/main-article-detail.do?rowId=4028d48191538d4c0192704098e201d3&columnId=8a808007651c972001651d88ebe20001&columnName=search",
            },
            {
                "name": "中标候选人公示",
                "url": "https://njggzy.nanjing.gov.cn/njweb/gchw/070003/20241101/b5cbf419-d1d5-48cb-93ac-1161cd26280b.html",
            },
            {
                "name": "中标结果公示",
                "url": "https://njggzy.nanjing.gov.cn/njweb/gchw/070004/20241210/34070c31-fcac-441c-89c2-ad9706ecef1c.html",
            },
        ],
        "download_links": [
            "http://221.226.86.168:8081",
            "http://221.226.86.168:8081/Authorized/DownloadFile/D4BB2330-2C52-4F42-8718-78906ABDEEB2",
            "http://221.226.86.168:8081/Authorized/DownloadFile/4006FA8F-5407-40C7-A171-AF63247917E0",
        ],
        "official_file_note": "公告指定登录南京市公共资源交易中心货物网上交易平台下载电子招标文件；当前根地址和公告隐藏下载地址均返回502/SSL异常，未能取得招标文件原件。",
    },
    {
        "slug": "02-中信银行杭州分行智慧食堂软硬件服务商集采入围项目",
        "title": "中信银行杭州分行智慧食堂软硬件服务商集采入围项目",
        "excel": BASE_DIR / "02-中信银行杭州分行智慧食堂软硬件服务商集采入围项目.xlsx",
        "materials": [
            {
                "name": "公开招标公告",
                "url": "https://ebid.cfhc.citic/cms/default/webfile/ywgg1/20250523/1110611760571744256.html",
            },
            {
                "name": "中标候选人公示",
                "url": "https://ebid.cfhc.citic/cms/default/webfile/ywgg2/20250605/1115365101285670912.html",
            },
        ],
        "download_links": [
            "https://ebid.cfhc.citic/",
            "https://ebid.cfhc.citic/ebidding/openinghall/",
        ],
        "official_file_note": "公告写明招标文件售价500元，需提交资料、购买标书，并在中信金控采购共享平台电子招采系统上传购买凭证后下载；公开网页没有直接附件。",
    },
    {
        "slug": "03-中国农业银行广东省分行智慧食堂场景建设设备项目",
        "title": "中国农业银行广东省分行智慧食堂场景建设设备项目",
        "excel": BASE_DIR / "03-中国农业银行广东省分行智慧食堂场景建设设备项目.xlsx",
        "materials": [
            {
                "name": "招标公告",
                "url": "https://e.sinochemitc.com/cms/channel/ywgg1fw/154689.htm",
            },
            {
                "name": "评标结果中标结果公示",
                "url": "https://e.sinochemitc.com/cms/channel/ywgg2fw/155952.htm",
            },
        ],
        "download_links": [
            "https://jc.abchina.com.cn",
            "https://d.sinochemitc.com",
        ],
        "official_file_note": "公告写明招标文件需先在中化商务数字化服务平台注册并支付平台使用费，再到农银e采平台电子招采中心报名审核后线上下载；公开网页没有直接附件。",
    },
]


class TextHTMLParser(HTMLParser):
    """Small HTML to text/table parser for official notice pages."""

    block_tags = {"p", "div", "section", "article", "br", "tr", "table", "h1", "h2", "h3", "h4", "li"}

    def __init__(self) -> None:
        super().__init__()
        self.parts: list[str] = []
        self.skip_depth = 0
        self.links: list[tuple[str, str]] = []
        self.current_href: str | None = None
        self.current_link_text: list[str] = []

    def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
        if tag in {"script", "style", "noscript"}:
            self.skip_depth += 1
            return
        if self.skip_depth:
            return
        if tag in self.block_tags:
            self.parts.append("\n")
        if tag == "a":
            attrs_dict = dict(attrs)
            self.current_href = attrs_dict.get("href")
            self.current_link_text = []

    def handle_endtag(self, tag: str) -> None:
        if tag in {"script", "style", "noscript"}:
            self.skip_depth = max(0, self.skip_depth - 1)
            return
        if self.skip_depth:
            return
        if tag == "a" and self.current_href:
            text = normalize_text("".join(self.current_link_text))
            if text or self.current_href:
                self.links.append((text or self.current_href, self.current_href))
            self.current_href = None
            self.current_link_text = []
        if tag in self.block_tags:
            self.parts.append("\n")

    def handle_data(self, data: str) -> None:
        if self.skip_depth:
            return
        text = unescape(data)
        self.parts.append(text)
        if self.current_href:
            self.current_link_text.append(text)

    def text(self) -> str:
        raw = "".join(self.parts)
        lines = [normalize_text(line) for line in raw.splitlines()]
        clean_lines: list[str] = []
        previous_blank = False
        for line in lines:
            if not line:
                if not previous_blank:
                    clean_lines.append("")
                previous_blank = True
                continue
            clean_lines.append(line)
            previous_blank = False
        return "\n".join(clean_lines).strip()


def normalize_text(value: str) -> str:
    value = value.replace("\xa0", " ")
    value = re.sub(r"[ \t\r\f\v]+", " ", value)
    return value.strip()


def fetch_url(url: str, timeout: int = 20) -> tuple[bytes | None, str]:
    request = urllib.request.Request(
        url,
        headers={
            "User-Agent": "Mozilla/5.0 tender-material-archiver/1.0",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        },
    )
    contexts = [
        ("verified", ssl.create_default_context()),
        ("unverified", ssl._create_unverified_context()),  # noqa: SLF001
    ]
    last_error = ""
    for context_name, context in contexts:
        try:
            with urllib.request.urlopen(request, timeout=timeout, context=context) as response:
                suffix = "" if context_name == "verified" else " (certificate verification skipped)"
                return response.read(), f"HTTP {response.status}{suffix}"
        except urllib.error.HTTPError as exc:
            return None, f"HTTP {exc.code}: {exc.reason}"
        except Exception as exc:  # noqa: BLE001
            last_error = f"{type(exc).__name__}: {exc}"
            if context_name == "verified" and "CERTIFICATE_VERIFY_FAILED" in str(exc):
                continue
            break
    return None, last_error


def decode_html(data: bytes) -> str:
    for encoding in ("utf-8", "gb18030", "gbk"):
        try:
            return data.decode(encoding)
        except UnicodeDecodeError:
            continue
    return data.decode("utf-8", errors="ignore")


def add_hyperlink(paragraph, text: str, url: str) -> None:
    part = paragraph.part
    r_id = part.relate_to(
        url,
        "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
        is_external=True,
    )
    hyperlink = OxmlElement("w:hyperlink")
    hyperlink.set(qn("r:id"), r_id)
    run = OxmlElement("w:r")
    r_pr = OxmlElement("w:rPr")
    color = OxmlElement("w:color")
    color.set(qn("w:val"), "0563C1")
    r_pr.append(color)
    underline = OxmlElement("w:u")
    underline.set(qn("w:val"), "single")
    r_pr.append(underline)
    run.append(r_pr)
    text_elem = OxmlElement("w:t")
    text_elem.text = text
    run.append(text_elem)
    hyperlink.append(run)
    paragraph._p.append(hyperlink)


def set_document_style(doc: Document) -> None:
    styles = doc.styles
    normal = styles["Normal"]
    normal.font.name = FONT
    normal._element.rPr.rFonts.set(qn("w:eastAsia"), FONT)
    normal.font.size = Pt(10.5)
    for style_name in ["Title", "Heading 1", "Heading 2", "Heading 3"]:
        style = styles[style_name]
        style.font.name = FONT
        style._element.rPr.rFonts.set(qn("w:eastAsia"), FONT)
    for section in doc.sections:
        section.top_margin = Inches(0.7)
        section.bottom_margin = Inches(0.7)
        section.left_margin = Inches(0.75)
        section.right_margin = Inches(0.75)


def add_metadata(doc: Document, title: str, source_name: str, url: str, status: str) -> None:
    heading = doc.add_paragraph()
    run = heading.add_run(title)
    run.bold = True
    run.font.name = FONT
    run._element.rPr.rFonts.set(qn("w:eastAsia"), FONT)
    run.font.size = Pt(16)
    run.font.color.rgb = RGBColor(31, 78, 121)
    for label, value in [
        ("材料类型", source_name),
        ("抓取时间", CAPTURED_AT),
        ("下载状态", status),
    ]:
        p = doc.add_paragraph()
        p.add_run(f"{label}：").bold = True
        p.add_run(value)
    p = doc.add_paragraph()
    p.add_run("原始链接：").bold = True
    add_hyperlink(p, url, url)
    doc.add_paragraph("以下内容由官方网页原文转换为 Word，保留正文文字和公开链接；如需招标文件正式附件，需按公告要求登录/购买/审核后下载。")


def write_material_docx(project: dict[str, Any], material: dict[str, str], html: str, status: str, out_path: Path) -> None:
    parser = TextHTMLParser()
    parser.feed(html)
    text = parser.text()
    doc = Document()
    set_document_style(doc)
    add_metadata(doc, project["title"], material["name"], material["url"], status)
    doc.add_heading("正文", level=1)
    chunks = [line for line in text.splitlines() if line.strip()]
    for line in chunks[:800]:
        if len(line) > 220:
            for part in re.findall(r".{1,220}(?:\s|$)", line):
                if normalize_text(part):
                    doc.add_paragraph(normalize_text(part))
        else:
            doc.add_paragraph(line)
    if parser.links:
        doc.add_page_break()
        doc.add_heading("网页内链接", level=1)
        for label, href in parser.links[:120]:
            p = doc.add_paragraph(style=None)
            p.add_run(f"{label}：")
            add_hyperlink(p, href, href)
    doc.save(out_path)


def write_summary_docx(project: dict[str, Any], rows: list[list[str]], out_path: Path) -> None:
    doc = Document()
    set_document_style(doc)
    title = doc.add_paragraph()
    run = title.add_run(f"{project['title']} 招标材料链接与下载状态")
    run.bold = True
    run.font.name = FONT
    run._element.rPr.rFonts.set(qn("w:eastAsia"), FONT)
    run.font.size = Pt(16)
    run.font.color.rgb = RGBColor(31, 78, 121)
    doc.add_paragraph(f"生成时间：{CAPTURED_AT}")
    p = doc.add_paragraph()
    p.add_run("对应Excel：").bold = True
    p.add_run(str(project["excel"]))
    doc.add_paragraph(project["official_file_note"])
    doc.add_heading("官方网页材料", level=1)
    table = doc.add_table(rows=1, cols=4)
    table.style = "Table Grid"
    hdr = table.rows[0].cells
    for idx, text in enumerate(["材料", "Word文件", "抓取状态", "原始链接"]):
        hdr[idx].text = text
    for name, filename, status, url in rows:
        cells = table.add_row().cells
        cells[0].text = name
        cells[1].text = filename
        cells[2].text = status
        cells[3].text = url
    doc.add_heading("招标文件正式附件下载入口", level=1)
    for link in project["download_links"]:
        p = doc.add_paragraph()
        add_hyperlink(p, link, link)
    doc.add_heading("结论", level=1)
    doc.add_paragraph("本目录已保存公开可访问的官方网页 Word 版和原始HTML。正式招标文件附件如未出现在公开网页中，需要按公告指定流程登录、购买、上传凭证、CA/平台审核后下载。")
    doc.save(out_path)


def write_manifest_xlsx(rows: list[list[str]], out_path: Path) -> None:
    wb = Workbook()
    ws = wb.active
    ws.title = "招标材料总索引"
    ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=5)
    title = ws.cell(1, 1, "招标材料 Word 版总索引")
    title.font = Font(name=FONT, size=16, bold=True, color=WHITE)
    title.fill = PatternFill("solid", fgColor=BLUE)
    title.alignment = Alignment(horizontal="center", vertical="center")
    title.border = BORDER
    ws.row_dimensions[1].height = 30

    headers = ["项目", "材料", "Word路径", "状态", "链接"]
    for col, header in enumerate(headers, 1):
        cell = ws.cell(2, col, header)
        cell.font = Font(name=FONT, size=10, bold=True, color="14344A")
        cell.fill = PatternFill("solid", fgColor=LIGHT_BLUE)
        cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
        cell.border = BORDER

    for row_idx, row in enumerate(rows, 3):
        for col_idx, value in enumerate(row, 1):
            cell = ws.cell(row_idx, col_idx, value)
            cell.font = Font(name=FONT, size=10, color="1F1F1F")
            cell.alignment = Alignment(vertical="top", wrap_text=True)
            cell.border = BORDER
            if col_idx in (3, 5) and value:
                if col_idx == 3:
                    cell.hyperlink = f"file://{value}"
                else:
                    cell.hyperlink = value
                cell.style = "Hyperlink"
                cell.font = Font(name=FONT, size=10, color="0563C1", underline="single")

    widths = [46, 24, 92, 32, 92]
    for idx, width in enumerate(widths, 1):
        ws.column_dimensions[get_column_letter(idx)].width = width
    for row_idx in range(3, ws.max_row + 1):
        ws.row_dimensions[row_idx].height = 46
    ws.freeze_panes = "A3"
    ws.auto_filter.ref = f"A2:E{ws.max_row}"
    wb.save(out_path)


def render_docx(docx_path: Path) -> str:
    render_script = Path("/Users/jack/.codex/plugins/cache/openai-primary-runtime/documents/26.614.11602/skills/documents/render_docx.py")
    if not render_script.exists():
        return "render_docx.py missing"
    out_dir = Path(tempfile.gettempdir()) / "bid-analysis-docx-render-check" / docx_path.parent.name / docx_path.stem
    out_dir.mkdir(parents=True, exist_ok=True)
    try:
        subprocess.run(
            ["/Users/jack/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/bin/python3", str(render_script), str(docx_path), "--output_dir", str(out_dir)],
            check=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=60,
        )
        return f"rendered to {out_dir}"
    except Exception as exc:  # noqa: BLE001
        return f"render skipped/failed: {type(exc).__name__}: {exc}"


def main() -> None:
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    RAW_DIR.mkdir(parents=True, exist_ok=True)
    manifest_rows: list[list[str]] = []
    render_statuses: list[str] = []
    for project in PROJECTS:
        project_dir = OUT_DIR / project["slug"]
        project_dir.mkdir(parents=True, exist_ok=True)
        rows: list[list[str]] = []
        for idx, material in enumerate(project["materials"], 1):
            data, status = fetch_url(material["url"])
            if data is None:
                html = f"<html><body><h1>{material['name']}</h1><p>{status}</p><p>{material['url']}</p></body></html>"
            else:
                html = decode_html(data)
            raw_path = RAW_DIR / f"{project['slug']}-{idx:02d}-{material['name']}.html"
            raw_path.write_text(html, encoding="utf-8")
            safe_name = re.sub(r"[\\\\/:*?\"<>|]", "-", material["name"])
            docx_name = f"{idx:02d}-{safe_name}.docx"
            docx_path = project_dir / docx_name
            write_material_docx(project, material, html, status, docx_path)
            rows.append([material["name"], docx_name, status, material["url"]])
            manifest_rows.append([project["title"], material["name"], str(docx_path), status, material["url"]])
        summary_path = project_dir / "00-招标材料链接与下载状态.docx"
        write_summary_docx(project, rows, summary_path)
        manifest_rows.append([project["title"], "链接与下载状态", str(summary_path), "generated", ""])
        # Render one summary and first source doc for a compact QA pass.
        render_statuses.append(f"{summary_path}: {render_docx(summary_path)}")
        first_docx = project_dir / rows[0][1]
        render_statuses.append(f"{first_docx}: {render_docx(first_docx)}")
    manifest = OUT_DIR / "招标材料Word版-总索引.xlsx"
    write_manifest_xlsx(manifest_rows, manifest)
    render_log = Path(tempfile.gettempdir()) / "bid-analysis-docx-render-check" / "render-check.log"
    render_log.write_text("\n".join(render_statuses), encoding="utf-8")
    print(OUT_DIR)
    print(manifest)
    print(render_log)


if __name__ == "__main__":
    main()
