import functools
import importlib.util
import inspect
import subprocess
import json
import os
from langchain_core.tools import StructuredTool
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
from llm_client import MODELS

import importlib.util
import inspect
import subprocess
import json
import os
import sys
from typing import Any, get_type_hints
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field, create_model

def build_react_agent(model_name: str, temperature: float, max_tokens: int, agency_mode: bool, obsidian_settings: dict, debug_callback=None):
    """Инициализация ядра на базе LangGraph."""
    m_cfg = MODELS.get(model_name)
    llm = ChatOpenAI(
        api_key=m_cfg["apiKey"],
        base_url=m_cfg["apiBase"],
        model_name=m_cfg["model_name"],
        temperature=temperature,
        max_tokens=max_tokens if max_tokens else None,
        streaming=True
    )
    tools = get_dynamic_tools(obsidian_settings, debug_callback) if agency_mode else []
    return create_react_agent(llm, tools=tools)

# Специфическая Pydantic-модель для инструментов без скрипта
class PurePromptSchema(BaseModel):
    context: str = Field(description="Контекст, данные или текст, к которому нужно применить эту инструкцию/промпт.")

class SystemHelpSchema(BaseModel):
    dummy: str = Field(default="", description="Необязательный параметр, оставь пустым.")

def create_subprocess_tool(script_path: str, method_name: str,
                           hidden_prompt: str, debug_callback=None):
    """Вид 2: Инструмент со скриптом + пост-процессинг."""

    script_dir = os.path.dirname(os.path.abspath(script_path))
    added_to_path = False    
    if script_dir not in sys.path:
        sys.path.insert(0, script_dir)
        added_to_path = True

    try:
        spec = importlib.util.spec_from_file_location("user_dynamic_tool", script_path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        func = getattr(module, method_name)
        docstring = inspect.getdoc(func) or f"Выполняет скрипт {method_name}"

        # ← КЛЮЧЕВОЕ: строим схему ДО того, как потеряем оригинальную сигнатуру
        args_schema = _build_pydantic_schema_from_func(func)

    except Exception as e:
        raise RuntimeError(f"Ошибка чтения сигнатуры {script_path}: {e}")
    finally:
        # Убираем путь сразу после импорта и получения функции
        if added_to_path:
            sys.path.remove(script_dir)

    @functools.wraps(func)
    def wrapper(**kwargs: Any) -> str:
        input_json = json.dumps(kwargs, indent=2, ensure_ascii=False)
        runner_code = f"""
import json
import sys
import importlib.util
import os

# Добавляем папку со скриптом в sys.path, чтобы работали локальные импорты (например, norminv)
path_to_add = os.path.dirname(os.path.abspath(r'{script_path}'))
original_path = sys.path.copy()
sys.path.insert(0, path_to_add)

try:
    spec = importlib.util.spec_from_file_location('sub_module', r'{script_path}')
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    
    kwargs = json.loads(sys.stdin.read())
    result = getattr(module, '{method_name}')(**kwargs)
    print(result)
except Exception as e:
    print(f"SCRIPT RUNTIME ERROR: {{e}}", file=sys.stderr)
    sys.exit(1)
finally:
    # Возвращаем sys.path в исходное состояние (чистота внутри процесса)
    sys.path = original_path
"""
        try:
            process = subprocess.run(
                [sys.executable, "-c", runner_code],
                input=json.dumps(kwargs),
                capture_output=True,
                text=True,
                encoding='utf-8',
                check=True
            )
            raw_output = process.stdout.strip()

            # ВЫЗЫВАЕМ КОЛБЭК, если он передан
            if debug_callback:
                debug_callback(input_json, raw_output)

            raw_stripped = raw_output.strip() if raw_output else ""
            prompt_stripped = hidden_prompt.strip() if hidden_prompt else ""

            if raw_stripped and prompt_stripped:
                # Случай 1: Есть и результат скрипта, и инструкция — полный вариант
                result = (
                    f"Инструмент вернул следующий результат:\n"
                    f"<tool_output>\n{raw_output}\n</tool_output>\n\n"
                    f"---\n"
                    f"Инструкция по оформлению финального ответа пользователю:\n"
                    f"<instruction>\n{hidden_prompt}\n</instruction>"
                )

            elif raw_stripped and not prompt_stripped:
                # Случай 2: Есть результат, нет инструкции 
                result = (
                    f"Инструмент успешно выполнен. Передай пользователю следующий результат "
                    f"ДОСЛОВНО, без изменений, сокращений и дополнений:\n\n"
                    f"<tool_output>\n{raw_output}\n</tool_output>"
                )

            elif not raw_stripped and prompt_stripped:
                # Случай 3: Скрипт вернул пустоту (заглушка/нет данных) — только промпт как инструкция
                result = prompt_stripped
                """ result = (
                    f"Скрипт не вернул данных. Выполни следующую инструкцию:\n\n{hidden_prompt}"
                )"""

            else:
                # Случай 4: Оба пустые — сообщаем агенту честно
                result = (
                    "Инструмент не вернул никаких данных и не содержит инструкций. "
                    "Сообщи пользователю, что инструмент настроен некорректно."
                )

            return result

        except subprocess.CalledProcessError as e:
            # Даже если скрипт упал, мы отдаем ошибку + промпт, чтобы агент мог обработать её по инструкции
            error_msg = f"ОШИБКА ИСПОЛНЕНИЯ (Код {e.returncode}):\nSTDOUT: {e.stdout}\nSTDERR: {e.stderr}"
            if hidden_prompt:
                return f"{error_msg}\n\nИНСТРУКЦИЯ НА СЛУЧАЙ ОШИБКИ:\n{hidden_prompt}"
            return error_msg

    # Явно передаём args_schema — LLM будет видеть реальную типизацию
    return StructuredTool.from_function(
        func=wrapper,
        name=func.__name__,
        description=docstring,
        args_schema=args_schema,   # ← вместо infer_schema=True
    )

def get_dynamic_tools(obsidian_settings: dict, debug_callback=None):
    """Собирает инструменты двух видов на основе настроек Obsidian."""
    tools = []
    custom_tools = obsidian_settings.get("customTools", [])

    for ct in custom_tools:
        name = ct.get("name")
        desc = ct.get("description")
        full_path = ct.get("scriptPath", "")
        # Поддерживаем оба ключа, если в JS/TS они не раскрылись
        hidden_prompt = ct.get("hiddenPromptExpanded") or ct.get("hiddenPrompt", "")

        if not name:
            continue  # Инструмент без имени создать нельзя

        # Парсим путь к скрипту (учитываем Windows-пути с буквой диска)
        script_path = ""
        method_name = "main"

        if full_path:
            parts = full_path.rsplit(":", 1) # Разделяем строку с конца 1 раз

            if len(parts) == 2:
                # Если слева от разделения ровно 1 символ-буква (например, 'D'),
                # значит это путь без метода, а двоеточие — от диска Windows.
                if len(parts[0]) == 1 and parts[0].isalpha():
                    script_path = full_path
                else:
                    script_path = parts[0]
                    method_name = parts[1]
            else:
                script_path = full_path

        # ПРОВЕРКА: Есть ли физический скрипт?
        is_valid_script = script_path and os.path.exists(script_path)

        if not is_valid_script:
            # ----------------------------------------------------
            # ВИД 1: Чистый промпт (Инструмент-инструкция / Системная подсказка)
            # ----------------------------------------------------
            def create_pure_prompt_wrapper(prompt_text: str):
                # Функция принимает context, который ИИ обязан туда положить
                def pure_prompt_wrapper(context: str) -> str:
                    return (
                        f"ВХОДНЫЕ ДАННЫЕ ОТ ПОЛЬЗОВАТЕЛЯ/МОДЕЛИ:\n{context}\n\n"
                        f"ИНСТРУКЦИЯ ПО ОБРАБОТКЕ И ПРИЛИЗЫВАНИЮ (ОБЯЗАТЕЛЬНО К ИСПОЛНЕНИЮ):\n{prompt_text}"
                    )
                return pure_prompt_wrapper

            tool = StructuredTool.from_function(
                func=create_pure_prompt_wrapper(hidden_prompt),
                name=name,
                description=desc or "Инструмент для применения специфических правил форматирования или инструкций.",
                args_schema=PurePromptSchema # Жестко навязываем схему с аргументом context!
            )
            tools.append(tool)
            print(f"🛠 Инструмент-промпт '{tool.name}' (без скрипта) успешно инициализирован.")

        else:
            # ----------------------------------------------------
            # ВИД 2: Скрипт + Прилизывание
            # ----------------------------------------------------
            try:
                tool = create_subprocess_tool(script_path, method_name, hidden_prompt, debug_callback)
                tool.name = name  # Приоритет имени из UI Obsidian
                if desc:
                    tool.description = desc
                tools.append(tool)
                print(f"🛠 Инструмент-скрипт '{tool.name}' успешно инициализирован.")
            except Exception as e:
                print(f"❌ Ошибка генерации инструмента-скрипта '{name}': {e}")

    def run_system_help(dummy: str = "") -> str:
        return _generate_commands_list(custom_tools)

    help_tool = StructuredTool.from_function(
        func=run_system_help,
        name="show_available_commands",
        description="""Используй этот инструмент, если пользователь спрашивает "Что ты умеешь?", 
        "Покажи help", "Какие есть команды" или просит список возможностей.""",
        args_schema=SystemHelpSchema
    )
    tools.append(help_tool)

    return tools

def _build_pydantic_schema_from_func(func) -> type[BaseModel]:
    """
    Динамически строит Pydantic BaseModel из сигнатуры функции.
    Используется для корректной передачи схемы в StructuredTool,
    чтобы LLM видела реальные типы аргументов, а не **kwargs.
    """
    sig = inspect.signature(func)
    
    # Пробуем получить аннотации типов (включая from __future__ import annotations)
    try:
        hints = get_type_hints(func)
    except Exception:
        hints = {}

    fields = {}
    for param_name, param in sig.parameters.items():
        if param_name in ("self", "cls"):
            continue
        if param.kind in (
            inspect.Parameter.VAR_POSITIONAL,   # *args
            inspect.Parameter.VAR_KEYWORD,      # **kwargs
        ):
            continue

        # Тип параметра
        annotation = hints.get(param_name, Any)

        # Дефолтное значение
        if param.default is inspect.Parameter.empty:
            # Обязательный параметр
            fields[param_name] = (annotation, Field(...))
        else:
            fields[param_name] = (annotation, Field(default=param.default))

    if not fields:
        # Фоллбек: если функция не имеет параметров или все **kwargs —
        # создаём схему с одним полем, чтобы LLM хоть что-то передала
        fields["kwargs"] = (dict, Field(default_factory=dict,
                                        description="Аргументы функции"))

    model = create_model(
        f"{func.__name__}_Schema",
        **fields
    )
    return model

def _generate_commands_list(custom_tools: list[dict[str, Any]]) -> str:
    """
    Генерирует форматированный список доступных команд и их описаний
    для использования в инструменте help.
    """
    if not custom_tools:
        return "В данный момент нет доступных дополнительных команд. Ты работаешь как обычный собеседник."
        
    lines = ["Я могу использовать следующие инструменты/команды:\n"]
    for ct in custom_tools:
        name = ct.get("name")
        desc = ct.get("description", "Описание отсутствует")
        if name:
            lines.append(f"- **{name}**: {desc}")
            
    lines.append("\nПерескажи этот список пользователю в удобном формате.")
    return "\n".join(lines)