"""
로컬 LLM (llama-cpp-python) 서비스 모듈.
STT 변환 텍스트를 받아 제목, 요약, 키워드, 긴급도를 추출합니다.

[설정 방법]
1. pip install llama-cpp-python
2. .env 파일에 다음 설정 추가:
     LLM_ENABLED=true
     LLM_MODEL_PATH=D:/models/Qwen2.5-0.5B-Instruct-Q8_0.gguf
3. GGUF 모델 다운로드:
     https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF
"""
import re
import logging
from typing import Optional
from app.core.config import settings

logger = logging.getLogger("uvicorn.error")

# LLM 라이브러리 조건부 임포트 (미설치 시 graceful 처리)
try:
    from llama_cpp import Llama
    _LLAMA_AVAILABLE = True
except ImportError:
    Llama = None
    _LLAMA_AVAILABLE = False


# LLM 분석 결과 반환 구조
class LLMMetadata:
    def __init__(self, title: str = "", summary: str = "", keywords: str = "", urgency: str = "일반"):
        self.title = title
        self.summary = summary
        self.keywords = keywords
        self.urgency = urgency


# 프롬프트 템플릿 — Qwen3 계열의 think 태그를 끄는 /no_think 지시어 포함
_SYSTEM_PROMPT = "당신은 철도 관제 무전 분석 시스템입니다. 주어진 무전에서 지정된 포맷으로만 답변하세요. /no_think"

_USER_PROMPT_TEMPLATE = """다음은 철도 무전 내용입니다. 예시처럼 정확히 4줄로 요약하세요. 추가 설명이나 인사말은 절대 금지합니다.

[예시]
무전 내용: "신호질로 확인하고 통과하겠습니다 다대포해수욕장 분기기 확인."
제목: 다대포 분기기 통과
요약: 다대포해수욕장 분기기의 신호를 확인하고 통과함.
키워드: 다대포해수욕장,분기기,신호,통과
긴급도: 일반

[실제 분석 대상]
무전 내용: "{text}"
"""


class LocalLLMService:
    """
    llama-cpp-python 기반 로컬 LLM 서비스.
    LLM_ENABLED=false 이거나 모델 파일이 없으면 즉시 기본값을 반환합니다.
    """
    _instance: Optional["LocalLLMService"] = None
    _model = None

    @classmethod
    def get_instance(cls) -> "LocalLLMService":
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    def __init__(self):
        if not settings.LLM_ENABLED:
            logger.info("ℹ️  LLM_ENABLED=false — LLM 분석이 비활성화 상태입니다.")
            return

        if not _LLAMA_AVAILABLE:
            logger.warning("⚠️  llama-cpp-python이 설치되어 있지 않습니다. 'pip install llama-cpp-python'")
            return

        if not settings.LLM_MODEL_PATH:
            logger.warning("⚠️  LLM_MODEL_PATH가 설정되어 있지 않습니다. .env 파일을 확인하세요.")
            return

        try:
            logger.info(f"🔄 LLM 모델 로딩 중 (GPU 우선): {settings.LLM_MODEL_PATH}")
            self._model = Llama(
                model_path=settings.LLM_MODEL_PATH,
                n_ctx=2048,
                n_threads=4,
                n_gpu_layers=-1,   # 전체 레이어를 GPU(CUDA)에 적재
                verbose=False,
            )
            logger.info("✅ LLM 모델 로딩 완료 (GPU 가속)")
        except Exception as gpu_err:
            logger.warning(f"⚠️  GPU 초기화 실패 → CPU Fallback 시도: {gpu_err}")
            try:
                self._model = Llama(
                    model_path=settings.LLM_MODEL_PATH,
                    n_ctx=2048,
                    n_threads=4,
                    n_gpu_layers=0,   # CPU only fallback
                    verbose=False,
                )
                logger.info("✅ LLM 모델 로딩 완료 (CPU 모드)")
            except Exception as cpu_err:
                logger.error(f"❌ LLM 모델 로딩 최종 실패: {cpu_err}")
                self._model = None

    def _parse_response(self, raw: str) -> LLMMetadata:
        """
        LLM이 뱉어낸 텍스트를 정규식으로 안전하게 파싱합니다.
        Qwen3의 <think>...</think> 태그를 먼저 제거한 뒤 파싱합니다.
        파싱 실패 시 각 필드에 안전한 기본값을 사용합니다 (Fallback).
        """
        # Qwen3 think 블록 제거
        clean = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()

        def extract(pattern: str, default: str = "") -> str:
            # 한글/영문 콜론 모두 허용, 줄 단위 탐색
            m = re.search(pattern, clean, re.MULTILINE)
            return m.group(1).strip() if m else default

        title    = extract(r"^제목\s*[:：]\s*(.+)$", default="무전 기록")
        summary  = extract(r"^요약\s*[:：]\s*(.+)$", default="")
        keywords = extract(r"^키워드\s*[:：]\s*(.+)$", default="")
        urgency_raw = extract(r"^긴급도\s*[:：]\s*(.+)$", default="일반")
        urgency = "긴급" if "긴급" in urgency_raw else "일반"
        
        logger.info(f"📝 LLM 파싱 결과 — 제목:{title} | 긴급도:{urgency} | 키워드:{keywords[:40] if keywords else '없음'}")
        return LLMMetadata(title=title, summary=summary, keywords=keywords, urgency=urgency)

    def generate_metadata(self, text: str) -> LLMMetadata:
        """
        STT 텍스트를 LLM에 넣어 제목/요약/키워드/긴급도를 추출합니다.
        LLM 비활성화 상태이거나 오류 발생 시 안전한 기본값을 반환합니다.
        """
        if self._model is None:
            return LLMMetadata()  # 비활성화 또는 미설정: 기본값 반환

        prompt = f"<|im_start|>system\n{_SYSTEM_PROMPT}<|im_end|>\n<|im_start|>user\n{_USER_PROMPT_TEMPLATE.format(text=text[:1200])}<|im_end|>\n<|im_start|>assistant\n제목:"

        try:
            output = self._model(
                prompt,
                max_tokens=256,
                temperature=0.1,    # 낮은 temperature로 일관된 포맷 출력 유도
                stop=["<|im_end|>", "<|im_start|>"],
                echo=False,
            )
            # 시작 문자열(제목:)을 수동으로 붙여줌 (echo=False이므로 출력에 미포함됨)
            raw_text = "제목:" + output["choices"][0]["text"]
            logger.info(f"✅ LLM 분석 완료 (raw 출력 {len(raw_text)}자)")
            return self._parse_response(raw_text)

        except Exception as e:
            logger.error(f"⚠️  LLM 생성 실패 (Fallback 사용): {e}")
            return LLMMetadata()

    def guess_speaker_with_llm(self, context_text: str, current_text: str) -> str:
        """
        문맥과 현재 문장을 비교하여 발화자가 관제인지 열차(모터카)인지 추론합니다.
        가벼운 단답 추론 (관제 / 열차 / 미상)만 수행합니다.
        """
        if self._model is None:
            return "미상"
        
        # 문맥이 없으면 '-'로 표시
        ctx = context_text if context_text.strip() else "-"
        # 간단한 1-Shot 지시문
        spk_prompt = (
            f"<|im_start|>system\n"
            f"주어진 철도 무전의 화자를 '관제' 또는 '열차' 중 하나로만 답변하세요.<|im_end|>\n"
            f"<|im_start|>user\n"
            f"맥락: {ctx}\n무전: {current_text}\n결과:<|im_end|>\n"
            f"<|im_start|>assistant\n"
            f"화자:"
        )
        
        try:
            out = self._model(
                spk_prompt,
                max_tokens=256,
                temperature=0.1,
                stop=["<|im_end|>", "<|im_start|>"],
                echo=False,
            )
            raw_ans = out["choices"][0]["text"]
            # Qwen3 think 블록 (혹은 생각 과정) 제거 후 실제 응답만 추출
            import re
            ans = re.sub(r"<think>.*?</think>", "", raw_ans, flags=re.DOTALL).strip()
            
            logger.info(f"🎤 LLM 화자 판별 (raw): {len(raw_ans)}자 | Context: '{ctx}' | Current: '{current_text}' | Ans: '{ans}'")
            # 안전하게 파싱
            if "관제" in ans: return "관제"
            if "열차" in ans or "모터카" in ans or "검측차" in ans or "검축차" in ans: return "열차"
            return "미상"
        except Exception:
            return "미상"

    def check_thread_continuation(self, context_text: str, current_text: str) -> bool:
        """
        [Chapter 5.4] 대화 쓰레드(맥락) 연결 판별.
        이전 무전(context_text)과 현재 무전(current_text)이 이어지는 상황인지 판별.
        반환: True (이어짐) / False (새로운 호출)
        실패 시 False가 아닌 True를 반환(Fallback)하여 불필요한 파편화를 방지.
        """
        if self._model is None:
            return True

        # 이전 문맥이 아예 없으면 무조건 새로운 대화(False)
        if not context_text or not context_text.strip():
            return False

        sys_prompt = (
            "당신은 철도 무전 맥락 분석기입니다. "
            "'이전 무전'과 '현재 무전'이 같은 상황에서 이어지는 대화인지 판별하세요. "
            "이어지면 {\"is_continuation\": true}, 새로운 호출이면 {\"is_continuation\": false} 로만 대답하세요. /no_think"
        )
        user_msg = f"이전 무전: {context_text.strip()}\n현재 무전: {current_text.strip()}"

        prompt = (
            f"<|im_start|>system\n{sys_prompt}<|im_end|>\n"
            f"<|im_start|>user\n{user_msg}<|im_end|>\n"
            f"<|im_start|>assistant\n"
        )

        try:
            out = self._model(
                prompt,
                max_tokens=32,
                temperature=0.0,
                stop=["<|im_end|>", "<|im_start|>", "\n\n"],
                echo=False,
            )
            raw: str = out["choices"][0]["text"].strip()
            raw = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()

            logger.debug(f"[LLM Thread] raw 응답: {raw!r}")

            import json
            # JSON 파싱 정규식 시도
            m = re.search(r'\{[^{}]*"is_continuation"\s*:\s*(true|false|True|False)[^{}]*\}', raw, re.IGNORECASE)
            if m:
                val = m.group(1).lower()
                return val == "true"
                
            # Fallback 텍스트 검사
            if "true" in raw.lower(): return True
            if "false" in raw.lower(): return False

            logger.warning(f"⚠️ [LLM Thread] JSON 파싱 실패. Fallback=True 유지. raw={raw!r}")
            return True

        except Exception as e:
            logger.warning(f"⚠️ [LLM Thread] LLM 호출 실패. Fallback=True 유지. {e}")
            return True

    def extract_keywords(self, text: str) -> list[str]:
        """
        [Chapter 6.0] 지식 연동을 위한 키워드 추출기.
        철도 무전에서 핵심 명사(역명, 열차 이름, 철도 장비 등)만 리스트로 반환합니다.
        """
        if self._model is None:
            return []

        if not text or not text.strip():
            return []

        sys_prompt = (
            "당신은 철도 무전 키워드 추출기입니다. "
            "주어진 무전 내용 중에서 철도 장비, 역명, 지명, 열차 번호 등 가장 핵심적인 명사만 추출하세요. "
            "반드시 JSON 형태로 답변하세요: {\"keywords\": [\"키워드1\", \"키워드2\"]} /no_think"
        )

        prompt = (
            f"<|im_start|>system\n{sys_prompt}<|im_end|>\n"
            f"<|im_start|>user\n{text.strip()}<|im_end|>\n"
            f"<|im_start|>assistant\n"
        )

        try:
            out = self._model(
                prompt, max_tokens=64, temperature=0.1, stop=["<|im_end|>", "<|im_start|>"], echo=False
            )
            raw = out["choices"][0]["text"].strip()
            raw = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()

            import json
            m = re.search(r'\{[^{}]*"keywords"\s*:\s*\[([^\]]*)\][^{}]*\}', raw, re.IGNORECASE)
            if m:
                kw_str = m.group(1)
                keywords = [k.strip().strip('"\'') for k in kw_str.split(',') if k.strip().strip('"\'')]
                return keywords
            logger.warning(f"[LLM Keyword] 추출 실패 (JSON 파싱). raw= {raw}")
            return []
        except Exception as e:
            logger.warning(f"[LLM Keyword] 추출 실패: {e}")
            return []

# 싱글톤 인스턴스
llm_service = LocalLLMService.get_instance()

