# -*- coding: utf-8 -*-
"""
브라우저 검증 결과 수신기  [리팩토링 5단계 2026-09-15]

무엇을 하나
  sandbox_test.html(불투명 오리진 재현 시험)이 브라우저에서 잰 결과를 여기로 보내면
  파일로 남긴다. 예전에는 화면에만 찍히고 탭을 닫으면 사라졌다 —
  즉 가장 강한 회귀 방지 자산(기능 10종 실측)이 증거를 남기지 못하고 있었다.

무엇이 바뀌었나
  · 결과(JSON)는 _build/snapshots/ 에 '판 이름 + 시각' 으로 스냅샷을 남긴다.
    같은 판의 최신 결과는 <판이름>_latest.json 으로도 덮어 써 둔다(비교의 기준점).
  · 스크린샷(PNG)은 파일 이름이 아니라 '내용'을 보고 판단해 .png 로 저장한다.
    예전에는 이름이 .png 로 안 끝나면 PNG 를 .txt 로 저장해 버려
    286KB 짜리 못 읽는 txt 가 쌓였다(block.txt·canada.txt·meas.txt·table.txt).

쓰는 법
  python rxsrv.py                    수신 대기(127.0.0.1:8800)
  python rxsrv.py --compare A B      스냅샷 두 개를 항목별로 비교한다

  브라우저 쪽 절차 (사람이 해야 한다 — 3D·마이크·IndexedDB 는 node 로 못 잰다)
    1) python rxsrv.py
    2) sandbox_test.html 을 연다. 판을 바꾸려면 ?f=제관웹_오프라인_단일파일_500PRO_축소판.html
    3) 결과가 다 나오면 '결과 저장' 단추를 누른다 → snapshots/ 에 파일이 생긴다
    4) 세 판 모두 반복한다 (기본 / 축소판 / 웹게시용)
"""
import base64
import io
import json
import os
import re
import sys
import urllib.parse
from datetime import datetime
from http.server import BaseHTTPRequestHandler, HTTPServer

HERE = os.path.dirname(os.path.abspath(__file__))
SNAPDIR = os.path.join(HERE, 'snapshots')

# 파일 이름에 쓸 수 없는 글자를 걸러낸다(판 이름이 그대로 들어오므로)
SAFE = re.compile(r'[^0-9A-Za-z가-힣._-]+')


def safe_name(s, default='out'):
    # 브라우저는 한글 판 이름을 encodeURIComponent 로 보낸다(%EC%B6%95…).
    #   먼저 풀지 않으면 파일 이름이 '500PRO__EC_B6_95…' 같은 글자 무덤이 된다(실측).
    try:
        s = urllib.parse.unquote(s or '', encoding='utf-8', errors='replace')
    except Exception:
        pass
    s = SAFE.sub('_', s.strip()).strip('_')
    return s[:80] or default


def is_png_payload(raw):
    """이름이 아니라 내용으로 판단한다 — data:image/... 또는 PNG 시그니처."""
    head = raw[:64].lstrip()
    if head.startswith(b'data:image'):
        return True
    return raw[:8] == b'\x89PNG\r\n\x1a\n'


def png_bytes(raw):
    b = raw.decode('utf-8', 'replace') if not isinstance(raw, str) else raw
    if ',' in b[:200] and b.lstrip().startswith('data:'):
        b = b.split(',', 1)[1]
    return base64.b64decode(b)


def save_result(name, raw):
    """결과 JSON 을 스냅샷으로 남긴다. 판 이름은 POST 경로에서 온다."""
    if not os.path.isdir(SNAPDIR):
        os.makedirs(SNAPDIR)
    ts = datetime.now().strftime('%Y%m%d_%H%M%S')
    try:
        obj = json.loads(raw.decode('utf-8'))
    except Exception:
        # JSON 이 아니면 그대로 글자로 남긴다(정보를 버리지 않는다)
        obj = {'_원문': raw.decode('utf-8', 'replace')}
    obj['_받은시각'] = ts
    obj['_판'] = name
    text = json.dumps(obj, ensure_ascii=False, indent=1, sort_keys=True)
    paths = [os.path.join(SNAPDIR, '%s_%s.json' % (name, ts)),
             os.path.join(SNAPDIR, '%s_latest.json' % name)]
    for p in paths:
        io.open(p, 'w', encoding='utf-8').write(text)
    return paths


class H(BaseHTTPRequestHandler):
    def _c(self):
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Headers', '*')
        self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')

    def do_OPTIONS(self):
        self.send_response(200)
        self._c()
        self.end_headers()

    def do_POST(self):
        n = int(self.headers.get('Content-Length', 0))
        raw = self.rfile.read(n)
        name = safe_name(self.path.strip('/'))
        if is_png_payload(raw):
            p = os.path.join(HERE, name + '.png')
            open(p, 'wb').write(png_bytes(raw))
            print('스크린샷 저장:', os.path.basename(p))
        else:
            for p in save_result(name, raw):
                print('스냅샷 저장:', os.path.relpath(p, HERE))
        self.send_response(200)
        self._c()
        self.end_headers()
        self.wfile.write(b'ok')

    def log_message(self, *a):
        pass


def flatten(obj, prefix=''):
    """{a:{b:1}} → {'a.b': 1}. 항목별로 견주기 위해 납작하게 편다."""
    out = {}
    if isinstance(obj, dict):
        for k in obj:
            out.update(flatten(obj[k], prefix + str(k) + '.'))
    elif isinstance(obj, list):
        out[prefix.rstrip('.')] = json.dumps(obj, ensure_ascii=False)
    else:
        out[prefix.rstrip('.')] = obj
    return out


def compare(a_path, b_path):
    a = flatten(json.load(io.open(a_path, encoding='utf-8')))
    b = flatten(json.load(io.open(b_path, encoding='utf-8')))
    skip = ('_받은시각',)
    keys = sorted(set(a) | set(b))
    diff = 0
    for k in keys:
        if any(k.endswith(s) for s in skip):
            continue
        if k not in a:
            print('  + 새 항목 %s = %s' % (k, b[k])); diff += 1
        elif k not in b:
            print('  - 사라진 항목 %s (예전 %s)' % (k, a[k])); diff += 1
        elif a[k] != b[k]:
            print('  ≠ %s : %s → %s' % (k, a[k], b[k])); diff += 1
    print('-' * 60)
    if diff:
        print('다른 항목 %d개. 의도한 변화인지 확인하라.' % diff)
    else:
        print('두 스냅샷이 완전히 같다 — 회귀 없음.')
    return 1 if diff else 0


def main():
    if '--compare' in sys.argv:
        i = sys.argv.index('--compare')
        rest = sys.argv[i + 1:]
        if len(rest) < 2:
            print('사용법: python rxsrv.py --compare 예전.json 새것.json')
            return 2
        return compare(rest[0], rest[1])
    print('브라우저 검증 결과 수신 대기 — http://127.0.0.1:8800')
    print('스냅샷 저장 위치:', SNAPDIR)
    HTTPServer(('127.0.0.1', 8800), H).serve_forever()


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