"""
Blender Persistent Server Manager (File-Based IPC)

Manages the lifecycle of the persistent Blender server using file-based communication.
Achieves 25-50x performance improvement by keeping Blender running in background.

Performance Impact:
- Old approach: 5-10s per operation (spawn new Blender each time)
- New approach: 5s startup + <200ms per operation (25-50x faster)

Architecture:
- Command file: Agent writes commands here
- Response file: Blender writes results here
- Ready file: Signals server is initialized

Based on: maya_server_manager_filebased.py
"""

import subprocess
import time
import os
import logging
import json
import tempfile
import glob
from pathlib import Path
from typing import Optional, Dict, Any

logger = logging.getLogger(__name__)

class BlenderServerManagerFileBased:
    """
    Manages persistent Blender server using file-based IPC.

    The server is launched once when the agent starts and handles all Blender
    operations via file queues, avoiding the 5-10 second initialization overhead
    of spawning new Blender processes.
    """

    def __init__(self, base_dir: Optional[Path] = None, blender_path: Optional[str] = None):
        """
        Initialize Blender server manager.

        Args:
            base_dir: Base directory for IPC files (default: temp directory)
            blender_path: Path to Blender executable (if None, will auto-detect)
        """
        self.blender_process: Optional[subprocess.Popen] = None
        self.is_ready = False
        self.server_startup_time = 0.0

        # Setup IPC file paths
        if base_dir is None:
            base_dir = Path(tempfile.gettempdir()) / "plumber_blender_persistent"

        self.base_dir = Path(base_dir)
        self.base_dir.mkdir(parents=True, exist_ok=True)

        self.command_file = self.base_dir / "command.json"
        self.response_file = self.base_dir / "response.json"
        self.ready_file = self.base_dir / "ready.json"

        # Use provided path or auto-detect
        if blender_path:
            self.blender_path = blender_path
            logger.info(f"✓ Using provided Blender path: {blender_path}")
        else:
            # Fallback to auto-detection
            self.blender_path = self._find_blender()
            logger.warning("⚠️ No Blender path provided, using auto-detection (may be inaccurate)")

    def _find_blender(self) -> str:
        """
        Find blender executable path (fallback auto-detection).

        Note: This is a fallback method. Prefer passing discovered path to __init__().
        """
        import platform
        import glob
        system = platform.system()

        # Check PATH first
        try:
            if system == "Windows":
                result = subprocess.run(["where", "blender"], capture_output=True, text=True)
            else:
                result = subprocess.run(["which", "blender"], capture_output=True, text=True)

            if result.returncode == 0 and result.stdout.strip():
                blender_path = result.stdout.strip().split('\n')[0]
                logger.info(f"✓ Found blender in PATH: {blender_path}")
                return blender_path
        except:
            pass

        # Check common installation directories (dynamically)
        if system == "Windows":
            # Scan Blender Foundation directory for any version
            blender_foundation = r"C:\Program Files\Blender Foundation"
            wsl_blender_foundation = r"/mnt/c/Program Files/Blender Foundation"

            for base_dir in [blender_foundation, wsl_blender_foundation]:
                if os.path.exists(base_dir):
                    # Find all Blender installations (sorted by version, newest first)
                    blender_dirs = glob.glob(os.path.join(base_dir, "Blender *"))
                    blender_dirs.sort(reverse=True)  # Newest version first

                    for blender_dir in blender_dirs:
                        blender_exe = os.path.join(blender_dir, "blender.exe")
                        if os.path.exists(blender_exe):
                            logger.info(f"✓ Found blender at: {blender_exe}")
                            return blender_exe

        elif system == "Darwin":  # macOS
            # Check multiple standard macOS locations
            search_patterns = [
                # System Applications
                "/Applications/Blender*.app/Contents/MacOS/Blender",

                # User Applications (common for downloaded installers)
                str(Path.home() / "Applications" / "Blender*.app" / "Contents" / "MacOS" / "Blender"),

                # Homebrew Cellar (versioned installations)
                "/opt/homebrew/Cellar/blender/*/Blender.app/Contents/MacOS/Blender",

                # Homebrew Cask link (may be symlink)
                "/opt/homebrew/Caskroom/blender/*/Blender.app/Contents/MacOS/Blender",

                # Alternative Homebrew path (Intel Macs)
                "/usr/local/Cellar/blender/*/Blender.app/Contents/MacOS/Blender"
            ]

            all_paths = []
            for pattern in search_patterns:
                found = glob.glob(pattern)
                if found:
                    logger.debug(f"[macOS Blender] Pattern '{pattern}' found {len(found)} installation(s)")
                all_paths.extend(found)

            if all_paths:
                # Sort by path (glob.glob returns sorted, newest version typically last)
                all_paths.sort(reverse=True)  # Newest first
                selected_path = all_paths[0]

                logger.info(f"[macOS Blender] ✓ Found blender at: {selected_path}")
                logger.info(f"[macOS Blender]   Total {len(all_paths)} installation(s) detected")

                # Log all found installations for debugging
                if len(all_paths) > 1:
                    logger.debug(f"[macOS Blender] Other installations found:")
                    for i, path in enumerate(all_paths[1:], start=2):
                        logger.debug(f"[macOS Blender]   {i}. {path}")

                return selected_path

            logger.debug("[macOS Blender] No Blender installations found in standard macOS locations")

        else:  # Linux
            # Check standard Linux paths
            possible_paths = [
                "/usr/bin/blender",
                "/usr/local/bin/blender",
                "/opt/blender/blender",
                "/snap/bin/blender",
            ]
            for path in possible_paths:
                if os.path.exists(path):
                    logger.info(f"✓ Found blender at: {path}")
                    return path

        logger.warning("⚠️ blender executable not found in standard locations")
        # Return generic command and hope it's in PATH
        return "blender"

    def start_server(self, timeout: int = 30) -> bool:
        """
        Start Blender persistent server.

        Args:
            timeout: Maximum seconds to wait for server startup

        Returns:
            True if server started successfully, False otherwise
        """
        logger.info("🚀 Starting Blender persistent server (file-based IPC)...")

        # Platform detection logging
        import platform
        import sys
        logger.debug(f"[Blender Server] Platform: {platform.system()} ({os.name})")
        logger.debug(f"[Blender Server] Python: {sys.version.split()[0]}")
        logger.debug(f"[Blender Server] Architecture: {platform.machine()}")
        if platform.system() == 'Darwin':
            logger.debug(f"[Blender Server] macOS Version: {platform.mac_ver()[0]}")

        start_time = time.time()

        # Find server script
        server_script = Path(__file__).parent.parent / "blender_persistent_server_filebased.py"

        if not server_script.exists():
            logger.error(f"❌ Server script not found: {server_script}")
            return False

        logger.info(f"📄 Server script: {server_script}")
        logger.info(f"🎨 Blender path: {self.blender_path}")
        logger.info(f"📁 IPC directory: {self.base_dir}")

        # Clean up old IPC files
        for f in [self.command_file, self.response_file, self.ready_file]:
            if f.exists():
                try:
                    f.unlink()
                except:
                    pass

        try:
            # Launch Blender server process
            # Use --background --python to run in headless mode
            CREATE_NO_WINDOW = 0x08000000 if os.name == 'nt' else 0

            self.blender_process = subprocess.Popen(
                [
                    self.blender_path,
                    "--background",  # Run without GUI
                    "--python", str(server_script),
                    "--",  # Arguments after this are passed to Python script
                    "--command-file", str(self.command_file),
                    "--response-file", str(self.response_file),
                    "--ready-file", str(self.ready_file)
                ],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                creationflags=CREATE_NO_WINDOW if os.name == 'nt' else 0
            )

            logger.info(f"📝 Blender server process started (PID: {self.blender_process.pid})")

            # Wait for ready file
            logger.info("⏳ Waiting for Blender server to initialize...")
            logger.info(f"   Monitoring ready file: {self.ready_file}")

            for attempt in range(timeout * 10):  # Check every 0.1s
                # Check if process is still alive
                if self.blender_process.poll() is not None:
                    logger.error(f"❌ Blender server process died (exit code: {self.blender_process.returncode})")
                    return False

                # Check for ready file
                if self.ready_file.exists():
                    try:
                        with open(self.ready_file, 'r') as f:
                            ready_data = json.load(f)

                        self.server_startup_time = time.time() - start_time
                        self.is_ready = True

                        logger.info("✅ Blender persistent server ready!")
                        logger.info(f"   Startup time: {self.server_startup_time:.2f}s")
                        logger.info(f"   Blender version: {ready_data.get('blender_version')}")
                        logger.info(f"   PID: {ready_data.get('pid')}")
                        logger.info(f"   IPC: File-based (command/response queues)")

                        return True

                    except (json.JSONDecodeError, IOError):
                        # File exists but not complete yet
                        pass

                time.sleep(0.1)

            # Timeout waiting for server
            elapsed = time.time() - start_time
            logger.error(f"❌ Blender server failed to start within {timeout}s (waited {elapsed:.1f}s)")
            self._cleanup_process()
            return False

        except Exception as e:
            logger.error(f"❌ Failed to start Blender server: {e}")
            import traceback
            logger.debug(traceback.format_exc())
            self._cleanup_process()
            return False

    def execute_command(self, command: str, operation_type: str = "generic", timeout: int = 30) -> Dict[str, Any]:
        """
        Execute Blender command via persistent server.

        Args:
            command: Python code to execute in Blender context
            operation_type: Type of operation (for logging)
            timeout: Maximum seconds to wait for response

        Returns:
            Dictionary with result:
                {
                    'success': bool,
                    'result': str,
                    'error': str,
                    'execution_time': float,
                    'total_time': float
                }

        Raises:
            RuntimeError: If server is not ready or request fails
        """
        if not self.is_ready:
            raise RuntimeError("Blender server not ready - call start_server() first")

        logger.debug(f"📝 Executing Blender command: {operation_type}")
        request_start = time.time()

        # Clean up old response file
        if self.response_file.exists():
            try:
                self.response_file.unlink()
            except:
                pass

        # Write command file
        command_data = {
            'command': command,
            'operation_type': operation_type
        }

        try:
            with open(self.command_file, 'w') as f:
                json.dump(command_data, f, indent=2)

            logger.debug(f"📝 Command written to {self.command_file}")

        except Exception as e:
            raise RuntimeError(f"Failed to write command file: {e}")

        # Wait for response file
        for attempt in range(timeout * 10):  # Check every 0.1s
            # Check if server is still alive
            if self.blender_process and self.blender_process.poll() is not None:
                raise RuntimeError(f"Blender server process died (exit code: {self.blender_process.returncode})")

            # Check for response file
            if self.response_file.exists():
                try:
                    with open(self.response_file, 'r') as f:
                        result = json.load(f)

                    total_time = time.time() - request_start
                    result['total_time'] = total_time

                    logger.debug(f"✓ {operation_type} completed in {total_time*1000:.0f}ms")

                    return result

                except (json.JSONDecodeError, IOError):
                    # File exists but not complete yet
                    pass

            time.sleep(0.1)

        # Timeout
        elapsed = time.time() - request_start
        raise TimeoutError(f"Blender command '{operation_type}' timed out after {elapsed:.1f}s")

    def _cleanup_process(self):
        """Clean up Blender server process."""
        if self.blender_process:
            try:
                self.blender_process.terminate()
                try:
                    self.blender_process.wait(timeout=5)
                except subprocess.TimeoutExpired:
                    self.blender_process.kill()
                    self.blender_process.wait()
            except:
                pass

            self.blender_process = None

    def shutdown(self):
        """
        Shutdown Blender server and clean up resources.
        """
        logger.info("🛑 Shutting down Blender persistent server...")

        self._cleanup_process()

        # Clean up IPC files
        for f in [self.command_file, self.response_file, self.ready_file]:
            if f.exists():
                try:
                    f.unlink()
                except:
                    pass

        self.is_ready = False
        logger.info("✓ Blender server shutdown complete")

    def is_alive(self) -> bool:
        """
        Check if server is alive and ready.

        Returns:
            True if server is running and ready
        """
        if not self.is_ready:
            return False

        if not self.blender_process:
            return False

        # Check if process is still running
        if self.blender_process.poll() is not None:
            logger.warning("⚠️ Blender server process died unexpectedly")
            self.is_ready = False
            return False

        return True

    def get_status(self) -> Dict[str, Any]:
        """
        Get server status information.

        Returns:
            Dictionary with server status
        """
        return {
            'is_ready': self.is_ready,
            'is_alive': self.is_alive(),
            'pid': self.blender_process.pid if self.blender_process else None,
            'startup_time': self.server_startup_time,
            'blender_path': self.blender_path,
            'ipc_directory': str(self.base_dir)
        }
