# -*- coding: utf-8 -*-
"""
나무위키 조선 왕 '썰 키워드' 스크래퍼
====================================

조선 27대 왕의 나무위키 문서에서 목차(섹션 제목)를 긁어와
숏폼 소재가 될 만한 '썰/일화 키워드'를 추출해 JSON / CSV 로 저장한다.

나무위키는 Cloudflare 보호 + JS 렌더링이라 requests 로는 막히므로
Playwright(Chromium)로 실제 브라우저를 띄워 렌더링된 목차를 읽는다.

설치:
    pip install playwright beautifulsoup4
    playwright install chromium   # (이미 설치돼 있으면 생략)

실행:
    python namu_joseon_scraper.py

옵션:
    python namu_joseon_scraper.py --headful   # 브라우저 창 보이게
    python namu_joseon_scraper.py --only 세종 광해군   # 특정 왕만
"""

import argparse
import asyncio
import csv
import json
import re
import sys
from datetime import datetime
from pathlib import Path

from playwright.async_api import async_playwright

# ----------------------------------------------------------------------------
# 설정
# ----------------------------------------------------------------------------
BASE = "https://namu.wiki/w/"
OUT_DIR = Path(__file__).parent / "data"   # 조선왕 일화 CSV/JSON(선행 데이터). ★ output/ 아님 — 거긴 mp4만.

# 조선 27대 왕: (대수, 묘호/군호, 나무위키 문서 제목)
# 군주는 보통 "묘호(조선)" 형태, 폐위된 임금(연산군/광해군)은 군호 단독.
JOSEON_KINGS = [
    (1,  "태조",   "태조(조선)"),
    (2,  "정종",   "정종(조선)"),
    (3,  "태종",   "태종(조선)"),
    (4,  "세종",   "세종(조선)"),
    (5,  "문종",   "문종(조선)"),
    (6,  "단종",   "단종(조선)"),
    (7,  "세조",   "세조(조선)"),
    (8,  "예종",   "예종(조선)"),
    (9,  "성종",   "성종(조선)"),
    (10, "연산군", "연산군"),
    (11, "중종",   "중종(조선)"),
    (12, "인종",   "인종(조선)"),
    (13, "명종",   "명종(조선)"),
    (14, "선조",   "선조(조선)"),
    (15, "광해군", "광해군"),
    (16, "인조",   "인조(조선)"),
    (17, "효종",   "효종(조선)"),
    (18, "현종",   "현종(조선)"),
    (19, "숙종",   "숙종(조선)"),
    (20, "경종",   "경종(조선)"),
    (21, "영조",   "영조(조선)"),
    (22, "정조",   "정조(조선)"),
    (23, "순조",   "순조"),
    (24, "헌종",   "헌종(조선)"),
    (25, "철종",   "철종(조선)"),
    (26, "고종",   "고종(대한제국)"),
    (27, "순종",   "순종(대한제국)"),
]

# 썰/일화/논란성 키워드가 모이는 섹션 (목차 제목에 이 단어가 들어가면 '썰' 우선순위↑)
ANECDOTE_HINTS = [
    "여담", "일화", "야사", "평가", "논란", "비판", "사건", "기타",
    "이야기", "에피소드", "어록", "대중매체", "미디어", "기록", "사망", "죽음",
    "독살", "암살", "치세", "업적", "가족", "후궁", "이름", "별명",
]


def clean_heading(text: str) -> str:
    """목차 항목 텍스트 정리: 앞 번호와 [편집] 등 제거."""
    t = text.replace("[편집]", "").strip()
    # 앞쪽 "1.", "2.3.", "10." 같은 목차 번호 제거
    t = re.sub(r"^\s*[\d.]+\s*", "", t)
    return t.strip()


def is_anecdote(keyword: str) -> bool:
    return any(h in keyword for h in ANECDOTE_HINTS)


async def scrape_king(page, title: str):
    """한 왕 문서를 열고 목차 키워드 목록을 반환."""
    url = BASE + title
    result = {"title": title, "url": url, "page_title": None,
              "keywords": [], "anecdote_keywords": [], "error": None}
    try:
        resp = await page.goto(url, wait_until="domcontentloaded", timeout=45000)

        # 고정 sleep 대신 목차 요소가 뜰 때까지만 대기 (없으면 짧게 폴백)
        try:
            await page.wait_for_selector(
                '.wiki-macro-toc a, [class*="toc"] a, .toc-item a',
                timeout=8000,
            )
        except Exception:
            # 목차가 없는 문서일 수 있으니 헤딩 렌더만 잠깐 기다림
            await page.wait_for_timeout(800)

        # 문서 실제 제목
        try:
            result["page_title"] = (await page.title()).split(" - ")[0].strip()
        except Exception:
            pass

        if resp and resp.status >= 400:
            result["error"] = f"HTTP {resp.status}"
            return result

        # 렌더링된 목차/제목에서 키워드 추출 (클래스명이 자주 바뀌므로 여러 선택자 시도)
        keywords = await page.evaluate(
            """
            () => {
              const clean = s => s.replace('[편집]', '').trim();
              let items = [];

              // 1순위: 목차(table of contents)
              const tocLinks = document.querySelectorAll(
                '.wiki-macro-toc a, [class*="toc"] a, .toc-item a'
              );
              if (tocLinks.length) {
                items = Array.from(tocLinks).map(a => clean(a.textContent));
              }

              // 2순위(목차가 없으면): 본문 헤딩 태그
              if (items.length === 0) {
                const hs = document.querySelectorAll('h1,h2,h3,h4,h5,h6');
                items = Array.from(hs).map(h => clean(h.textContent));
              }
              return items.filter(Boolean);
            }
            """
        )

        # 중복 제거 + 번호 정리
        seen, cleaned = set(), []
        for raw in keywords:
            kw = clean_heading(raw)
            if kw and kw not in seen and len(kw) <= 60:
                seen.add(kw)
                cleaned.append(kw)

        result["keywords"] = cleaned
        result["anecdote_keywords"] = [k for k in cleaned if is_anecdote(k)]
    except Exception as e:
        result["error"] = f"{type(e).__name__}: {e}"
    return result


async def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--headful", action="store_true", help="브라우저 창 표시")
    parser.add_argument("--only", nargs="*", help="특정 왕만 (묘호/군호로 지정)")
    parser.add_argument("--concurrency", type=int, default=4,
                        help="동시에 열 페이지 수 (기본 4)")
    args = parser.parse_args()

    kings = JOSEON_KINGS
    if args.only:
        wanted = set(args.only)
        kings = [k for k in JOSEON_KINGS if k[1] in wanted]
        if not kings:
            print(f"[!] 매칭되는 왕 없음: {args.only}")
            print("    가능한 값:", ", ".join(k[1] for k in JOSEON_KINGS))
            return

    OUT_DIR.mkdir(parents=True, exist_ok=True)
    all_results = []

    total = len(kings)
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=not args.headful)
        context = await browser.new_context(
            user_agent=(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/124.0.0.0 Safari/537.36"
            ),
            locale="ko-KR",
        )

        # 목차 텍스트만 필요하므로 이미지/CSS/폰트/미디어는 받지 않음 (로딩 대폭 단축)
        async def _block(route):
            if route.request.resource_type in {"image", "stylesheet", "font", "media"}:
                await route.abort()
            else:
                await route.continue_()
        await context.route("**/*", _block)

        sem = asyncio.Semaphore(max(1, args.concurrency))
        done = 0

        async def worker(idx, name, title):
            nonlocal done
            async with sem:
                page = await context.new_page()
                try:
                    # 나무위키는 동시 요청 시 간헐적으로 404/차단을 주므로
                    # 실패하면 백오프하며 최대 3회까지 재시도
                    r = await scrape_king(page, title)
                    for attempt in range(3):
                        if not r["error"]:
                            break
                        await page.wait_for_timeout(1500 * (attempt + 1))
                        r = await scrape_king(page, title)
                finally:
                    await page.close()
            r["order"] = idx
            r["name"] = name
            done += 1
            if r["error"]:
                print(f"[{done:2d}/{total}] {name} ({title}) 실패: {r['error']}", flush=True)
            else:
                print(f"[{done:2d}/{total}] {name} ({title}) "
                      f"키워드 {len(r['keywords'])}개 "
                      f"(썰후보 {len(r['anecdote_keywords'])}개)", flush=True)
            return r

        all_results = await asyncio.gather(
            *(worker(idx, name, title) for idx, name, title in kings)
        )
        all_results = sorted(all_results, key=lambda r: r["order"])

        await browser.close()

    # 저장 ---------------------------------------------------------------
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")

    json_path = OUT_DIR / f"joseon_keywords_{ts}.json"
    json_path.write_text(
        json.dumps(all_results, ensure_ascii=False, indent=2), encoding="utf-8"
    )

    csv_path = OUT_DIR / f"joseon_keywords_{ts}.csv"
    with csv_path.open("w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["대수", "왕", "문서제목", "썰후보여부", "키워드", "URL"])
        for r in all_results:
            anec = set(r["anecdote_keywords"])
            for kw in r["keywords"]:
                w.writerow([r["order"], r["name"], r["title"],
                            "Y" if kw in anec else "", kw, r["url"]])

    total_kw = sum(len(r["keywords"]) for r in all_results)
    total_anec = sum(len(r["anecdote_keywords"]) for r in all_results)
    fails = [r["name"] for r in all_results if r["error"]]

    print("\n" + "=" * 50)
    print(f"완료: 왕 {len(all_results)}명 / 키워드 {total_kw}개 / 썰후보 {total_anec}개")
    if fails:
        print(f"수집 실패: {', '.join(fails)}")
    print(f"JSON -> {json_path}")
    print(f"CSV  -> {csv_path}")


if __name__ == "__main__":
    if sys.platform == "win32":
        asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
    asyncio.run(main())
