#!/usr/bin/env python3
"""Генератор страницы слепого сравнения редакторов (эксперимент 04).

⚠ ГИГИЕНА СЛЕПОТЫ (D13.5 / 07-review §4.5 / POLYGON handoff Task 2): предыдущая версия была
структурно НЕ-слепой при LLM-оценке — (1) ключ «метка→модель» и цена лежали В ТОМ ЖЕ артефакте
(за <details> — для LLM-фетча это НЕ скрытие, весь DOM виден), (2) маппинг A/B/C/D был
ЗАФИКСИРОВАН одинаково на всех фрагментах (узнав раз — знаешь везде). Исправлено:
  - метки рандомизируются ПО ФРАГМЕНТУ (детерминированная перестановка на соли, разная у каждого
    фрагмента) — узнать порядок на одном фрагменте не даёт его на других;
  - ключ (метка→модель) + цена/латентность пишутся в ОТДЕЛЬНЫЙ файл (--key), КОТОРЫЙ НЕ
    ПУБЛИКУЕТСЯ до вынесения оценки; в самом артефакте НЕТ ни имён моделей, ни цены;
  - соль генерится случайно на сборку и хранится ТОЛЬКО в ключ-файле → даже код-осведомлённый
    оценщик не инвертирует перестановку без ключа.

Читает eval/data/editor_bench/<fragment>/*.txt, эмитит слепой HTML (контент для Artifact-обёртки)
и рядом — приватный ключ-файл. Запуск:
  eval/.venv/bin/python eval/build_editor_artifact.py <out.html> [--key <out.key.json>]
"""
import argparse
import hashlib
import html
import json
import re
import secrets
from pathlib import Path

ROOT = Path(__file__).resolve().parent
BENCH = ROOT / "data" / "editor_bench"

MODELS = ["grok-4.3", "glm-4.6", "gemini-3.1-pro", "kimi-k2.6"]
LABELS = ["A", "B", "C", "D"]

# стоимость/латентность — В КЛЮЧ-ФАЙЛ, НЕ в артефакт (цена коррелирует с моделью → утечка слепоты)
COST = {"grok-4.3": "~13 с, без «размышлений» — самый быстрый",
        "glm-4.6": "~105 с (thinking off)",
        "gemini-3.1-pro": "~60–86 с, обязательный thinking",
        "kimi-k2.6": "~150 с, ~11 000 токенов «размышлений» на абзац — самый дорогой"}

FRAGMENTS = [
    ("rashomon", "японский → русский", "Акутагава Рюноскэ, «Расёмон»", False),
    ("ahq", "китайский → русский", "Лу Синь, «Подлинная история А-Кью»", False),
    ("merosu", "японский → русский", "Дадзай Осаму, «Беги, Мелос!»", False),
    ("wells", "английский → русский", "Г. Уэллс, «Машина времени»", True),
]


def label_map(salt: str, frag: str) -> list[tuple[str, str]]:
    """Детерминированная перестановка меток ПО ФРАГМЕНТУ на секретной соли: сортируем модели по
    sha256(salt|frag|model) и присваиваем A,B,C,D. Разная у каждого фрагмента; без соли (в ключе)
    не инвертируется. Возвращает [(label, model), …] в порядке меток."""
    order = sorted(MODELS, key=lambda m: hashlib.sha256(f"{salt}|{frag}|{m}".encode()).hexdigest())
    return list(zip(LABELS, order))


def paras(text: str) -> str:
    text = text.strip()
    blocks = re.split(r"\n\s*\n", text) or [text]
    if len(blocks) == 1:
        blocks = [b for b in text.split("\n") if b.strip()]
    return "".join(f"<p>{html.escape(b.strip())}</p>" for b in blocks if b.strip())


def read(frag: str, name: str) -> str:
    f = BENCH / frag / f"{name}.txt"
    return f.read_text() if f.exists() else ""


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("out")
    ap.add_argument("--key", default="", help="приватный ключ-файл (по умолчанию <out>.key.json)")
    ap.add_argument("--salt", default="", help="фиксировать соль (для воспроизводимости; иначе случайная)")
    args = ap.parse_args()
    out = Path(args.out)
    key_path = Path(args.key) if args.key else out.with_suffix(".key.json")
    salt = args.salt or secrets.token_hex(8)

    S = []
    S.append("<title>Сравнение редакторов ru-стиля · Эксперимент 04</title>")
    S.append(f"<style>{CSS}</style>")
    S.append('<div class="wrap">')
    S.append('<header class="intro">')
    S.append('<p class="eyebrow">TextMachine · полигон · эксперимент 04</p>')
    S.append("<h1>Кто лучше правит русский стиль</h1>")
    S.append('<p class="lede">Четыре модели-редактора получили один и тот же черновой перевод и '
             'задание: превратить его в живую художественную прозу, ничего не переврав. '
             'Версии обезличены — <strong>A, B, C, D</strong>. Оцени по двум осям:</p>')
    S.append('<ul class="criteria">'
             '<li><span class="k k-style">стиль</span> — какая русская проза читается живее, без канцелярита и калек;</li>'
             '<li><span class="k k-fidel">верность</span> — кто не потерял смысл и не добавил отсебятины (сверяй со «смысловым якорем»).</li>'
             '</ul>')
    S.append('<p class="note">⚠ Метки A–D перемешаны СВОИМ порядком в каждом фрагменте — узнав, кто есть кто '
             'в одном, ты НЕ знаешь этого в другом. Имена моделей и цена в артефакте НЕ приводятся '
             '(ключ — в отдельном файле, вскрывается только ПОСЛЕ оценки). Фрагмент Уэллса — англ.→рус.: '
             'оригинал перед тобой, судишь и стиль, и верность напрямую.</p>')
    S.append("</header>")

    key = {"salt": salt, "note": "ПРИВАТНЫЙ КЛЮЧ эксп.04 — НЕ публиковать вместе с артефактом; "
           "вскрывать только ПОСЛЕ вынесения слепой оценки (урок 07-review §4.5). "
           "Метки рандомизированы по фрагменту.", "cost": COST, "fragments": {}}

    for i, (frag, direction, work, is_en) in enumerate(FRAGMENTS, 1):
        lm = label_map(salt, frag)
        key["fragments"][frag] = {lab: model for lab, model in lm}
        anchor = read(frag, "source") if is_en else read(frag, "english_ref")
        anchor_label = "Оригинал (английский)" if is_en else "Смысловой якорь — дословный английский перевод"
        draft = read(frag, "draft")
        S.append('<section class="frag">')
        S.append(f'<div class="frag-head"><span class="fnum">{i:02d}</span>'
                 f'<div><p class="eyebrow">{html.escape(direction)}</p>'
                 f'<h2>{html.escape(work)}</h2></div>'
                 + ('<span class="badge">твой полный контроль</span>' if is_en else '') +
                 '</div>')
        S.append('<details class="context"><summary>Смысловой якорь и черновик</summary>'
                 '<div class="context-grid">')
        S.append(f'<div class="ctx"><p class="ctx-label">{html.escape(anchor_label)}</p>'
                 f'<div class="prose muted">{paras(anchor)}</div></div>')
        S.append(f'<div class="ctx"><p class="ctx-label">Черновой перевод (вход редактора)</p>'
                 f'<div class="prose muted">{paras(draft)}</div></div>')
        S.append("</div></details>")
        S.append('<div class="versions">')
        for letter, model in lm:  # per-fragment shuffled order
            txt = read(frag, model)
            S.append(f'<article class="ver"><header class="ver-head"><span class="chip">{letter}</span></header>'
                     f'<div class="prose">{paras(txt)}</div></article>')
        S.append("</div>")
        S.append("</section>")

    # NO reveal/key section in the artifact (the exp04 blindness break). Key → separate file.
    S.append('<footer class="foot"><p>Выбор дефолтного редактора для Фазы 1. '
             'Твоя оценка — золотой стандарт: LLM-судьи на художественном качестве расходятся с людьми. '
             'Ключ «метка→модель» и цена — в приватном файле, вскрываются после оценки.</p></footer>')
    S.append("</div>")
    out.write_text("\n".join(S), encoding="utf-8")
    key_path.write_text(json.dumps(key, ensure_ascii=False, indent=2), encoding="utf-8")
    print("written", out, out.stat().st_size, "bytes")
    print("KEY (do NOT publish until scored):", key_path)


CSS = r"""
:root{
  --paper:#f7f6f3; --surface:#fffffe; --ink:#1c1b19; --muted:#6f6b64;
  --hair:#e4e0d8; --accent:#9e2b25; --accent-soft:#f0e2df;
  --style:#3f6f52; --fidel:#9e2b25;
  --serif:Georgia,'Iowan Old Style','Times New Roman',serif;
  --sans:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --measure:64ch;
}
@media (prefers-color-scheme:dark){
  :root{--paper:#17181a; --surface:#1f2124; --ink:#e9e7e2; --muted:#9a978f;
    --hair:#31343a; --accent:#d67a72; --accent-soft:#33231f;
    --style:#7db38f; --fidel:#d67a72;}
}
:root[data-theme="light"]{--paper:#f7f6f3; --surface:#fffffe; --ink:#1c1b19; --muted:#6f6b64;
  --hair:#e4e0d8; --accent:#9e2b25; --accent-soft:#f0e2df; --style:#3f6f52; --fidel:#9e2b25;}
:root[data-theme="dark"]{--paper:#17181a; --surface:#1f2124; --ink:#e9e7e2; --muted:#9a978f;
  --hair:#31343a; --accent:#d67a72; --accent-soft:#33231f; --style:#7db38f; --fidel:#d67a72;}

*{box-sizing:border-box;}
body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);
  line-height:1.5;-webkit-font-smoothing:antialiased;}
.wrap{max-width:1080px;margin:0 auto;padding:clamp(1.2rem,4vw,3.5rem) clamp(1rem,4vw,2rem) 4rem;}

.eyebrow{font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
  font-weight:600;margin:0 0 .5rem;}
h1{font-family:var(--serif);font-weight:600;font-size:clamp(1.9rem,5vw,3rem);line-height:1.08;
  margin:.1rem 0 .8rem;text-wrap:balance;letter-spacing:-.01em;}
.lede{font-size:1.08rem;max-width:var(--measure);color:var(--ink);margin:0 0 1rem;}
.criteria{list-style:none;padding:0;margin:0 0 1.2rem;max-width:var(--measure);
  display:flex;flex-direction:column;gap:.4rem;}
.criteria li{padding-left:.2rem;color:var(--muted);}
.k{display:inline-block;font-family:var(--sans);font-size:.72rem;font-weight:700;text-transform:uppercase;
  letter-spacing:.08em;padding:.12em .5em;border-radius:3px;margin-right:.4em;color:#fff;}
.k-style{background:var(--style);} .k-fidel{background:var(--fidel);}
.note{font-size:.9rem;color:var(--muted);max-width:var(--measure);}
.intro{border-bottom:1px solid var(--hair);padding-bottom:2rem;margin-bottom:2.5rem;}

.frag{margin:0 0 3.5rem;}
.frag-head{display:flex;align-items:baseline;gap:1rem;margin-bottom:1.1rem;flex-wrap:wrap;}
.fnum{font-family:var(--serif);font-size:1.5rem;color:var(--accent);font-weight:600;
  font-variant-numeric:tabular-nums;line-height:1;}
.frag-head h2{font-family:var(--serif);font-weight:600;font-size:1.4rem;margin:.1rem 0 0;letter-spacing:-.01em;}
.badge{margin-left:auto;align-self:center;font-size:.7rem;font-weight:600;text-transform:uppercase;
  letter-spacing:.08em;color:var(--accent);background:var(--accent-soft);
  padding:.28em .7em;border-radius:999px;}

.context{margin:0 0 1.3rem;border:1px solid var(--hair);border-radius:10px;background:var(--surface);}
.context>summary{cursor:pointer;padding:.75rem 1rem;font-size:.82rem;font-weight:600;
  text-transform:uppercase;letter-spacing:.06em;color:var(--muted);list-style:none;}
.context>summary::-webkit-details-marker{display:none;}
.context>summary::before{content:"▸ ";color:var(--accent);}
.context[open]>summary::before{content:"▾ ";}
.context-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem;padding:0 1rem 1.1rem;}
.ctx-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.07em;color:var(--accent);
  font-weight:600;margin:0 0 .4rem;}
@media(max-width:680px){.context-grid{grid-template-columns:1fr;}}

.versions{display:grid;grid-template-columns:1fr 1fr;gap:1.1rem;}
@media(max-width:760px){.versions{grid-template-columns:1fr;}}
.ver{background:var(--surface);border:1px solid var(--hair);border-radius:12px;
  padding:1.1rem 1.3rem 1.3rem;position:relative;}
.ver-head{margin:0 0 .5rem;}
.chip{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;
  font-family:var(--serif);font-weight:700;font-size:1.05rem;color:#fff;background:var(--accent);
  border-radius:50%;}

.prose{font-family:var(--serif);font-size:1.02rem;line-height:1.62;max-width:var(--measure);}
.prose p{margin:0 0 .75em;}
.prose p:last-child{margin-bottom:0;}
.prose.muted{color:var(--muted);font-size:.94rem;line-height:1.55;}

.foot{margin-top:2.5rem;padding-top:1.5rem;border-top:1px solid var(--hair);color:var(--muted);font-size:.9rem;}
a:focus-visible,summary:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:4px;}
@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important;}}
"""

if __name__ == "__main__":
    main()
