"""
AUTARCH Path Resolution
Centralized path management for cross-platform portability.

All paths resolve relative to the application root directory.
Tool lookup checks project directories first, then system PATH.
"""

import os
import platform
import shutil
from pathlib import Path
from typing import Optional, List


# ── Application Root ────────────────────────────────────────────────
#
# Two directories matter:
#   _BUNDLE_DIR — read-only bundled assets (templates, static, default modules)
#                 Points to sys._MEIPASS in a frozen PyInstaller build,
#                 otherwise same as _APP_DIR.
#   _APP_DIR    — writable application root (config, data, results, user modules)
#                 Points to the .exe's parent directory in a frozen build,
#                 otherwise the project root (parent of core/).

import sys as _sys

_FROZEN = getattr(_sys, 'frozen', False)

if _FROZEN:
    # PyInstaller frozen build
    _BUNDLE_DIR = Path(_sys._MEIPASS)
    _APP_DIR = Path(_sys.executable).resolve().parent
else:
    # Normal Python execution
    _APP_DIR = Path(__file__).resolve().parent.parent
    _BUNDLE_DIR = _APP_DIR


def is_frozen() -> bool:
    """Return True if running from a PyInstaller bundle."""
    return _FROZEN


def get_app_dir() -> Path:
    """Return the writable application root directory."""
    return _APP_DIR


def get_bundle_dir() -> Path:
    """Return the bundle directory (read-only assets: templates, static, default modules)."""
    return _BUNDLE_DIR


def get_core_dir() -> Path:
    return _BUNDLE_DIR / 'core'


def get_modules_dir() -> Path:
    """Return the bundled modules directory (read-only in frozen mode)."""
    return _BUNDLE_DIR / 'modules'


def get_user_modules_dir() -> Path:
    """Return the user modules directory (writable, next to exe).
    New or modified modules go here; scanned in addition to bundled modules."""
    d = _APP_DIR / 'modules'
    if _FROZEN:
        d.mkdir(parents=True, exist_ok=True)
    return d


def get_data_dir() -> Path:
    d = _APP_DIR / 'data'
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_config_path() -> Path:
    """Return config path.  Writable copy lives next to the exe;
    falls back to the bundled default if the writable copy doesn't exist yet."""
    writable = _APP_DIR / 'autarch_settings.conf'
    if not writable.exists() and _FROZEN:
        bundled = _BUNDLE_DIR / 'autarch_settings.conf'
        if bundled.exists():
            import shutil as _sh
            _sh.copy2(str(bundled), str(writable))
    return writable


def get_results_dir() -> Path:
    d = _APP_DIR / 'results'
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_reports_dir() -> Path:
    d = get_results_dir() / 'reports'
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_dossiers_dir() -> Path:
    d = _APP_DIR / 'dossiers'
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_uploads_dir() -> Path:
    d = get_data_dir() / 'uploads'
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_backups_dir() -> Path:
    d = _APP_DIR / 'backups'
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_templates_dir() -> Path:
    return _APP_DIR / '.config'


def get_custom_configs_dir() -> Path:
    d = _APP_DIR / '.config' / 'custom'
    d.mkdir(parents=True, exist_ok=True)
    return d


# ── Platform Detection ──────────────────────────────────────────────

def _get_arch() -> str:
    """Return architecture string: 'x86_64', 'arm64', etc."""
    machine = platform.machine().lower()
    if machine in ('aarch64', 'arm64'):
        return 'arm64'
    elif machine in ('x86_64', 'amd64'):
        return 'x86_64'
    return machine


def get_platform() -> str:
    """Return platform: 'linux', 'windows', or 'darwin'."""
    return platform.system().lower()


def get_platform_tag() -> str:
    """Return platform-arch tag like 'linux-arm64', 'windows-x86_64'."""
    return f"{get_platform()}-{_get_arch()}"


def is_windows() -> bool:
    return platform.system() == 'Windows'


def is_linux() -> bool:
    return platform.system() == 'Linux'


def is_mac() -> bool:
    return platform.system() == 'Darwin'


# ── Tool / Binary Lookup ───────────────────────────────────────────
#
# Priority order:
#   1. System PATH (shutil.which — native binaries, correct arch)
#   2. Platform-specific well-known install locations
#   3. Platform-specific project tools (tools/linux-arm64/, etc.)
#   4. Generic project directories (android/, tools/, bin/)
#   5. Extra paths passed by caller
#

# Well-known install locations by platform (last resort)
_PLATFORM_SEARCH_PATHS = {
    'windows': [
        Path(os.environ.get('LOCALAPPDATA', '')) / 'Android' / 'Sdk' / 'platform-tools',
        Path(os.environ.get('USERPROFILE', '')) / 'Android' / 'Sdk' / 'platform-tools',
        Path('C:/Program Files (x86)/Nmap'),
        Path('C:/Program Files/Nmap'),
        Path('C:/Program Files/Wireshark'),
        Path('C:/Program Files (x86)/Wireshark'),
        Path('C:/metasploit-framework/bin'),
    ],
    'darwin': [
        Path('/opt/homebrew/bin'),
        Path('/usr/local/bin'),
    ],
    'linux': [
        Path('/usr/local/bin'),
        Path('/snap/bin'),
    ],
}

# Tools that need extra environment setup when run from bundled copies
_TOOL_ENV_SETUP = {
    'nmap': '_setup_nmap_env',
}


def _setup_nmap_env(tool_path: str):
    """Set NMAPDIR so bundled nmap finds its data files."""
    tool_dir = Path(tool_path).parent
    nmap_data = tool_dir / 'nmap-data'
    if nmap_data.is_dir():
        os.environ['NMAPDIR'] = str(nmap_data)


def _is_native_binary(path: str) -> bool:
    """Check if an ELF binary matches the host architecture."""
    try:
        with open(path, 'rb') as f:
            magic = f.read(20)
        if magic[:4] != b'\x7fELF':
            return True  # Not ELF (script, etc.) — assume OK
        # ELF e_machine at offset 18 (2 bytes, little-endian)
        e_machine = int.from_bytes(magic[18:20], 'little')
        arch = _get_arch()
        if arch == 'arm64' and e_machine == 183:     # EM_AARCH64
            return True
        if arch == 'x86_64' and e_machine == 62:     # EM_X86_64
            return True
        if arch == 'arm64' and e_machine == 62:       # x86-64 on arm64 host
            return False
        if arch == 'x86_64' and e_machine == 183:     # arm64 on x86-64 host
            return False
        return True  # Unknown arch combo — let it try
    except Exception:
        return True  # Can't read — assume OK


def find_tool(name: str, extra_paths: Optional[List[str]] = None) -> Optional[str]:
    """
    Find an executable binary by name.

    Search order:
      1. System PATH (native binaries, correct architecture)
      2. Platform-specific well-known install locations
      3. Platform-specific project tools (tools/linux-arm64/ etc.)
      4. Generic project directories (android/, tools/, bin/)
      5. Extra paths provided by caller

    Skips binaries that don't match the host architecture (e.g. x86-64
    binaries on ARM64 hosts) to avoid FEX/emulation issues with root.

    Returns absolute path string, or None if not found.
    """
    # On Windows, append .exe if no extension
    names = [name]
    if is_windows() and '.' not in name:
        names.append(name + '.exe')

    # 1. System PATH (most reliable — native packages)
    found = shutil.which(name)
    if found and _is_native_binary(found):
        return found

    # 2. Platform-specific well-known locations
    plat = get_platform()
    for search_dir in _PLATFORM_SEARCH_PATHS.get(plat, []):
        if search_dir.is_dir():
            for n in names:
                full = search_dir / n
                if full.is_file() and os.access(str(full), os.X_OK) and _is_native_binary(str(full)):
                    return str(full)

    # 3-4. Bundled project directories
    plat_tag = get_platform_tag()
    search_dirs = [
        _APP_DIR / 'tools' / plat_tag,   # Platform-specific (tools/linux-arm64/)
        _APP_DIR / 'android',             # Android tools
        _APP_DIR / 'tools',               # Generic tools/
        _APP_DIR / 'bin',                 # Generic bin/
    ]

    for tool_dir in search_dirs:
        if tool_dir.is_dir():
            for n in names:
                full = tool_dir / n
                if full.is_file() and os.access(str(full), os.X_OK):
                    found = str(full)
                    if not _is_native_binary(found):
                        continue  # Wrong arch — skip
                    # Apply environment setup for bundled tools
                    env_fn = _TOOL_ENV_SETUP.get(name)
                    if env_fn:
                        globals()[env_fn](found)
                    return found

    # 5. Extra paths from caller
    if extra_paths:
        for p in extra_paths:
            for n in names:
                full = os.path.join(p, n)
                if os.path.isfile(full) and os.access(full, os.X_OK) and _is_native_binary(full):
                    return full

    # Last resort: return system PATH result even if wrong arch (FEX may work for user)
    found = shutil.which(name)
    if found:
        return found

    return None


def tool_available(name: str) -> bool:
    """Check if a tool is available anywhere."""
    return find_tool(name) is not None
