#!/usr/bin/env python3
"""Summarize possible original save-point event hints from text/event evidence."""
from __future__ import annotations

import argparse
import html
import json
import re
from collections import Counter
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"

FIELD_MAP_RE = re.compile(r"^map\d+_[0-9a-z]+\.cns$")

USER_MEMORY = (
    "Player memory says original saving was not available anywhere from the field menu. "
    "Saving happened through specific event interactions: village inns, field statues, "
    "and some HP-recovery NPCs."
)

CATEGORY_RULES = {
    "save-ui": {
        "keywords": ["데이터 저장", "데이터 로드", "데이터가 없습니다"],
        "description": "original save/load UI text; not an event save-point by itself",
    },
    "inn-context": {
        "keywords": ["여관", "숙박", "하룻밤"],
        "description": "inn or lodging dialogue context",
    },
    "recovery-context": {
        "keywords": ["회복", "치료", "체력", "상처"],
        "description": "HP/recovery wording that may relate to recovery NPCs, but also has UI/battle false positives",
    },
    "statue-shrine-context": {
        "keywords": ["석상", "신상", "신의 산", "신의 사자"],
        "description": "statue/shrine wording; exact statue words are stronger than location-only wording",
    },
}

EXACT_STATUE_WORDS = {"석상", "신상"}
SYSTEM_SAVE_WORDS = {"데이터 저장", "데이터 로드", "데이터가 없습니다", "파일 기록 실패", "파일 읽기 실패"}
SYSTEM_OR_BATTLE_CLASSES = {"system-text-like", "battle-dialogue-like"}


def load_json(path: Path) -> dict:
    return json.loads(path.read_text(encoding="utf-8"))


def matches(text: str, keywords: list[str]) -> list[str]:
    return [keyword for keyword in keywords if keyword in text]


def unique(values: list[str]) -> list[str]:
    seen = set()
    result = []
    for value in values:
        if value in seen:
            continue
        seen.add(value)
        result.append(value)
    return result


def block_text_lines(block: dict) -> list[str]:
    return [row.get("text", "") for row in block.get("textLines") or [] if row.get("text")]


def block_field_maps(block: dict) -> list[str]:
    names = []
    for name in block.get("resourceNames") or []:
        if FIELD_MAP_RE.match(name):
            names.append(Path(name).stem)
    for name in block.get("fieldMaps") or []:
        if name.endswith(".cns"):
            names.append(Path(name).stem)
        else:
            names.append(name)
    return unique(names)


def classify_event_candidate(block: dict, category: str, keywords: list[str], matched_lines: list[str]) -> tuple[str, str]:
    classification = block.get("classification") or ""
    keyword_set = set(keywords)
    if category == "save-ui":
        return "system-ui-not-savepoint", "Direct save/load words are in a system/menu block, not a map event proof."
    if category == "inn-context":
        if classification in SYSTEM_OR_BATTLE_CLASSES:
            return "weak", "Inn word appears in a system/battle-like block."
        return "medium", "Inn wording matches the player-memory save-point type, but no save opcode/event has been proven."
    if category == "recovery-context":
        if keyword_set <= {"체력"} or classification in SYSTEM_OR_BATTLE_CLASSES:
            return "false-positive-likely", "Recovery wording is status/battle/system-like here."
        return "weak", "Recovery wording exists in dialogue context, but it does not prove a recovery NPC save event."
    if category == "statue-shrine-context":
        if keyword_set & EXACT_STATUE_WORDS:
            return "medium", "Exact statue/shrine wording matches the player-memory save-point type."
        return "weak-location-only", "Only broad shrine/mountain wording is present; no exact statue text was found."
    return "weak", "Keyword-only match."


def event_block_candidates(event_dialogue_blocks: dict) -> list[dict]:
    rows = []
    for block in event_dialogue_blocks.get("blocks") or []:
        lines = block_text_lines(block)
        joined = "\n".join(lines)
        for category, rule in CATEGORY_RULES.items():
            hit_keywords = matches(joined, rule["keywords"])
            if not hit_keywords:
                continue
            matched_lines = [
                line
                for line in lines
                if any(keyword in line for keyword in hit_keywords)
            ]
            strength, reason = classify_event_candidate(block, category, hit_keywords, matched_lines)
            rows.append({
                "source": "event_dialogue_blocks",
                "blockId": block.get("blockId"),
                "rangeHex": f"{block.get('startVaHex')}..{block.get('endVaHex')}",
                "classification": block.get("classification"),
                "category": category,
                "strength": strength,
                "keywords": hit_keywords,
                "fieldMaps": block_field_maps(block),
                "resources": block.get("resourceNames") or [],
                "matchedLines": matched_lines[:12],
                "sampleText": block.get("sampleText") or lines[:12],
                "reason": reason,
            })
    return rows


def text_table_findings(text_tables: dict) -> list[dict]:
    findings = []
    for table in text_tables.get("tables") or []:
        for entry in table.get("entries") or []:
            text = entry.get("text") or ""
            categories = [
                category
                for category, rule in CATEGORY_RULES.items()
                if matches(text, rule["keywords"])
            ]
            if not categories:
                continue
            findings.append({
                "source": "text_tables",
                "table": table.get("key"),
                "tableTitle": table.get("title"),
                "category": categories[0],
                "strength": "false-positive-likely",
                "text": text,
                "refVaHex": entry.get("refVaHex"),
                "textVaHex": entry.get("textVaHex"),
                "reason": "This is a named UI/item/action table entry, not an event save-point script.",
            })
    return findings


def korean_text_findings(korean_text_candidates: dict) -> list[dict]:
    findings = []
    for row in korean_text_candidates.get("candidates") or []:
        text = row.get("text") or ""
        hit_keywords = [word for word in SYSTEM_SAVE_WORDS if word in text]
        if not hit_keywords:
            continue
        findings.append({
            "source": "korean_text_candidates",
            "category": "save-ui",
            "strength": "system-ui-not-savepoint",
            "text": text,
            "vaHex": row.get("vaHex"),
            "classification": row.get("classification"),
            "refCount": row.get("refCount", 0),
            "keywords": hit_keywords,
            "reason": "System file/save string evidence supports the save UI/path, not a map save-point trigger.",
        })
    return findings


def build_summary(
    event_dialogue_blocks: dict,
    korean_text_candidates: dict,
    text_tables: dict,
) -> dict:
    candidates = event_block_candidates(event_dialogue_blocks)
    table_findings = text_table_findings(text_tables)
    system_findings = korean_text_findings(korean_text_candidates)
    candidate_counts = Counter(row["category"] for row in candidates)
    strength_counts = Counter(row["strength"] for row in candidates)
    inn_candidates = [row for row in candidates if row["category"] == "inn-context"]
    exact_statue_candidates = [
        row
        for row in candidates
        if row["category"] == "statue-shrine-context"
        and set(row.get("keywords") or []) & EXACT_STATUE_WORDS
    ]
    recovery_candidates = [
        row
        for row in candidates
        if row["category"] == "recovery-context"
        and row.get("strength") not in {"false-positive-likely"}
        and set(row.get("keywords") or []) & {"회복", "치료"}
    ]
    save_ui_candidates = [row for row in candidates if row["category"] == "save-ui"]
    return {
        "scope": "First-pass save-point candidate index from event dialogue blocks and Korean text tables.",
        "userMemory": USER_MEMORY,
        "eventBlockCount": event_dialogue_blocks.get("blockCount", 0),
        "candidateEventBlockCount": len(candidates),
        "candidateCounts": dict(sorted(candidate_counts.items())),
        "strengthCounts": dict(sorted(strength_counts.items())),
        "innContextCount": len(inn_candidates),
        "recoveryNpcContextCount": len(recovery_candidates),
        "exactStatueMentionCount": len(exact_statue_candidates),
        "saveUiTextBlockCount": len(save_ui_candidates),
        "textTableFalsePositiveCount": len(table_findings),
        "systemSaveTextFindingCount": len(system_findings),
        "exactSavePointScriptProof": False,
        "originalSavePointRuntimeImplemented": False,
        "conclusion": (
            "This pass separates original save/load UI text from possible map-event save-point contexts. "
            "The only direct save text found in event dialogue blocks is system/menu text, while inn wording "
            "appears in several dialogue contexts. No exact 석상/신상 text and no proven HP-recovery NPC save "
            "event were found yet, so browser 임시 저장 remains a debug path and original save-point behavior "
            "is still unimplemented."
        ),
        "candidates": candidates,
        "nonSavePointTextFindings": table_findings + system_findings,
    }


def escape_cell(text: str) -> str:
    return str(text).replace("|", "\\|")


def sample_text(row: dict, limit: int = 8) -> str:
    return " / ".join((row.get("matchedLines") or row.get("sampleText") or [])[:limit]) or "-"


def candidate_sort_key(row: dict) -> tuple[int, str, str]:
    category_order = {
        "inn-context": 0,
        "statue-shrine-context": 1,
        "recovery-context": 2,
        "save-ui": 3,
    }
    strength_order = {
        "medium": 0,
        "weak": 1,
        "weak-location-only": 2,
        "false-positive-likely": 3,
        "system-ui-not-savepoint": 4,
    }
    return (
        category_order.get(row.get("category"), 9),
        strength_order.get(row.get("strength"), 9),
        row.get("blockId") or "",
    )


def runtime_candidates(summary: dict) -> dict:
    candidates = []
    for row in sorted(summary.get("candidates") or [], key=candidate_sort_key):
        candidates.append({
            "source": row.get("source"),
            "blockId": row.get("blockId"),
            "rangeHex": row.get("rangeHex"),
            "category": row.get("category"),
            "strength": row.get("strength"),
            "keywords": row.get("keywords") or [],
            "fieldMaps": row.get("fieldMaps") or [],
            "resources": row.get("resources") or [],
            "matchedLines": row.get("matchedLines") or [],
            "sampleText": row.get("sampleText") or [],
            "reason": row.get("reason") or "",
        })
    return {
        "scope": summary.get("scope"),
        "userMemory": summary.get("userMemory"),
        "candidateCount": len(candidates),
        "innContextCount": summary.get("innContextCount", 0),
        "recoveryNpcContextCount": summary.get("recoveryNpcContextCount", 0),
        "exactStatueMentionCount": summary.get("exactStatueMentionCount", 0),
        "saveUiTextBlockCount": summary.get("saveUiTextBlockCount", 0),
        "exactSavePointScriptProof": summary.get("exactSavePointScriptProof") is True,
        "originalSavePointRuntimeImplemented": summary.get("originalSavePointRuntimeImplemented") is True,
        "candidates": candidates,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Point Candidates",
        "",
        summary["conclusion"],
        "",
        f"- event dialogue blocks scanned: {summary['eventBlockCount']}",
        f"- candidate event rows: {summary['candidateEventBlockCount']}",
        f"- inn contexts: {summary['innContextCount']}",
        f"- recovery NPC contexts: {summary['recoveryNpcContextCount']}",
        f"- exact statue/shrine mentions: {summary['exactStatueMentionCount']}",
        f"- save UI text blocks: {summary['saveUiTextBlockCount']}",
        f"- exact save-point script proof: {summary['exactSavePointScriptProof']}",
        f"- original save-point runtime implemented: {summary['originalSavePointRuntimeImplemented']}",
        "",
        "## Player Memory",
        "",
        summary["userMemory"],
        "",
        "## Event Candidates",
        "",
        "| block | category | strength | keywords | field maps | matched text | reason |",
        "| --- | --- | --- | --- | --- | --- | --- |",
    ]
    for row in sorted(summary.get("candidates") or [], key=candidate_sort_key):
        lines.append(
            f"| `{row.get('blockId')}` | {row.get('category')} | {row.get('strength')} | "
            f"{escape_cell(', '.join(row.get('keywords') or []))} | "
            f"{escape_cell(', '.join(row.get('fieldMaps') or []) or '-')} | "
            f"{escape_cell(sample_text(row))} | {escape_cell(row.get('reason') or '-')} |"
        )
    if not summary.get("candidates"):
        lines.append("| - | - | - | - | - | - | - |")
    lines.extend([
        "",
        "## Non-Save-Point Text Findings",
        "",
        "| source | category | strength | text/table | reason |",
        "| --- | --- | --- | --- | --- |",
    ])
    for row in summary.get("nonSavePointTextFindings") or []:
        label = row.get("text") or row.get("tableTitle") or row.get("table") or "-"
        if row.get("table"):
            label = f"{row.get('table')}: {label}"
        lines.append(
            f"| {row.get('source')} | {row.get('category')} | {row.get('strength')} | "
            f"{escape_cell(label)} | {escape_cell(row.get('reason') or '-')} |"
        )
    if not summary.get("nonSavePointTextFindings"):
        lines.append("| - | - | - | - | - |")
    lines.extend([
        "",
        "## Next Work",
        "",
        "- Trace the event/object VM opcode that opens original save UI from inn/statue/recovery interactions.",
        "- Find exact map/object coordinates for any candidate save-point NPC or statue before enabling original save behavior in `/web/index.html`.",
        "- Keep browser-local `임시 저장/임시 불러오기` separate from original save-point rules.",
    ])
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    candidate_rows = []
    for row in sorted(summary.get("candidates") or [], key=candidate_sort_key):
        candidate_rows.append(
            "<tr>"
            f"<td><code>{html.escape(str(row.get('blockId')))}</code></td>"
            f"<td>{html.escape(str(row.get('category')))}</td>"
            f"<td>{html.escape(str(row.get('strength')))}</td>"
            f"<td>{html.escape(', '.join(row.get('keywords') or []))}</td>"
            f"<td>{html.escape(', '.join(row.get('fieldMaps') or []) or '-')}</td>"
            f"<td>{html.escape(sample_text(row))}</td>"
            f"<td>{html.escape(row.get('reason') or '-')}</td>"
            "</tr>"
        )
    non_save_rows = []
    for row in summary.get("nonSavePointTextFindings") or []:
        label = row.get("text") or row.get("tableTitle") or row.get("table") or "-"
        if row.get("table"):
            label = f"{row.get('table')}: {label}"
        non_save_rows.append(
            "<tr>"
            f"<td>{html.escape(str(row.get('source')))}</td>"
            f"<td>{html.escape(str(row.get('category')))}</td>"
            f"<td>{html.escape(str(row.get('strength')))}</td>"
            f"<td>{html.escape(str(label))}</td>"
            f"<td>{html.escape(row.get('reason') or '-')}</td>"
            "</tr>"
        )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Point Candidates</title>",
        "  <style>body{font-family:system-ui,sans-serif;background:#101010;color:#eee;margin:24px}table{border-collapse:collapse;width:100%;margin:16px 0 28px}td,th{border:1px solid #333;padding:6px 8px;vertical-align:top}th{background:#1d1d1d}code{color:#f5d76e}.badge{display:inline-block;margin-right:12px}</style>",
        "</head>",
        "<body>",
        "  <h1>Save Point Candidates</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p>{html.escape(summary['userMemory'])}</p>",
        "  <p>"
        f"<span class=\"badge\">inn contexts: {summary['innContextCount']}</span>"
        f"<span class=\"badge\">recovery contexts: {summary['recoveryNpcContextCount']}</span>"
        f"<span class=\"badge\">exact statue mentions: {summary['exactStatueMentionCount']}</span>"
        f"<span class=\"badge\">save UI blocks: {summary['saveUiTextBlockCount']}</span>"
        f"<span class=\"badge\">script proof: {summary['exactSavePointScriptProof']}</span>"
        "</p>",
        "  <h2>Event Candidates</h2>",
        "  <table><thead><tr><th>block</th><th>category</th><th>strength</th><th>keywords</th><th>field maps</th><th>matched text</th><th>reason</th></tr></thead>",
        f"  <tbody>{''.join(candidate_rows) or '<tr><td colspan=\"7\">No candidates.</td></tr>'}</tbody></table>",
        "  <h2>Non-Save-Point Text Findings</h2>",
        "  <table><thead><tr><th>source</th><th>category</th><th>strength</th><th>text/table</th><th>reason</th></tr></thead>",
        f"  <tbody>{''.join(non_save_rows) or '<tr><td colspan=\"5\">No findings.</td></tr>'}</tbody></table>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_point_candidates.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "save_point_candidates.html").write_text(html_page(summary), encoding="utf-8")
    (out_dir / "save_point_candidates_runtime.js").write_text(
        "window.HWANSE_SAVE_POINT_CANDIDATES = "
        + json.dumps(runtime_candidates(summary), ensure_ascii=False, separators=(",", ":"))
        + ";\n",
        encoding="utf-8",
    )


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--event-dialogue", type=Path, default=OUT / "event_dialogue_blocks.json")
    parser.add_argument("--korean-text", type=Path, default=OUT / "korean_text_candidates.json")
    parser.add_argument("--text-tables", type=Path, default=OUT / "text_tables.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    summary = build_summary(
        load_json(args.event_dialogue),
        load_json(args.korean_text),
        load_json(args.text_tables),
    )
    write_outputs(summary, args.out_dir)


if __name__ == "__main__":
    main()
