from __future__ import annotations

import logging
import os
import re
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional

from .models import ChatMessage, MessageType

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# 정규식
# ---------------------------------------------------------------------------

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

# 한국어 명사 후보 (2~6글자 한글 단어)
_RE_KO_NOUN = re.compile(r"[가-힣]{2,6}")

# 유형 추출 패턴: "#궁금증" 메시지 내 "유형 :" 뒤 텍스트
_RE_CATEGORY = re.compile(r"유형\s*:\s*(.+)")

# 스레드 시간 간격 임계값 (분)
_THREAD_GAP_MINUTES = 15  # 30 → 15로 축소


# ---------------------------------------------------------------------------
# 카테고리 매핑
# ---------------------------------------------------------------------------

_CATEGORY_KEYWORDS: dict[str, str] = {
    "보상": "보상",
    "고지의무": "고지의무",
    "약관": "약관",
    "상품": "상품",
}


def _extract_category(content: str) -> str:
    """#궁금증 메시지에서 유형 카테고리를 추출한다."""
    m = _RE_CATEGORY.search(content)
    if not m:
        return "기타"
    type_text = m.group(1).strip()
    for keyword, category in _CATEGORY_KEYWORDS.items():
        if keyword in type_text:
            return category
    return "기타"


def _mask_phone(text: str) -> str:
    """전화번호 패턴을 마스킹한다."""
    return _RE_PHONE.sub("***-****-****", text)


def _extract_keywords(text: str) -> list[str]:
    """간단한 규칙 기반 한국어 명사 추출 (2~6글자 한글 단어)."""
    words = _RE_KO_NOUN.findall(text)
    # 중복 제거 + 최대 10개
    seen: set[str] = set()
    result: list[str] = []
    for w in words:
        if w not in seen:
            seen.add(w)
            result.append(w)
        if len(result) >= 10:
            break
    return result


def _parse_datetime(date: str, time_str: str) -> Optional[datetime]:
    """날짜 + 시간 문자열을 datetime으로 변환한다."""
    if not date or not time_str:
        return None
    try:
        return datetime.strptime(f"{date} {time_str}", "%Y-%m-%d %H:%M")
    except ValueError:
        return None


# ---------------------------------------------------------------------------
# Thread 데이터클래스
# ---------------------------------------------------------------------------


@dataclass
class Thread:
    messages: list[ChatMessage] = field(default_factory=list)
    start_time: str = ""  # "YYYY-MM-DD HH:MM"
    has_question_tag: bool = False  # #궁금증 태그 존재 여부


# ---------------------------------------------------------------------------
# 스레드 분리 로직
# ---------------------------------------------------------------------------

_EXCLUDED_TYPES = frozenset(
    [
        MessageType.JOIN,
        MessageType.LEAVE,
        MessageType.KICK,
        MessageType.PHOTO,
        MessageType.EMOTICON,
        MessageType.VIDEO,
        MessageType.BOT,
        MessageType.SYSTEM,
    ]
)

# 단순 인사/리액션 패턴 (이 패턴만 있는 메시지는 노이즈)
_RE_NOISE_GREETING = re.compile(
    r"^(안녕하세요[~!.]*|감사합니다[~!.]*|고맙습니다[~!.]*|넵[~!.]*|네[~!.]*|"
    r"👏+|🎉+|👍+|💪+|ㅋ{2,}|ㅎ{2,}|ㄴㄴ|ㅇㅇ|ㅜㅜ|ㅠㅠ|"
    r"이사왔습니당?[~!.^]*|반갑습니다[~!.]*|잘 부탁드립니다[~!.]*)$",
    re.IGNORECASE,
)

# 환영 메시지 패턴 (BOT이 아닌 경우에도 필터링)
_WELCOME_PATTERNS = ("📚 한 발 앞서가는", "━━━━━", "여기서 배우고")


def _is_noise_message(msg: ChatMessage) -> bool:
    """단순 인사/리액션/환영 등 노이즈 메시지인지 판별."""
    content = msg.content.strip()
    # 환영 메시지 패턴
    for pat in _WELCOME_PATTERNS:
        if pat in content:
            return True
    # 단순 인사/리액션 (정규식)
    if _RE_NOISE_GREETING.match(content):
        return True
    return False


# 질문 시작 패턴
_RE_QUESTION_PATTERN = re.compile(
    r"(질문\s*드립니다|질문드립니다|문의\s*드립니다|궁금합니다|여쭤봅니다|확인\s*부탁)",
    re.IGNORECASE,
)


def _split_into_threads(messages: list[ChatMessage]) -> list[Thread]:
    """파싱된 메시지 리스트를 대화 스레드로 분리한다.

    분리 기준 (우선순위):
    1. #궁금증 태그 → 무조건 새 스레드
    2. 질문 패턴 (질문 드립니다, 문의 드립니다 등) → 새 스레드
    3. 날짜 변경 → 새 스레드
    4. 15분 이상 시간 gap → 새 스레드
    """
    # 입퇴장/미디어 메시지 제외
    filtered = [m for m in messages if m.type not in _EXCLUDED_TYPES]
    # 노이즈 메시지 제외
    filtered = [m for m in filtered if not _is_noise_message(m)]

    if not filtered:
        return []

    threads: list[Thread] = []
    current_thread: Thread = Thread()

    for msg in filtered:
        if not current_thread.messages:
            # 첫 메시지 — 새 스레드 시작
            current_thread.messages.append(msg)
            current_thread.start_time = f"{msg.date} {msg.time}"
            if "#궁금증" in msg.content:
                current_thread.has_question_tag = True
            continue

        prev_msg = current_thread.messages[-1]
        should_split = False

        # 1a. #궁금증 태그 → 무조건 새 스레드
        if "#궁금증" in msg.content:
            should_split = True

        # 1b. 질문 패턴 감지
        elif _RE_QUESTION_PATTERN.search(msg.content):
            should_split = True

        # 날짜 변경 → 새 스레드
        elif msg.date != prev_msg.date:
            should_split = True

        # 시간 gap 확인 (15분)
        else:
            prev_dt = _parse_datetime(prev_msg.date, prev_msg.time)
            curr_dt = _parse_datetime(msg.date, msg.time)
            if prev_dt and curr_dt:
                gap_minutes = (curr_dt - prev_dt).total_seconds() / 60
                if gap_minutes >= _THREAD_GAP_MINUTES:
                    should_split = True

        if should_split:
            threads.append(current_thread)
            current_thread = Thread()
            current_thread.messages.append(msg)
            current_thread.start_time = f"{msg.date} {msg.time}"
            if "#궁금증" in msg.content or _RE_QUESTION_PATTERN.search(msg.content):
                current_thread.has_question_tag = True
        else:
            current_thread.messages.append(msg)
            if "#궁금증" in msg.content:
                current_thread.has_question_tag = True

    if current_thread.messages:
        threads.append(current_thread)

    # 최소 2개 메시지가 있는 스레드만 유지
    return [t for t in threads if len(t.messages) >= 2]


# ---------------------------------------------------------------------------
# wiki_entry 빌더 (규칙 기반)
# ---------------------------------------------------------------------------


def _build_wiki_entry_rule_based(thread: Thread, index: int, source_chat: str) -> dict:  # type: ignore[type-arg]
    """스레드에서 규칙 기반으로 wiki_entry 딕셔너리를 생성한다."""
    msgs = thread.messages

    # #궁금증 태그가 있는 메시지를 질문 시작점으로 사용
    question_idx = 0
    for i, m in enumerate(msgs):
        if "#궁금증" in m.content:
            question_idx = i
            break

    question_msg = msgs[question_idx]
    answer_msgs = msgs[question_idx + 1 :]

    # 카테고리 추출
    category = _extract_category(question_msg.content)

    # 답변 텍스트 연결
    answer_text = "\n".join(_mask_phone(m.content) for m in answer_msgs)

    # expert: 답변 메시지에서 가장 많이 발언한 사용자
    expert = ""
    if answer_msgs:
        user_count: dict[str, int] = {}
        for m in answer_msgs:
            user_count[m.user] = user_count.get(m.user, 0) + 1
        expert = max(user_count, key=lambda u: user_count[u])

    # 제목: 첫 메시지 첫 50자
    title = question_msg.content[:50].replace("\n", " ")

    # 키워드 추출
    full_text = question_msg.content + " " + answer_text
    keywords = _extract_keywords(full_text)

    # raw_thread: 전화번호 마스킹 적용
    raw_thread = [
        f"[{m.user}] {_mask_phone(m.content)}" for m in msgs
    ]

    source_date = thread.start_time.split(" ")[0] if thread.start_time else ""

    return {
        "id": f"kakao-{index:03d}",
        "title": title,
        "category": category,
        "subcategory": "",
        "question": _mask_phone(question_msg.content),
        "answer": answer_text,
        "expert": expert,
        "source_date": source_date,
        "source_chat": source_chat,
        "keywords": keywords,
        "confidence": "medium",
        "raw_thread": raw_thread,
    }


# ---------------------------------------------------------------------------
# Anthropic Haiku LLM 분석
# ---------------------------------------------------------------------------

_LLM_PROMPT_TEMPLATE = """당신은 보험 전문 지식 정리 전문가입니다.
아래 보험설계사 오픈채팅방 대화 스레드를 분석하여 **하나의 Q&A 위키 항목**으로 정제하세요.

## 원칙
1. **원자적**: 하나의 질문-답변만 포함. 2개 이상 주제가 섞여있으면 가장 핵심 Q&A만 추출
2. **자기완결적**: 이 항목만 읽어도 완전히 이해 가능
3. **전문가 답변 중심**: 일반인 의견보다 전문가(손해사정사, 경력 설계사)의 답변을 우선
4. **노이즈 제거**: 인사, 이모티콘, 광고, 공지 내용 제외

## 출력 형식 (JSON)
{{
  "title": "핵심 주제를 한 문장으로 (예: 안저 광응고술의 수술 해당 여부)",
  "category": "보상|고지의무|약관|상품|기타",
  "question": "질문자의 핵심 궁금증 1~2문장 요약",
  "answer": "전문가 답변의 핵심 결론 2~4문장 요약. 근거나 판례도 포함.",
  "keywords": ["관련", "키워드", "5~8개"],
  "confidence": "high|medium|low",
  "is_noise": false
}}

만약 대화가 의미 있는 Q&A가 아니라면 (단순 인사, 공지, 광고 등):
{{
  "is_noise": true,
  "reason": "노이즈 사유"
}}

## 대화 스레드
{thread_text}"""


def _analyze_thread_with_llm(
    thread: Thread,
    client: object,
    index: int,
    source_chat: str,
) -> Optional[dict]:  # type: ignore[type-arg]
    """Anthropic Haiku로 스레드를 분석한다. is_noise이면 None 반환. 실패 시 규칙 기반 fallback."""
    import json as _json

    thread_text = "\n".join(
        f"[{m.user}] {_mask_phone(m.content)}" for m in thread.messages
    )
    prompt = _LLM_PROMPT_TEMPLATE.format(thread_text=thread_text)

    try:
        response = client.messages.create(  # type: ignore[union-attr]
            model="claude-haiku-4-5-20251001",
            max_tokens=1024,
            messages=[{"role": "user", "content": prompt}],
        )
        raw_text: str = response.content[0].text.strip()  # type: ignore[union-attr]

        # JSON 블록 추출 (```json ... ``` 형식 처리)
        if raw_text.startswith("```"):
            raw_text = re.sub(r"^```(?:json)?\n?", "", raw_text)
            raw_text = re.sub(r"\n?```$", "", raw_text)

        parsed = _json.loads(raw_text)

        # 노이즈 판정
        if parsed.get("is_noise", False):
            logger.info("스레드 %d: 노이즈로 판정 (%s)", index, parsed.get("reason", ""))
            return None

        # 품질 검증
        title = str(parsed.get("title", ""))
        question = str(parsed.get("question", ""))
        answer = str(parsed.get("answer", ""))
        keywords = list(parsed.get("keywords", []))

        # 제목 검증: 20자 미만이거나 원본 그대로면 규칙 기반 fallback
        if len(title) < 5 or title.startswith("#궁금증"):
            logger.warning("스레드 %d: 제목 품질 불량 — 규칙 기반으로 전환", index)
            return _build_wiki_entry_rule_based(thread, index, source_chat)

        # 답변 = 질문이면 재정제 (규칙 기반 fallback)
        if answer == question:
            logger.warning("스레드 %d: 답변이 질문과 동일 — 규칙 기반으로 전환", index)
            return _build_wiki_entry_rule_based(thread, index, source_chat)

        msgs = thread.messages
        source_date = thread.start_time.split(" ")[0] if thread.start_time else ""

        # expert: 답변 메시지에서 가장 많이 발언한 사용자
        expert = ""
        if len(msgs) > 1:
            answer_msgs = msgs[1:]
            user_count: dict[str, int] = {}
            for m in answer_msgs:
                user_count[m.user] = user_count.get(m.user, 0) + 1
            if user_count:
                expert = max(user_count, key=lambda u: user_count[u])

        raw_thread = [
            f"[{m.user}] {_mask_phone(m.content)}" for m in msgs
        ]

        return {
            "id": f"kakao-{index:03d}",
            "title": title,
            "category": str(parsed.get("category", "기타")),
            "subcategory": str(parsed.get("subcategory", "")),
            "question": question,
            "answer": answer,
            "expert": expert,
            "source_date": source_date,
            "source_chat": source_chat,
            "keywords": keywords if len(keywords) >= 3 else _extract_keywords(question + " " + answer),
            "confidence": str(parsed.get("confidence", "medium")),
            "raw_thread": raw_thread,
        }

    except Exception as exc:
        logger.warning("Anthropic API 분석 실패 (스레드 %d): %s — 규칙 기반으로 전환", index, exc)
        return _build_wiki_entry_rule_based(thread, index, source_chat)


# ---------------------------------------------------------------------------
# 메인 추출 함수
# ---------------------------------------------------------------------------


def extract_knowledge(
    messages: list[ChatMessage],
    use_llm: bool = False,
    api_key: str | None = None,
    source_chat: str = "",
) -> list[dict]:  # type: ignore[type-arg]
    """
    파싱된 메시지 리스트에서 보험 실무 지식을 추출한다.

    Parameters
    ----------
    messages:
        parse_kakao_chat() 로부터 얻은 ChatMessage 리스트
    use_llm:
        True이면 Anthropic Haiku로 고급 분석, False이면 규칙 기반만 사용
    api_key:
        Anthropic API 키. 없으면 환경변수 ANTHROPIC_API_KEY 참조
    source_chat:
        채팅방 이름 (wiki_entry의 source_chat 필드)

    Returns
    -------
    wiki_entry 딕셔너리 리스트
    """
    threads = _split_into_threads(messages)
    logger.info("총 %d개 스레드 분리됨", len(threads))

    # 규칙 기반: #궁금증 태그가 있는 스레드만 추출
    if not use_llm:
        tagged_threads = [t for t in threads if t.has_question_tag]
        logger.info("#궁금증 태그 스레드: %d개", len(tagged_threads))
        results: list[dict] = []  # type: ignore[type-arg]
        for i, thread in enumerate(tagged_threads, start=1):
            entry = _build_wiki_entry_rule_based(thread, i, source_chat)
            results.append(entry)
        return results

    # LLM 분석 경로 (Anthropic Haiku)
    resolved_key = api_key or os.environ.get("ANTHROPIC_API_KEY")
    if not resolved_key:
        logger.warning(
            "Anthropic API 키가 없습니다 (ANTHROPIC_API_KEY). "
            "규칙 기반으로 fallback합니다."
        )
        return extract_knowledge(messages, use_llm=False, source_chat=source_chat)

    try:
        import anthropic

        client = anthropic.Anthropic(api_key=resolved_key)
    except Exception as exc:
        logger.warning("Anthropic 클라이언트 초기화 실패: %s — 규칙 기반으로 fallback", exc)
        return extract_knowledge(messages, use_llm=False, source_chat=source_chat)

    llm_results: list[dict] = []  # type: ignore[type-arg]
    batch_size = 5

    for batch_start in range(0, len(threads), batch_size):
        batch = threads[batch_start : batch_start + batch_size]
        for thread in batch:
            idx = batch_start + batch.index(thread) + 1
            entry = _analyze_thread_with_llm(thread, client, idx, source_chat)
            if entry is not None:  # None = noise, 필터링
                llm_results.append(entry)
        # 배치 간 rate limit 대기 (마지막 배치 제외)
        if batch_start + batch_size < len(threads):
            time.sleep(1)

    return llm_results
