"""
core/vibe_bridge.py — Claude CLI SDK 비동기 브리지.

subprocess 방식 대신 Python SDK를 직접 연동하여
Claude CLI 세션 수명주기를 관리한다.

보안:
  - CLAUDE_CLI_PATH 환경변수로 경로 지정 (기본: /usr/local/bin/claude)
  - 워크스페이스는 CLAUDE_WORKSPACE_ROOT 이하로만 허용
  - 세션 메타는 tb_vibe_session.claude_session_id 에 영속화
  - 외부 API 호출 없음 — 로컬 Claude CLI 전용

환경변수:
  CLAUDE_CLI_PATH          클로드 CLI 실행 파일 경로 (default: /usr/local/bin/claude)
  CLAUDE_WORKSPACE_ROOT    코딩 워크스페이스 루트 (default: /opt/guardia/workspaces)
  CLAUDE_SESSION_TIMEOUT   세션 비활성 타임아웃 초 (default: 3600)
"""
from __future__ import annotations

import asyncio
import json
import logging
import os
import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from typing import AsyncGenerator, Optional

logger = logging.getLogger(__name__)

# ── 환경변수 ──────────────────────────────────────────────────────────────────
_CLI_PATH        = os.getenv("CLAUDE_CLI_PATH",       "/usr/local/bin/claude")
_WORKSPACE_ROOT  = Path(os.getenv("CLAUDE_WORKSPACE_ROOT", "/opt/guardia/workspaces"))
_SESSION_TIMEOUT = int(os.getenv("CLAUDE_SESSION_TIMEOUT", "3600"))


# ── 데이터 클래스 ────────────────────────────────────────────────────────────

@dataclass
class SessionState:
    session_id:    str
    sr_id:         Optional[str]
    project_path:  Path
    active:        bool        = True
    last_used:     datetime    = field(default_factory=datetime.utcnow)
    tokens_used:   int         = 0
    last_response: str         = ""


@dataclass
class SessionStatus:
    active:        bool
    last_response: str
    tokens_used:   int
    last_used:     Optional[datetime]


# ── VibeBridge ───────────────────────────────────────────────────────────────

class VibeBridge:
    """
    Claude CLI SDK 비동기 브리지.

    사용 예시:
        bridge = VibeBridge()
        session_id = await bridge.start_session("SR-2026-0001", "/opt/src/proj")
        response   = await bridge.send_message(session_id, "이 버그를 수정해주세요")
        await bridge.close_session(session_id)
    """

    def __init__(self) -> None:
        self._sessions: dict[str, SessionState] = {}

    # ── 세션 시작 ─────────────────────────────────────────────────────────────

    async def start_session(self, sr_id: str, project_path: str) -> str:
        """
        새 Claude CLI 세션을 시작하고 session_id를 반환한다.

        Args:
            sr_id:        연결된 SR 번호 (예: SR-20260526-000001)
            project_path: 작업 대상 디렉터리 (CLAUDE_WORKSPACE_ROOT 이하여야 함)

        Returns:
            session_id: Claude CLI가 반환한 세션 ID 문자열

        Raises:
            ValueError:  경로 순회 공격 감지 시
            RuntimeError: Claude CLI 실행 실패 시
        """
        safe_path = self._validate_path(project_path)

        # Claude CLI SDK 초기화 명령
        # --output-format json: 구조화 응답
        # --session-id auto: CLI가 새 세션 ID 생성
        cmd = [
            _CLI_PATH,
            "--print",             # 비대화형 모드
            "--output-format", "json",
            "--cwd", str(safe_path),
            "세션을 시작합니다. 프로젝트 구조를 파악해주세요.",
        ]

        logger.info("Claude 세션 시작: sr_id=%s path=%s", sr_id, safe_path)

        output, session_id = await self._run_claude(cmd)

        state = SessionState(
            session_id=session_id,
            sr_id=sr_id,
            project_path=safe_path,
            last_response=output,
        )
        self._sessions[session_id] = state
        logger.info("Claude 세션 등록: session_id=%s", session_id)
        return session_id

    # ── 메시지 전송 ───────────────────────────────────────────────────────────

    async def send_message(self, session_id: str, message: str) -> str:
        """
        기존 세션에 메시지를 전송하고 응답을 반환한다.

        Args:
            session_id: start_session() 이 반환한 세션 ID
            message:    사용자(또는 ITSM)가 Claude에게 전달할 지시문

        Returns:
            Claude의 텍스트 응답

        Raises:
            KeyError:    세션이 존재하지 않을 때
            RuntimeError: CLI 실행 실패 시
        """
        state = self._get_state(session_id)

        cmd = [
            _CLI_PATH,
            "--print",
            "--output-format", "json",
            "--resume", session_id,
            "--cwd", str(state.project_path),
            message,
        ]

        logger.debug("Claude 메시지 전송: session_id=%s msg_len=%d", session_id, len(message))
        response, _ = await self._run_claude(cmd)

        state.last_response = response
        state.last_used = datetime.utcnow()
        return response

    # ── 세션 재개 ─────────────────────────────────────────────────────────────

    async def resume_session(self, session_id: str, message: str) -> str:
        """
        비활성 세션을 재개하거나 메모리 내 상태가 없는 세션을 복원하여 메시지 전송.

        Args:
            session_id: 복원할 세션 ID (tb_vibe_session.claude_session_id)
            message:    전달할 메시지

        Returns:
            Claude의 텍스트 응답
        """
        # 메모리에 없으면 workspace_root / session_id 경로로 가정
        if session_id not in self._sessions:
            workspace = _WORKSPACE_ROOT / session_id
            workspace.mkdir(parents=True, exist_ok=True)
            state = SessionState(
                session_id=session_id,
                sr_id=None,
                project_path=workspace,
                active=True,
            )
            self._sessions[session_id] = state

        return await self.send_message(session_id, message)

    # ── 세션 상태 조회 ────────────────────────────────────────────────────────

    async def get_session_status(self, session_id: str) -> SessionStatus:
        """
        세션의 현재 상태를 반환한다.

        Returns:
            SessionStatus(active, last_response, tokens_used, last_used)
        """
        state = self._sessions.get(session_id)
        if state is None:
            return SessionStatus(active=False, last_response="", tokens_used=0, last_used=None)

        # 타임아웃 확인
        idle = (datetime.utcnow() - state.last_used).total_seconds()
        if idle > _SESSION_TIMEOUT:
            state.active = False
            logger.info("세션 타임아웃: session_id=%s idle=%.0fs", session_id, idle)

        return SessionStatus(
            active=state.active,
            last_response=state.last_response,
            tokens_used=state.tokens_used,
            last_used=state.last_used,
        )

    # ── 세션 종료 ─────────────────────────────────────────────────────────────

    async def close_session(self, session_id: str) -> bool:
        """
        세션을 종료하고 메모리에서 제거한다.

        Returns:
            True: 성공적으로 종료됨
            False: 세션이 존재하지 않음
        """
        state = self._sessions.pop(session_id, None)
        if state is None:
            logger.warning("종료 요청: 세션 없음 session_id=%s", session_id)
            return False

        state.active = False
        logger.info("Claude 세션 종료: session_id=%s tokens=%d", session_id, state.tokens_used)
        return True

    # ── 스트리밍 응답 ────────────────────────────────────────────────────────

    async def stream_message(
        self, session_id: str, message: str
    ) -> AsyncGenerator[str, None]:
        """
        메시지를 전송하고 응답을 청크 단위로 스트리밍한다.
        SSE 엔드포인트와 연동하여 실시간 피드백에 사용한다.

        Yields:
            str: 응답 텍스트 청크
        """
        state = self._get_state(session_id)

        cmd = [
            _CLI_PATH,
            "--print",
            "--output-format", "stream-json",  # 스트리밍 JSON 모드
            "--resume", session_id,
            "--cwd", str(state.project_path),
            message,
        ]

        proc = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

        assert proc.stdout is not None
        full_response: list[str] = []

        async for line_bytes in proc.stdout:
            line = line_bytes.decode("utf-8", errors="replace").strip()
            if not line:
                continue
            try:
                obj = json.loads(line)
                chunk = obj.get("delta", {}).get("text", "") or obj.get("text", "")
                if chunk:
                    full_response.append(chunk)
                    yield chunk
                # 토큰 집계
                usage = obj.get("usage", {})
                if usage:
                    state.tokens_used += usage.get("output_tokens", 0)
            except json.JSONDecodeError:
                yield line

        state.last_response = "".join(full_response)
        state.last_used = datetime.utcnow()
        await proc.wait()

    # ── 내부 유틸 ─────────────────────────────────────────────────────────────

    def _validate_path(self, path_str: str) -> Path:
        """경로 순회(path traversal) 공격 방지."""
        _WORKSPACE_ROOT.mkdir(parents=True, exist_ok=True)
        p = Path(path_str).resolve()
        try:
            p.relative_to(_WORKSPACE_ROOT.resolve())
        except ValueError:
            raise ValueError(
                f"허용되지 않는 경로: {path_str} "
                f"(CLAUDE_WORKSPACE_ROOT={_WORKSPACE_ROOT} 이하만 허용)"
            )
        return p

    def _get_state(self, session_id: str) -> SessionState:
        """세션 상태 조회 — 없으면 KeyError."""
        state = self._sessions.get(session_id)
        if state is None:
            raise KeyError(f"세션을 찾을 수 없습니다: {session_id}")
        return state

    async def _run_claude(self, cmd: list[str]) -> tuple[str, str]:
        """
        Claude CLI 프로세스를 실행하고 (응답 텍스트, 세션 ID) 를 반환한다.

        Returns:
            (response_text, session_id)
        Raises:
            RuntimeError: 프로세스 실행 실패 또는 비정상 종료 시
        """
        try:
            proc = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
        except FileNotFoundError:
            raise RuntimeError(
                f"Claude CLI 실행 파일을 찾을 수 없습니다: {_CLI_PATH}. "
                "CLAUDE_CLI_PATH 환경변수를 확인하세요."
            )

        try:
            stdout, stderr = await asyncio.wait_for(
                proc.communicate(),
                timeout=float(_SESSION_TIMEOUT),
            )
        except asyncio.TimeoutError:
            proc.kill()
            raise RuntimeError("Claude CLI 응답 타임아웃")

        if proc.returncode != 0:
            err = stderr.decode("utf-8", errors="replace")[:500]
            raise RuntimeError(f"Claude CLI 실행 실패 (rc={proc.returncode}): {err}")

        raw = stdout.decode("utf-8", errors="replace").strip()

        # JSON 파싱 시도
        session_id = _extract_session_id(raw)
        response_text = _extract_response_text(raw)

        return response_text, session_id


# ── 파싱 헬퍼 ────────────────────────────────────────────────────────────────

def _extract_session_id(raw: str) -> str:
    """CLI 출력에서 session_id 추출."""
    for line in raw.splitlines():
        try:
            obj = json.loads(line)
            sid = (
                obj.get("session_id")
                or obj.get("sessionId")
                or (obj.get("session", {}) or {}).get("id")
            )
            if sid:
                return str(sid)
        except json.JSONDecodeError:
            continue

    # UUID 패턴으로 폴백
    m = re.search(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", raw, re.I)
    if m:
        return m.group(0)

    # 최후 수단: 타임스탬프 기반 ID 생성
    import uuid
    return str(uuid.uuid4())


def _extract_response_text(raw: str) -> str:
    """CLI JSON 출력에서 텍스트 응답 추출."""
    texts: list[str] = []
    for line in raw.splitlines():
        try:
            obj = json.loads(line)
            text = (
                obj.get("result")
                or obj.get("text")
                or obj.get("content")
                or (obj.get("message", {}) or {}).get("content")
            )
            if isinstance(text, str) and text:
                texts.append(text)
            elif isinstance(text, list):
                for block in text:
                    if isinstance(block, dict) and block.get("type") == "text":
                        texts.append(block.get("text", ""))
        except json.JSONDecodeError:
            continue

    return "\n".join(texts) if texts else raw[:2000]


# ── 싱글턴 ──────────────────────────────────────────────────────────────────

_bridge_instance: Optional[VibeBridge] = None


def get_vibe_bridge() -> VibeBridge:
    """싱글턴 VibeBridge 인스턴스를 반환한다."""
    global _bridge_instance
    if _bridge_instance is None:
        _bridge_instance = VibeBridge()
    return _bridge_instance
