"""
NX Session Manager - Tracks NX sessions to prevent accidental closure of user sessions.

This module ensures that Atomizer only closes NX sessions that it explicitly started,
never touching sessions that were already running or started by the user.

Key Features:
- Automatically starts NX if not running
- Tracks PID of the NX instance we started
- Only closes NX instances we started (never user's sessions)
- Waits for NX to fully load before proceeding
"""

import os
import json
import time
import subprocess
from pathlib import Path
from typing import Optional, Tuple, List


# Session tracking file location
SESSION_LOCK_DIR = Path(os.environ.get('TEMP', '/tmp')) / 'atomizer_nx_sessions'

# Default NX installation paths (in order of preference)
DEFAULT_NX_PATHS = [
    Path(r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN\ugraf.exe"),  # DesignCenter (preferred)
    Path(r"C:\Program Files\Siemens\NX2506\NXBIN\ugraf.exe"),
    Path(r"C:\Program Files\Siemens\NX2412\NXBIN\ugraf.exe"),
    Path(r"C:\Program Files\Siemens\Simcenter3D_2506\NXBIN\ugraf.exe"),
    Path(r"C:\Program Files\Siemens\Simcenter3D_2412\NXBIN\ugraf.exe"),
]


class NXSessionManager:
    """
    Manages NX sessions to ensure we only close sessions we started.

    Usage:
        manager = NXSessionManager()

        # Check if NX is already running before starting optimization
        was_running = manager.is_nx_running()

        # Start NX if needed (records that we started it)
        if not was_running:
            manager.start_nx()

        # ... run optimization ...

        # Only close NX if we started it
        if manager.can_close_nx():
            manager.close_nx()
    """

    def __init__(self, session_id: Optional[str] = None, nx_exe_path: Optional[Path] = None):
        """
        Initialize the session manager.

        Args:
            session_id: Unique identifier for this optimization session.
                       If None, uses process ID.
            nx_exe_path: Path to ugraf.exe. If None, auto-detects from common locations.
        """
        self.session_id = session_id or f"atomizer_{os.getpid()}"
        self.lock_file = SESSION_LOCK_DIR / f"{self.session_id}.lock"
        self._nx_was_running_before_start = None
        self._started_nx_pid = None

        # Find NX executable
        self.nx_exe_path = nx_exe_path or self._find_nx_executable()

        # Ensure lock directory exists
        SESSION_LOCK_DIR.mkdir(parents=True, exist_ok=True)

    def _find_nx_executable(self) -> Optional[Path]:
        """Find NX executable from common installation paths."""
        for path in DEFAULT_NX_PATHS:
            if path.exists():
                return path
        return None

    def is_nx_running(self) -> bool:
        """Check if NX (ugraf.exe) is currently running."""
        try:
            result = subprocess.run(
                ['tasklist', '/FI', 'IMAGENAME eq ugraf.exe'],
                capture_output=True,
                text=True,
                timeout=10
            )
            return 'ugraf.exe' in result.stdout.lower()
        except Exception:
            return False

    def get_nx_pids(self) -> list:
        """Get list of all NX process IDs currently running."""
        pids = []
        try:
            result = subprocess.run(
                ['tasklist', '/FI', 'IMAGENAME eq ugraf.exe', '/FO', 'CSV'],
                capture_output=True,
                text=True,
                timeout=10
            )
            for line in result.stdout.strip().split('\n')[1:]:  # Skip header
                if 'ugraf.exe' in line.lower():
                    parts = line.strip('"').split('","')
                    if len(parts) >= 2:
                        try:
                            pids.append(int(parts[1]))
                        except ValueError:
                            pass
        except Exception:
            pass
        return pids

    def start_nx(self, wait_for_ready: bool = True, timeout_seconds: int = 60) -> Optional[int]:
        """
        Start a new NX instance and track its PID.

        Args:
            wait_for_ready: If True, wait for NX to fully load before returning
            timeout_seconds: Maximum time to wait for NX to start (default: 60s)

        Returns:
            PID of the started NX process, or None if failed

        Raises:
            RuntimeError: If NX executable not found or failed to start
        """
        if not self.nx_exe_path or not self.nx_exe_path.exists():
            raise RuntimeError(
                f"NX executable not found. Checked paths:\n"
                f"{chr(10).join(str(p) for p in DEFAULT_NX_PATHS)}\n"
                f"Please install NX or specify the path manually."
            )

        # Get PIDs before starting
        pids_before = set(self.get_nx_pids())

        print(f"[NX SESSION] Starting NX from: {self.nx_exe_path}")
        print(f"[NX SESSION]   Existing NX PIDs: {pids_before or 'none'}")

        try:
            # Start NX as a detached process (won't block)
            # Using subprocess.Popen with CREATE_NEW_PROCESS_GROUP
            process = subprocess.Popen(
                [str(self.nx_exe_path)],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
            )

            # The Popen PID might be a launcher, not ugraf.exe itself
            # We need to wait and detect the actual ugraf.exe PID
            if wait_for_ready:
                print(f"[NX SESSION]   Waiting for NX to start (up to {timeout_seconds}s)...")
                start_time = time.time()

                while (time.time() - start_time) < timeout_seconds:
                    current_pids = set(self.get_nx_pids())
                    new_pids = current_pids - pids_before

                    if new_pids:
                        # Found a new NX process!
                        self._started_nx_pid = list(new_pids)[0]
                        elapsed = time.time() - start_time
                        print(f"[NX SESSION]   NX started! PID: {self._started_nx_pid} (took {elapsed:.1f}s)")
                        return self._started_nx_pid

                    # Show progress
                    elapsed = time.time() - start_time
                    if int(elapsed) % 5 == 0 and elapsed > 0:
                        print(f"[NX SESSION]   Still waiting... ({elapsed:.0f}s)")

                    time.sleep(1)

                # Timeout reached
                print(f"[NX SESSION]   WARNING: Timeout waiting for NX to start")
                return None

            else:
                # Don't wait - just return the Popen PID (may not be ugraf.exe)
                self._started_nx_pid = process.pid
                return process.pid

        except Exception as e:
            print(f"[NX SESSION]   ERROR starting NX: {e}")
            raise RuntimeError(f"Failed to start NX: {e}")

    def record_session_start(self, nx_was_running: bool, started_pid: Optional[int] = None):
        """
        Record that we're starting an optimization session.

        Args:
            nx_was_running: Whether NX was already running before we started
            started_pid: PID of NX process we started (if any)
        """
        self._nx_was_running_before_start = nx_was_running

        session_data = {
            'session_id': self.session_id,
            'start_time': time.time(),
            'nx_was_running_before': nx_was_running,
            'started_nx_pid': started_pid,
            'original_nx_pids': self.get_nx_pids() if nx_was_running else []
        }

        with open(self.lock_file, 'w') as f:
            json.dump(session_data, f, indent=2)

        print(f"[NX SESSION] Recorded session start:")
        print(f"  Session ID: {self.session_id}")
        print(f"  NX was already running: {nx_was_running}")
        if started_pid:
            print(f"  Started NX PID: {started_pid}")

    def load_session_data(self) -> Optional[dict]:
        """Load session data from lock file."""
        if self.lock_file.exists():
            try:
                with open(self.lock_file, 'r') as f:
                    return json.load(f)
            except Exception:
                pass
        return None

    def can_close_nx(self) -> bool:
        """
        Determine if we're allowed to close NX.

        Returns True only if:
        1. We have a valid session record
        2. NX was NOT running before we started
        3. We explicitly started NX for this session
        """
        session_data = self.load_session_data()

        if not session_data:
            print("[NX SESSION] WARNING: No session data found - refusing to close NX")
            return False

        if session_data.get('nx_was_running_before', True):
            print("[NX SESSION] NX was running before optimization - NOT closing")
            return False

        if session_data.get('started_nx_pid') is None:
            print("[NX SESSION] We didn't start NX - NOT closing")
            return False

        print("[NX SESSION] We started NX for this session - safe to close")
        return True

    def get_pids_we_can_close(self) -> list:
        """
        Get list of NX PIDs that we're allowed to close.

        Only returns PIDs that:
        1. Are currently running
        2. Were started by us (not in original_nx_pids)
        """
        session_data = self.load_session_data()
        if not session_data:
            return []

        original_pids = set(session_data.get('original_nx_pids', []))
        current_pids = set(self.get_nx_pids())

        # PIDs we can close = current PIDs minus original PIDs
        closeable = list(current_pids - original_pids)

        if closeable:
            print(f"[NX SESSION] PIDs we can close: {closeable}")
        else:
            print(f"[NX SESSION] No PIDs to close (original: {original_pids}, current: {current_pids})")

        return closeable

    def close_nx_if_allowed(self) -> bool:
        """
        Close NX only if we're allowed to.

        Returns:
            True if NX was closed, False if not allowed or already closed
        """
        if not self.can_close_nx():
            return False

        pids_to_close = self.get_pids_we_can_close()

        if not pids_to_close:
            print("[NX SESSION] No NX processes to close")
            return False

        for pid in pids_to_close:
            try:
                print(f"[NX SESSION] Closing NX PID {pid}...")
                subprocess.run(
                    ['taskkill', '/F', '/PID', str(pid)],
                    capture_output=True,
                    timeout=30
                )
            except Exception as e:
                print(f"[NX SESSION] Failed to close PID {pid}: {e}")

        return True

    def cleanup(self):
        """Remove session lock file."""
        if self.lock_file.exists():
            try:
                self.lock_file.unlink()
                print(f"[NX SESSION] Cleaned up session lock: {self.lock_file}")
            except Exception:
                pass


def ensure_nx_running(
    session_id: Optional[str] = None,
    auto_start: bool = True,
    start_timeout: int = 60
) -> Tuple[NXSessionManager, bool]:
    """
    Ensure NX is running for optimization. Starts NX automatically if needed.

    This is the main entry point for optimization scripts. It:
    1. Checks if NX is already running
    2. If not, starts a fresh NX instance
    3. Records the session so we know which instance to close later

    Args:
        session_id: Unique session identifier (defaults to process ID)
        auto_start: If True, automatically start NX if not running
        start_timeout: Seconds to wait for NX to start (default: 60)

    Returns:
        Tuple of (session_manager, nx_was_started)
        - session_manager: Use this to close NX when done
        - nx_was_started: True if we started NX, False if it was already running

    Example:
        manager, we_started_nx = ensure_nx_running()

        # ... run optimization ...

        # This will only close NX if we started it
        manager.close_nx_if_allowed()
        manager.cleanup()
    """
    manager = NXSessionManager(session_id)

    was_running = manager.is_nx_running()
    started_pid = None

    if was_running:
        print("[NX SESSION] NX is already running - will NOT close after optimization")
        # Record existing PIDs so we don't close them
        original_pids = manager.get_nx_pids()
        print(f"[NX SESSION]   Existing PIDs: {original_pids}")
    else:
        if auto_start:
            print("[NX SESSION] NX is not running - starting fresh instance...")
            started_pid = manager.start_nx(wait_for_ready=True, timeout_seconds=start_timeout)

            if started_pid:
                print(f"[NX SESSION] Successfully started NX (PID: {started_pid})")
                print(f"[NX SESSION] Will close this instance when optimization completes")
            else:
                raise RuntimeError(
                    "Failed to start NX. Please ensure NX is installed and try again.\n"
                    f"Checked paths: {', '.join(str(p) for p in DEFAULT_NX_PATHS)}"
                )
        else:
            raise RuntimeError(
                "NX is not running and auto_start=False.\n"
                "Please start NX manually before running optimization."
            )

    manager.record_session_start(
        nx_was_running=was_running,
        started_pid=started_pid
    )

    return manager, not was_running


# Global session manager for module-level usage
_global_session_manager: Optional[NXSessionManager] = None


def get_session_manager() -> Optional[NXSessionManager]:
    """Get the global session manager if one exists."""
    return _global_session_manager


def set_session_manager(manager: NXSessionManager):
    """Set the global session manager."""
    global _global_session_manager
    _global_session_manager = manager


def validate_nx_running(
    require_nx: bool = True,
    timeout_seconds: int = 0,
    auto_prompt: bool = True
) -> bool:
    """
    Validate that NX is running before starting optimization.

    This function checks if NX (ugraf.exe) is running and provides
    clear feedback to the user if it's not.

    Args:
        require_nx: If True, raise an error if NX is not running.
                   If False, just return the status.
        timeout_seconds: If > 0, wait this many seconds for user to open NX.
                        Check every 5 seconds with a countdown.
        auto_prompt: If True, print helpful message when NX is not running.

    Returns:
        True if NX is running, False otherwise.

    Raises:
        RuntimeError: If require_nx=True and NX is not running (after timeout).

    Example:
        # At the start of optimization:
        validate_nx_running(require_nx=True, timeout_seconds=60)
        # Will wait up to 60 seconds for user to open NX
    """
    manager = NXSessionManager("validation_check")

    # Initial check
    if manager.is_nx_running():
        print("[NX VALIDATION] ✓ NX is running")
        return True

    # NX is not running
    if auto_prompt:
        print("\n" + "="*70)
        print("  NX IS NOT RUNNING")
        print("="*70)
        print("\n  Atomizer requires NX to be open to run FEA simulations.")
        print("  Please open NX (Simcenter 3D) and try again.")
        print("\n  Note: You can leave NX open - Atomizer will not close it")
        print("        since it was already running before optimization started.")
        print("="*70 + "\n")

    # If timeout specified, wait for user to open NX
    if timeout_seconds > 0:
        print(f"[NX VALIDATION] Waiting up to {timeout_seconds}s for NX to start...")

        start_time = time.time()
        check_interval = 5  # seconds

        while (time.time() - start_time) < timeout_seconds:
            remaining = int(timeout_seconds - (time.time() - start_time))

            if manager.is_nx_running():
                print(f"\n[NX VALIDATION] ✓ NX detected! Proceeding with optimization...")
                return True

            print(f"  Waiting for NX... ({remaining}s remaining)")
            time.sleep(check_interval)

        print(f"\n[NX VALIDATION] Timeout reached. NX was not started.")

    # NX is still not running
    if require_nx:
        raise RuntimeError(
            "NX is not running. Please open NX (Simcenter 3D) before starting optimization.\n"
            "Atomizer requires NX to execute FEA simulations via journal scripts."
        )

    return False


def require_nx_or_exit(
    auto_start: bool = True,
    start_timeout: int = 60
) -> NXSessionManager:
    """
    Convenience function: Ensure NX is running, starting it if needed.

    This is the recommended function to call at the start of any optimization.
    It will:
    1. Check if NX is already running
    2. If not, automatically start NX (takes ~10-15 seconds)
    3. Track which NX instance we started
    4. Only close that specific instance when done (never user's sessions)

    Args:
        auto_start: If True, start NX automatically if not running (default: True)
        start_timeout: Seconds to wait for NX to start (default: 60)

    Returns:
        NXSessionManager configured with session tracking

    Example:
        # At start of run_optimization.py:
        from optimization_engine.utils import require_nx_or_exit

        session_manager = require_nx_or_exit()
        # ... run optimization ...

        # At end - only closes NX if we started it!
        session_manager.close_nx_if_allowed()
        session_manager.cleanup()
    """
    # Use ensure_nx_running which handles auto-start
    manager, nx_was_started = ensure_nx_running(
        auto_start=auto_start,
        start_timeout=start_timeout
    )

    return manager


if __name__ == '__main__':
    # Test the session manager
    print("Testing NX Session Manager...")

    manager = NXSessionManager("test_session")

    print(f"NX running: {manager.is_nx_running()}")
    print(f"NX PIDs: {manager.get_nx_pids()}")

    # Simulate session start
    manager.record_session_start(
        nx_was_running=manager.is_nx_running(),
        started_pid=None
    )

    print(f"Can close NX: {manager.can_close_nx()}")
    print(f"PIDs we can close: {manager.get_pids_we_can_close()}")

    # Cleanup
    manager.cleanup()
    print("Test complete.")
