"""Tool execution infrastructure for Promethian.

Executes Python tools in subprocesses with:
- Isolated virtual environments for dependencies
- Timeout protection
- Output capture
- Working directory sandboxing
"""

from __future__ import annotations

import asyncio
import hashlib
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Any

from promethian.config import PromethianConfig
from promethian.core.types import Tool
from promethian.utils.credentials import (
    CredentialRequirement,
    CredentialManager,
    MissingCredentialsError,
    get_credential_manager,
    OPENAI_API_KEY,
    ANTHROPIC_API_KEY,
    STRIPE_API_KEY,
    GITHUB_TOKEN,
    LINKEDIN_EMAIL,
    LINKEDIN_PASSWORD,
)


class ToolExecutionError(Exception):
    """Raised when tool execution fails."""

    def __init__(self, message: str, stdout: str = "", stderr: str = ""):
        super().__init__(message)
        self.stdout = stdout
        self.stderr = stderr


# Registry of known credentials for common env var names
KNOWN_CREDENTIALS: dict[str, CredentialRequirement] = {
    "OPENAI_API_KEY": OPENAI_API_KEY,
    "ANTHROPIC_API_KEY": ANTHROPIC_API_KEY,
    "STRIPE_SECRET_KEY": STRIPE_API_KEY,
    "STRIPE_API_KEY": STRIPE_API_KEY,
    "GITHUB_TOKEN": GITHUB_TOKEN,
    "LINKEDIN_EMAIL": LINKEDIN_EMAIL,
    "LINKEDIN_PASSWORD": LINKEDIN_PASSWORD,
}


def get_credential_info(env_var: str) -> CredentialRequirement:
    """
    Get credential info for an environment variable.

    Returns known credential info if available, otherwise creates a generic one.
    """
    if env_var in KNOWN_CREDENTIALS:
        return KNOWN_CREDENTIALS[env_var]

    # Create a generic credential requirement
    display_name = env_var.replace("_", " ").title()
    return CredentialRequirement(
        env_var=env_var,
        display_name=display_name,
        description=f"Required API key or credential: {env_var}",
    )


class ToolRunner:
    """
    Executes Python tools in isolated environments.

    Follows Claude Code's execution model:
    - Direct subprocess execution (no VM)
    - Dependency isolation via venvs
    - Timeout protection
    - Output capture for visibility
    """

    def __init__(self, config: PromethianConfig):
        self.config = config
        self.envs_dir = Path(config.tool_envs_dir).expanduser()
        self.envs_dir.mkdir(parents=True, exist_ok=True)
        self._env_cache: dict[str, Path] = {}

    def check_credentials(self, tool: Tool) -> list[CredentialRequirement]:
        """
        Check if all required credentials for a tool are available.

        Args:
            tool: The tool to check credentials for

        Returns:
            List of missing CredentialRequirement objects (empty if all set)
        """
        missing = []

        for env_var in tool.required_env_vars:
            value = os.environ.get(env_var)
            if not value:
                missing.append(get_credential_info(env_var))

        return missing

    def get_missing_credentials_message(self, tool: Tool) -> str | None:
        """
        Get a user-friendly message about missing credentials.

        Args:
            tool: The tool to check

        Returns:
            Message string if credentials are missing, None otherwise
        """
        missing = self.check_credentials(tool)
        if not missing:
            return None

        lines = [
            f"This tool ({tool.name}) requires the following credentials that are not set:\n",
        ]

        for req in missing:
            lines.append(f"  {req.display_name}")
            lines.append(f"    Environment variable: {req.env_var}")
            lines.append(f"    Description: {req.description}")
            if req.signup_url:
                lines.append(f"    Get it at: {req.signup_url}")
            lines.append("")

        lines.append("To set credentials, add to your shell profile or .env file:")
        for req in missing:
            lines.append(f'  export {req.env_var}="your-key-here"')

        return "\n".join(lines)

    def _get_deps_hash(self, dependencies: list[str]) -> str:
        """Get a hash of the dependency list for venv caching."""
        deps_str = ",".join(sorted(dependencies))
        return hashlib.sha256(deps_str.encode()).hexdigest()[:12]

    def _get_venv_path(self, dependencies: list[str]) -> Path:
        """Get the venv path for a set of dependencies."""
        if not dependencies:
            return Path(sys.executable).parent.parent  # Use system Python

        deps_hash = self._get_deps_hash(dependencies)
        return self.envs_dir / f"venv_{deps_hash}"

    async def _ensure_venv(self, dependencies: list[str]) -> Path:
        """
        Ensure a venv exists with the required dependencies.

        Returns the path to the venv's Python interpreter.
        """
        if not dependencies:
            return Path(sys.executable)

        venv_path = self._get_venv_path(dependencies)
        deps_hash = self._get_deps_hash(dependencies)

        # Check cache
        if deps_hash in self._env_cache:
            return self._env_cache[deps_hash]

        python_path = venv_path / "bin" / "python"
        if sys.platform == "win32":
            python_path = venv_path / "Scripts" / "python.exe"

        # Check if venv already exists and has deps installed
        marker_file = venv_path / ".deps_installed"
        if venv_path.exists() and marker_file.exists():
            self._env_cache[deps_hash] = python_path
            return python_path

        # Create venv
        if not venv_path.exists():
            proc = await asyncio.create_subprocess_exec(
                sys.executable, "-m", "venv", str(venv_path),
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            await proc.wait()

        # Install dependencies
        if dependencies:
            pip_path = venv_path / "bin" / "pip"
            if sys.platform == "win32":
                pip_path = venv_path / "Scripts" / "pip.exe"

            proc = await asyncio.create_subprocess_exec(
                str(pip_path), "install", *dependencies,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await proc.communicate()

            if proc.returncode != 0:
                raise ToolExecutionError(
                    f"Failed to install dependencies: {dependencies}",
                    stdout=stdout.decode(),
                    stderr=stderr.decode(),
                )

        # Mark as installed
        marker_file.write_text(",".join(dependencies))
        self._env_cache[deps_hash] = python_path

        return python_path

    def _build_runner_script(
        self,
        tool: Tool,
        params: dict[str, Any],
    ) -> str:
        """
        Build a Python script that runs the tool and outputs JSON result.

        The script:
        1. Defines the tool function
        2. Calls it with parameters
        3. Outputs result as JSON to stdout
        """
        # Escape the code and params for embedding
        params_json = json.dumps(params)

        script = f'''
import json
import sys

# Tool code
{tool.code}

# Execute and capture result
try:
    params = json.loads({repr(params_json)})
    result = {tool.function_name}(**params)
    output = {{"success": True, "result": result}}
except Exception as e:
    import traceback
    output = {{
        "success": False,
        "error": str(e),
        "traceback": traceback.format_exc()
    }}

print(json.dumps(output))
'''
        return script

    async def execute(
        self,
        tool: Tool,
        params: dict[str, Any],
    ) -> Any:
        """
        Execute a tool with the given parameters.

        Args:
            tool: The Tool to execute
            params: Parameters to pass to the tool function

        Returns:
            The tool's return value

        Raises:
            ToolExecutionError: If execution fails
        """
        # Ensure venv with dependencies
        python_path = await self._ensure_venv(tool.dependencies)

        # Build runner script
        script = self._build_runner_script(tool, params)

        # Create temp directory for the runner script only
        # Tool executes in user's working directory so files are saved there
        with tempfile.TemporaryDirectory(prefix="promethian_") as script_dir:
            script_path = Path(script_dir) / "runner.py"
            script_path.write_text(script)

            # Use user's working directory for tool execution
            execution_dir = str(self.config.working_dir) if self.config.working_dir else os.getcwd()

            # Execute in subprocess
            try:
                proc = await asyncio.create_subprocess_exec(
                    str(python_path), str(script_path),
                    stdout=asyncio.subprocess.PIPE,
                    stderr=asyncio.subprocess.PIPE,
                    cwd=execution_dir,
                    env={**os.environ, "PYTHONUNBUFFERED": "1"},
                )

                try:
                    stdout, stderr = await asyncio.wait_for(
                        proc.communicate(),
                        timeout=self.config.tool_timeout,
                    )
                except asyncio.TimeoutError:
                    proc.kill()
                    await proc.wait()
                    raise ToolExecutionError(
                        f"Tool execution timed out after {self.config.tool_timeout}s",
                    )

            except Exception as e:
                if isinstance(e, ToolExecutionError):
                    raise
                raise ToolExecutionError(f"Failed to execute tool: {e}")

            # Parse output
            stdout_str = stdout.decode()
            stderr_str = stderr.decode()

            # Truncate if too large
            if len(stdout_str) > self.config.tool_max_output:
                stdout_str = stdout_str[:self.config.tool_max_output] + "\n... (truncated)"

            if proc.returncode != 0:
                raise ToolExecutionError(
                    f"Tool exited with code {proc.returncode}",
                    stdout=stdout_str,
                    stderr=stderr_str,
                )

            # Parse JSON result
            try:
                result = json.loads(stdout_str)
            except json.JSONDecodeError:
                raise ToolExecutionError(
                    f"Tool output is not valid JSON",
                    stdout=stdout_str,
                    stderr=stderr_str,
                )

            if not result.get("success"):
                raise ToolExecutionError(
                    result.get("error", "Unknown error"),
                    stdout=stdout_str,
                    stderr=result.get("traceback", ""),
                )

            return result.get("result")

    async def execute_by_name(
        self,
        tool_name: str,
        tool_input: dict[str, Any],
        available_tools: list[Tool],
    ) -> Any:
        """
        Execute a tool by name from a list of available tools.

        This is the interface expected by the Executor.
        """
        for tool in available_tools:
            if tool.name == tool_name:
                return await self.execute(tool, tool_input)

        raise ToolExecutionError(f"Tool not found: {tool_name}")


class ToolExecutor:
    """
    Adapter class that matches the interface expected by Executor.

    Wraps ToolRunner with the tool list context.
    """

    def __init__(self, runner: ToolRunner, tools: list[Tool]):
        self.runner = runner
        self.tools = tools

    async def execute(
        self,
        tool_name: str,
        tool_input: dict[str, Any],
    ) -> Any:
        """Execute a tool by name."""
        return await self.runner.execute_by_name(
            tool_name=tool_name,
            tool_input=tool_input,
            available_tools=self.tools,
        )
