#!/usr/bin/env python3
"""
hwpx_fill.py — 한글 서식 템플릿에 AI가 만든 내용을 채워 넣는 도구

[이 도구가 존재하는 이유]
생성형 AI에게 .hwpx를 통째로 만들게 하면 '열리기는 하지만 결재 못 올릴 문서'가 나온다.
(제목 정렬·굵기 없음, 표 열너비 뭉개짐 — 검증보고서 §2)
그래서 역할을 나눈다.
    서식  = 한글로 만든 템플릿이 책임진다
    내용  = AI가 책임진다
이 도구가 그 둘을 합친다.

[반드시 알아야 할 함정]
HWPX는 문단별 줄 배치 결과를 <hp:linesegarray>에 캐시해 둔다.
텍스트만 바꾸고 이 캐시를 남기면, 한글이 옛 계산값대로 그려서
치환된 긴 문장이 한 줄에 전부 겹쳐 찍힌다. (검증보고서 §3)
→ 치환 후 반드시 캐시를 제거한다. 이 도구는 그 처리를 내장하고 있다.

[사용법]
    # 1) JSON으로 값 넘기기
    python hwpx_fill.py templates/공문_템플릿.hwpx 결과.hwpx --json 값.json

    # 2) 커맨드라인으로 직접
    python hwpx_fill.py templates/공문_템플릿.hwpx 결과.hwpx \
        --set 수신="수신자 참조" --set 제목="교육 실시 알림"

    # 3) 템플릿에 어떤 빈칸이 있는지 확인
    python hwpx_fill.py templates/공문_템플릿.hwpx --list
"""
from __future__ import annotations

import argparse
import json
import re
import shutil
import sys
import zipfile
from pathlib import Path

PLACEHOLDER = re.compile(r"\{\{([^{}]+)\}\}")
LINESEG = re.compile(rb"<hp:linesegarray>.*?</hp:linesegarray>", re.DOTALL)
SECTION = re.compile(r"Contents/section\d+\.xml$")

# XML 특수문자 — 치환값에 들어가면 문서가 깨진다
XML_ESCAPE = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}


def xml_escape(text: str) -> str:
    for ch, esc in XML_ESCAPE.items():
        text = text.replace(ch, esc)
    return text


def list_placeholders(hwpx: Path) -> list[str]:
    """템플릿에 들어있는 {{빈칸}} 이름을 순서대로 (중복 제거) 반환."""
    found: list[str] = []
    with zipfile.ZipFile(hwpx) as z:
        for name in z.namelist():
            if SECTION.search(name):
                xml = z.read(name).decode("utf-8")
                for key in PLACEHOLDER.findall(xml):
                    if key not in found:
                        found.append(key)
    return found


def fill(template: Path, output: Path, values: dict[str, str],
         strict: bool = True) -> dict:
    """템플릿의 {{키}}를 values로 치환하고, 레이아웃 캐시를 제거해 저장."""
    keys = list_placeholders(template)
    missing = [k for k in keys if k not in values]
    unused = [k for k in values if k not in keys]
    if strict and missing:
        raise KeyError(f"채우지 않은 빈칸이 있습니다: {missing}")

    replaced = 0
    cache_removed = 0

    with zipfile.ZipFile(template) as zin, \
            zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as zout:
        for info in zin.infolist():
            data = zin.read(info.filename)

            if SECTION.search(info.filename):
                xml = data.decode("utf-8")
                for key, val in values.items():
                    token = "{{" + key + "}}"
                    n = xml.count(token)
                    if n:
                        xml = xml.replace(token, xml_escape(str(val)))
                        replaced += n
                data = xml.encode("utf-8")
                # 핵심: 줄 배치 캐시 제거 → 한글이 열 때 레이아웃 재계산
                data, n = LINESEG.subn(b"", data)
                cache_removed += n

            # mimetype 파트는 반드시 무압축(ZIP_STORED)으로 유지해야 한다
            if info.filename == "mimetype":
                zi = zipfile.ZipInfo("mimetype")
                zi.compress_type = zipfile.ZIP_STORED
                zout.writestr(zi, data)
            else:
                zout.writestr(info, data)

    return {
        "replaced": replaced,
        "cache_removed": cache_removed,
        "missing": missing,
        "unused": unused,
        "output": str(output),
    }


def main() -> int:
    ap = argparse.ArgumentParser(description="한글 템플릿 빈칸 채우기")
    ap.add_argument("template", type=Path)
    ap.add_argument("output", type=Path, nargs="?")
    ap.add_argument("--json", type=Path, help="{키: 값} 형태의 JSON 파일")
    ap.add_argument("--set", action="append", default=[], metavar="키=값")
    ap.add_argument("--list", action="store_true", help="빈칸 목록만 출력")
    ap.add_argument("--loose", action="store_true", help="빈칸이 남아도 진행")
    args = ap.parse_args()

    if not args.template.exists():
        print(f"템플릿을 찾을 수 없습니다: {args.template}", file=sys.stderr)
        return 1

    if args.list:
        keys = list_placeholders(args.template)
        print(f"{args.template.name} 의 빈칸 {len(keys)}개:")
        for k in keys:
            print(f"  {{{{{k}}}}}")
        return 0

    if not args.output:
        print("출력 경로가 필요합니다.", file=sys.stderr)
        return 1

    values: dict[str, str] = {}
    if args.json:
        values.update(json.loads(args.json.read_text(encoding="utf-8")))
    for pair in args.set:
        if "=" not in pair:
            print(f"--set 형식이 잘못됐습니다: {pair}", file=sys.stderr)
            return 1
        k, v = pair.split("=", 1)
        values[k] = v

    try:
        r = fill(args.template, args.output, values, strict=not args.loose)
    except KeyError as e:
        print(f"[오류] {e}", file=sys.stderr)
        print("  → --list 로 빈칸 목록을 확인하거나 --loose 로 강행할 수 있습니다.",
              file=sys.stderr)
        return 1

    print(f"[완료] {r['output']}")
    print(f"  치환: {r['replaced']}곳")
    print(f"  레이아웃 캐시 제거: {r['cache_removed']}개  ← 글자 겹침 방지")
    if r["unused"]:
        print(f"  ⚠ 템플릿에 없는 키(무시됨): {r['unused']}")
    if r["missing"]:
        print(f"  ⚠ 채우지 않은 빈칸: {r['missing']}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
