"""
자연어 → 메신저 봇 명령어 파서 (NL Command Parser)

Ollama(로컬 LLM) 기반으로 자연어 입력을 봇 명령어로 변환.
Ollama 미연결 시 규칙 기반 폴백.

반환 형태:
{
    "command": "!scrap",
    "args": ["https://example.com"],
    "full_command": "!scrap https://example.com",
    "confidence": 0.92,
    "explanation": "URL 스크랩 요청으로 판단"
}
"""
from __future__ import annotations

import json
import logging
import os
import re
from typing import Any, Dict, List, Optional, Tuple

import httpx

logger = logging.getLogger(__name__)

OLLAMA_URL    = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434") + "/api/generate"
NL_MODEL      = os.getenv("NL_COMMAND_MODEL", "llama3")
TIMEOUT       = 20  # Ollama 호출 타임아웃
MIN_CONFIDENCE = 0.55  # 이 이하면 "이해 못함"으로 처리


# ── Few-shot 프롬프트 ─────────────────────────────────────────────────────────

_SYSTEM_PROMPT = """\
너는 GUARDiA ITSM 메신저 봇의 자연어 명령 해석기다.
사용자의 자연어 입력을 분석해 아래 봇 명령어 중 하나로 변환하라.

[지원 명령어 목록]
!vibe <SR-ID> [project_id]     - SR에 대한 바이브 코딩 세션 시작
!build <session_id>             - 빌드 실행
!deploy <session_id>            - 배포 실행
!status <SR-ID>                 - SR 상태 조회
!cancel <session_id>            - 세션 취소
!health <server>                - 서버 헬스체크
!log <server> [path]            - 서버 로그 분석
!sm <server> <script>           - SM 스크립트 실행
/sr <제목>                      - SR 빠른 접수
/status                         - 전체 시스템 현황
/assign <SR-ID> <담당자>        - SR 담당자 배정
/approve <SR-ID> [의견]         - SR 승인
/reject <SR-ID> [사유]          - SR 반려
/incident <제목> [P1-P4]        - 인시던트 등록
/rca <INC-ID>                   - AI RCA 분석
/escalate <SR-ID>               - 에스컬레이션
/sla                            - SLA 위반 현황
/kb <검색어>                    - KB 문서 검색
/pms <프로젝트코드>             - 프로젝트 현황
/report <코드> [daily|weekly]   - 보고서 발송
/oncall                         - 당직자 조회
/scan                           - 보안 스캔
/perf [url]                     - 성능 테스트
!scrap <url>                    - URL 스크랩
!scrap list [n]                 - 스크랩 목록
!scrap publish <id>             - 스크랩 게시
!scrap del <id>                 - 스크랩 삭제
!scrap restore <id>             - 스크랩 원복
!scrap status <id>              - 스크랩 상태 조회
/autoq                          - 자율 운영 대기 목록
!help                           - 도움말

[규칙]
1. 반드시 JSON만 반환. 자연어 설명 없음.
2. 입력에서 SR ID를 찾으면 그대로 사용 (SR-20260531-XXXX 형태 유지).
3. 확신이 없으면 confidence를 낮게 설정.
4. 명확히 매핑 불가능하면 command를 null로.

[예시 입력 → 출력]
입력: "SR-20260531-ABCD 배포해줘"
출력: {"command":"!deploy","args":["SR-20260531-ABCD"],"full_command":"!deploy SR-20260531-ABCD","confidence":0.95,"explanation":"배포 요청으로 판단"}

입력: "서버1 헬스체크 해줘"
출력: {"command":"!health","args":["서버1"],"full_command":"!health 서버1","confidence":0.92,"explanation":"헬스체크 요청"}

입력: "최근 스크랩 5개 보여줘"
출력: {"command":"!scrap","args":["list","5"],"full_command":"!scrap list 5","confidence":0.90,"explanation":"스크랩 목록 조회"}

입력: "https://example.com 스크랩해줘"
출력: {"command":"!scrap","args":["https://example.com"],"full_command":"!scrap https://example.com","confidence":0.95,"explanation":"URL 스크랩 요청"}

입력: "#3 게시해줘"
출력: {"command":"!scrap","args":["publish","3"],"full_command":"!scrap publish 3","confidence":0.88,"explanation":"스크랩 게시 요청"}

입력: "전체 시스템 현황 알려줘"
출력: {"command":"/status","args":[],"full_command":"/status","confidence":0.85,"explanation":"시스템 현황 조회"}

입력: "오늘 서버 배포 요청 접수해줘 - web01 Tomcat 재기동 필요"
출력: {"command":"/sr","args":["[배포] web01 Tomcat 재기동"],"full_command":"/sr [배포] web01 Tomcat 재기동","confidence":0.82,"explanation":"SR 접수 요청"}

입력: "P1 긴급 장애 발생 - 결제 시스템 전면 중단"
출력: {"command":"/incident","args":["결제 시스템 전면 중단","P1"],"full_command":"/incident 결제 시스템 전면 중단 P1","confidence":0.95,"explanation":"P1 인시던트 등록"}

입력: "홍길동에게 SR-20260531-XXXX 배정해줘"
출력: {"command":"/assign","args":["SR-20260531-XXXX","홍길동"],"full_command":"/assign SR-20260531-XXXX 홍길동","confidence":0.93,"explanation":"SR 담당자 배정"}

입력: "날씨 어때?"
출력: {"command":null,"args":[],"full_command":null,"confidence":0.1,"explanation":"ITSM과 무관한 질문"}

"""


# ── Ollama 호출 ───────────────────────────────────────────────────────────────

async def parse_nl_command(text: str) -> Dict[str, Any]:
    """
    자연어 텍스트를 봇 명령어로 변환.
    Ollama 실패 시 규칙 기반 폴백.
    """
    # 이미 명령어 형식이면 그대로 반환
    if _is_explicit_command(text):
        cmd_parts = text.strip().split()
        return {
            "command": cmd_parts[0],
            "args": cmd_parts[1:],
            "full_command": text.strip(),
            "confidence": 1.0,
            "explanation": "명시적 명령어",
        }

    # Ollama 시도
    try:
        result = await _call_ollama(text)
        if result and result.get("confidence", 0) >= MIN_CONFIDENCE:
            return result
    except Exception as e:
        logger.warning("[NL Command] Ollama 실패, 규칙 기반 폴백: %s", e)

    # 규칙 기반 폴백
    return _rule_based_parse(text)


async def _call_ollama(text: str) -> Optional[Dict[str, Any]]:
    """Ollama에 자연어 명령어 파싱 요청."""
    prompt = (
        _SYSTEM_PROMPT
        + f'\n입력: "{text}"\n출력: '
    )

    payload = {
        "model": NL_MODEL,
        "prompt": prompt,
        "stream": False,
        "format": "json",
        "options": {
            "temperature": 0.1,   # 결정론적 출력
            "top_p": 0.9,
            "num_predict": 200,
        },
    }

    async with httpx.AsyncClient(timeout=TIMEOUT) as client:
        resp = await client.post(OLLAMA_URL, json=payload)
        resp.raise_for_status()
        data = resp.json()

    raw = data.get("response", "").strip()

    # JSON 추출
    json_match = re.search(r'\{.*\}', raw, re.DOTALL)
    if not json_match:
        return None

    parsed = json.loads(json_match.group())

    # 필수 필드 검증
    if "command" not in parsed or "confidence" not in parsed:
        return None

    # full_command 자동 생성
    if parsed.get("command") and not parsed.get("full_command"):
        args_str = " ".join(str(a) for a in parsed.get("args", []))
        parsed["full_command"] = (
            f"{parsed['command']} {args_str}".strip()
            if args_str else parsed["command"]
        )

    return parsed


# ── 규칙 기반 폴백 ────────────────────────────────────────────────────────────

_RULES: List[Tuple[List[str], str, callable]] = [
    # (키워드목록, command, args_extractor)
    (["배포해", "배포 해", "deploy", "릴리즈해"],
     "!deploy", lambda t: [_extract_sr(t) or _extract_session(t) or ""]),

    (["빌드해", "빌드 해", "build"],
     "!build", lambda t: [_extract_session(t) or ""]),

    (["바이브", "vibe", "코딩 세션"],
     "!vibe", lambda t: [_extract_sr(t) or ""]),

    (["헬스체크", "상태 확인", "health", "헬스 체크"],
     "!health", lambda t: [_extract_server(t) or ""]),

    (["로그 분석", "로그 확인", "로그 봐", "log 분석"],
     "!log", lambda t: [_extract_server(t) or ""]),

    (["스크랩 목록", "스크랩 리스트", "scrap list", "수집 목록",
       "최근 스크랩", "스크랩 보여", "수집 결과", "스크랩 결과"],
     "!scrap", lambda t: ["list", _extract_number(t) or "5"]),

    (["스크랩해줘", "스크랩 해줘", "수집해줘", "크롤링", "스크래핑"],
     "!scrap", lambda t: [_extract_url(t) or ""]),

    (["게시해", "publish", "발행"],
     "!scrap", lambda t: ["publish", _extract_id(t) or ""]),

    (["삭제해", "지워", "delete"],
     "!scrap", lambda t: ["del", _extract_id(t) or ""]),

    (["원복", "복구", "restore"],
     "!scrap", lambda t: ["restore", _extract_id(t) or ""]),

    (["상태 조회", "현황 조회", "상태 알려", "어떻게 돼"],
     "!status", lambda t: [_extract_sr(t) or ""]),

    (["시스템 현황", "전체 현황", "현황 알려", "/status"],
     "/status", lambda t: []),

    (["SLA 현황", "SLA 위반", "sla"],
     "/sla", lambda t: []),

    (["당직자", "온콜", "oncall"],
     "/oncall", lambda t: []),

    (["인시던트", "장애 등록", "장애 신고", "incident",
       "장애 발생", "장애 다운", "서비스 중단", "전면 장애",
       "긴급 장애", "P1 장애", "P2 장애"],
     "/incident", lambda t: [_extract_incident_title(t), _extract_priority(t)]),

    (["승인해", "approve", "승인 처리"],
     "/approve", lambda t: [_extract_sr(t) or ""]),

    (["반려해", "거절", "reject"],
     "/reject", lambda t: [_extract_sr(t) or ""]),

    (["배정해", "assign", "담당자 지정"],
     "/assign", lambda t: [_extract_sr(t) or "", _extract_person(t) or ""]),

    (["SR 접수", "서비스 요청", "티켓 등록", "sr 올려"],
     "/sr", lambda t: [_extract_title(t)]),

    (["보안 스캔", "취약점 점검", "/scan"],
     "/scan", lambda t: []),

    (["성능 테스트", "부하 테스트", "/perf"],
     "/perf", lambda t: [_extract_url(t) or ""]),

    (["프로젝트 현황", "pms", "/pms"],
     "/pms", lambda t: [_extract_code(t) or ""]),

    (["자율 운영", "autoq", "승인 대기"],
     "/autoq", lambda t: []),

    (["도움말", "명령어", "help"],
     "!help", lambda t: []),
]


def _rule_based_parse(text: str) -> Dict[str, Any]:
    text_lower = text.lower()
    best_cmd = None
    best_args: List[str] = []
    best_score = 0

    for keywords, command, args_fn in _RULES:
        score = sum(1 for kw in keywords if kw.lower() in text_lower)
        if score > best_score:
            best_score = score
            best_cmd = command
            try:
                best_args = [a for a in args_fn(text) if a]
            except Exception:
                best_args = []

    if not best_cmd or best_score == 0:
        return {
            "command": None,
            "args": [],
            "full_command": None,
            "confidence": 0.0,
            "explanation": "해당 요청을 이해하지 못했습니다. !help 로 명령어를 확인하세요.",
        }

    args_str = " ".join(best_args)
    full = f"{best_cmd} {args_str}".strip() if args_str else best_cmd
    confidence = min(0.5 + best_score * 0.1, 0.75)

    return {
        "command": best_cmd,
        "args": best_args,
        "full_command": full,
        "confidence": confidence,
        "explanation": f"규칙 기반 매핑 (키워드 {best_score}개 일치)",
    }


# ── 엔티티 추출 헬퍼 ─────────────────────────────────────────────────────────

def _is_explicit_command(text: str) -> bool:
    """이미 명시적 명령어 형식인지 확인."""
    t = text.strip()
    return bool(re.match(r'^[!/]', t))


def _extract_sr(text: str) -> Optional[str]:
    m = re.search(r'SR-\d{8}-[A-Z0-9]+', text, re.IGNORECASE)
    return m.group().upper() if m else None


def _extract_session(text: str) -> Optional[str]:
    m = re.search(r'(?:세션|session)[\s#]*(\d+)', text, re.IGNORECASE)
    if m:
        return m.group(1)
    m = re.search(r'#(\d+)', text)
    return m.group(1) if m else None


def _extract_id(text: str) -> Optional[str]:
    m = re.search(r'#(\d+)', text)
    if m:
        return m.group(1)
    m = re.search(r'\b(\d+)\b', text)
    return m.group(1) if m else None


def _extract_url(text: str) -> Optional[str]:
    m = re.search(r'https?://[^\s]+', text)
    return m.group() if m else None


def _extract_server(text: str) -> Optional[str]:
    # "서버1", "서버-prod", "서버 web01" 형태
    m = re.search(r'서버\s*([A-Za-z0-9\-_가-힣]+)', text)
    if m:
        return '서버' + m.group(1)  # "서버1" 전체 반환
    m = re.search(r'server[\s:]*([A-Za-z0-9\-_]+)', text, re.IGNORECASE)
    if m:
        return m.group(1)
    # web01, was-prod, app01 등 서버명 패턴
    m = re.search(r'\b([A-Za-z가-힣][A-Za-z0-9\-_가-힣]*(?:web|was|db|app|srv|prod|dev)\w*)\b',
                  text, re.IGNORECASE)
    return m.group(1) if m else None


def _extract_number(text: str) -> Optional[str]:
    m = re.search(r'\b(\d+)\b', text)
    return m.group(1) if m else None


def _extract_priority(text: str) -> str:
    m = re.search(r'\b(P[1-4]|P1|P2|P3|P4|긴급|critical)\b', text, re.IGNORECASE)
    if m:
        v = m.group().upper()
        if v in ("긴급", "CRITICAL"):
            return "P1"
        return v
    return "P3"


def _extract_person(text: str) -> Optional[str]:
    m = re.search(r'(?:에게|한테|담당자?\s*)([가-힣]{2,4}|[A-Za-z]+)', text)
    return m.group(1) if m else None


def _extract_code(text: str) -> Optional[str]:
    m = re.search(r'\b([A-Z][A-Z0-9\-]{1,10})\b', text)
    return m.group(1) if m else None


def _extract_title(text: str) -> str:
    for kw in ["SR 접수", "서비스 요청", "티켓 등록", "sr 올려", "접수해줘"]:
        text = re.sub(kw, "", text, flags=re.IGNORECASE)
    return text.strip() or "자연어 SR 접수"


def _extract_incident_title(text: str) -> str:
    for kw in ["인시던트", "장애 등록", "incident", "P1", "P2", "P3", "P4",
               "긴급", "등록해", "신고해"]:
        text = re.sub(kw, "", text, flags=re.IGNORECASE)
    return text.strip() or "자동 인시던트"
