import json
import logging
from typing import List, Dict, Any, AsyncGenerator

from nexus_agent.core.llm import llm_client
from nexus_agent.core.mcp_manager import mcp_manager, parse_namespaced_tool
from nexus_agent.core.memory_manager import memory_manager
from nexus_agent.core.skill_manager import skill_manager, SKILL_TOOL_NAMES
from nexus_agent.core.tool_errors import format_error_for_llm, is_error_result
from nexus_agent.core.workspace_tools import (
    WORKSPACE_TOOL_NAMES,
    get_workspace_tools,
    handle_workspace_tool_call,
    get_workspace_system_prompt,
)

logger = logging.getLogger(__name__)

MAX_TOOL_ROUNDS = 25

SKILL_SYSTEM_PROMPT = """당신은 Nexus Agent 플랫폼의 AI 어시스턴트입니다.
사용자의 요청을 처리할 때 반드시 아래 절차를 따르세요.

## 요청 처리 절차 (재귀적 문제 해결)

모든 사용자 요청에 대해 텍스트만으로 답변하기 전에 반드시 도구와 스킬 활용을 시도하세요:

1. **도구 탐색**: 사용 가능한 MCP 도구 중 요청을 처리할 수 있는 것이 있는지 확인하고 호출합니다
2. **스킬 탐색**: 등록된 스킬 목록에서 관련 스킬을 찾고, 있으면 `read_skill`로 지시사항을 읽어 따릅니다
3. **스킬 생성**: 도구와 스킬 모두 없으면, `read_skill`로 **skill-creator** 스킬을 읽고 요청을 처리할 수 있는 새 스킬을 생성합니다
4. **생성된 스킬 활용**: 새로 만든 스킬의 지시사항과 스크립트를 사용하여 원래 요청을 처리합니다

### 핵심 원칙

**정보 제공 ≠ 해결.** "~해줘", "~재생해줘", "~열어줘", "~만들어줘" 등 행동을 요청하면 정보 검색에서 끝내지 말고 실제 행동까지 완수하세요.

- 검색으로 URL을 찾았다면 → 스킬 스크립트로 실제로 열기/실행하기
- 데이터를 분석하라고 했다면 → 스크립트로 실제 분석 실행, 결과 반환
- 파일을 만들라고 했다면 → 워크스페이스 도구 또는 스킬 스크립트로 실제 파일 생성까지 완료

**스킬 스크립트는 시스템 명령을 실행할 수 있습니다.** `add_skill_script`로 Python 스크립트를 추가하고 `run_skill_script`로 실행하면 `subprocess`, `webbrowser.open()`, 파일 I/O 등 모든 시스템 작업이 가능합니다. 검색한 URL을 브라우저로 여는 것, 파일을 생성하는 것, API를 호출하는 것 모두 가능합니다.

"할 수 없습니다"로 끝내지 말고, 도구/스킬을 찾거나 만들어서라도 해결을 시도하세요.

## 스킬 사용 방법
- `read_skill` — 스킬의 전체 지시사항(SKILL.md) 로드
- `run_skill_script` — 스킬에 포함된 스크립트 실행
- `read_skill_reference` — 스킬의 참조 문서 로드

## 스킬 생성 및 수정
스킬을 생성하거나 수정할 때는 반드시 `read_skill`로 **skill-creator** 스킬을 먼저 읽고 그 절차를 따르세요.
시스템 프롬프트의 정보만으로 스킬을 생성하지 마세요. 반드시 skill-creator의 가이드를 읽은 후 진행하세요.
절대 `workspace_write_file` 등 워크스페이스 도구로 스킬 파일을 생성하지 마세요.

{available_skills_xml}"""

ERROR_HANDLING_PROMPT = """## 도구 오류 대응 원칙
- 도구 호출이 [TOOL_ERROR] 블록을 반환하면 error type과 recovery_hint를 참고하여 대응하세요.
- **동일한 방법으로 재시도하지 마세요.** 같은 도구를 같은 인자로 다시 호출하는 것은 금지입니다.
- 오류 유형별 전략:
  - module_not_found → 표준 라이브러리 대안 사용 또는 설치 명령 실행
  - command_not_found → 대체 명령어 탐색
  - file_not_found → workspace_list_dir로 실제 경로 확인
  - timeout → 작업을 작은 단위로 분할
  - server_disconnected → 해당 서버의 도구 대신 다른 방법 사용
  - env_restricted → uv 또는 pipx 사용
  - ssl_error → 환경변수 설정 또는 우회 방법 안내
- 2~3회 연속 실패하면 사용자에게 상황을 설명하고 수동 개입을 요청하세요."""


class AgentOrchestrator:
    def __init__(self):
        self.llm = llm_client

    def _build_system_prompt(self) -> str | None:
        parts = []

        # 사용자 커스텀 시스템 프롬프트
        custom = self.llm.get_system_prompt()
        if custom:
            parts.append(custom)

        # 장기 기억 주입
        memory_prompt = memory_manager.build_memory_prompt()
        if memory_prompt:
            parts.append(memory_prompt)

        # 스킬 시스템 프롬프트 (스킬이 없어도 create_skill 안내를 위해 항상 포함)
        skills_xml = skill_manager.generate_skills_xml()
        parts.append(SKILL_SYSTEM_PROMPT.format(available_skills_xml=skills_xml))

        # 워크스페이스 시스템 프롬프트
        workspace_prompt = get_workspace_system_prompt()
        if workspace_prompt:
            parts.append(workspace_prompt)

        # 도구 오류 대응 프롬프트
        parts.append(ERROR_HANDLING_PROMPT)

        return "\n\n".join(parts) if parts else None

    def _process_tool_result(self, result: str) -> tuple[str, bool]:
        """도구 결과를 분석하여 (LLM에 전달할 내용, 성공여부)를 반환합니다."""
        result_str = str(result)
        if is_error_result(result_str):
            return format_error_for_llm(result_str), False
        return result_str, True

    async def _execute_tool_call(self, tool_call: Dict[str, Any]) -> str:
        """단일 도구 호출 실행 및 결과 반환"""
        function_name = tool_call["function"]["name"]
        arguments_str = tool_call["function"]["arguments"]

        args = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str

        if function_name in SKILL_TOOL_NAMES:
            return await skill_manager.handle_tool_call(function_name, args)
        elif function_name in WORKSPACE_TOOL_NAMES:
            return await handle_workspace_tool_call(function_name, args)
        else:
            try:
                server_name, tool_name = parse_namespaced_tool(function_name)
                return await mcp_manager.call_tool(server_name, tool_name, args)
            except ValueError:
                return f"Error: Invalid tool name format '{function_name}'"

    async def run(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]:
        # Shallow copy to avoid mutating the caller's list
        messages = list(messages)

        # Inject system prompt with skill metadata
        system_prompt = self._build_system_prompt()
        if system_prompt:
            messages = [{"role": "system", "content": system_prompt}] + messages

        # Merge MCP tools + skill tools + workspace tools
        active_tools = await mcp_manager.get_all_tools()
        skill_tools = skill_manager.get_skill_tools()
        workspace_tools = get_workspace_tools()
        all_tools = active_tools + skill_tools + workspace_tools

        # Multi-round tool call loop
        for round_num in range(MAX_TOOL_ROUNDS + 1):
            response = await self.llm.chat_completion(
                messages, tools=all_tools if all_tools else None
            )

            message = response["choices"][0]["message"]

            # No tool calls — return final response
            if not message.get("tool_calls"):
                return response

            # Safety: max rounds reached
            if round_num >= MAX_TOOL_ROUNDS:
                logger.warning("Max tool call rounds (%d) reached, returning last response", MAX_TOOL_ROUNDS)
                return response

            # Process tool calls
            tool_calls = message["tool_calls"]
            messages.append(message)

            for tool_call in tool_calls:
                function_name = tool_call["function"]["name"]
                tool_call_id = tool_call["id"]

                try:
                    result = await self._execute_tool_call(tool_call)
                except Exception as e:
                    logger.error("Tool call error (%s): %s", function_name, e)
                    result = f"Error: {e}"

                content_for_llm, _success = self._process_tool_result(result)

                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call_id,
                    "name": function_name,
                    "content": content_for_llm,
                })

            logger.debug("Tool round %d complete, %d tool(s) executed", round_num + 1, len(tool_calls))

        return response

    async def run_stream(self, messages: List[Dict[str, Any]]) -> AsyncGenerator[Dict[str, Any], None]:
        """run()과 동일한 로직이지만 각 단계를 SSE 이벤트로 yield하는 제너레이터."""
        messages = list(messages)

        system_prompt = self._build_system_prompt()
        if system_prompt:
            messages = [{"role": "system", "content": system_prompt}] + messages

        active_tools = await mcp_manager.get_all_tools()
        skill_tools = skill_manager.get_skill_tools()
        workspace_tools = get_workspace_tools()
        all_tools = active_tools + skill_tools + workspace_tools

        for round_num in range(MAX_TOOL_ROUNDS + 1):
            yield {"type": "thinking", "content": f"LLM 호출 중... (라운드 {round_num + 1})"}

            response = await self.llm.chat_completion(
                messages, tools=all_tools if all_tools else None
            )

            message = response["choices"][0]["message"]

            if not message.get("tool_calls"):
                yield {"type": "content", "content": message.get("content", "")}
                yield {"type": "done", "full_response": response}
                return

            if round_num >= MAX_TOOL_ROUNDS:
                logger.warning("Max tool call rounds (%d) reached, returning last response", MAX_TOOL_ROUNDS)
                yield {"type": "content", "content": message.get("content", "")}
                yield {"type": "done", "full_response": response}
                return

            tool_calls = message["tool_calls"]
            messages.append(message)

            for tool_call in tool_calls:
                function_name = tool_call["function"]["name"]
                tool_call_id = tool_call["id"]

                yield {
                    "type": "tool_call",
                    "name": function_name,
                    "arguments": tool_call["function"]["arguments"],
                }

                try:
                    result = await self._execute_tool_call(tool_call)
                except Exception as e:
                    logger.error("Tool call error (%s): %s", function_name, e)
                    result = f"Error: {e}"

                content_for_llm, success = self._process_tool_result(result)

                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call_id,
                    "name": function_name,
                    "content": content_for_llm,
                })

                yield {
                    "type": "tool_result",
                    "name": function_name,
                    "result": str(result)[:500],
                    "success": success,
                }

            logger.debug("Tool round %d complete, %d tool(s) executed", round_num + 1, len(tool_calls))


orchestrator = AgentOrchestrator()
