"""고급 규칙 기반 + 휴리스틱 스레드 정제 스크립트.

각 스레드를 분석하여:
1. 노이즈 판별 및 필터링
2. Q&A 추출 및 원자적 분리
3. 고품질 위키 항목 생성
"""
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Optional

# ---------------------------------------------------------------------------
# 정규식 패턴
# ---------------------------------------------------------------------------

# 질문 패턴
RE_QUESTION = re.compile(
    r"(질문\s*드립니다|질문드려요|질문이요|문의\s*드립니다|문의드려요|"
    r"궁금합니다|궁금한데요|궁금해요|여쭤봅니다|여쭈어봅니다|"
    r"확인\s*부탁|어떻게\s*해야|어떻게\s*되나요|"
    r"가능한가요|되나요|할\s*수\s*있나요|"
    r"혹시.*알\s*수|아시는\s*분|도움.*부탁|"
    r"이런\s*경우|이\s*경우에|이때)",
    re.IGNORECASE,
)

# 물음표가 포함된 문장 (질문 가능성)
RE_HAS_QUESTION_MARK = re.compile(r"\?|할까요|인가요|나요|인데요|인건가|될까|일까")

# 노이즈 인사/축하 패턴
RE_NOISE_ONLY = re.compile(
    r"^(안녕하세요[~!.^]*|감사합니다[~!.^]*|고맙습니다[~!.^]*|"
    r"넵[~!.^]*|네[~!.^]*|ㅋ{2,}|ㅎ{2,}|"
    r"뿌듯.*|축하.*|대단.*|👏+|🎉+|👍+|💪+|"
    r"화이팅.*|파이팅.*|수고.*|"
    r"반갑습니다.*|잘\s*부탁.*|"
    r"정말.*감사.*|진짜.*감사.*)$",
    re.IGNORECASE,
)

# 공지/광고 패턴
RE_ANNOUNCEMENT = re.compile(
    r"(🚨\s*공지|📢|공지사항|[줌zoom]\s*특강|무료\s*강의|"
    r"신청\s*링크|등록\s*링크|오프라인\s*교육|세미나|"
    r"홍보|광고|이벤트|프로모션)",
    re.IGNORECASE,
)

# 전화번호 마스킹
RE_PHONE = re.compile(r"010-\d{3,4}-\d{4}")

# 오픈채팅봇 메시지 패턴
RE_BOT_WELCOME = re.compile(r"(📚|한 발 앞서가는|━━━|여기서 배우고)")

# 제목 정리용 노이즈 제거
RE_TITLE_NOISE = re.compile(
    r"(안녕하세요[~!.^😄😊]*\s*|질문\s*드립니다[~!.]*\s*|"
    r"#궁금증\s*|질문이요[~!.]*\s*|"
    r"네\s+|넵\s+|ㅠ+\s*|ㅜ+\s*|"
    r"^\s*다시\s+|^\s*혹시\s+)",
    re.IGNORECASE,
)

# 카테고리 키워드
CATEGORY_KEYWORDS = {
    "보상": [
        "보상", "보험금", "청구", "지급", "면책", "부지급", "손해사정",
        "수술비", "입원비", "통원비", "실비", "실손", "진단비",
        "후유장해", "장해", "사망보험금", "상해", "질병",
        "3대비급여", "비급여", "MRI", "도수치료", "치료비",
    ],
    "고지의무": [
        "고지", "고지의무", "고지위반", "계약전알릴의무",
        "알릴의무", "통보의무", "건강고지", "과거병력",
        "건강이음", "건강보험", "처방", "약처방", "투약",
        "할증", "부담보", "인수", "심사",
    ],
    "약관": [
        "약관", "특약", "보장", "면책", "면책기간",
        "보험기간", "납입", "갱신", "비갱신",
        "자동갱신", "해지", "환급", "해지환급금",
        "보장개시", "대기기간", "소멸시효",
    ],
    "상품": [
        "상품", "가입", "설계", "보험료", "보험사",
        "삼성", "한화", "교보", "메리츠", "DB손해",
        "현대해상", "KB손해", "롯데", "AIG",
        "종신", "변액", "연금", "저축", "CI",
    ],
}

# 보험 도메인 의미 있는 키워드
INSURANCE_KEYWORDS = set()
for kws in CATEGORY_KEYWORDS.values():
    INSURANCE_KEYWORDS.update(kws)
INSURANCE_KEYWORDS.update([
    "손해사정사", "설계사", "FA", "보험설계사",
    "판례", "대법원", "법원", "분쟁", "민원",
    "금감원", "금융감독원", "소비자보호", "분쟁조정",
    "진단서", "소견서", "의무기록", "차트",
    "수술", "입원", "통원", "치료",
    "암", "뇌출혈", "심근경색", "고혈압", "당뇨",
    "갑상선", "유방", "자궁", "전립선",
    "골절", "인대", "디스크", "추간판",
    "레이저", "내시경", "복강경", "관절경",
])


# ---------------------------------------------------------------------------
# 헬퍼 함수
# ---------------------------------------------------------------------------


def mask_phone(text: str) -> str:
    return RE_PHONE.sub("***-****-****", text)


def classify_category(text: str) -> str:
    """텍스트에서 카테고리를 추론한다."""
    scores: dict[str, int] = {cat: 0 for cat in CATEGORY_KEYWORDS}
    lower = text.lower()
    for cat, keywords in CATEGORY_KEYWORDS.items():
        for kw in keywords:
            if kw in lower:
                scores[cat] += 1
    best = max(scores, key=lambda c: scores[c])
    if scores[best] == 0:
        return "기타"
    return best


def extract_keywords(text: str, max_count: int = 8) -> list[str]:
    """텍스트에서 보험 도메인 관련 의미 있는 키워드를 추출한다."""
    found: list[str] = []
    seen: set[str] = set()
    for kw in INSURANCE_KEYWORDS:
        if kw in text and kw not in seen and len(kw) >= 2:
            seen.add(kw)
            found.append(kw)
    # 추가: 한글 명사 2~6자 (도메인 키워드에 없는 것)
    ko_nouns = re.findall(r"[가-힣]{2,6}", text)
    noun_counts: dict[str, int] = {}
    for n in ko_nouns:
        if n not in seen and len(n) >= 2:
            noun_counts[n] = noun_counts.get(n, 0) + 1
    # 빈도 높은 명사 추가 (노이즈 제거)
    noise_words = {"안녕하세요", "감사합니다", "질문드립니다", "그런데", "그래서",
                   "합니다", "입니다", "습니다", "거든요", "인데요", "건데요",
                   "이렇게", "저렇게", "어떻게", "그렇게", "하는데", "되는데"}
    for noun, cnt in sorted(noun_counts.items(), key=lambda x: -x[1]):
        if noun not in noise_words and len(found) < max_count:
            found.append(noun)
            seen.add(noun)
    return found[:max_count]


def is_noise_thread(thread: dict) -> tuple[bool, str]:
    """스레드가 노이즈인지 판별한다. (is_noise, reason)"""
    msgs = thread["messages"]
    contents = [m["content"] for m in msgs]
    all_text = "\n".join(contents)
    users = set(m["user"] for m in msgs)

    # 1. 봇 환영 메시지만 있는 스레드
    if all(RE_BOT_WELCOME.search(c) for c in contents):
        return True, "봇 환영 메시지만 있음"

    # 2. 인사/축하만 있는 스레드
    non_noise = [c for c in contents if not RE_NOISE_ONLY.match(c.strip())]
    if not non_noise:
        return True, "인사/축하만 있음"

    # 3. 공지/광고만 있는 스레드 (Q&A 없이)
    has_announcement = any(RE_ANNOUNCEMENT.search(c) for c in contents)
    has_question = any(RE_QUESTION.search(c) for c in contents)
    has_question_mark = any("?" in c or RE_HAS_QUESTION_MARK.search(c) for c in contents)
    if has_announcement and not has_question and not has_question_mark:
        return True, "공지/광고만 있음"

    # 4. Q&A 구조 검증: 최소 2명 참여 + 질문 패턴 필요
    if len(users) < 2:
        if not thread.get("has_question_tag"):
            return True, "혼잣말 (참여자 1명)"

    # 5. 짧은 대화 + 질문 패턴 없음
    if len(msgs) <= 3 and not has_question and not thread.get("has_question_tag"):
        insurance_related = sum(1 for kw in INSURANCE_KEYWORDS if kw in all_text)
        if insurance_related < 2:
            return True, "짧은 잡담 (보험 관련 없음)"
        if not has_question_mark:
            return True, "짧은 대화 (질문 없음)"

    # 6. 감사/감탄/인사 위주 (실질적 Q&A 없음)
    noise_msgs = sum(1 for c in contents
                     if RE_NOISE_ONLY.match(c.strip()) or
                     re.search(r"(감사|뿌듯|축하|대단|파이팅|화이팅|수고)", c))
    if noise_msgs > len(msgs) * 0.5 and not has_question:
        return True, "감사/인사/감탄 위주"

    # 7. 매우 짧은 내용
    if all(len(c.strip()) < 15 for c in contents):
        return True, "매우 짧은 내용"

    # 8. 정보 공유만 있고 Q&A가 없는 스레드 (공지 스타일)
    if not has_question and not has_question_mark and not thread.get("has_question_tag"):
        # 한 사람만 길게 말하는 경우 = 정보 공유/공지
        msg_by_user: dict[str, int] = {}
        for m in msgs:
            msg_by_user[m["user"]] = msg_by_user.get(m["user"], 0) + 1
        if msg_by_user:
            top_user_msgs = max(msg_by_user.values())
            if top_user_msgs > len(msgs) * 0.7:
                return True, "정보 공유/공지 (Q&A 아님)"

    # 9. 답변 없는 질문만 있는 스레드 (다른 사용자 응답 없음)
    if has_question and len(users) < 2:
        return True, "질문만 있고 답변 없음 (1인)"

    # 10. URL/링크 공유만 있는 스레드
    url_msgs = sum(1 for c in contents if re.search(r"https?://", c))
    if url_msgs >= len(msgs) * 0.5 and not has_question:
        return True, "URL/링크 공유만 있음"

    # 11. 보험 관련 키워드가 전혀 없는 스레드
    insurance_count = sum(1 for kw in INSURANCE_KEYWORDS if kw in all_text)
    if insurance_count == 0 and not thread.get("has_question_tag"):
        return True, "보험 관련 키워드 없음"

    # 12. 줌/특강 관련 질문 (기술적 문의 - 보험 지식 아님)
    if re.search(r"(zoom|줌|입장|접속|링크|들어가)", all_text, re.IGNORECASE):
        insurance_terms = ["보상", "고지", "청구", "약관", "수술", "입원", "보험금",
                          "실비", "실손", "진단", "장해", "사망", "상해", "질병"]
        if not any(kw in all_text for kw in insurance_terms):
            return True, "줌/기술 문의 (보험 지식 아님)"

    # 13. URL이 질문의 주요 내용인 스레드
    for c in contents:
        if c.strip().startswith("http") and len(c.strip().split("\n")[0]) > 30:
            # URL이 메시지의 주요 내용
            non_url_text = re.sub(r"https?://\S+", "", c).strip()
            if len(non_url_text) < 15:
                # URL 빼면 내용이 거의 없는 메시지가 질문인 경우
                insurance_terms = ["보상", "고지", "청구", "약관", "수술", "입원", "보험금",
                                  "실비", "실손", "진단", "장해", "사망", "상해", "질병"]
                if not any(kw in all_text for kw in insurance_terms):
                    return True, "URL 공유 (보험 질문 아님)"

    return False, ""


def find_question_answer_pairs(
    thread: dict,
) -> list[dict]:
    """스레드에서 질문-답변 쌍을 추출한다."""
    msgs = thread["messages"]
    pairs: list[dict] = []

    # #궁금증 태그가 있는 경우
    if thread.get("has_question_tag"):
        q_idx = 0
        for i, m in enumerate(msgs):
            if "#궁금증" in m["content"]:
                q_idx = i
                break
        q_msg = msgs[q_idx]
        a_msgs = [m for m in msgs[q_idx + 1:] if m["user"] != q_msg["user"]]
        pairs.append({
            "question_msg": q_msg,
            "answer_msgs": a_msgs,
            "all_msgs": msgs,
        })
        return pairs

    # 일반 스레드: 실질적 질문 패턴이 있는 메시지만 찾기
    question_indices: list[int] = []
    for i, m in enumerate(msgs):
        content = m["content"]
        # 실질적 질문 (단순 물음표가 아닌, 질문 패턴이 있는 경우)
        has_q_pattern = RE_QUESTION.search(content)
        has_q_mark = "?" in content or RE_HAS_QUESTION_MARK.search(content)
        # 질문 내용이 충분히 길어야 함 (한 줄 짜리 "네?" 같은 건 제외)
        is_substantial = len(content.strip()) >= 15
        # URL로 시작하는 메시지는 질문으로 취급하지 않음
        is_url_msg = content.strip().startswith("http")
        if has_q_pattern and is_substantial and not is_url_msg:
            question_indices.append(i)
        elif has_q_mark and is_substantial and len(content.strip()) >= 20 and not is_url_msg:
            question_indices.append(i)

    if not question_indices:
        # 질문 패턴이 없으면 첫 메시지가 충분히 길고 보험 관련일 때만
        users = set(m["user"] for m in msgs)
        first_content = msgs[0]["content"]
        insurance_in_first = sum(1 for kw in INSURANCE_KEYWORDS if kw in first_content)
        if len(users) >= 2 and len(first_content) >= 30 and insurance_in_first >= 1:
            question_indices = [0]
        else:
            return []  # Q&A 구조 아님

    # 각 질문에 대해 답변 찾기 (답변이 있는 경우만)
    for qi in question_indices:
        q_msg = msgs[qi]
        # 다음 질문 위치 또는 끝
        qi_pos = question_indices.index(qi)
        next_q = question_indices[qi_pos + 1] if qi_pos < len(question_indices) - 1 else len(msgs)
        a_msgs = [m for m in msgs[qi + 1:next_q] if m["user"] != q_msg["user"]]

        # 답변이 있어야만 유효한 Q&A
        meaningful_answers = [m for m in a_msgs
                              if not RE_NOISE_ONLY.match(m["content"].strip())
                              and len(m["content"].strip()) >= 5]

        if meaningful_answers:
            pairs.append({
                "question_msg": q_msg,
                "answer_msgs": a_msgs,
                "all_msgs": msgs[qi:next_q] if qi_pos < len(question_indices) - 1 else msgs[qi:],
            })

    return pairs


def clean_title(question_text: str, answer_text: str = "") -> str:
    """질문+답변 텍스트에서 핵심 주제 제목을 생성한다."""
    full_text = question_text + " " + answer_text

    # 여러 줄이면 가장 의미 있는 줄 선택
    lines = [l.strip() for l in question_text.split("\n") if l.strip()]
    lines = [l for l in lines if not l.startswith("#궁금증") and "유형 :" not in l
             and not l.startswith("http") and not RE_NOISE_ONLY.match(l)]

    # 보험 키워드가 포함된 줄 우선 선택
    scored_lines: list[tuple[int, str]] = []
    for line in lines:
        cleaned = RE_TITLE_NOISE.sub("", line).strip()
        cleaned = re.sub(r"[~!.^😄😊🫡]+$", "", cleaned).strip()
        if len(cleaned) < 5:
            continue
        score = sum(1 for kw in INSURANCE_KEYWORDS if kw in cleaned) * 10
        score += min(len(cleaned), 60)  # 적절한 길이 보너스
        if "?" in cleaned or RE_HAS_QUESTION_MARK.search(cleaned):
            score += 5  # 질문형 보너스
        scored_lines.append((score, cleaned))

    scored_lines.sort(key=lambda x: -x[0])
    best_line = scored_lines[0][1] if scored_lines else ""

    if not best_line:
        best_line = lines[0] if lines else question_text[:50].replace("\n", " ")
        best_line = RE_TITLE_NOISE.sub("", best_line).strip()

    # 제목 정리
    title = best_line
    title = re.sub(r"[~!.^😄😊🫡]+$", "", title).strip()
    title = re.sub(r"\?+$", "", title).strip()
    title = re.sub(r"^(저기|근데|그리고|아|네|넵)\s+", "", title).strip()

    # "~인데요", "~인데", "~해요" 등 구어체 종결 정리
    title = re.sub(r"(인데요|거든요|는데요|어요|해요|이요|했어요|있어요|없어요|될까요|인가요|나요)[.~!]*$", "", title).strip()

    # 너무 길면 자르기 (핵심만)
    if len(title) > 55:
        # 문장의 핵심 부분 찾기
        for sep in [",", " ", "("]:
            idx = title[:55].rfind(sep)
            if idx > 20:
                title = title[:idx]
                break
        else:
            title = title[:52] + "..."

    # 제목에 주제어가 없으면 보험 키워드로 보강
    insurance_in_title = sum(1 for kw in INSURANCE_KEYWORDS if kw in title)
    if insurance_in_title == 0 and len(title) < 30:
        keywords = extract_keywords(full_text, 3)
        insurance_kw = [k for k in keywords if k in INSURANCE_KEYWORDS]
        if insurance_kw:
            title = f"{' '.join(insurance_kw[:2])} 관련 {title}" if title else f"{' '.join(insurance_kw[:3])} 관련 문의"

    # 최종 길이 체크
    if len(title) < 10:
        kws = extract_keywords(full_text, 4)
        if kws:
            title = f"{' '.join(kws[:3])} 관련 질문"
        else:
            title = question_text[:50].replace("\n", " ")

    return title


def summarize_answer(answer_msgs: list[dict]) -> str:
    """답변 메시지들을 요약한다. 전문가 답변 우선."""
    if not answer_msgs:
        return "답변 미확인"

    # 전문가 답변 우선 (가장 긴 답변을 가진 사용자)
    user_texts: dict[str, list[str]] = {}
    for m in answer_msgs:
        user = m["user"]
        if user not in user_texts:
            user_texts[user] = []
        user_texts[user].append(mask_phone(m["content"]))

    # 가장 많이/길게 답변한 사용자의 텍스트 우선
    scored_users = []
    for user, texts in user_texts.items():
        total_len = sum(len(t) for t in texts)
        insurance_score = sum(1 for kw in INSURANCE_KEYWORDS for t in texts if kw in t)
        scored_users.append((total_len + insurance_score * 20, user, texts))
    scored_users.sort(key=lambda x: -x[0])

    # 답변 텍스트 합치기 (전문가 우선, 기타 보충)
    all_texts = []
    for _, user, texts in scored_users:
        all_texts.extend(texts)

    combined = "\n".join(all_texts)

    # 노이즈 제거
    lines = combined.split("\n")
    meaningful_lines = []
    for line in lines:
        stripped = line.strip()
        if not stripped:
            continue
        if RE_NOISE_ONLY.match(stripped):
            continue
        if len(stripped) < 3:
            continue
        meaningful_lines.append(stripped)

    if not meaningful_lines:
        return "답변 미확인"

    result = "\n".join(meaningful_lines)

    # 너무 길면 핵심 부분만 (보험 키워드가 포함된 줄 우선)
    if len(result) > 400:
        # 보험 키워드 포함 줄 우선 선택
        important_lines = []
        other_lines = []
        for line in meaningful_lines:
            if any(kw in line for kw in INSURANCE_KEYWORDS):
                important_lines.append(line)
            else:
                other_lines.append(line)
        # 중요 줄 먼저, 나머지 보충
        selected = important_lines[:8] + other_lines[:3]
        result = "\n".join(selected)
        if len(result) > 400:
            result = result[:397] + "..."

    return result


def determine_confidence(answer_msgs: list[dict], question_msg: dict) -> str:
    """답변의 신뢰도를 판단한다."""
    if not answer_msgs:
        return "low"

    all_answer_text = " ".join(m["content"] for m in answer_msgs)

    # 판례/법적 근거가 있으면 high
    if re.search(r"(판례|판결|대법원|법원|약관\s*제|조항|조문)", all_answer_text):
        return "high"

    # 전문 용어가 많으면 high
    expert_terms = sum(1 for kw in INSURANCE_KEYWORDS if kw in all_answer_text)
    if expert_terms >= 5:
        return "high"

    # 확실한 어조
    if re.search(r"(확실|분명|반드시|당연히|맞습니다|해당됩니다)", all_answer_text):
        return "high"

    # 불확실한 어조
    if re.search(r"(아마|것 같|모르겠|확인.*필요|잘 모르)", all_answer_text):
        return "low"

    return "medium"


def find_expert(answer_msgs: list[dict]) -> str:
    """답변 메시지 중 가장 전문적인 답변자를 찾는다."""
    if not answer_msgs:
        return ""

    user_scores: dict[str, int] = {}
    for m in answer_msgs:
        user = m["user"]
        content = m["content"]
        score = len(content)  # 긴 답변 = 더 전문적
        # 보험 전문 용어 보너스
        for kw in INSURANCE_KEYWORDS:
            if kw in content:
                score += 10
        user_scores[user] = user_scores.get(user, 0) + score

    if not user_scores:
        return ""

    return max(user_scores, key=lambda u: user_scores[u])


# ---------------------------------------------------------------------------
# 메인 정제 로직
# ---------------------------------------------------------------------------


def refine_thread(thread: dict, global_index: int) -> list[dict]:
    """스레드를 정제하여 wiki_entry 리스트를 반환한다."""
    # 노이즈 판별
    is_noise, reason = is_noise_thread(thread)
    if is_noise:
        return []

    # Q&A 쌍 추출
    pairs = find_question_answer_pairs(thread)
    if not pairs:
        return []

    entries: list[dict] = []
    source_date = thread.get("start_time", "").split(" ")[0] if thread.get("start_time") else ""

    for pair in pairs:
        q_msg = pair["question_msg"]
        a_msgs = pair["answer_msgs"]
        all_msgs = pair["all_msgs"]

        question_text = mask_phone(q_msg["content"])
        answer_text = summarize_answer(a_msgs)

        # 답변이 질문과 동일하면 스킵
        if answer_text.strip() == question_text.strip():
            continue

        # 제목 생성
        title = clean_title(question_text, answer_text)

        # 카테고리 분류
        full_text = question_text + " " + answer_text
        category = classify_category(full_text)

        # 전문가 찾기
        expert = find_expert(a_msgs)

        # 키워드 추출
        keywords = extract_keywords(full_text)

        # 신뢰도 판단
        confidence = determine_confidence(a_msgs, q_msg)

        # raw_thread 구성
        raw_thread = [f"[{m['user']}] {mask_phone(m['content'])}" for m in all_msgs]

        entry = {
            "id": f"kakao-{global_index:03d}",
            "title": title,
            "category": category,
            "subcategory": "",
            "question": question_text,
            "answer": answer_text,
            "expert": expert,
            "source_date": source_date,
            "source_chat": "앞서가는설계사",
            "keywords": keywords,
            "confidence": confidence,
            "raw_thread": raw_thread,
        }
        entries.append(entry)
        global_index += 1

    return entries


def main() -> None:
    threads = json.loads(Path("/tmp/threads.json").read_text(encoding="utf-8"))

    all_entries: list[dict] = []
    noise_count = 0
    noise_reasons: dict[str, int] = {}
    total_threads = len(threads)

    for thread in threads:
        is_noise, reason = is_noise_thread(thread)
        if is_noise:
            noise_count += 1
            noise_reasons[reason] = noise_reasons.get(reason, 0) + 1
            continue

        entries = refine_thread(thread, len(all_entries) + 1)
        all_entries.extend(entries)

    # ID 재부여 (순차)
    for i, entry in enumerate(all_entries, start=1):
        entry["id"] = f"kakao-{i:03d}"

    # 품질 검증 및 필터링
    quality_issues: list[str] = []
    final_entries: list[dict] = []

    for entry in all_entries:
        title = entry.get("title", "")
        question = entry.get("question", "")
        answer = entry.get("answer", "")
        full_text = question + " " + answer

        # 제목 품질 체크
        if len(title) < 10:
            entry["title"] = clean_title(question, answer)
            title = entry["title"]

        if title.startswith("#궁금증"):
            entry["title"] = clean_title(question, answer)

        # 답변 = 질문 체크
        if answer.strip() == question.strip():
            quality_issues.append(f"  [{entry['id']}] 답변 = 질문 동일 → 스킵")
            continue

        # 카테고리 "기타"이면서 보험 키워드 부족한 항목 필터
        insurance_kw_count = sum(1 for kw in INSURANCE_KEYWORDS if kw in full_text)
        if entry.get("category") == "기타" and insurance_kw_count < 2:
            quality_issues.append(f"  [{entry['id']}] 카테고리 기타 + 보험 키워드 부족 → 스킵")
            continue

        # URL이 질문의 주요 내용인 경우 필터
        if question.strip().startswith("http") or question.strip().startswith("톡게시판"):
            non_url = re.sub(r"https?://\S+", "", question).strip()
            if insurance_kw_count < 2:
                quality_issues.append(f"  [{entry['id']}] URL 기반 질문 → 스킵")
                continue

        # 키워드 부족 체크
        if len(entry.get("keywords", [])) < 3:
            entry["keywords"] = extract_keywords(full_text)

        # 제목에서 URL 제거
        entry["title"] = re.sub(r"https?://\S+", "", entry["title"]).strip()
        if len(entry["title"]) < 10:
            entry["title"] = clean_title(question, answer)

        final_entries.append(entry)

    # 최종 ID 재부여
    for i, entry in enumerate(final_entries, start=1):
        entry["id"] = f"kakao-{i:03d}"

    # 카테고리 분포
    cat_dist: dict[str, int] = {}
    for entry in final_entries:
        cat = entry.get("category", "기타")
        cat_dist[cat] = cat_dist.get(cat, 0) + 1

    # confidence 분포
    conf_dist: dict[str, int] = {}
    for entry in final_entries:
        conf = entry.get("confidence", "medium")
        conf_dist[conf] = conf_dist.get(conf, 0) + 1

    # 저장
    output_path = Path("/tmp/refined_all.json")
    output_path.write_text(
        json.dumps(final_entries, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )

    # 통계 출력
    print(f"=== 정제 결과 ===")
    print(f"전체 스레드: {total_threads}개")
    print(f"노이즈 필터링: {noise_count}개")
    print(f"  - 노이즈 사유:")
    for reason, cnt in sorted(noise_reasons.items(), key=lambda x: -x[1]):
        print(f"    {reason}: {cnt}개")
    print(f"최종 위키 항목: {len(final_entries)}개")
    print(f"\n카테고리 분포:")
    for cat, cnt in sorted(cat_dist.items(), key=lambda x: -x[1]):
        print(f"  {cat}: {cnt}건")
    print(f"\n신뢰도 분포:")
    for conf, cnt in sorted(conf_dist.items(), key=lambda x: -x[1]):
        print(f"  {conf}: {cnt}건")
    if quality_issues:
        print(f"\n품질 이슈 ({len(quality_issues)}건):")
        for issue in quality_issues[:30]:
            print(issue)
    print(f"\n저장: {output_path}")
    print(f"파일 크기: {output_path.stat().st_size / 1024:.1f}KB")

    # 샘플 5건 출력
    print(f"\n=== 샘플 5건 ===")
    for entry in final_entries[:5]:
        print(f"\n[{entry['id']}] {entry['title']}")
        print(f"  카테고리: {entry['category']} | 신뢰도: {entry['confidence']}")
        print(f"  질문: {entry['question'][:80]}...")
        print(f"  답변: {entry['answer'][:80]}...")
        print(f"  전문가: {entry['expert']}")
        print(f"  키워드: {', '.join(entry['keywords'][:5])}")


if __name__ == "__main__":
    main()
