from pathlib import Path
import json
from html import escape
from datetime import datetime, timezone

ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'out'
WEB = ROOT / 'web'

GROUPS = [
    {
        'id': 'core-data',
        'title': '핵심 데이터/레코드',
        'desc': '기술 payload, actor 스탯 레이아웃, 대상 선택, result-family 스키마처럼 전투 계산과 직접 연결되는 근거.',
        'files': [
            'battle_action_mapping', 'battle_monster_shared_action_effect_review',
            'battle_monster_action_selection_review', 'battle_monster_action_catalog',
            'battle_skill_records', 'battle_actor_stat_layout_review',
            'battle_player_actor_stat_layout_review', 'battle_selected_target_latch_review',
            'battle_result_family_dispatch_review', 'battle_mode_coefficient_review',
        ],
    },
    {
        'id': 'formula-status',
        'title': '데미지/회복/상태 변화',
        'desc': 'HP/MP 적용, 자세/상태 계수, 명중/회피/크리티컬/상태 성공, 회복 아이템, 전투 종료 보상.',
        'files': [
            'battle_damage_formula_trace_review', 'battle_stat_flag_status_semantics_review',
            'battle_support_status_policy_review',
            'battle_status_transition_review', 'battle_recovery_item_effect_review',
            'battle_end_reward_review', 'battle_per_hit_result_flag_review',
            'battle_result_flag_lifecycle_review', 'battle_one_turn_flag_review',
            'battle_timed_lock_flag_review', 'battle_knockout_inactive_flag_review',
            'battle_sukyeong_element_guard_review',
        ],
    },
    {
        'id': 'presentation',
        'title': '표시/사운드/결과 연출',
        'desc': '데미지 숫자, MISS/guard/critical 표시, 결과음, actor 표시 상태와 wait/slot 생명주기.',
        'files': [
            'battle_result_display_branch_review', 'battle_result_helper_label_review',
            'battle_damage_digit_display_review', 'battle_critical_presentation_review',
            'battle_status_success_sound_review', 'battle_sound_role_review',
            'battle_sound_effect_review', 'battle_actor_display_state_review',
            'battle_actor_display_wait_semantics_review', 'battle_actor_display_slot_lifecycle_review',
        ],
    },
    {
        'id': 'animation-vm',
        'title': '액션/애니메이션 VM',
        'desc': 'display VM, frame gate, helper opcode/body/child/frameScript, 이펙트 spawn-tree와 위치/이동 규칙.',
        'files': [
            'battle_display_vm_static_decode', 'battle_fallback_record_review',
            'battle_monster_action_frame_probe', 'battle_monster_action_effect_review',
            'battle_skill_pattern_review', 'battle_movement_pattern_review',
            'battle_caster_position_rule_review', 'battle_frame_gate_review',
            'battle_display_wait_opcode_review', 'battle_display_opcode_semantics_review',
            'battle_hit_window_gap_review',
            'battle_action_event_timeline_review', 'battle_effect_object_review',
            'battle_effect_semantics_review', 'battle_helper_opcode_review',
            'battle_helper_body_review', 'battle_helper_child_script_review',
            'battle_helper_frame_script_review', 'battle_helper_effect_runtime_review',
            'battle_helper_sync_timing_review', 'battle_helper_resource_flow_review',
            'battle_helper_position_motion_review', 'battle_helper_transform_solver_review',
            'battle_helper_spawn_tree_review', 'battle_helper_visual_behavior_review',
            'battle_skill_implementation_gap_review',
        ],
    },
    {
        'id': 'resources',
        'title': '전투 리소스/후보 데이터',
        'desc': '전투 배경, 적 스프라이트 후보, 이벤트 후보, 브라우저 smoke 결과처럼 계산식과 직접 무관한 보조 데이터.',
        'files': [
            'battle_backgrounds', 'battle_enemy_candidates', 'battle_event_candidates',
            'battle_action_handler_context', 'battle_background_review_browser_smoke',
        ],
    },
]

HIGHLIGHTS = [
    {
        'title': '전투 계산 파이프라인',
        'body': 'hit unit 8바이트 schema, target scope, result-family dispatch, HP/MP apply, 상태 성공 gate, critical/guard/miss branch를 모두 분리했다.',
        'json': 'battle_damage_formula_trace_review.json',
    },
    {
        'title': '상태/모드/취기',
        'body': '+0x2a 자세/상태, +0x65 취기, +0x18 주량, +0x1a 주량 경험치, +0x69 곤드레만드레 pending flag까지 정리했다.',
        'json': 'battle_status_transition_review.json',
    },
    {
        'title': 'Support/Status 전투 행',
        'body': '도주, 방어, 회복, 술, 도발, 눈요기, 수경은 공격 애니메이션 누락이 아니라 result-family/status 정책 행으로 분리했다.',
        'json': 'battle_support_status_policy_review.json',
    },
    {
        'title': '결과 표시와 소리',
        'body': 'MISS/guard/normal/critical 표시 분기, 데미지 숫자 btl_etc selector, WLK 결과음/시전음 역할을 분리했다.',
        'json': 'battle_result_display_branch_review.json',
    },
    {
        'title': '전투 액션/이펙트 VM',
        'body': 'phase display table, frame gate, helper child script, spawn-tree, caster position/motion opcode를 전투 러너가 참고할 수 있는 수준으로 색인화했다.',
        'json': 'battle_display_vm_static_decode.json',
    },
    {
        'title': '전투 기술 구현 gap 해소',
        'body': '전투 기술 unresolved flag는 0으로 정리했다. 도주/방어/회복/술/도발/눈요기/수경 같은 support/status 25행은 gap이 아니라 별도 정책 행으로 complete pattern에서 관리한다.',
        'json': 'battle_skill_implementation_gap_review.json',
    },
    {
        'title': '몬스터/공용 기술 페이로드',
        'body': '몬스터/공용 기술은 actor +0x59 shared action id에서 payload/result-family로 이어지고, actor +0x5a는 몬스터 CNS local display slot으로 분리된다.',
        'json': 'battle_monster_shared_action_effect_review.json',
    },
    {
        'title': '몬스터 행동 선택자',
        'body': '몬스터 행동 선택자는 descriptor/vtable VM script의 opcode 0x10 pair로 확정됐다. `10 c0 59 xx`가 shared action id, `10 c0 5a yy`가 visible slot을 세팅하고, 공유 descriptor의 조건부 action id는 actor id 분기로 해소된다.',
        'json': 'battle_monster_action_selection_review.json',
    },
    {
        'title': '몬스터 액션 카탈로그',
        'body': '선택자(+0x59/+0x5a), 공용 payload, CNS local display slot을 결합해 몬스터별 행동 RNG 가중치와 프레임/사운드/이동 슬롯을 한 파일로 묶었다.',
        'json': 'battle_monster_action_catalog.json',
    },
    {
        'title': '몬스터 행동 표시/이펙트',
        'body': '몬스터 행동의 local frame, helper call, effect/result WLK, repeat loop, movement를 정적 카탈로그로 재집계했고 helper graph/frameScript preview까지 연결했다.',
        'json': 'battle_monster_action_effect_review.json',
    },
]

FORMULAS = [
    ('Normal damage', 'atk = floor(attacker.attack * attacker.attackCoef * skillDamageCoef / 10000)\n'
     'def = floor(target.defense * target.defenseCoef / 100)\n'
     'base = floor((atk * 2 - def) / 3) + rand(floor(attacker.level / 2)) - rand(floor(target.level / 2))\n'
     'damage = floor(base * target.familyResistance[damageFamily] / 100)\n'
     'if damage < 3: damage = rand(3) + 1'),
    ('Critical / alternate damage', 'base = floor((atk * 2 - def) / 2) + rand(floor(attacker.level / 2)) - rand(floor(target.level / 2))\n'
     'damage = floor(base * target.familyResistance[damageFamily] / 100)\n'
     'if damage < 3: damage = rand(5) + 1\n'
     'critical is presented by flash + dedicated WLK + higher damage, not by text'),
    ('Ataho drunk hazard', 'after alcohol-family result: 취기 += inc, 주량경험치 += inc\n'
     'if 주량경험치 > 100 and 주량 < 100: 주량 += 1; 주량경험치 = 0\n'
     'status by 취기: >0 얼근히, >33 만취, >66 정신없이, >100 대호\n'
     'post-action cleanup: if rand(100) + 주량 < 취기 then 곤드레만드레'),
]


def load_meta(stem):
    path = OUT / f'{stem}.json'
    if not path.exists():
        return {
            'stem': stem,
            'json': f'{stem}.json',
            'exists': False,
            'title': stem.replace('_', ' '),
            'kind': '',
            'status': 'missing',
            'summary': 'missing JSON',
            'size': 0,
        }
    try:
        data = json.loads(path.read_text(encoding='utf-8'))
    except Exception as exc:
        return {
            'stem': stem,
            'json': f'{stem}.json',
            'exists': True,
            'title': stem.replace('_', ' '),
            'kind': '',
            'status': f'json-error: {exc}',
            'summary': '',
            'size': path.stat().st_size,
        }
    title = data.get('title') or data.get('kind') or stem.replace('_', ' ')
    status = data.get('status') or data.get('promotionStatus') or data.get('scope') or ''
    runtime_used = data.get('runtimeUsed')
    status_text = str(status).lower()
    kind_text = str(data.get('kind', '')).lower()
    if runtime_used is False:
        evidence = 'static'
    elif runtime_used is True or 'runtime' in status_text or 'runtime' in kind_text:
        evidence = 'legacy-runtime'
    elif 'smoke' in stem or 'smoke' in status_text or 'browser' in status_text:
        evidence = 'browser-smoke'
    else:
        evidence = 'static-inferred'
    summary = data.get('summary')
    if isinstance(summary, list):
        summary_text = '; '.join(str(x) for x in summary[:4])
    elif isinstance(summary, dict):
        summary_text = '; '.join(f'{k}: {v}' for k, v in list(summary.items())[:4])
    elif summary is None:
        summary_text = ''
    else:
        summary_text = str(summary)
    return {
        'stem': stem,
        'json': f'{stem}.json',
        'exists': True,
        'title': str(title),
        'kind': str(data.get('kind', '')),
        'status': str(status),
        'runtimeUsed': runtime_used,
        'evidence': evidence,
        'summary': summary_text[:500],
        'size': path.stat().st_size,
    }

entries = {}
for group in GROUPS:
    group_entries = []
    for stem in group['files']:
        meta = load_meta(stem)
        entries[stem] = meta
        group_entries.append(meta)
    group['entries'] = group_entries

all_json = sorted(p.stem for p in OUT.glob('battle*.json'))
covered = set(entries)
other_entries = [load_meta(stem) for stem in all_json if stem not in covered]
if other_entries:
    GROUPS.append({
        'id': 'other-json',
        'title': '기타 JSON 근거',
        'desc': '통합 그룹에 직접 배치하지 않은 남은 전투 JSON 근거.',
        'files': [e['stem'] for e in other_entries],
        'entries': other_entries,
    })

report = {
    'version': 1,
    'kind': 'hwanse-battle-analysis-summary',
    'source': 'out/battle_*.json',
    'updatedAt': datetime.now(timezone.utc).isoformat(),
    'policy': {
        'canonicalEvidence': 'JSON files are retained as raw structured evidence.',
        'removedDerivedFiles': 'Per-topic out/battle*.html and out/battle*.md are regeneratable and should not be linked from the home page.',
        'userEntry': 'web/battle_analysis.html',
    },
    'metrics': {
        'jsonFiles': len(all_json),
        'groupCount': len(GROUPS),
        'groupedJsonFiles': sum(len(g['entries']) for g in GROUPS),
    },
    'highlights': HIGHLIGHTS,
    'formulas': [{'title': t, 'code': c} for t, c in FORMULAS],
    'groups': GROUPS,
}

(OUT / 'battle_analysis_summary.json').write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding='utf-8')

html = f'''<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="icon" href="../favicon.ico" />
  <title>전투 EXE 분석 통합</title>
  <style>
    :root {{ color-scheme: light; --bg:#f6f7f9; --fg:#17202a; --muted:#607080; --line:#d8dee6; --head:#eef2f6; --link:#185abc; --good:#0f766e; }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; background:var(--bg); color:var(--fg); font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; line-height:1.45; }}
    main {{ max-width:1440px; margin:0 auto; padding:18px; }}
    header {{ display:flex; justify-content:space-between; gap:16px; align-items:flex-start; margin-bottom:14px; }}
    h1 {{ margin:0 0 6px; font-size:24px; letter-spacing:0; }}
    h2 {{ margin:0; font-size:17px; letter-spacing:0; }}
    h3 {{ margin:0; font-size:15px; letter-spacing:0; }}
    .sub, .muted {{ color:var(--muted); }}
    nav {{ display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end; }}
    a {{ color:var(--link); text-decoration:none; }} a:hover {{ text-decoration:underline; }}
    nav a, .pill {{ display:inline-flex; align-items:center; min-height:30px; padding:4px 9px; border:1px solid var(--line); border-radius:5px; background:white; font-size:13px; }}
    section {{ margin:14px 0; background:white; border:1px solid var(--line); border-radius:8px; overflow:hidden; }}
    .section-head {{ display:flex; justify-content:space-between; gap:12px; padding:12px 14px; border-bottom:1px solid var(--line); background:var(--head); }}
    .body {{ padding:14px; }}
    .metrics {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:10px; }}
    .metric {{ border:1px solid var(--line); border-radius:6px; background:#f8fafc; padding:10px; }}
    .metric strong {{ display:block; font-size:22px; }}
    .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:10px; }}
    .card {{ border:1px solid var(--line); border-radius:6px; padding:10px; background:#fff; }}
    pre {{ margin:0; white-space:pre-wrap; overflow:auto; background:#101418; color:#eef6ff; border-radius:6px; padding:10px; font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
    table {{ width:100%; border-collapse:collapse; }}
    th,td {{ padding:8px 10px; border-bottom:1px solid var(--line); text-align:left; vertical-align:top; font-size:13px; }}
    th {{ background:#f8fafc; color:#344050; }}
    td code {{ font:12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
    .status {{ color:var(--good); font-weight:700; }}
    .evidence {{ font-weight:700; white-space:nowrap; }}
    .evidence.static {{ color:#0f766e; }}
    .evidence.legacy-runtime {{ color:#b45309; }}
    .evidence.static-inferred {{ color:#2563eb; }}
    .evidence.browser-smoke {{ color:#7c3aed; }}
    .table-wrap {{ overflow:auto; }}
    @media (max-width:760px) {{ header,.section-head {{ display:block; }} nav {{ justify-content:flex-start; margin-top:10px; }} }}
  </style>
</head>
<body>
<main>
  <header>
    <div>
      <h1>전투 EXE 분석 통합</h1>
      <p class="sub">세부 검토 HTML/MD를 하나로 합친 사용자용 허브. 구조화 근거는 `out/battle_*.json`으로 보존한다.</p>
    </div>
    <nav aria-label="battle analysis links">
      <a href="index.html">홈</a>
      <a href="battle_simulator.html">전투 샌드박스</a>
      <a href="battle_simulator.html">전투 기술 실행</a>
      <a href="battle_effect_visual_review.html">이펙트 시각 검토</a>
      <a href="../out/battle_analysis_summary.json">통합 JSON</a>
    </nav>
  </header>

  <section>
    <div class="section-head"><h2>요약</h2><span class="muted">{report['metrics']['jsonFiles']} JSON evidence files</span></div>
    <div class="body metrics">
      <div class="metric"><strong>{report['metrics']['jsonFiles']}</strong><span class="muted">전투 JSON 근거</span></div>
      <div class="metric"><strong>{report['metrics']['groupCount']}</strong><span class="muted">통합 그룹</span></div>
      <div class="metric"><strong>1</strong><span class="muted">사용자용 전투 분석 허브</span></div>
      <div class="metric"><strong>JSON</strong><span class="muted">세부 근거 보존 형식</span></div>
    </div>
  </section>

  <section>
    <div class="section-head"><h2>핵심 결론</h2><span class="muted">confirmed battle pipeline</span></div>
    <div class="body cards">
      {''.join(f'<article class="card"><h3>{escape(h["title"])}</h3><p>{escape(h["body"])}</p><p><a href="../out/{escape(h["json"])}">{escape(h["json"])}</a></p></article>' for h in HIGHLIGHTS)}
    </div>
  </section>

  <section>
    <div class="section-head"><h2>계산 공식</h2><span class="muted">runner/engine reference</span></div>
    <div class="body cards">
      {''.join(f'<article class="card"><h3>{escape(title)}</h3><pre>{escape(code)}</pre></article>' for title, code in FORMULAS)}
    </div>
  </section>

  <section id="battleNumericBoundaryPanel">
    <div class="section-head"><h2>전투 수치/공식 근거 위치</h2><span class="muted">canonical links</span></div>
    <div class="body cards">
      <article class="card">
        <h3>계산식/시뮬레이션</h3>
        <p>사용자용 계산식과 전투 수치 대입은 전투 공식 계산기에서 관리한다.</p>
        <p><a href="battle_formula_calculator.html">battle_formula_calculator.html</a></p>
      </article>
      <article class="card">
        <h3>원본 숫자 context</h3>
        <p>descriptor-local numeric run은 근거 보존 자료로 남기되 enemy row/stat/reward/formula로 승격하지 않는다.</p>
        <p><a href="battle_formula_calculator.html">battle_formula_calculator.html</a> · <a href="../out/battle_formula_calculator_notes.json">battle_formula_calculator_notes.json</a></p>
      </article>
      <article class="card" id="numericTriagePanel">
        <h3>숫자 후보 triage</h3>
        <p>구 battle review의 직접 앵커 호환 지점이다. 현재 triage 세부 근거는 구조화 JSON과 통합 목록에서 추적한다.</p>
        <p><a href="../out/battle_analysis_summary.json">battle_analysis_summary.json</a></p>
      </article>
    </div>
  </section>

  {''.join(f'''<section id="{escape(group['id'])}">
    <div class="section-head"><h2>{escape(group['title'])}</h2><span class="muted">{len(group['entries'])} files</span></div>
    <div class="body">
      <p class="muted">{escape(group['desc'])}</p>
      <div class="table-wrap"><table>
        <thead><tr><th>근거 JSON</th><th>Evidence</th><th>상태</th><th>요약</th><th>크기</th></tr></thead>
        <tbody>
          {''.join(f'<tr><td><a href="../out/{escape(e["json"])}"><code>{escape(e["json"])}</code></a></td><td class="evidence {escape(e["evidence"])}">{escape(e["evidence"])}</td><td class="status">{escape(e["status"] or "n/a")}</td><td>{escape(e["summary"] or e["title"])}</td><td>{e["size"]:,} B</td></tr>' for e in group['entries'])}
        </tbody>
      </table></div>
    </div>
  </section>''' for group in GROUPS)}

  <script>
    window.HWANSE_BATTLE_ANALYSIS_SUMMARY = {json.dumps({'jsonEvidenceCount': report['metrics']['jsonFiles'], 'groupCount': report['metrics']['groupCount'], 'userPage': 'web/battle_analysis.html'}, ensure_ascii=False)};
  </script>
</main>
</body>
</html>
'''
(WEB / 'battle_analysis.html').write_text(html, encoding='utf-8')
print('wrote', OUT / 'battle_analysis_summary.json')
print('wrote', WEB / 'battle_analysis.html')
