"""
Godot MCP Core Module

Contains the core GodotServer class and utility functions used by all tools.
"""

import asyncio
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Optional

from mcp.server.fastmcp import FastMCP

# Create MCP server - this is the shared instance used by all tool modules
mcp = FastMCP("godot-mcp", instructions="MCP server for interfacing with Godot game engine")

# Configuration
DEBUG_MODE = os.environ.get("DEBUG") == "true"
GODOT_DEBUG_MODE = True


class GodotServer:
    """Main server class for Godot MCP operations."""

    def __init__(self) -> None:
        """Initialize the GodotServer instance."""
        self.godot_path: Optional[str] = None
        self.active_process: Optional[subprocess.Popen] = None
        self.validated_paths: dict[str, bool] = {}

    def detect_godot_path(self) -> str:
        """Detect the Godot executable path based on the operating system.

        Returns:
            The path to the Godot executable.

        Raises:
            RuntimeError: If no valid Godot executable can be found.
        """
        # If godot_path is already set and valid, use it
        if self.godot_path and self.is_valid_godot_path_sync(self.godot_path):
            self.log_debug(f"Using existing Godot path: {self.godot_path}")
            return self.godot_path

        # Check environment variable next
        if os.environ.get("GODOT_PATH"):
            env_path = os.environ["GODOT_PATH"]
            self.log_debug(f"Checking GODOT_PATH environment variable: {env_path}")
            if self.is_valid_godot_path_sync(env_path):
                self.godot_path = env_path
                self.log_debug(f"Using Godot path from environment: {self.godot_path}")
                return self.godot_path
            else:
                self.log_debug("GODOT_PATH environment variable is invalid")

        # Auto-detect based on platform
        os_platform = sys.platform
        self.log_debug(f"Auto-detecting Godot path for platform: {os_platform}")

        possible_paths = ["godot"]

        # Add platform-specific paths
        if os_platform == "darwin":
            possible_paths.extend(
                [
                    "/Applications/Godot.app/Contents/MacOS/Godot",
                    "/Applications/Godot_4.app/Contents/MacOS/Godot",
                    f"{os.environ.get('HOME', '')}/Applications/Godot.app/Contents/MacOS/Godot",
                    f"{os.environ.get('HOME', '')}/Applications/Godot_4.app/Contents/MacOS/Godot",
                    f"{os.environ.get('HOME', '')}/Library/Application Support/Steam/steamapps/common/Godot Engine/Godot.app/Contents/MacOS/Godot",
                ]
            )
        elif os_platform == "win32":
            possible_paths.extend(
                [
                    r"C:\Program Files\Godot\Godot.exe",
                    r"C:\Program Files (x86)\Godot\Godot.exe",
                    r"C:\Program Files\Godot_4\Godot.exe",
                    r"C:\Program Files (x86)\Godot_4\Godot.exe",
                    f"{os.environ.get('USERPROFILE', '')}\\Godot\\Godot.exe",
                ]
            )
        else:  # Linux
            possible_paths.extend(
                [
                    "/usr/bin/godot",
                    "/usr/local/bin/godot",
                    "/snap/bin/godot",
                    f"{os.environ.get('HOME', '')}/.local/bin/godot",
                ]
            )

        # Try each possible path
        for path in possible_paths:
            if self.is_valid_godot_path_sync(path):
                self.godot_path = path
                self.log_debug(f"Found Godot at: {path}")
                return path

        # If we get here, we couldn't find Godot
        self.log_debug(f"Warning: Could not find Godot in common locations for {os_platform}")
        raise RuntimeError(
            "Could not find a valid Godot executable. Set GODOT_PATH=/path/to/godot environment variable to specify the correct path."
        )

    def is_valid_godot_path_sync(self, path: str) -> bool:
        """Synchronous validation for constructor use.

        Args:
            path: The path to validate.

        Returns:
            True if the path is valid, False otherwise.
        """
        try:
            self.log_debug(f"Quick-validating Godot path: {path}")
            return path == "godot" or Path(path).exists()
        except Exception as error:
            self.log_debug(f"Invalid Godot path: {path}, error: {error}")
            return False

    async def validate_godot_path(self, path: str) -> bool:
        """Validate if a Godot path is valid and executable.

        Args:
            path: The path to validate.

        Returns:
            True if the path is valid and executable, False otherwise.
        """
        # Check cache first
        if path in self.validated_paths:
            return self.validated_paths[path]

        try:
            self.log_debug(f"Validating Godot path: {path}")

            # Check if the file exists (skip for 'godot' which might be in PATH)
            if path != "godot" and not Path(path).exists():
                self.log_debug(f"Path does not exist: {path}")
                self.validated_paths[path] = False
                return False

            # Try to execute Godot with --version flag
            proc = await asyncio.create_subprocess_shell(
                f'"{path}" --version',
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            await proc.wait()

            self.log_debug(f"Valid Godot path: {path}")
            self.validated_paths[path] = True
            return True
        except (OSError, FileNotFoundError, PermissionError) as error:
            self.log_debug(f"Invalid Godot path: {path}, error: {error}")
            self.validated_paths[path] = False
            return False
        except Exception as error:
            self.log_debug(f"Invalid Godot path: {path}, error: {error}")
            self.validated_paths[path] = False
            return False

    def log_debug(self, message: str) -> None:
        """Log debug messages if debug mode is enabled.

        Args:
            message: The message to log.
        """
        if DEBUG_MODE:
            print(f"[DEBUG] {message}", file=sys.stderr)

    def camel_to_snake(self, name: str) -> str:
        """Convert camelCase to snake_case.

        Args:
            name: The camelCase string to convert.

        Returns:
            The snake_case version of the string.
        """
        import re

        s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
        return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()

    def normalize_parameters(self, params: dict) -> dict:
        """Normalize parameters to snake_case format for Godot script.

        Args:
            params: The parameters dictionary with potentially camelCase keys.

        Returns:
            A new dictionary with snake_case keys.
        """
        if not params:
            return params

        result = {}
        for key, value in params.items():
            # Convert camelCase to snake_case
            snake_key = self.camel_to_snake(key)
            result[snake_key] = value

        return result

    def run_subprocess(self, cmd: str, cwd: str = None) -> tuple[str, str]:
        """Run a subprocess synchronously and return stdout, stderr.

        Args:
            cmd: The command to run.
            cwd: The working directory for the command.

        Returns:
            A tuple of (stdout, stderr) strings.
        """
        self.log_debug(f"Running: {cmd}")
        proc = subprocess.Popen(
            cmd,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            cwd=cwd,
            text=True,
        )
        stdout, stderr = proc.communicate()
        return stdout, stderr

    async def execute_operation(
        self, operation: str, params: dict, project_path: str
    ) -> tuple[str, str]:
        """Execute a Godot operation using the operations script.

        Args:
            operation: The operation name to execute.
            params: The parameters for the operation.
            project_path: The path to the Godot project.

        Returns:
            A tuple of (stdout, stderr) strings.

        Raises:
            RuntimeError: If no valid Godot executable can be found.
        """
        self.log_debug(f"Executing operation: {operation} in project: {project_path}")
        self.log_debug(f"Original operation params: {json.dumps(params)}")

        # Ensure godot_path is set
        if not self.godot_path:
            self.detect_godot_path()
            if not self.godot_path:
                raise RuntimeError("Could not find a valid Godot executable path")

        # Convert camelCase parameters to snake_case for Godot script
        snake_case_params = self.normalize_parameters(params)
        self.log_debug(f"Converted snake_case params: {json.dumps(snake_case_params)}")

        operations_script = Path(__file__).parent / "scripts" / "godot_operations.gd"

        # Build command
        json_params = json.dumps(snake_case_params)

        args = [
            "--headless",
            "--path",
            project_path,
            "--script",
            str(operations_script),
            operation,
            json_params,
        ]

        if GODOT_DEBUG_MODE:
            args.append("--debug-godot")

        # Build command with proper quoting - use shlex for safer argument handling
        import shlex

        quoted_args = []
        for arg in args:
            # Always quote arguments to handle spaces and special characters
            quoted_args.append(shlex.quote(arg))

        cmd = f'"{self.godot_path}" {" ".join(quoted_args)}'
        self.log_debug(f"Executing: {cmd}")

        return await asyncio.to_thread(
            lambda: self.run_subprocess(cmd, project_path),
        )


# Global server instance - shared by all tool modules
godot_server = GodotServer()


def validate_path(path: str) -> bool:
    """Validate a path to prevent path traversal attacks.

    Args:
        path: The path to validate.

    Returns:
        True if the path is safe, False if it contains path traversal patterns.
    """
    if not path or ".." in path:
        return False
    return True


def create_error_response(message: str, possible_solutions: list[str] = None) -> str:
    """Create a standardized error response with possible solutions.

    Args:
        message: The error message.
        possible_solutions: Optional list of possible solutions.

    Returns:
        A formatted error message string.
    """
    result = message

    if possible_solutions:
        result += "\n\nPossible solutions:\n- " + "\n- ".join(possible_solutions)

    return result


def extract_json_from_output(output: str) -> str:
    """Extract JSON object or array from GDScript output.

    GDScript operations may print log messages before the JSON result.
    This function finds and extracts the JSON portion of the output.

    Args:
        output: The raw stdout from a Godot operation.

    Returns:
        The extracted JSON string, or the original output if no JSON found.
    """
    if not output:
        return output

    # Try to find JSON object (starts with {) or array (starts with [)
    lines = output.strip().split("\n")

    for i, line in enumerate(lines):
        line = line.strip()
        if line.startswith("{") or line.startswith("["):
            # Found potential JSON start, join remaining lines
            json_str = "\n".join(lines[i:])
            try:
                # Validate it's actual JSON
                json.loads(json_str)
                return json_str
            except json.JSONDecodeError:
                # Not valid JSON, try just this line
                try:
                    json.loads(line)
                    return line
                except json.JSONDecodeError:
                    continue

    # No JSON found, return original
    return output
