#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Оберт «правки Редактора → нова сторінка в панелі», однією командою.

    .venv/bin/python ua_build/apply_edits.py Level4_A12_UA_v01

Аргумент — стем сторінкових PNG у панелі (він же ім'я інтерактивного PDF).
Що робиться, по порядку:

  1. збірка друку з накладеними правками (`build.py --edits`);
  2. геометричний контроль (`qa.py`) — правка легко виштовхує текст на
     ілюстрацію, і це має спинити оберт, а не поїхати далі;
  3. звірка біблійних цитат (`quotes_check.py`);
  4. детектор полів і збірка інтерактиву — під ТИМ САМИМ іменем, щоб не
     змінився стем PNG, до якого прив'язані покажчик тексту й правки;
  5. новий покажчик тексту (`text_index.py`);
  6. перемальовування сторінок у `editor/` (`make_page.py --force`).

Виходить нулем, лише якщо пройшли всі кроки. Якщо `qa.py` або звірка цитат
знайшли проблему — зупиняємось і НЕ підмінюємо сторінки: краще Редактор
побачить стару сторінку й пояснення, ніж зіпсовану верстку.
"""
import argparse
import os
import re
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
sys.path.insert(0, HERE)

from lessons import Lesson, INTERACTIVE_DIR                    # noqa: E402

PY = sys.executable
EDITOR = os.path.join(ROOT, 'editor')
STEM_RE = re.compile(r'^(Level\d_[ABC]\d{1,2})_UA(_v\d+)?$')


def run(step, args, quiet=False):
    print('\n=== %s ===' % step, flush=True)
    r = subprocess.run([PY] + args, cwd=HERE, capture_output=True, text=True)
    out = (r.stdout or '').strip()
    err = (r.stderr or '').strip()
    if out:
        print(out, flush=True)
    if err:
        print(err, file=sys.stderr, flush=True)
    return r.returncode, out, err


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('png_stem', help='напр. Level4_A12_UA_v01')
    ap.add_argument('--notes', default=None)
    a = ap.parse_args()

    m = STEM_RE.match(a.png_stem)
    if not m:
        raise SystemExit('не схоже на стем буклета: %r' % a.png_stem)
    stem = m.group(1)
    lesson = Lesson(stem)
    notes = a.notes or os.path.join(EDITOR, 'notes_%s.json' % a.png_stem)
    if not os.path.exists(notes):
        raise SystemExit('немає файлу правок %s — спершу збережіть їх у панелі'
                         % os.path.relpath(notes, ROOT))

    # Постійні правки поповнюємо ДО збірки, а не після. Інакше виходить
    # так: PDF збирається з накладеною правкою і стає новим, а покажчик
    # тексту, який іде п'ятим кроком, читає ще СТАРИЙ постійний файл — і
    # панель показує старий текст поруч зі свіжою картинкою. Саме це
    # Редактор і побачив.
    from edits import load_overlay, save_overlay
    before = load_overlay(stem)
    added = _merge_overlay(stem, notes)
    if added:
        print('постійних правок додано: %d' % added)

    def rollback(why):
        """Крок не пройшов — постійний файл має лишитися, як був.

        Інакше невдала правка осіла б у ньому назавжди й лягала на кожну
        наступну збірку.
        """
        if added:
            save_overlay(stem, before)
            print('постійні правки відкочено до попереднього стану')
        raise SystemExit(why)

    rc, out, _ = run('збірка друку', ['build.py', stem])
    if rc:
        rollback('збірка не вдалася')
    m2 = re.search(r'зібрано:\s*(.+)$', out, re.M)
    if not m2:
        rollback('збірка не назвала файл — далі йти нема з чим')
    printed = m2.group(1).strip()

    rc, out, _ = run('геометричний контроль', ['qa.py', stem])
    if rc:
        rollback('геометрія зіпсована — сторінки НЕ підмінено. '
                 'Правку треба вкладати в макет руками.')

    # Звірка цитат більше не спиняє оберт через правку Редактора: рішення
    # замовника від 7 вересня 2026 — його слово важливіше за дослівність,
    # а розбіжність лишається попередженням. Ненульовий код повертається
    # лише тоді, коли цитату спотворено у файлі перекладу, тобто помилку
    # вніс розробник.
    rc, qout, _ = run('звірка цитат', ['quotes_check.py', stem])
    if rc:
        rollback('цитату спотворено у файлі перекладу — сторінки НЕ '
                 'підмінено. Це не ваша правка, це помилка розробника.')

    # Екранна версія: є блоки, які на екрані звучать інакше, ніж на друку
    # (`SCREEN_TEXT` уроку). Інтерактив робиться саме з неї; якщо уроку
    # такого немає, вона просто дорівнює друкованій.
    rc, sout, _ = run('екранна версія', ['build.py', stem, '--screen'])
    if rc:
        rollback('екранна версія не зібралася')
    m3 = re.search(r'зібрано:\s*(.+)$', sout, re.M)
    if not m3:
        rollback('екранна збірка не назвала файл')
    screen = m3.group(1).strip()

    fields = os.path.join(HERE, 'fields_%s.json' % stem)
    rc, _, _ = run('детектор полів',
                   ['fields_detect.py', screen, '--out', fields, '--quiet'])
    if rc:
        rollback('детектор полів не впорався')

    # Кожна перезбірка — НОВИЙ номер версії.
    #
    # Доти ім'я лишалося тим самим, щоб не рвати прив'язку: до нього
    # прив'язані сторінки панелі, покажчик тексту й нитки зауважень. Але
    # ціна виявилася більшою за зручність: Редактор не бачила, що їй
    # надіслано новий файл, і плуталася, дивлячись на стару назву
    # (8 вересня 2026). Номер версії — це єдине, що каже людині «це вже
    # інше».
    #
    # Прив'язку тепер переносимо разом із номером: сторінки, покажчик і
    # нитки переїздять на новий стем. Втратити тут нічого не можна —
    # переносимо, а не створюємо заново.
    os.makedirs(INTERACTIVE_DIR, exist_ok=True)
    new_stem = _next_stem(a.png_stem)
    target = os.path.join(INTERACTIVE_DIR, '%s.pdf' % new_stem)
    rc, _, _ = run('збірка інтерактиву',
                   ['fields_build.py', '--fields', fields, '--archive',
                    '--stem', '%s_UA' % stem, '--outdir', INTERACTIVE_DIR,
                    '--out', target])
    if rc or not os.path.exists(target):
        rollback('інтерактив не зібрано')

    # ── області розмальовування ─────────────────────────────────────────
    # Інтерактив щойно зібрано з нуля, і разом зі старим файлом зникли б
    # усі клікабельні області ілюстрацій: їх додає окремий крок
    # (`art_apply.py`) ПОВЕРХ готового уроку. Мовчки — бо жодна перевірка
    # їх не рахує, а Редактор побачив би це лише тицьнувши в малюнок.
    # Тому накладаємо їх наново, по одному набору на завдання.
    applied_art = _reapply_art(stem, target, rollback)
    if applied_art:
        print('областей розмальовування накладено: %d набір(и)' % applied_art)

    rc, _, _ = run('покажчик тексту',
                   ['text_index.py', stem, '--png-stem', new_stem])
    if rc:
        rollback('покажчик тексту не оновлено')

    # `--fields` тут не зайвий: разом зі сторінками перемальовуються й
    # картинки «як буде розфарбовано». Без них Редактор після перезбірки
    # бачив би кольори від ПОПЕРЕДНЬОЇ версії — тобто те, чого на сторінці
    # вже немає, і не мав би як про це здогадатися.
    rc, _, _ = run('сторінки для панелі',
                   [os.path.join(EDITOR, 'make_page.py'), target,
                    '--all', '--force', '--paint', fields])
    if rc:
        rollback('сторінки не перемальовано')

    # Застосоване прибираємо зі списку: інакше Редактор бачить свої вже
    # виконані правки як відкриті й не розрізняє їх від тих, що чекають на
    # розробника. Повний набір лягає в журнал `editor/applied/`.
    kept = _split_notes(notes, stem, a.png_stem)
    _move_notes(notes, new_stem)

    for ln in qout.splitlines():
        if ln.strip().startswith('РОЗБІЖНІСТЬ'):
            print(ln.strip())

    print('\nготово: %s' % os.path.relpath(printed, ROOT))
    print('нова версія інтерактиву: %s' % os.path.basename(target))
    print('сторінки в панелі оновлено')
    if kept:
        print('лишилося правок для розробника: %d' % kept)
    return 0


def _next_stem(png_stem):
    """Наступний вільний номер версії буклета: `Level2_A12_UA_v02` -> `_v03`.

    Не «стем відкритої сторінки плюс один». 12 вересня 2026 Редактор
    натиснула «Застосувати правки» на старій сторінці `Level1_A12_UA_v01`,
    коли жила вже `v30`: збірка дала `v02`, `--archive` прибрав `v30` у
    backup, а нитки зауважень лишилися під `v30`, куди панель уже не
    дивилася. Те саме з `Level2_A12` (`v47` -> `v19`). Стем сторінки — це
    те, ЗВІДКИ взято правки; номер нової версії рахує лічильник: більше за
    всі відомі — у теці інтерактиву, у її `backup/` і в покажчику сторінок
    панелі — і не менше за стем сторінки.
    """
    m = re.match(r'^(.*)_v(\d+)$', png_stem)
    family = m.group(1) if m else png_stem
    used = [int(m.group(2))] if m else [0]
    rx = re.compile(re.escape(family) + r'_v(\d+)(?:_[\d-]+)?\.pdf$')
    for d in (INTERACTIVE_DIR, os.path.join(INTERACTIVE_DIR, 'backup')):
        if os.path.isdir(d):
            used += [int(mm.group(1)) for fn in os.listdir(d)
                     if (mm := rx.fullmatch(fn))]
    try:
        import json as _json
        docs = _json.load(open(os.path.join(EDITOR, 'pages.json'),
                               encoding='utf-8')).get('docs') or {}
        rxs = re.compile(re.escape(family) + r'_v(\d+)$')
        used += [int(mm.group(1)) for k in docs if (mm := rxs.match(k))]
    except (OSError, ValueError):
        pass
    return '%s_v%02d' % (family, max(used) + 1)


def _move_notes(notes_path, new_stem):
    """Перенести нитки зауважень на новий стем.

    Нитки прив'язані до імені файла, а воно щойно змінилося. Якщо їх не
    перенести, Редактор відкриє новий буклет і побачить порожньо — а
    відповіді розробника лишаться під старою назвою, куди вже ніхто не
    дивиться.
    """
    if not os.path.exists(notes_path):
        return
    dst = os.path.join(EDITOR, 'notes_%s.json' % new_stem)
    try:
        import json as _json
        doc = _json.load(open(notes_path, encoding='utf-8'))
        doc['stem'] = new_stem
        with open(dst, 'w', encoding='utf-8') as fh:
            _json.dump(doc, fh, ensure_ascii=False, indent=1)
        os.remove(notes_path)
        print('нитки зауважень перенесено на %s' % os.path.basename(dst))
    except (OSError, ValueError) as exc:
        print('нитки перенести не вдалося: %s' % exc)


def _reapply_art(stem, target, rollback):
    """Накласти на свіжозібраний інтерактив набори областей цього буклета.

    Набір належить буклетові, якщо в його полі `pdf` стоїть той самий стем
    без номера версії: панель пише туди файл, на якому Редактор обводив.
    """
    import glob
    import json as _json

    sets = []
    for path in sorted(glob.glob(os.path.join(EDITOR, 'regions_p*.json'))):
        try:
            doc = _json.load(open(path, encoding='utf-8'))
        except (OSError, ValueError):
            continue
        named = os.path.splitext(os.path.basename(doc.get('pdf') or ''))[0]
        if re.sub(r'_v\d+$', '', named) == '%s_UA' % stem:
            sets.append(path)
    if not sets:
        return 0

    tmp = target + '.art'
    src = target
    for path in sets:
        rc, _, _ = run('області: %s' % os.path.basename(path),
                       ['art_apply.py', path, '--src', src, '--out', tmp])
        if rc or not os.path.exists(tmp):
            rollback('області з %s не наклалися' % os.path.basename(path))
        os.replace(tmp, target)
        src = target
    return len(sets)


def _merge_overlay(stem, notes):
    """Перенести застосовні правки із зауважень у постійні файли.

    Класифікуємо проти ВИХІДНОГО перекладу з уже накладеними постійними
    правками — тобто проти того, що зараз на сторінці.

    Файлів два, і плутати їх не можна. У блоків, оголошених у `SCREEN_TEXT`,
    поле «текст із правками» показує ЕКРАННИЙ текст (панель показує екранну
    сторінку), тож його правка йде в екранний накладний файл. Друкований
    текст такого блока Редактор править в окремому полі «на друку», і воно
    йде у звичайний накладний файл. Якби обидві правки лягали в один файл,
    правка екранного тексту мовчки замінила б друкований.
    """
    import json
    from edits import (classify, load_overlay, save_overlay, apply_overlay,
                       load_screen_overlay, save_screen_overlay)

    for mod in list(sys.modules):
        if mod == 'tr_%s' % stem:
            del sys.modules[mod]
    tr_mod = __import__('tr_%s' % stem)
    TR = tr_mod.TR
    apply_overlay(TR, stem)
    blocks = load_overlay(stem)
    screen = load_screen_overlay(stem)
    # Екранний блок — і той, що оголошено в `SCREEN_TEXT`, і той, що став
    # екранним через позначку «тільки для інтерактиву»: панель показує йому
    # два поля, і правка з головного поля має йти в екранний файл. Доти
    # бралися лише ключі `SCREEN_TEXT`, і друга правка такого блока мовчки
    # пішла б у друк (9 вересня 2026, стор. 4 Level1_A12).
    screen_keys = set(getattr(tr_mod, 'SCREEN_TEXT', {})) | set(screen)
    added = 0
    for n in json.load(open(notes, encoding='utf-8')).get('notes') or []:
        key = n.get('key')
        pairs = []
        if key in screen_keys:
            pairs.append(((n.get('note') or '').strip(), screen))
            pairs.append(((n.get('print_note') or '').strip(), blocks))
        elif n.get('screen_only'):
            # Позначка Редактора «тільки для інтерактиву»: правка йде в
            # екранний накладний файл, друк лишається як був. Після
            # перезбірки блок стає екранним — з окремим полем «на друку».
            pairs.append(((n.get('note') or '').strip(), screen))
        else:
            pairs.append(((n.get('note') or '').strip(), blocks))
        for text, target in pairs:
            if text and classify(TR.get(key), text)[0]:
                target[key] = text
                added += 1
    if added:
        save_overlay(stem, blocks)
        if screen:
            save_screen_overlay(stem, screen)
    return added


def _split_notes(notes, stem, png_stem):
    """Лишити у файлі лише те, що НЕ застосувалося; решту — в журнал."""
    import json
    from datetime import datetime
    from edits import classify

    # Класифікуємо проти ВИХІДНОГО перекладу, а не проти вже зміненого:
    # імпортуємо модуль наново, без накладених правок.
    for mod in list(sys.modules):
        if mod == 'tr_%s' % stem:
            del sys.modules[mod]
    TR = __import__('tr_%s' % stem).TR

    def outstanding(n):
        """Чи лишається запис у списку після перезбірки.

        Лишається у двох випадках: правку не вдалося підставити, або в
        записі є ЗАУВАЖЕННЯ. Зауваження знімає лише розробник — тому блок
        і далі помаранчевий, доки на нього не відповіли.
        """
        if (n.get('comment') or '').strip():
            return True
        texts = [(n.get('note') or '').strip(),
                 (n.get('print_note') or '').strip()]
        texts = [t for t in texts if t]
        if not texts:
            return False
        return any(not classify(TR.get(n.get('key')), t)[0] for t in texts)

    doc = json.load(open(notes, encoding='utf-8'))
    all_notes = doc.get('notes') or []
    keep = [n for n in all_notes if outstanding(n)]


    applied_dir = os.path.join(EDITOR, 'applied')
    os.makedirs(applied_dir, exist_ok=True)
    stamp = datetime.now().strftime('%Y-%m-%d_%H%M')
    with open(os.path.join(applied_dir, 'notes_%s_%s.json' % (png_stem, stamp)),
              'w', encoding='utf-8') as fh:
        json.dump(doc, fh, ensure_ascii=False, indent=1)

    if keep:
        doc['notes'] = keep
        with open(notes, 'w', encoding='utf-8') as fh:
            json.dump(doc, fh, ensure_ascii=False, indent=1)
    else:
        os.remove(notes)
    return len(keep)


if __name__ == '__main__':
    sys.exit(main())
