#!/usr/bin/env python3
"""Render the audited chain data as a one-row-per-chain review table."""

from __future__ import annotations

import html
import json
from pathlib import Path


HERE = Path(__file__).resolve().parent
DATA_PATH = HERE / "chain-audit-2026-08-21.json"
PROJECT = HERE.parents[1]
OUTPUT = PROJECT / "reports" / "2026-08-21-chain-catalog-domestic-audit.html"
PREVIEW = PROJECT / ".runtime" / "chain-catalog-review" / "index.html"


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


def status_class(status: str) -> str:
    if status.startswith("복수"):
        return "multi"
    if "1곳 +" in status:
        return "one-internal"
    if status.startswith("국내 부자재 판매처 1곳"):
        return "one"
    if status.startswith("사내"):
        return "internal"
    if status.startswith("완제품"):
        return "finished"
    return "missing"


def listing_markup(listing: dict) -> str:
    verification = listing.get("verification", {})
    reachable = verification.get("reachable", False)
    badge = "확인됨" if reachable else "접속 실패"
    css = "link-ok" if reachable else "link-fail"
    kind = "부자재" if listing["listing_type"] == "raw" else "완제품"
    return f'''<li>
      <a href="{esc(listing['url'])}" target="_blank" rel="noopener noreferrer">
        <strong>{esc(listing['seller'])}</strong><span class="external" aria-hidden="true">↗</span>
      </a>
      <span class="kind {esc(listing['listing_type'])}">{kind}</span>
      <span class="link-state {css}">{badge}</span>
      <small>{esc(listing['title'])}</small>
    </li>'''


def render_row(row: dict, display_index: int) -> str:
    listings = "".join(listing_markup(item) for item in row["representative_listings"])
    if not listings:
        listings = '<li class="none">현재 확인된 공개 상품 페이지 없음</li>'
    suppliers = "".join(f"<span class='supplier'>{esc(name)}</span>" for name in row["internal_supplier_names"])
    if not suppliers:
        suppliers = '<span class="none-inline">없음</span>'
    internal = []
    if row["internal_supplier_record_count"]:
        internal.append(f"<span class='evidence'>POM 공급처 기록 {row['internal_supplier_record_count']}건</span>")
    if row["chainbook_evidence"]:
        internal.append("<span class='evidence'>TN 체인북</span>")
    if not internal:
        internal.append("<span class='none-inline'>없음</span>")
    aliases = " / ".join(row["aliases"]) if row["aliases"] else "—"
    terms = " · ".join(row["searched_terms"])
    css = status_class(row["status"])
    seller_names = " ".join(row["public_raw_sellers"] + row["public_finished_sellers"])
    search_blob = " ".join([row["name"], aliases, terms, seller_names, " ".join(row["internal_supplier_names"]), row["status"]])
    return f'''<tr data-status="{css}" data-search="{esc(search_blob.lower())}">
      <td class="number">{display_index:02d}</td>
      <td class="identity">
        <strong>{esc(row['name'])}</strong>
        <span class="aliases">{esc(aliases)}</span>
        <details><summary>검색어 {len(row['searched_terms'])}개</summary><small>{esc(terms)}</small></details>
      </td>
      <td class="decision">
        <span class="status {css}">{esc(row['status'])}</span>
        <strong>{esc(row['recommendation'])}</strong>
      </td>
      <td><ul class="listings">{listings}</ul></td>
      <td><div class="suppliers">{suppliers}</div></td>
      <td><div class="evidences">{''.join(internal)}</div></td>
      <td class="rights">판매처 자산 비복제<br><small>독자 촬영·별도 생성 이미지만 사용</small></td>
    </tr>'''


def main() -> None:
    data = json.loads(DATA_PATH.read_text(encoding="utf-8"))
    rows = data["rows"]
    status_order = {
        "복수 국내 부자재 판매처 확인": 0,
        "국내 부자재 1곳 + 사내 공급처 확인": 1,
        "국내 부자재 판매처 1곳 확인": 2,
        "사내 공급처만 확인": 3,
        "완제품 판매명만 확인": 4,
        "현재 조달 근거 미확인": 5,
    }
    ordered_rows = sorted(rows, key=lambda row: (status_order[row["status"]], row["index"]))
    raw_count = sum(bool(row["public_raw_sellers"]) for row in rows)
    multi_count = sum(len(row["public_raw_sellers"]) >= 2 for row in rows)
    one_internal_count = sum(row["status"] == "국내 부자재 1곳 + 사내 공급처 확인" for row in rows)
    one_only_count = sum(row["status"] == "국내 부자재 판매처 1곳 확인" for row in rows)
    internal_only = sum(not row["public_raw_sellers"] and bool(row["internal_supplier_names"]) for row in rows)
    finished_only = sum(not row["public_raw_sellers"] and not row["internal_supplier_names"] and bool(row["public_finished_sellers"]) for row in rows)
    missing = sum(not row["public_raw_sellers"] and not row["internal_supplier_names"] and not row["public_finished_sellers"] for row in rows)
    table_rows = "\n".join(render_row(row, index) for index, row in enumerate(ordered_rows, start=1))
    source_cards = "".join(
        f"<li><strong>{esc(name)}</strong><span>{count:,}개 상품 대조</span></li>"
        for name, count in data["catalog_scope"].items()
    )
    document = f'''<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="color-scheme" content="light">
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23126b77'/%3E%3Cpath d='M8 11h16M8 16h16M8 21h16' stroke='white' stroke-width='2.5'/%3E%3C/svg%3E">
  <title>TN 체인 국내 조달 전수조사</title>
  <style>
    :root{{--ink:#233033;--ink-soft:#51626a;--muted:#566b71;--bg:#eef4f5;--surface:#f8fbfb;--line:#d9e4e6;--accent:#126b77;--accent-soft:#d8ecef;--good:#1d6b59;--warn:#8b5d20;--bad:#934257;--info:#496c91;--page-bg:#e1eaeb;--font:"Pretendard","Apple SD Gothic Neo","Malgun Gothic",system-ui,-apple-system,sans-serif;--shadow:0 12px 40px rgba(35,48,51,.08)}}
    *{{box-sizing:border-box}} html{{scroll-behavior:smooth}} body{{margin:0;min-width:320px;background:var(--bg);color:var(--ink);font-family:var(--font);line-height:1.55}} a{{color:inherit}} button,input,select{{font:inherit}} button:focus-visible,input:focus-visible,select:focus-visible,a:focus-visible,summary:focus-visible{{outline:3px solid rgba(31,138,153,.3);outline-offset:2px}}
    .page{{width:min(1680px,calc(100% - 40px));margin:0 auto;padding:36px 0 72px}} .masthead{{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:end;padding-bottom:24px;border-bottom:1px solid var(--line)}} .eyebrow{{margin:0 0 6px;color:var(--accent);font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase}} h1{{margin:0;font-size:clamp(30px,4vw,52px);line-height:1.06;letter-spacing:-.045em}} .lead{{max-width:920px;margin:14px 0 0;color:var(--ink-soft);font-size:15px}} .meta{{color:var(--ink-soft);font-size:12px;text-align:right}} .meta strong{{display:block;color:var(--ink);font-size:14px}}
    .stats{{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px;margin:18px 0}} .stat{{padding:13px 14px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}} .stat strong{{display:block;font-size:27px;line-height:1.1}} .stat span{{display:block;margin-top:4px;color:var(--ink-soft);font-size:12px}} .stat.good strong{{color:var(--good)}} .stat.warn strong{{color:var(--warn)}} .stat.bad strong{{color:var(--bad)}}
    .finding{{display:grid;grid-template-columns:1.25fr .75fr;gap:12px;margin-bottom:14px}} .card{{padding:16px 18px;border:1px solid var(--line);border-radius:12px;background:var(--surface)}} .card h2{{margin:0 0 7px;font-size:17px}} .card p{{margin:0;color:var(--ink-soft);font-size:13px}} .card strong{{color:var(--ink)}} .source-list{{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:6px;margin:11px 0 0;padding:0;list-style:none}} .source-list li{{padding:8px;border-radius:7px;background:var(--bg)}} .source-list strong,.source-list span{{display:block;font-size:11px}} .source-list span{{color:var(--muted)}}
    .controls{{position:sticky;z-index:10;top:8px;display:grid;grid-template-columns:minmax(260px,1.4fr) minmax(230px,.7fr) auto;gap:8px;margin:14px 0 8px;padding:9px;border:1px solid var(--line);border-radius:12px;background:rgba(248,251,251,.95);box-shadow:var(--shadow);backdrop-filter:blur(12px)}} input,select,button{{min-height:43px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);padding:9px 11px}} button{{cursor:pointer}} .visible-count{{display:flex;align-items:center;justify-content:flex-end;min-width:120px;color:var(--ink-soft);font-size:13px}} .visible-count strong{{margin-right:3px;color:var(--ink);font-size:18px}} .sort-note{{margin:0 0 10px;color:var(--ink-soft);font-size:12px}} .sort-note strong{{color:var(--ink)}}
    .table-wrap{{overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--surface);box-shadow:var(--shadow)}} table{{width:100%;min-width:1430px;border-collapse:separate;border-spacing:0;table-layout:fixed;font-size:12.5px}} col.no{{width:48px}} col.identity{{width:210px}} col.decision{{width:210px}} col.public{{width:390px}} col.internal{{width:170px}} col.evidence{{width:170px}} col.rights{{width:180px}} thead th{{position:sticky;z-index:2;top:0;padding:11px 10px;border-bottom:1px solid var(--line);background:#edf5f6;color:var(--ink-soft);text-align:left;font-size:11px}} tbody td{{vertical-align:top;padding:12px 10px;border-bottom:1px solid var(--line)}} tbody tr:last-child td{{border-bottom:0}} tbody tr:hover{{background:#f2f8f8}} tr[hidden]{{display:none}} .number{{color:var(--muted);font-variant-numeric:tabular-nums}} .identity>strong{{display:block;font-size:14px}} .aliases{{display:block;margin-top:3px;color:var(--ink-soft)}} details{{margin-top:7px;color:var(--muted)}} summary{{cursor:pointer;font-size:11px}} details small{{display:block;margin-top:4px;line-height:1.4}} .decision>strong{{display:block;margin-top:6px;font-size:12px}}
    .status,.kind,.link-state,.supplier,.evidence{{display:inline-flex;align-items:center;border-radius:999px;padding:3px 7px;font-size:10.5px;line-height:1.25}} .status.multi{{background:#dcefe8;color:#246b58}} .status.one-internal{{background:#e3eef5;color:#3e6680}} .status.one{{background:#fff0d8;color:#875b1e}} .status.internal{{background:#ece7f5;color:#685181}} .status.finished{{background:#eceff1;color:#59676d}} .status.missing{{background:#f7e3e7;color:#93485b}} .listings{{display:grid;gap:10px;margin:0;padding:0;list-style:none}} .listings li{{position:relative;padding-left:10px;border-left:2px solid var(--line)}} .listings a{{text-decoration:none}} .listings a:hover strong{{text-decoration:underline}} .listings small{{display:block;margin-top:3px;color:var(--ink-soft);line-height:1.35}} .external{{margin-left:3px;color:var(--accent)}} .kind{{margin-left:6px;background:var(--accent-soft);color:#346c74}} .kind.finished{{background:#eceff1;color:#59676d}} .link-state{{margin-left:3px;padding:2px 5px}} .link-ok{{color:var(--good)}} .link-fail{{color:var(--bad)}} .none,.none-inline{{color:var(--muted)}} .suppliers,.evidences{{display:flex;flex-wrap:wrap;gap:5px}} .supplier{{background:#e7edf4;color:#425d79}} .evidence{{background:#edf0ec;color:#586259}} .rights{{color:var(--ink-soft)}} .rights small{{color:var(--muted)}}
    .notes{{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:14px}} .notes h2{{margin:0 0 6px;font-size:15px}} .notes p,.notes li{{font-size:12px;color:var(--ink-soft)}} .notes ul{{margin:0;padding-left:18px}} .raw-link{{display:inline-flex;margin-top:10px;color:var(--accent);font-weight:700;text-decoration:none}}
    @media(max-width:900px){{.page{{width:min(100% - 20px,1680px);padding-top:22px}}.masthead,.finding{{grid-template-columns:1fr}}.meta{{text-align:left}}.stats{{grid-template-columns:repeat(2,minmax(0,1fr))}}.source-list{{grid-template-columns:repeat(2,minmax(0,1fr))}}.controls{{position:static;grid-template-columns:1fr}}.visible-count{{justify-content:flex-start}}.notes{{grid-template-columns:1fr}}.table-wrap{{margin:0 -10px;border-radius:0;border-left:0;border-right:0}}}}
    @media print{{@page{{size:A4 portrait;margin:10mm}}body{{background:#fff;font-size:9pt}}.page{{width:100%;padding:0}}.controls{{display:none}}.stats{{grid-template-columns:repeat(3,1fr)}}.finding{{grid-template-columns:1fr}}.table-wrap{{overflow:visible;border:0;box-shadow:none}}table{{min-width:0;font-size:7pt;table-layout:auto}}thead th{{position:static}}tbody tr{{break-inside:avoid}}.notes,.card,.stat{{break-inside:avoid}}}}
  </style>
</head>
<body>
<main class="page">
  <header class="masthead">
    <div><p class="eyebrow">TN Silver · 국내 조달 조사</p><h1>체인 후보 77개 전수조사</h1><p class="lead"><strong>결론:</strong> 국내 부자재 상품 페이지가 확인된 체인은 {raw_count}개이며, 그중 {multi_count}개는 복수 판매처에서 확인됐다. 표는 조달 근거가 강한 분류부터 약한 분류 순으로 77개를 모두 연속 배치했다.</p></div>
    <p class="meta"><strong>조사일 2026-08-21</strong>직접 링크 {data['link_verification']['unique_urls']}개 · 접속 성공 {data['link_verification']['reachable']}개</p>
  </header>
  <section class="stats" aria-label="조사 요약">
    <div class="stat"><strong>77</strong><span>전체 체인 후보</span></div><div class="stat good"><strong>{multi_count}</strong><span>① 복수 판매처</span></div><div class="stat good"><strong>{one_internal_count}</strong><span>② 판매처 1곳 + 사내</span></div><div class="stat warn"><strong>{one_only_count}</strong><span>③ 판매처 1곳</span></div><div class="stat warn"><strong>{internal_only}</strong><span>④ 사내 공급처만</span></div><div class="stat bad"><strong>{finished_only}</strong><span>⑤ 완제품 판매만</span></div><div class="stat bad"><strong>{missing}</strong><span>⑥ 조달 근거 미확인</span></div>
  </section>
  <section class="finding">
    <article class="card"><h2>판정 기준</h2><p><strong>판매처는 실제 국내 업체의 체인 부자재 상품 페이지가 있을 때만 셌다.</strong> POM은 데이터 저장소이고 체인북은 사내 참고자료이므로 판매처가 아니다. 완제품에서만 이름이 확인된 경우도 부자재 판매처로 세지 않았다.</p><ul class="source-list">{source_cards}</ul></article>
    <article class="card"><h2>게시 자산 원칙</h2><p><strong>판매처 수와 관계없이 해당 사이트의 사진·설명·상품코드·카탈로그 배열은 복제하지 않는다.</strong> 체인 이름과 구조 확인에만 참고하고, 공개용 이미지는 TN의 독자 촬영 또는 별도 생성본을 사용한다. 단일 판매처 명칭은 일반 체인명인지 한 번 더 검토한다.</p></article>
  </section>
  <div class="controls" aria-label="표 필터">
    <input id="query" type="search" placeholder="체인명·별칭·판매처·사내 공급처 검색" aria-label="체인 검색">
    <select id="status" aria-label="조달 상태"><option value="all">모든 상태</option><option value="multi">복수 국내 판매처</option><option value="one-internal">판매처 1곳 + 사내 공급처</option><option value="one">국내 판매처 1곳</option><option value="internal">사내 공급처만</option><option value="finished">완제품 판매만</option><option value="missing">조달 근거 미확인</option></select>
    <div class="visible-count"><strong id="visible">77</strong> / 77개 표시</div>
  </div>
  <p class="sort-note"><strong>표시 순서:</strong> ① 복수 판매처 → ② 판매처 1곳 + 사내 공급처 → ③ 판매처 1곳 → ④ 사내 공급처만 → ⑤ 완제품 판매만 → ⑥ 조달 근거 미확인</p>
  <div class="table-wrap" tabindex="0" aria-label="체인별 조사 결과 표">
    <table><colgroup><col class="no"><col class="identity"><col class="decision"><col class="public"><col class="internal"><col class="evidence"><col class="rights"></colgroup>
      <thead><tr><th>No.</th><th>체인명 · 별칭</th><th>판정</th><th>국내 판매처 · 직접 상품</th><th>사내 실제 공급처</th><th>내부 근거</th><th>게시 시 처리</th></tr></thead>
      <tbody>{table_rows}</tbody>
    </table>
  </div>
  <section class="notes">
    <article class="card"><h2>이번 조사로 말할 수 있는 것</h2><ul><li>{raw_count}개는 조사일 현재 국내 체인 부자재 상품 페이지가 열렸다.</li><li>{internal_only}개는 공개 판매 페이지 없이 POM의 실제 공급처 기록만 확인됐다.</li><li>동일 업체의 소매몰·도매몰·입점몰은 독립 판매처로 중복 계산하지 않았다.</li></ul></article>
    <article class="card"><h2>이번 조사로 말할 수 없는 것</h2><ul><li>로그인 이후 재고, 전화 주문, 최소수량과 실제 납기는 공개 페이지로 확정하지 않았다.</li><li>검색 결과가 없다는 것은 국내에 제품이 없다는 뜻이 아니라 공개 조달 근거를 찾지 못했다는 뜻이다.</li><li>단일 판매처 상품명이 업계 일반명인지 판매처 고유명인지는 이미지 제작 전에 최종 확인이 필요하다.</li></ul><a class="raw-link" href="/.runtime/chain-catalog-data/chain-audit-2026-08-21.json">JSON 원자료 보기 →</a></article>
  </section>
</main>
<script>
  const query=document.querySelector('#query');const status=document.querySelector('#status');const rows=[...document.querySelectorAll('tbody tr')];const visible=document.querySelector('#visible');
  function filter(){{const term=query.value.trim().toLowerCase();const state=status.value;let count=0;for(const row of rows){{const show=(!term||row.dataset.search.includes(term))&&(state==='all'||row.dataset.status===state);row.hidden=!show;if(show)count++}}visible.textContent=count}}
  query.addEventListener('input',filter);status.addEventListener('change',filter);
</script>
</body>
</html>'''
    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    PREVIEW.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT.write_text(document, encoding="utf-8")
    PREVIEW.write_text(document, encoding="utf-8")
    print(f"wrote {OUTPUT}")
    print(f"wrote {PREVIEW}")


if __name__ == "__main__":
    main()
