from __future__ import annotations

import json
import re
from dataclasses import dataclass
from pathlib import Path

from docx import Document
from docx.enum.section import WD_SECTION_START
from docx.enum.text import WD_LINE_SPACING
from docx.oxml.ns import qn
from docx.shared import Cm, Pt


ROOT = Path("/Users/hedy/Desktop/zhct")
WORK_DIR = ROOT / "zhctprompt/product_software_copyright/locker-pickup-source-20260520"
DOCX_PATH = WORK_DIR / "locker-pickup-source-30-pages.docx"
CLEAN_TEXT_PATH = WORK_DIR / "selected_source.clean.txt"
MANIFEST_PATH = WORK_DIR / "source-selection-manifest.json"
CHECK_PATH = WORK_DIR / "pre-submission-check.md"

LINES_PER_PAGE = 42
PAGE_COUNT = 30
TOTAL_LINES = LINES_PER_PAGE * PAGE_COUNT


@dataclass(frozen=True)
class SourceSlice:
    path: str
    reason: str
    start: int | None = None
    end: int | None = None


SOURCES: list[SourceSlice] = [
    SourceSlice(
        "zhctproject/store/application/api/controller/LockerPickup.php",
        "取餐柜设备前置核验与存取回调入口",
    ),
    SourceSlice(
        "zhctproject/store/application/api/service/LockerPickup.php",
        "订单核验、设备匹配、柜格回调、存餐取餐撤销状态流转和异常记录",
    ),
    SourceSlice(
        "zhctproject/store/application/common/service/MealOrderPickup.php",
        "取餐号生成、取餐记录创建、Redis 计数与异常兜底",
    ),
    SourceSlice(
        "zhctproject/store/application/api/service/Consume.php",
        "备餐完成呼叫、取餐号预占、设备消费订单绑定取餐记录",
        145,
        250,
    ),
    SourceSlice(
        "zhctproject/store/application/api/service/Consume.php",
        "订单保存后写入取餐记录",
        735,
        770,
    ),
    SourceSlice(
        "zhctproject/store/application/api/service/Consume.php",
        "队列订单保存后写入取餐记录",
        1070,
        1120,
    ),
    SourceSlice(
        "zhctproject/store/application/cron/command/MealOrderPickupComplete.php",
        "过期取餐记录自动完结",
    ),
    SourceSlice(
        "zhctproject/ai_api/app/api/controller/MealCheckout.php",
        "下单页取餐方式解析、取餐柜列表查询和取餐柜合法性校验",
        180,
        310,
    ),
    SourceSlice(
        "zhctproject/ai_api/app/api/controller/MealCheckout.php",
        "按档口规则生成取餐柜选项、取餐方式校验、取餐柜设备列表过滤",
        450,
        690,
    ),
    SourceSlice(
        "zhctproject/ai_api/app/api/service/meal/Checkout.php",
        "订单初始化、取餐柜字段写入、取餐柜表单校验和取餐柜设备核验",
    ),
    SourceSlice(
        "zhctproject/ai_api/app/api/service/meal/Order.php",
        "用户订单列表详情中的取餐状态、柜门状态和取餐号展示",
    ),
    SourceSlice(
        "zhctproject/ai_api/app/api/service/meal/DirectPay.php",
        "直接支付订单生成与取餐号记录写入",
    ),
    SourceSlice(
        "zhctproject/ai_api/app/api/listener/meal/PaySuccess.php",
        "支付成功后取餐记录补建、Redis 异常处理",
    ),
    SourceSlice(
        "zhctproject/ai_api/app/common/enum/order/LockerStatus.php",
        "取餐柜状态枚举",
    ),
    SourceSlice(
        "zhctproject/ai_api/app/common/enum/order/TakeMethod.php",
        "取餐方式枚举",
    ),
    SourceSlice(
        "zhctproject/store/application/common/enum/EquipmentType.php",
        "设备类型中的取餐柜设备标识",
        1,
        105,
    ),
]


EXCLUDED_HINTS = [
    "passport/Login.php",
    "controller/Login.php",
    "logic/Login.php",
    "route.php",
    "config.php",
    "helper",
    "vendor",
    "node_modules",
    "tools",
    "tests",
    "权限",
    "登录",
]


def strip_php_comments(source: str) -> str:
    result: list[str] = []
    i = 0
    n = len(source)
    state = "normal"
    while i < n:
        ch = source[i]
        nxt = source[i + 1] if i + 1 < n else ""
        if state == "normal":
            if ch == "/" and nxt == "/":
                i += 2
                while i < n and source[i] not in "\r\n":
                    i += 1
                continue
            if ch == "#":
                i += 1
                while i < n and source[i] not in "\r\n":
                    i += 1
                continue
            if ch == "/" and nxt == "*":
                i += 2
                while i < n - 1 and not (source[i] == "*" and source[i + 1] == "/"):
                    if source[i] in "\r\n":
                        result.append(source[i])
                    i += 1
                i += 2
                continue
            if ch == "'":
                state = "single"
                result.append(ch)
                i += 1
                continue
            if ch == '"':
                state = "double"
                result.append(ch)
                i += 1
                continue
            result.append(ch)
            i += 1
            continue
        if state == "single":
            result.append(ch)
            if ch == "\\" and i + 1 < n:
                result.append(source[i + 1])
                i += 2
                continue
            if ch == "'":
                state = "normal"
            i += 1
            continue
        if state == "double":
            result.append(ch)
            if ch == "\\" and i + 1 < n:
                result.append(source[i + 1])
                i += 2
                continue
            if ch == '"':
                state = "normal"
            i += 1
            continue
    return "".join(result)


def read_slice(item: SourceSlice) -> list[str]:
    full_path = ROOT / item.path
    lines = full_path.read_text(encoding="utf-8", errors="ignore").splitlines()
    start = item.start or 1
    end = item.end or len(lines)
    selected = "\n".join(lines[start - 1 : end])
    stripped = strip_php_comments(selected)
    return [line.rstrip() for line in stripped.splitlines() if line.strip()]


def normalize_lines(raw_lines: list[str]) -> list[str]:
    cleaned: list[str] = []
    for line in raw_lines:
        line = line.replace("\t", "    ").rstrip()
        if not line.strip():
            continue
        cleaned.append(line)
    return cleaned


def collect_source() -> tuple[list[str], list[dict[str, object]]]:
    source_lines: list[str] = []
    manifest: list[dict[str, object]] = []
    for item in SOURCES:
        before = len(source_lines)
        lines = normalize_lines(read_slice(item))
        source_lines.extend(lines)
        manifest.append(
            {
                "path": str((ROOT / item.path).resolve()),
                "range": [item.start or 1, item.end or "EOF"],
                "reason": item.reason,
                "clean_lines": len(lines),
                "selected_output_lines": [before + 1, len(source_lines)],
            }
        )
    if len(source_lines) < TOTAL_LINES:
        raise RuntimeError(f"selected source has only {len(source_lines)} clean lines, need {TOTAL_LINES}")
    return source_lines[:TOTAL_LINES], manifest


def set_run_font(run) -> None:
    run.font.name = "Courier New"
    run.font.size = Pt(7.5)
    run._element.rPr.rFonts.set(qn("w:ascii"), "Courier New")
    run._element.rPr.rFonts.set(qn("w:hAnsi"), "Courier New")
    run._element.rPr.rFonts.set(qn("w:eastAsia"), "SimSun")


def build_docx(lines: list[str]) -> None:
    document = Document()
    section = document.sections[0]
    section.start_type = WD_SECTION_START.NEW_PAGE
    section.page_width = Cm(21)
    section.page_height = Cm(29.7)
    section.top_margin = Cm(1.2)
    section.bottom_margin = Cm(1.2)
    section.left_margin = Cm(1.0)
    section.right_margin = Cm(1.0)
    section.header_distance = Cm(0.4)
    section.footer_distance = Cm(0.4)
    styles = document.styles
    normal = styles["Normal"]
    normal.font.name = "Courier New"
    normal.font.size = Pt(7.5)
    normal._element.rPr.rFonts.set(qn("w:eastAsia"), "SimSun")
    for page_index in range(PAGE_COUNT):
        start = page_index * LINES_PER_PAGE
        end = start + LINES_PER_PAGE
        page_lines = lines[start:end]
        paragraph = document.add_paragraph()
        if page_index > 0:
            paragraph.paragraph_format.page_break_before = True
        paragraph.paragraph_format.space_before = Pt(0)
        paragraph.paragraph_format.space_after = Pt(0)
        paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
        paragraph.paragraph_format.line_spacing = Pt(11)
        run = paragraph.add_run("\n".join(page_lines))
        set_run_font(run)
    document.save(DOCX_PATH)


def write_manifest(lines: list[str], manifest: list[dict[str, object]]) -> None:
    payload = {
        "project": "取餐柜系统",
        "generated_at": "2026-05-20",
        "workspace": str(ROOT),
        "output_docx": str(DOCX_PATH),
        "clean_text": str(CLEAN_TEXT_PATH),
        "pages": PAGE_COUNT,
        "lines_per_page": LINES_PER_PAGE,
        "total_clean_lines": len(lines),
        "source_selection": manifest,
        "excluded_scope": EXCLUDED_HINTS,
    }
    MANIFEST_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


def has_comment_marker(line: str) -> bool:
    stripped = line.strip()
    if stripped.startswith("//") or stripped.startswith("#") or stripped.startswith("*"):
        return True
    if "/*" in stripped or "*/" in stripped:
        return True
    if re.search(r"(^|[^:])//", stripped):
        return True
    return False


def write_check(lines: list[str]) -> None:
    blank_count = sum(1 for line in lines if not line.strip())
    comment_marker_count = sum(1 for line in lines if has_comment_marker(line))
    paths = [str((ROOT / item.path).resolve()) for item in SOURCES]
    content = [
        "# 取餐柜系统软著源码材料提交前自查",
        "",
        f"- 本地工作目录：{WORK_DIR}",
        f"- Word 材料：{DOCX_PATH}",
        f"- 清洗后源码文本：{CLEAN_TEXT_PATH}",
        f"- 选码清单：{MANIFEST_PATH}",
        f"- 页数目标：{PAGE_COUNT} 页",
        f"- 每页源码行数：{LINES_PER_PAGE} 行",
        f"- 清洗后入册源码行数：{len(lines)} 行",
        f"- 空行检查：{blank_count} 行",
        f"- 注释标记检查：{comment_marker_count} 行",
        "- 核心覆盖：取餐流程、取餐柜选择、订单核验、柜端存取回调、柜格编号、设备类型、取餐号生成、状态展示、异常兜底。",
        "- 已避开：登录、权限、通用工具、框架配置、路由、测试、vendor、node_modules、工具脚本。",
        "",
        "## 本地相关项目路径",
        "",
    ]
    content.extend(f"- {path}" for path in paths)
    content.append("")
    content.append("## 自查结论")
    content.append("")
    if blank_count == 0 and comment_marker_count == 0 and len(lines) == TOTAL_LINES:
        content.append("- 通过：源码已去空行、去注释，并按 30 页模板生成。")
    else:
        content.append("- 需复查：清洗结果存在异常，请查看上方统计。")
    CHECK_PATH.write_text("\n".join(content) + "\n", encoding="utf-8")


def main() -> None:
    WORK_DIR.mkdir(parents=True, exist_ok=True)
    lines, manifest = collect_source()
    CLEAN_TEXT_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")
    build_docx(lines)
    write_manifest(lines, manifest)
    write_check(lines)
    print(json.dumps({
        "docx": str(DOCX_PATH),
        "clean_text": str(CLEAN_TEXT_PATH),
        "manifest": str(MANIFEST_PATH),
        "check": str(CHECK_PATH),
        "pages": PAGE_COUNT,
        "lines": len(lines),
    }, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
