# -*- coding: utf-8 -*-
"""Path utilities for different packaging environments.

Provides cross-platform path resolution for:
- Development environment (direct Python execution)
- Nuitka compiled applications (standalone/onefile)
- PyInstaller frozen applications (onefile/onedir)

This module ensures that file paths are correctly resolved regardless of
how the application is packaged and deployed.
"""

import os
import sys
import logging
from pathlib import Path
from typing import Optional, Union

logger = logging.getLogger(__name__)


# =============================================================================
# Packaging Environment Detection
# =============================================================================

def is_frozen() -> bool:
    """Check if running in any frozen/packaged environment.

    Returns:
        bool: True if frozen (Nuitka/PyInstaller), False in development
    """
    return getattr(sys, 'frozen', False)


def is_pyinstaller_frozen() -> bool:
    """Check if running in PyInstaller frozen environment.

    Returns:
        bool: True if PyInstaller frozen, False otherwise
    """
    return is_frozen() and hasattr(sys, '_MEIPASS')


def is_nuitka_compiled() -> bool:
    """Check if running in Nuitka compiled environment.

    Returns:
        bool: True if Nuitka compiled, False otherwise
    """
    # Nuitka sets __compiled__ attribute or sys.__compiled__
    return '__compiled__' in globals() or hasattr(sys, '__compiled__') or (
        is_frozen() and not hasattr(sys, '_MEIPASS')
    )


def get_packaging_mode() -> str:
    """Get current packaging mode.

    Returns:
        str: 'development', 'nuitka_onefile', 'nuitka_standalone',
             'pyinstaller_onefile', 'pyinstaller_onedir'
    """
    if not is_frozen():
        return 'development'

    if is_pyinstaller_frozen():
        meipass = getattr(sys, '_MEIPASS', '')
        # Check if running from temp directory (onefile characteristic)
        if '_MEI' in meipass or 'Temp' in meipass or 'tmp' in meipass:
            return 'pyinstaller_onefile'
        return 'pyinstaller_onedir'

    if is_nuitka_compiled():
        exe_dir = os.path.dirname(sys.executable)
        # Nuitka onefile extracts to temp with 'onefile' in path
        if 'onefile' in exe_dir.lower() or '_MEI' in exe_dir:
            return 'nuitka_onefile'
        return 'nuitka_standalone'

    return 'unknown'


# =============================================================================
# Base Path Resolution
# =============================================================================

def get_base_path() -> Path:
    """Get application base path, supporting all packaging modes.

    Returns the directory containing the application code:
    - Development: Project root directory
    - PyInstaller onefile: sys._MEIPASS (temporary extraction directory)
    - PyInstaller onedir: Directory containing the executable
    - Nuitka onefile: Temporary extraction directory
    - Nuitka standalone: Directory containing the executable

    Returns:
        Path: Absolute path to application base directory
    """
    if is_pyinstaller_frozen():
        # PyInstaller extracts to sys._MEIPASS
        base = Path(getattr(sys, '_MEIPASS', os.path.dirname(sys.executable)))
        logger.debug(f"Base path (PyInstaller): {base}")
        return base

    if is_nuitka_compiled():
        # Nuitka onefile also extracts to temp directory
        # In onefile mode, the extraction directory is the parent of sys.executable
        if 'onefile' in os.path.dirname(sys.executable).lower():
            # Onefile mode: use the extraction directory
            base = Path(os.path.dirname(sys.executable))
        else:
            # Standalone mode: use executable directory
            base = Path(sys.executable).parent
        logger.debug(f"Base path (Nuitka): {base}")
        return base

    # Development mode: use the directory containing this file
    # Go up one level from cullinan/path_utils.py to get project root
    base = Path(__file__).parent.parent
    logger.debug(f"Base path (development): {base}")
    return base


def get_cullinan_package_path() -> Path:
    """Get the path to the cullinan package directory.

    Returns:
        Path: Absolute path to cullinan package
    """
    if is_frozen():
        # In frozen mode, cullinan is extracted to base_path/cullinan
        base = get_base_path()
        cullinan_path = base / 'cullinan'

        # Fallback: if not found in base_path/cullinan, try base_path directly
        # (some packaging configurations may flatten the structure)
        if not cullinan_path.exists():
            cullinan_path = base

        logger.debug(f"Cullinan package path (frozen): {cullinan_path}")
        return cullinan_path

    # Development mode: use the directory containing this file
    cullinan_path = Path(__file__).parent
    logger.debug(f"Cullinan package path (development): {cullinan_path}")
    return cullinan_path


def get_resource_path(relative_path: Union[str, Path]) -> Path:
    """Get absolute path to a resource file.

    Args:
        relative_path: Relative path from base directory

    Returns:
        Path: Absolute path to resource

    Example:
        >>> get_resource_path('config.yaml')
        Path('C:/app/config.yaml')
        >>> get_resource_path('templates/index.html')
        Path('C:/app/templates/index.html')
    """
    base = get_base_path()
    resource = base / relative_path
    logger.debug(f"Resource path for '{relative_path}': {resource}")
    return resource


def get_module_file_path(module_name: str, relative_to_cullinan: bool = True) -> Optional[Path]:
    """Get file path for a module within the cullinan package.

    Args:
        module_name: Module filename (e.g., 'controller.py', 'config.py')
        relative_to_cullinan: If True, resolve relative to cullinan package

    Returns:
        Path: Absolute path to module file, or None if not found

    Example:
        >>> get_module_file_path('controller.py')
        Path('C:/app/cullinan/controller.py')
    """
    if relative_to_cullinan:
        base = get_cullinan_package_path()
    else:
        base = get_base_path()

    module_path = base / module_name

    if module_path.exists():
        logger.debug(f"Found module file: {module_path}")
        return module_path

    logger.debug(f"Module file not found: {module_path}")
    return None


def get_executable_dir() -> Path:
    """Get directory containing the executable or main script.

    Returns:
        Path: Directory containing executable (frozen) or main script (development)
    """
    if is_frozen():
        exe_dir = Path(sys.executable).parent
        logger.debug(f"Executable directory (frozen): {exe_dir}")
        return exe_dir

    # Development: return directory of main script
    main_file = sys.argv[0] if sys.argv else __file__
    exe_dir = Path(main_file).parent.resolve()
    logger.debug(f"Executable directory (development): {exe_dir}")
    return exe_dir


def get_user_data_dir() -> Path:
    """Get directory for user data (logs, config, cache).

    This directory is outside the application bundle and persists across updates.

    Returns:
        Path: User data directory
    """
    if is_frozen():
        # For frozen apps, use executable directory
        # Users can override this with environment variable
        data_dir = os.environ.get('APP_DATA_DIR')
        if data_dir:
            return Path(data_dir)
        return get_executable_dir()

    # Development: use current working directory
    return Path.cwd()


# =============================================================================
# Path Resolution with Fallbacks
# =============================================================================

def find_file_with_fallbacks(filename: str, search_paths: list[Path] = None) -> Optional[Path]:
    """Find a file by searching multiple paths with fallbacks.

    Args:
        filename: Name of file to find
        search_paths: List of paths to search (default: smart defaults)

    Returns:
        Path: First existing path, or None if not found
    """
    if search_paths is None:
        # Default search paths
        search_paths = [
            get_cullinan_package_path(),
            get_base_path(),
            get_executable_dir(),
            Path.cwd(),
        ]

    for search_path in search_paths:
        candidate = search_path / filename
        if candidate.exists():
            logger.debug(f"Found '{filename}' at: {candidate}")
            return candidate

    logger.warning(f"File '{filename}' not found in any search path")
    return None


# =============================================================================
# Module Import Utilities
# =============================================================================

def import_module_from_path(module_name: str, file_path: Path):
    """Import a module from a specific file path.

    Args:
        module_name: Name to register module as
        file_path: Absolute path to module file

    Returns:
        module: Imported module object

    Raises:
        ImportError: If module cannot be loaded
    """
    import importlib.util

    if not file_path.exists():
        raise ImportError(f"Module file not found: {file_path}")

    spec = importlib.util.spec_from_file_location(module_name, file_path)
    if spec is None or spec.loader is None:
        raise ImportError(f"Cannot create spec for module: {module_name}")

    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module  # Register in sys.modules
    spec.loader.exec_module(module)

    logger.debug(f"Imported module '{module_name}' from {file_path}")
    return module


# =============================================================================
# Debug and Diagnostics
# =============================================================================

def get_path_info() -> dict:
    """Get diagnostic information about current path configuration.

    Returns:
        dict: Path configuration details
    """
    return {
        'packaging_mode': get_packaging_mode(),
        'is_frozen': is_frozen(),
        'is_pyinstaller': is_pyinstaller_frozen(),
        'is_nuitka': is_nuitka_compiled(),
        'base_path': str(get_base_path()),
        'cullinan_package_path': str(get_cullinan_package_path()),
        'executable_dir': str(get_executable_dir()),
        'user_data_dir': str(get_user_data_dir()),
        'cwd': str(Path.cwd()),
        'sys.executable': sys.executable,
        'sys.argv[0]': sys.argv[0] if sys.argv else 'N/A',
        'sys.frozen': getattr(sys, 'frozen', False),
        'sys._MEIPASS': getattr(sys, '_MEIPASS', 'N/A'),
        '__file__': __file__ if not is_frozen() else 'N/A',
    }


def log_path_info():
    """Log diagnostic information about current path configuration."""
    info = get_path_info()
    logger.info("=" * 70)
    logger.info("Path Configuration Diagnostics")
    logger.info("=" * 70)
    for key, value in info.items():
        logger.info(f"  {key:25s}: {value}")
    logger.info("=" * 70)


# =============================================================================
# Initialization
# =============================================================================

# Log path info on module import (debug level)
logger.debug(f"Packaging mode: {get_packaging_mode()}")

