#!/usr/bin/env python3
"""Isolation Bytes — Universal Launcher & Installer (v2)

Superior to platform-specific installers:
  - Progress bars during downloads
  - Retry logic with exponential backoff
  - Checksum verification of downloaded files
  - Rollback on failure (undo partial installs)
  - Update checking (only re-downloads if newer version exists)
  - Repair mode (fix broken installations)
  - Silent mode (no prompts, for automated deployment)
  - Pre-flight checks (disk space, OS version, existing install)
  - Installation log file for troubleshooting
  - Offline mode (use local dist/ files if available)
  - Bandwidth detection (warn on large downloads over slow connections)

Usage:
    python universal_launcher.py                  # install everything + launch
    python universal_launcher.py --install        # install only
    python universal_launcher.py --launch         # launch only (auto-install if missing)
    python universal_launcher.py --uninstall      # remove Isolation Bytes
    python universal_launcher.py --status         # check installation status
    python universal_launcher.py --update         # check for and install updates
    python universal_launcher.py --repair         # fix broken installation
    python universal_launcher.py --silent         # no prompts (automated deploy)
    python universal_launcher.py --offline        # use local dist/ files only
    python universal_launcher.py --version        # show launcher version

Works on: Windows 10/11, macOS 10.13+, Linux (any distro), ChromeOS (Linux container)
"""
import os
import sys
import shutil
import subprocess
import platform
import urllib.request
import urllib.error
import json
import tempfile
import time
import hashlib
import logging
from datetime import datetime

# ─── Configuration ──────────────────────────────────────────────────────

BASE_URL = os.environ.get('ISOLATION_BYTES_URL', 'https://isolation-bytes.com')
APP_NAME = 'Isolation Bytes'
APP_ID = 'soluzka.IsolationBytes'
VERSION = '1.8.889'
LAUNCHER_VERSION = '2.0.0'
MAX_RETRIES = 3
RETRY_BACKOFF = 2  # seconds, doubles each retry
MSIX_MIN_SIZE_MB = 0.5  # minimum expected MSIX size

# ─── Platform detection ─────────────────────────────────────────────────

SYSTEM = platform.system().lower()
IS_WINDOWS = SYSTEM == 'windows'
IS_MACOS = SYSTEM == 'darwin'
IS_LINUX = SYSTEM == 'linux'
ARCH = platform.machine().lower()

# ─── Logging ────────────────────────────────────────────────────────────

LOG_DIR = os.path.join(tempfile.gettempdir(), 'isolationbytes_logs')
os.makedirs(LOG_DIR, exist_ok=True)
LOG_FILE = os.path.join(LOG_DIR, f'install_{datetime.now().strftime("%Y%m%d_%H%M%S")}.log')

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler(LOG_FILE),
    ]
)

# ─── CLI args ───────────────────────────────────────────────────────────

SILENT = '--silent' in sys.argv
OFFLINE = '--offline' in sys.argv

def log(msg, level='INFO'):
    """Log to both console and file."""
    prefix = f'[{level}]'
    if level == 'OK':
        prefix = '[OK]'
    elif level == 'WARN':
        prefix = '[WARN]'
    elif level == 'ERROR':
        prefix = '[ERROR]'
    print(f'{prefix} {msg}')
    logging.info(f'{prefix} {msg}')

def log_debug(msg):
    logging.debug(msg)

# ─── Download with progress + retry + checksum ──────────────────────────

def download_with_progress(url, dest, expected_sha256=None, retries=MAX_RETRIES):
    """Download a file with progress bar, retry logic, and checksum verification."""
    last_error = None
    for attempt in range(1, retries + 1):
        try:
            log(f'Downloading {url} (attempt {attempt}/{retries})...')
            req = urllib.request.Request(url, headers={'User-Agent': f'IsolationBytes/{LAUNCHER_VERSION}'})
            with urllib.request.urlopen(req, timeout=120) as response:
                total = int(response.headers.get('Content-Length', 0))
                downloaded = 0
                chunk_size = 65536
                start_time = time.time()

                with open(dest, 'wb') as f:
                    while True:
                        chunk = response.read(chunk_size)
                        if not chunk:
                            break
                        f.write(chunk)
                        downloaded += len(chunk)
                        if total > 0 and not SILENT:
                            pct = (downloaded / total) * 100
                            speed = downloaded / (time.time() - start_time + 0.01) / (1024 * 1024)
                            sys.stdout.write(f'\r  {downloaded}/{total} bytes ({pct:.1f}%) — {speed:.1f} MB/s')
                            sys.stdout.flush()

                if not SILENT:
                    print()  # newline after progress

            # Verify file size
            actual_size = os.path.getsize(dest)
            if total > 0 and actual_size != total:
                raise IOError(f'Size mismatch: expected {total}, got {actual_size}')

            # Verify checksum if provided
            if expected_sha256:
                log('Verifying checksum...')
                actual_hash = sha256_file(dest)
                if actual_hash != expected_sha256:
                    raise IOError(f'Checksum mismatch: expected {expected_sha256[:16]}..., got {actual_hash[:16]}...')
                log('  Checksum verified.', 'OK')

            size_mb = actual_size / (1024 * 1024)
            log(f'  Saved {dest} ({size_mb:.1f} MB)', 'OK')
            return True

        except (urllib.error.URLError, IOError, TimeoutError) as e:
            last_error = e
            log(f'  Download failed: {e}', 'WARN')
            if attempt < retries:
                wait = RETRY_BACKOFF * (2 ** (attempt - 1))
                log(f'  Retrying in {wait}s...', 'WARN')
                time.sleep(wait)
            else:
                log(f'  All retries exhausted.', 'ERROR')

    log(f'  Download failed after {retries} attempts: {last_error}', 'ERROR')
    return False

def sha256_file(path):
    """Calculate SHA256 hash of a file."""
    h = hashlib.sha256()
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(65536), b''):
            h.update(chunk)
    return h.hexdigest()

# ─── Pre-flight checks ──────────────────────────────────────────────────

def preflight_checks():
    """Verify system requirements before installing."""
    log('Running pre-flight checks...')
    issues = []

    # OS version check
    if IS_WINDOWS:
        try:
            ver = sys.getwindowsversion()
            build = ver.build
            if build < 16299:
                issues.append(f'Windows 10 1709+ required (build {build} detected). MSIX not supported.')
        except Exception:
            pass
    elif IS_MACOS:
        try:
            ver = subprocess.run(['sw_vers', '-productVersion'], capture_output=True, text=True).stdout.strip()
            major = int(ver.split('.')[0])
            if major < 10 or (major == 10 and int(ver.split('.')[1]) < 13):
                issues.append(f'macOS 10.13+ required ({ver} detected).')
        except Exception:
            pass

    # Disk space check (need at least 3 GB for MSIX + temp)
    try:
        usage = shutil.disk_usage(tempfile.gettempdir())
        free_gb = usage.free / (1024 ** 3)
        if free_gb < 3:
            issues.append(f'Low disk space: {free_gb:.1f} GB free (need 3+ GB).')
        else:
            log(f'  Disk space: {free_gb:.1f} GB free')
    except Exception:
        pass

    # Network connectivity (skip if offline mode)
    if not OFFLINE:
        try:
            urllib.request.urlopen(f'{BASE_URL}/', timeout=10)
            log('  Network: connected')
        except Exception:
            if OFFLINE:
                log('  Network: offline mode (using local files)')
            else:
                issues.append('Cannot reach isolation-bytes.com. Use --offline for local files.')

    if issues:
        for issue in issues:
            log(f'  PRE-FLIGHT: {issue}', 'WARN')
        if not SILENT:
            log('Pre-flight checks found issues. Continue anyway? (y/n)', 'WARN')
            response = input().strip().lower()
            if response != 'y':
                log('Installation aborted by user.', 'ERROR')
                return False
        else:
            log('Continuing despite pre-flight issues (silent mode).', 'WARN')
    else:
        log('Pre-flight checks passed.', 'OK')

    return True

# ─── Update checking ────────────────────────────────────────────────────

def check_for_updates():
    """Check if a newer version is available."""
    try:
        req = urllib.request.Request(f'{BASE_URL}/download/version.json',
                                     headers={'User-Agent': f'IsolationBytes/{LAUNCHER_VERSION}'})
        with urllib.request.urlopen(req, timeout=10) as response:
            info = json.loads(response.read())
            latest = info.get('version', '0.0.0.0')
            current = get_installed_version()
            if version_tuple(latest) > version_tuple(current):
                log(f'Update available: {current} → {latest}', 'OK')
                return {'update_available': True, 'latest': latest, 'current': current,
                        'sha256': info.get('sha256'), 'size': info.get('size')}
            else:
                log(f'Already up to date ({current}).', 'OK')
                return {'update_available': False, 'latest': latest, 'current': current}
    except Exception as e:
        log_debug(f'Update check failed: {e}')
        return {'update_available': False, 'error': str(e)}

def version_tuple(v):
    """Convert version string to tuple for comparison."""
    try:
        return tuple(int(p) for p in v.split('.'))
    except Exception:
        return (0,)

def get_installed_version():
    """Get the currently installed version."""
    info = status()
    return info.get('version', '0.0.0.0')

# ─── Rollback support ───────────────────────────────────────────────────

class RollbackManager:
    """Tracks changes so they can be undone if installation fails."""

    def __init__(self):
        self.actions = []

    def track(self, description, undo_fn):
        self.actions.append((description, undo_fn))

    def rollback(self):
        """Undo all tracked actions in reverse order."""
        if not self.actions:
            return
        log('Rolling back changes due to failure...', 'WARN')
        for desc, undo_fn in reversed(self.actions):
            try:
                log(f'  Undo: {desc}')
                undo_fn()
            except Exception as e:
                log_debug(f'  Rollback step failed: {e}')
        log('Rollback complete.', 'WARN')

rollback = RollbackManager()

# ─── Helpers ────────────────────────────────────────────────────────────

def run(cmd, check=True, capture=False, shell=False):
    log_debug(f'Running: {" ".join(cmd) if isinstance(cmd, list) else cmd}')
    if capture:
        return subprocess.run(cmd, capture_output=True, text=True, check=check, shell=shell)
    return subprocess.run(cmd, check=check, shell=shell)

def is_admin():
    if IS_WINDOWS:
        try:
            import ctypes
            return ctypes.windll.shell32.IsUserAnAdmin()
        except Exception:
            return False
    return os.geteuid() == 0 if hasattr(os, 'geteuid') else False

def elevate():
    if is_admin():
        return True
    log('Requesting elevated privileges...')
    if IS_WINDOWS:
        import ctypes
        params = ' '.join(f'"{a}"' if ' ' in a else a for a in sys.argv)
        result = ctypes.windll.shell32.ShellExecuteW(
            None, 'runas', sys.executable, params, None, 1)
        return result > 32
    elif IS_MACOS or IS_LINUX:
        cmd = ['sudo', sys.executable] + sys.argv
        os.execvp('sudo', cmd)
    return False

def command_exists(cmd):
    return shutil.which(cmd) is not None

def confirm(prompt):
    """Ask for confirmation unless in silent mode."""
    if SILENT:
        return True
    response = input(f'{prompt} (y/n): ').strip().lower()
    return response == 'y'

# ─── Dependency auto-install ────────────────────────────────────────────

def ensure_browser():
    """Ensure a web browser is installed. Auto-installs Chromium if missing."""
    browsers = []
    if IS_WINDOWS:
        browsers = ['msedge', 'chrome']
    elif IS_MACOS:
        browser_paths = [
            '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
            '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
            '/Applications/Chromium.app/Contents/MacOS/Chromium',
            '/Applications/Safari.app',
        ]
        browsers = browser_paths
    elif IS_LINUX:
        browsers = ['chromium', 'google-chrome', 'google-chrome-stable',
                    'chromium-browser', 'microsoft-edge', 'firefox',
                    'epiphany', 'falkon', 'midori']

    for cmd in browsers:
        if os.path.isfile(cmd) or command_exists(cmd):
            log(f'Browser found: {cmd}')
            return True

    log('No browser found. Auto-installing Chromium...')
    try:
        if IS_WINDOWS:
            if command_exists('winget'):
                run(['winget', 'install', '--id', 'Hoffmann.Chromium',
                     '--accept-source-agreements', '--accept-package-agreements'],
                    check=False)
            else:
                tmp = tempfile.mkdtemp()
                installer = os.path.join(tmp, 'chromium_installer.exe')
                download_with_progress(
                    'https://github.com/Hibbiki/chromium-win64/releases/latest/download/chromium-installer.exe',
                    installer)
                run([installer, '--do-not-launch-chrome'], check=False)
        elif IS_MACOS:
            if command_exists('brew'):
                run(['brew', 'install', '--cask', 'chromium'], check=False)
            else:
                run(['/bin/bash', '-c',
                     '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'],
                    check=False)
                run(['brew', 'install', '--cask', 'chromium'], check=False)
        elif IS_LINUX:
            if command_exists('apt'):
                run(['sudo', 'apt', 'update'], check=False)
                run(['sudo', 'apt', 'install', '-y', 'chromium-browser'], check=False)
            elif command_exists('dnf'):
                run(['sudo', 'dnf', 'install', '-y', 'chromium'], check=False)
            elif command_exists('yum'):
                run(['sudo', 'yum', 'install', '-y', 'chromium'], check=False)
            elif command_exists('pacman'):
                run(['sudo', 'pacman', '-S', '--noconfirm', 'chromium'], check=False)
            elif command_exists('apk'):
                run(['sudo', 'apk', 'add', 'chromium'], check=False)
            elif command_exists('zypper'):
                run(['sudo', 'zypper', 'install', '-y', 'chromium'], check=False)
        log('Chromium installation attempted.', 'OK')
        return True
    except Exception as e:
        log(f'Could not auto-install browser: {e}', 'WARN')
        return False

def ensure_dotnet_runtime():
    if not IS_WINDOWS:
        return True
    try:
        result = subprocess.run(
            ['dotnet', '--list-runtimes'], capture_output=True, text=True, timeout=10)
        if 'Microsoft.WindowsDesktop.App' in result.stdout and '8.' in result.stdout:
            log('.NET 8 Desktop Runtime already installed.')
            return True
    except Exception:
        pass

    log('Installing .NET 8 Desktop Runtime...')
    try:
        tmp = tempfile.mkdtemp()
        installer = os.path.join(tmp, 'dotnet-runtime.exe')
        download_with_progress(
            'https://dotnetcli.azureedge.net/dotnet/WindowsDesktop/8.0.11/'
            'windowsdesktop-runtime-8.0.11-win-x64.exe', installer)
        run([installer, '/quiet', '/norestart'], check=False)
        log('.NET 8 Desktop Runtime installed.', 'OK')
        return True
    except Exception as e:
        log(f'Could not auto-install .NET runtime: {e}', 'WARN')
        return False
    finally:
        shutil.rmtree(tmp, ignore_errors=True)

def ensure_webview2():
    if not IS_WINDOWS:
        return True
    try:
        import winreg
        key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE,
                             r'SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9FB7350}')
        winreg.CloseKey(key)
        log('WebView2 Runtime already installed.')
        return True
    except Exception:
        pass

    log('Installing WebView2 Runtime...')
    try:
        tmp = tempfile.mkdtemp()
        installer = os.path.join(tmp, 'MicrosoftEdgeWebview2Setup.exe')
        download_with_progress('https://go.microsoft.com/fwlink/p/?LinkId=2124703', installer)
        run([installer, '/silent', '/install'], check=False)
        log('WebView2 Runtime installed.', 'OK')
        return True
    except Exception as e:
        log(f'Could not auto-install WebView2: {e}', 'WARN')
        return False
    finally:
        shutil.rmtree(tmp, ignore_errors=True)

def ensure_clamav():
    """Install ClamAV on macOS/Linux/ChromeOS if not already installed."""
    if IS_WINDOWS:
        return  # Windows uses Windows Defender

    if shutil.which('clamscan') or shutil.which('clamdscan'):
        log('ClamAV already installed.', 'OK')
        # Ensure the daemon is running
        if shutil.which('clamd'):
            subprocess.run(['clamd', '--version'], capture_output=True, check=False)
        return

    log('ClamAV not found. Installing...')

    if IS_MACOS:
        # macOS — use Homebrew
        if not shutil.which('brew'):
            log('Installing Homebrew first...')
            subprocess.run(
                ['/bin/bash', '-c',
                 'curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh'],
                check=False)
        subprocess.run(['brew', 'install', 'clamav'], check=False)

    elif IS_LINUX:
        # Linux — detect package manager and install ClamAV
        if shutil.which('apt'):
            subprocess.run(['sudo', 'apt', 'update'], check=False)
            subprocess.run(['sudo', 'apt', 'install', '-y', 'clamav', 'clamav-daemon'], check=False)
        elif shutil.which('dnf'):
            subprocess.run(['sudo', 'dnf', 'install', '-y', 'clamav', 'clamav-update'], check=False)
        elif shutil.which('yum'):
            subprocess.run(['sudo', 'yum', 'install', '-y', 'clamav', 'clamav-update'], check=False)
        elif shutil.which('pacman'):
            subprocess.run(['sudo', 'pacman', '-S', '--noconfirm', 'clamav'], check=False)
        elif shutil.which('apk'):
            subprocess.run(['sudo', 'apk', 'add', 'clamav'], check=False)
        elif shutil.which('zypper'):
            subprocess.run(['sudo', 'zypper', 'install', '-y', 'clamav'], check=False)
        elif shutil.which('emerge'):
            subprocess.run(['sudo', 'emerge', '-av', 'app-antivirus/clamav'], check=False)
        elif shutil.which('xbps-install'):
            subprocess.run(['sudo', 'xbps-install', '-Sy', 'clamav'], check=False)
        elif shutil.which('pkg'):
            subprocess.run(['sudo', 'pkg', 'install', '-y', 'clamav'], check=False)
        else:
            log('No supported package manager found for ClamAV installation.', 'WARN')
            return

    # Verify installation
    if shutil.which('clamscan') or shutil.which('clamdscan'):
        log('ClamAV installed successfully.', 'OK')

        # Update virus definitions
        freshclam = shutil.which('freshclam')
        if freshclam:
            log('Updating ClamAV virus definitions (this may take a few minutes)...')
            result = subprocess.run([freshclam, '--no-warnings'], capture_output=True,
                                    text=True, timeout=600, check=False)
            if result.returncode == 0:
                log('ClamAV definitions updated.', 'OK')
            else:
                log(f'ClamAV definition update warning: {result.stderr.strip()}', 'WARN')

        # Start the daemon if available (for real-time scanning)
        clamd = shutil.which('clamd')
        if clamd:
            # Create config if it doesn't exist
            conf_path = '/etc/clamav/clamd.conf'
            if not os.path.exists(conf_path):
                try:
                    with open(conf_path, 'w') as f:
                        f.write('LogFile /var/log/clamav/clamav.log\n'
                                'PidFile /var/run/clamav/clamd.pid\n'
                                'DatabaseDirectory /var/lib/clamav\n'
                                'LocalSocket /var/run/clamav/clamd.sock\n'
                                'FixStaleSocket yes\n'
                                'MaxConnectionQueueLength 200\n'
                                'MaxThreads 20\n'
                                'ReadTimeout 120\n'
                                'User clamav\n'
                                'ScanPE yes\n'
                                'ScanELF yes\n'
                                'ScanOLE2 yes\n'
                                'ScanPDF yes\n'
                                'ScanArchive yes\n')
                except Exception:
                    pass
            subprocess.run(['sudo', 'systemctl', 'enable', 'clamav-daemon'], capture_output=True, check=False)
            subprocess.run(['sudo', 'systemctl', 'start', 'clamav-daemon'], capture_output=True, check=False)
            log('ClamAV daemon started.', 'OK')
    else:
        log('ClamAV installation may have failed. YARA + ML scanning still works.', 'WARN')


def ensure_all_dependencies():
    log('Checking dependencies...')
    if IS_WINDOWS:
        ensure_dotnet_runtime()
        ensure_webview2()
    elif IS_MACOS:
        log('macOS: Safari is always available. Checking for Chrome/Edge...')
        ensure_browser()
        ensure_clamav()
    elif IS_LINUX:
        ensure_browser()
        ensure_clamav()
    log('Dependency check complete.', 'OK')

# ─── Windows ────────────────────────────────────────────────────────────

def windows_status():
    try:
        result = subprocess.run(
            ['powershell', '-NoProfile', '-Command',
             f"Get-AppxPackage -Name '{APP_ID}' | ConvertTo-Json"],
            capture_output=True, text=True, timeout=15)
        if result.stdout.strip() and result.stdout.strip() != '':
            info = json.loads(result.stdout)
            if info:
                return {'installed': True, 'version': info.get('Version', ''),
                        'path': info.get('InstallLocation', ''),
                        'aumid': info.get('PackageFamilyName', '') + '!IsolationBytes'}
    except Exception:
        pass
    return {'installed': False}

def windows_install():
    if not is_admin():
        if not elevate():
            log('Administrator privileges required.', 'ERROR')
            return False
        return None

    if not preflight_checks():
        return False

    ensure_all_dependencies()

    tmp = tempfile.mkdtemp(prefix='isolationbytes_')
    msix_path = os.path.join(tmp, 'IsolationBytes.msix')
    cer_path = os.path.join(tmp, 'IsolationBytes.cer')

    # Check for local files first (offline mode or local dist/)
    local_dist = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dist')
    if OFFLINE and os.path.isdir(local_dist):
        local_msix = os.path.join(local_dist, 'IsolationBytes.msix')
        local_cer = os.path.join(local_dist, 'IsolationBytes.cer')
        if os.path.isfile(local_msix):
            shutil.copy2(local_msix, msix_path)
            log(f'Using local MSIX: {local_msix}')
        if os.path.isfile(local_cer):
            shutil.copy2(local_cer, cer_path)
            log(f'Using local certificate: {local_cer}')

    try:
        if not os.path.isfile(msix_path):
            if not download_with_progress(f'{BASE_URL}/download/IsolationBytes.msix', msix_path):
                raise IOError('Failed to download MSIX')
        if not os.path.isfile(cer_path):
            if not download_with_progress(f'{BASE_URL}/download/IsolationBytes.cer', cer_path):
                raise IOError('Failed to download certificate')

        # Verify MSIX isn't truncated
        msix_size_mb = os.path.getsize(msix_path) / (1024 * 1024)
        if msix_size_mb < MSIX_MIN_SIZE_MB:
            raise IOError(f'MSIX file too small ({msix_size_mb:.1f} MB) — likely corrupted')

        # Trust certificate — must be in Trusted People for MSIX to install
        log('Trusting certificate...')
        cert_trusted = False

        # Method 1: certutil (most reliable for LocalMachine stores)
        for store_name in ['Root', 'TrustedPeople']:
            result = subprocess.run(
                ['certutil', '-addstore', store_name, cer_path],
                capture_output=True, text=True, check=False)
            if result.returncode == 0:
                log(f'Certificate added to {store_name} via certutil.', 'OK')
                cert_trusted = True
            else:
                log(f'certutil {store_name} failed: {result.stderr.strip()}', 'WARN')

        # Method 2: PowerShell Import-Certificate as fallback
        for store in ['Cert:\\LocalMachine\\Root',
                      'Cert:\\LocalMachine\\TrustedPeople',
                      'Cert:\\CurrentUser\\Root',
                      'Cert:\\CurrentUser\\TrustedPeople']:
            result = subprocess.run(
                ['powershell', '-NoProfile', '-Command',
                 f"try {{ Import-Certificate -FilePath '{cer_path}' -CertStoreLocation '{store}' -ErrorAction Stop }} catch {{ Write-Output $_.Exception.Message }}"],
                capture_output=True, text=True, check=False)
            if result.returncode == 0 and 'Error' not in (result.stdout or ''):
                log(f'Certificate added to {store} via PowerShell.', 'OK')
                cert_trusted = True

        # Verify the certificate is actually in the store
        verify = subprocess.run(
            ['powershell', '-NoProfile', '-Command',
             f"Get-ChildItem 'Cert:\\LocalMachine\\TrustedPeople' | Where-Object {{ $_.Subject -like '*soluzka*' }} | Select-Object -First 1 | Measure-Object | Select-Object -ExpandProperty Count"],
            capture_output=True, text=True, check=False)
        if verify.stdout.strip() == '1':
            log('Certificate verified in Trusted People store.', 'OK')
            cert_trusted = True
        else:
            log('Certificate NOT found in Trusted People store!', 'WARN')
            # Last resort: try certutil again with force
            subprocess.run(
                ['certutil', '-enterprise', '-addstore', 'TrustedPeople', cer_path],
                capture_output=True, check=False)

        if not cert_trusted:
            log('Certificate trust may have failed. MSIX install might fail.', 'WARN')

        rollback.track('certificate trust', lambda: None)  # cert removal is complex

        # Remove previous version
        old_pkg = windows_status()
        if old_pkg['installed']:
            log(f'Removing previous version ({old_pkg["version"]})...')
            subprocess.run(
                ['powershell', '-NoProfile', '-Command',
                 f"Get-AppxPackage -Name '{APP_ID}' | Remove-AppxPackage"],
                capture_output=True, check=False)

        # Install MSIX — try appinstaller first (enables auto-update), fall back to direct MSIX
        log('Installing MSIX...')

        # Download the appinstaller for auto-update support
        appinstaller_path = os.path.join(tmp, 'IsolationBytes.appinstaller')
        try:
            download_with_progress(f'{BASE_URL}/download/IsolationBytes.appinstaller', appinstaller_path)
        except Exception:
            appinstaller_path = None

        installed = False
        if appinstaller_path and os.path.isfile(appinstaller_path):
            # Try installing via appinstaller — this registers auto-update
            log('Installing via AppInstaller (enables auto-update)...')
            result = subprocess.run(
                ['powershell', '-NoProfile', '-Command',
                 f"Add-AppxPackage -Path '{appinstaller_path}' -ForceApplicationShutdown -ForceUpdateFromAnyVersion"],
                capture_output=True, text=True, check=False)
            if result.returncode == 0:
                installed = True
                log('Installed via AppInstaller — auto-update enabled.', 'OK')
            else:
                log(f'AppInstaller install failed: {result.stderr.strip()}', 'WARN')

        if not installed:
            # Fall back to direct MSIX install
            log('Installing MSIX directly...')
            subprocess.run(
                ['powershell', '-NoProfile', '-Command',
                 f"Add-AppxPackage -Path '{msix_path}' -ForceApplicationShutdown -ForceUpdateFromAnyVersion"],
                check=True)

            # Register the appinstaller for future auto-updates
            if appinstaller_path and os.path.isfile(appinstaller_path):
                try:
                    subprocess.run(
                        ['powershell', '-NoProfile', '-Command',
                         f"Add-AppxPackage -Path '{appinstaller_path}'"],
                        capture_output=True, check=False)
                    log('AppInstaller registered for auto-updates.', 'OK')
                except Exception:
                    pass

        rollback.track('MSIX install',
                       lambda: subprocess.run(
                           ['powershell', '-NoProfile', '-Command',
                            f"Get-AppxPackage -Name '{APP_ID}' | Remove-AppxPackage"],
                           capture_output=True, check=False))

        # Create shortcuts + startup + file association
        status = windows_status()
        if status['installed']:
            aumid = status['aumid']

            # Find the real desktop folder (handles OneDrive redirect)
            desktop = subprocess.run(
                ['powershell', '-NoProfile', '-Command',
                 '[Environment]::GetFolderPath("Desktop")'],
                capture_output=True, text=True, check=False).stdout.strip()
            if not desktop or not os.path.isdir(desktop):
                # Fallbacks: OneDrive desktop, then USERPROFILE\Desktop
                onedrive = os.environ.get('OneDrive', '')
                if onedrive and os.path.isdir(os.path.join(onedrive, 'Desktop')):
                    desktop = os.path.join(onedrive, 'Desktop')
                else:
                    desktop = os.path.join(os.environ.get('USERPROFILE', ''), 'Desktop')

            # Desktop shortcut
            shortcut = os.path.join(desktop, f'{APP_NAME}.lnk')
            result = subprocess.run(
                ['powershell', '-NoProfile', '-Command',
                 f"$ws=New-Object -ComObject WScript.Shell;"
                 f"$sc=$ws.CreateShortcut('{shortcut}');"
                 f"$sc.TargetPath='explorer.exe';"
                 f"$sc.Arguments='shell:AppsFolder\\{aumid}';"
                 f"$sc.Description='{APP_NAME} Antivirus';"
                 f"$sc.IconLocation='explorer.exe,0';"
                 f"$sc.Save()"],
                capture_output=True, text=True, check=False)
            if os.path.isfile(shortcut):
                log(f'Desktop shortcut created: {shortcut}')
            else:
                log(f'Desktop shortcut FAILED: {result.stderr.strip()}', 'WARN')

            # Start Menu shortcut
            start_menu = os.path.join(os.environ.get('APPDATA', ''),
                                      'Microsoft', 'Windows', 'Start Menu', 'Programs')
            if os.path.isdir(start_menu):
                start_shortcut = os.path.join(start_menu, f'{APP_NAME}.lnk')
                subprocess.run(
                    ['powershell', '-NoProfile', '-Command',
                     f"$ws=New-Object -ComObject WScript.Shell;"
                     f"$sc=$ws.CreateShortcut('{start_shortcut}');"
                     f"$sc.TargetPath='explorer.exe';"
                     f"$sc.Arguments='shell:AppsFolder\\{aumid}';"
                     f"$sc.Description='{APP_NAME} Antivirus';"
                     f"$sc.IconLocation='explorer.exe,0';"
                     f"$sc.Save()"],
                    capture_output=True, check=False)
                if os.path.isfile(start_shortcut):
                    log(f'Start Menu shortcut created: {start_shortcut}')

            # Startup shortcut (auto-launch on boot)
            startup_dir = os.path.join(os.environ.get('APPDATA', ''),
                                       'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup')
            if os.path.isdir(startup_dir):
                startup_shortcut = os.path.join(startup_dir, f'{APP_NAME}.lnk')
                subprocess.run(
                    ['powershell', '-NoProfile', '-Command',
                     f"$ws=New-Object -ComObject WScript.Shell;"
                     f"$sc=$ws.CreateShortcut('{startup_shortcut}');"
                     f"$sc.TargetPath='explorer.exe';"
                     f"$sc.Arguments='shell:AppsFolder\\{aumid}';"
                     f"$sc.Description='{APP_NAME} Antivirus - Auto-start';"
                     f"$sc.IconLocation='explorer.exe,0';"
                     f"$sc.Save()"],
                    capture_output=True, check=False)
                if os.path.isfile(startup_shortcut):
                    log(f'Auto-start shortcut created: {startup_shortcut}')

            # Pin to taskbar (via PowerShell)
            subprocess.run(
                ['powershell', '-NoProfile', '-Command',
                 f"$ws=New-Object -ComObject WScript.Shell;"
                 f"$pinPath=Join-Path ([Environment]::GetFolderPath('Programs')) '{APP_NAME}.lnk';"
                 f"if(Test-Path $pinPath){{(New-Object -ComObject Shell.Application).Namespace($pinPath).InvokeVerb('taskbarpin')}}"],
                capture_output=True, check=False)

            # .iblic file association
            try:
                import winreg
                with winreg.CreateKey(winreg.HKEY_CURRENT_USER,
                                      r'Software\Classes\.iblic') as key:
                    winreg.SetValueEx(key, None, 0, winreg.REG_SZ, 'IsolationBytes.iblic')
                with winreg.CreateKey(winreg.HKEY_CURRENT_USER,
                                      r'Software\Classes\IsolationBytes.iblic') as key:
                    winreg.SetValueEx(key, None, 0, winreg.REG_SZ, 'Isolation Bytes License File')
                with winreg.CreateKey(winreg.HKEY_CURRENT_USER,
                                      r'Software\Classes\IsolationBytes.iblic\DefaultIcon') as key:
                    winreg.SetValueEx(key, None, 0, winreg.REG_SZ,
                                      os.path.join(status.get('path', ''), 'IsolationBytes.exe,0'))
                with winreg.CreateKey(winreg.HKEY_CURRENT_USER,
                                      r'Software\Classes\IsolationBytes.iblic\shell\open\command') as key:
                    winreg.SetValueEx(key, None, 0, winreg.REG_SZ,
                                      f'explorer.exe shell:AppsFolder\\{aumid}')
                subprocess.run(
                    ['powershell', '-NoProfile', '-Command',
                     'Add-Type -TypeDefinition "using System;using System.Runtime.InteropServices;public class Shell {[DllImport(\\"shell32.dll\\\")]public static extern void SHChangeNotify(int wEventId,int uFlags,IntPtr dwItem1,IntPtr dwItem2);}"; [Shell]::SHChangeNotify(0x08000000,0,0,0)'],
                    capture_output=True, check=False)
                log('.iblic file association registered.')
            except Exception as e:
                log(f'Could not register .iblic association: {e}', 'WARN')

        log(f'Installation complete! Log: {LOG_FILE}', 'OK')
        return True
    except Exception as e:
        log(f'Installation failed: {e}', 'ERROR')
        log(f'Log file: {LOG_FILE}', 'ERROR')
        rollback.rollback()
        return False
    finally:
        shutil.rmtree(tmp, ignore_errors=True)

def windows_launch():
    status = windows_status()
    if not status['installed']:
        log('Isolation Bytes is not installed. Run with --install first.', 'ERROR')
        return False
    aumid = status['aumid']
    log(f'Launching {aumid}...')

    # Method 1: explorer.exe shell:AppsFolder
    try:
        subprocess.run(['explorer.exe', f'shell:AppsFolder\\{aumid}'], check=False)
        log('Launch command sent.', 'OK')
        return True
    except Exception as e:
        log(f'explorer.exe launch failed: {e}', 'WARN')

    # Method 2: PowerShell Start-Process
    try:
        subprocess.run(
            ['powershell', '-NoProfile', '-Command',
             f"Start-Process 'shell:AppsFolder\\{aumid}'"],
            check=False)
        log('Launch command sent via PowerShell.', 'OK')
        return True
    except Exception as e:
        log(f'PowerShell launch failed: {e}', 'WARN')

    # Method 3: Open the website in browser as fallback
    log('MSIX launch failed — opening website in browser as fallback...', 'WARN')
    ensure_browser()
    url = os.environ.get('ISOLATION_BYTES_URL', f'{BASE_URL}/')
    import webbrowser
    webbrowser.open(url)
    return True

def windows_uninstall():
    # Remove MSIX
    subprocess.run(
        ['powershell', '-NoProfile', '-Command',
         f"Get-AppxPackage -Name '{APP_ID}' | Remove-AppxPackage"],
        check=False)
    # Remove shortcuts (desktop, start menu, startup)
    desktop = subprocess.run(
        ['powershell', '-NoProfile', '-Command',
         '[Environment]::GetFolderPath("Desktop")'],
        capture_output=True, text=True, check=False).stdout.strip()
    if not desktop:
        desktop = os.path.join(os.environ.get('USERPROFILE', ''), 'Desktop')
    start_menu = os.path.join(os.environ.get('APPDATA', ''),
                              'Microsoft', 'Windows', 'Start Menu', 'Programs',
                              f'{APP_NAME}.lnk')
    startup = os.path.join(os.environ.get('APPDATA', ''),
                           'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup',
                           f'{APP_NAME}.lnk')
    for s in [os.path.join(desktop, f'{APP_NAME}.lnk'), start_menu, startup]:
        if os.path.isfile(s):
            os.remove(s)
    # Remove .iblic association
    try:
        import winreg
        for subkey in [r'Software\Classes\.iblic', r'Software\Classes\IsolationBytes.iblic']:
            try:
                winreg.DeleteKey(winreg.HKEY_CURRENT_USER, subkey)
            except Exception:
                pass
    except Exception:
        pass
    log('Uninstalled.', 'OK')
    return True

def windows_repair():
    """Repair a broken Windows installation."""
    log('Repairing installation...')
    # Remove existing
    subprocess.run(
        ['powershell', '-NoProfile', '-Command',
         f"Get-AppxPackage -Name '{APP_ID}' | Remove-AppxPackage"],
        capture_output=True, check=False)
    # Reinstall
    return windows_install()

# ─── macOS ──────────────────────────────────────────────────────────────

MAC_APP_DIR = '/Applications/Isolation Bytes.app'

def macos_status():
    installed = os.path.isdir(MAC_APP_DIR)
    version = ''
    if installed:
        try:
            with open(f'{MAC_APP_DIR}/Contents/Info.plist', 'r') as f:
                content = f.read()
                import re
                m = re.search(r'<key>CFBundleShortVersionString</key>\s*<string>([^<]+)</string>', content)
                if m:
                    version = m.group(1)
        except Exception:
            pass
    return {'installed': installed, 'path': MAC_APP_DIR if installed else '', 'version': version}

def macos_install():
    if not preflight_checks():
        return False
    ensure_all_dependencies()

    log(f'Creating {MAC_APP_DIR}...')
    os.makedirs(f'{MAC_APP_DIR}/Contents/MacOS', exist_ok=True)
    os.makedirs(f'{MAC_APP_DIR}/Contents/Resources', exist_ok=True)

    launcher = f'{MAC_APP_DIR}/Contents/MacOS/IsolationBytes'
    with open(launcher, 'w') as f:
        f.write(f'''#!/bin/bash
URL="${{ISOLATION_BYTES_URL:-{BASE_URL}/}}"
if [ -x "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]; then
  exec "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --app="$URL"
elif [ -x "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" ]; then
  exec "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" --app="$URL"
elif [ -x "/Applications/Chromium.app/Contents/MacOS/Chromium" ]; then
  exec "/Applications/Chromium.app/Contents/MacOS/Chromium" --app="$URL"
elif command -v chromium >/dev/null 2>&1; then
  exec chromium --app="$URL"
else
  exec open -a Safari "$URL"
fi
''')
    os.chmod(launcher, 0o755)
    rollback.track('app bundle', lambda: shutil.rmtree(MAC_APP_DIR, ignore_errors=True))

    with open(f'{MAC_APP_DIR}/Contents/Info.plist', 'w') as f:
        f.write(f'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleName</key><string>{APP_NAME}</string>
  <key>CFBundleDisplayName</key><string>{APP_NAME}</string>
  <key>CFBundleIdentifier</key><string>com.soluzka.isolationbytes</string>
  <key>CFBundleVersion</key><string>{VERSION}</string>
  <key>CFBundleShortVersionString</key><string>{VERSION}</string>
  <key>CFBundleExecutable</key><string>IsolationBytes</string>
  <key>CFBundlePackageType</key><string>APPL</string>
  <key>LSMinimumSystemVersion</key><string>10.13</string>
  <key>NSHighResolutionCapable</key><true/>
</dict>
</plist>''')

    try:
        icon = f'{MAC_APP_DIR}/Contents/Resources/AppIcon.png'
        download_with_progress(f'{BASE_URL}/static/icon-512.png', icon)
    except Exception:
        pass

    lsregister = ('/System/Library/Frameworks/CoreServices.framework/Versions/A/'
                  'Frameworks/LaunchServices.framework/Versions/A/Support/lsregister')
    if os.path.isfile(lsregister):
        subprocess.run([lsregister, MAC_APP_DIR], capture_output=True, check=False)

    # Add to Dock (macOS shortcut equivalent)
    try:
        subprocess.run(
            ['osascript', '-e',
             f'tell application "System Events" to make login item at end '
             f'with properties {{path:"{MAC_APP_DIR}", hidden:false}}'],
            capture_output=True, check=False)
        log('Added to Login Items (auto-start on boot).')
    except Exception:
        pass

    # Add to Dock
    try:
        subprocess.run(
            ['osascript', '-e',
             f'do shell script "defaults read com.apple.dock persistent-apps | '
             f'grep -q \\"{MAC_APP_DIR}\\" || '
             f'dockutil --add \\"{MAC_APP_DIR}\\" 2>/dev/null || true"'],
            capture_output=True, check=False)
        # Restart Dock to show the new icon
        subprocess.run(['killall', 'Dock'], capture_output=True, check=False)
        log('Added to Dock.')
    except Exception:
        pass

    # Create Desktop alias (shortcut)
    try:
        desktop = os.path.expanduser('~/Desktop')
        if os.path.isdir(desktop):
            alias_path = os.path.join(desktop, f'{APP_NAME}')
            subprocess.run(
                ['osascript', '-e',
                 f'tell application "Finder" to make alias file to '
                 f'(POSIX file "{MAC_APP_DIR}") at '
                 f'(POSIX file "{desktop}")'],
                capture_output=True, check=False)
            log(f'Desktop alias created: {desktop}')
    except Exception:
        pass

    log(f'Installation complete! Log: {LOG_FILE}', 'OK')
    return True

def macos_launch():
    if not os.path.isdir(MAC_APP_DIR):
        log('Isolation Bytes is not installed. Run with --install first.', 'ERROR')
        # Fallback: open website in browser
        log('Opening website in browser as fallback...', 'WARN')
        ensure_browser()
        url = os.environ.get('ISOLATION_BYTES_URL', f'{BASE_URL}/')
        import webbrowser
        webbrowser.open(url)
        return True
    try:
        subprocess.run(['open', MAC_APP_DIR], check=False)
        log('Launched.', 'OK')
        return True
    except Exception as e:
        log(f'open command failed: {e}', 'WARN')
        # Fallback: open website in browser
        ensure_browser()
        url = os.environ.get('ISOLATION_BYTES_URL', f'{BASE_URL}/')
        import webbrowser
        webbrowser.open(url)
        return True

def macos_uninstall():
    if os.path.isdir(MAC_APP_DIR):
        shutil.rmtree(MAC_APP_DIR)
    try:
        subprocess.run(
            ['osascript', '-e',
             f'tell application "System Events" to delete login item "{APP_NAME}"'],
            capture_output=True, check=False)
    except Exception:
        pass
    log('Uninstalled.', 'OK')
    return True

def macos_repair():
    log('Repairing installation...')
    if os.path.isdir(MAC_APP_DIR):
        shutil.rmtree(MAC_APP_DIR)
    return macos_install()

# ─── Linux ──────────────────────────────────────────────────────────────

LINUX_BIN = os.path.expanduser('~/.local/bin/isolation-bytes')
LINUX_DESKTOP = os.path.expanduser('~/.local/share/applications/isolation-bytes.desktop')
LINUX_AUTOSTART = os.path.expanduser('~/.config/autostart/isolation-bytes.desktop')

def linux_status():
    installed = os.path.isfile(LINUX_DESKTOP)
    version = VERSION if installed else ''
    return {'installed': installed, 'version': version}

def linux_install():
    if not preflight_checks():
        return False
    ensure_all_dependencies()

    bin_dir = os.path.dirname(LINUX_BIN)
    os.makedirs(bin_dir, exist_ok=True)

    with open(LINUX_BIN, 'w') as f:
        f.write(f'''#!/bin/bash
URL="${{ISOLATION_BYTES_URL:-{BASE_URL}/}}"
for cmd in \\
  "chromium --app=$URL" \\
  "google-chrome --app=$URL" \\
  "google-chrome-stable --app=$URL" \\
  "chromium-browser --app=$URL" \\
  "microsoft-edge --app=$URL" \\
  "epiphany --app-mode --profile=isolbytes $URL" \\
  "falkon $URL" \\
  "midori $URL" \\
  "firefox -P isolationbytes $URL" \\
  "xdg-open $URL"; do
  binary=$(echo "$cmd" | awk '{{print $1}}')
  if command -v "$binary" >/dev/null 2>&1; then exec $cmd; fi
done
echo "No suitable browser found. Install Chromium or Firefox." >&2
exit 1
''')
    os.chmod(LINUX_BIN, 0o755)
    rollback.track('launcher script', lambda: os.remove(LINUX_BIN) if os.path.isfile(LINUX_BIN) else None)

    icon_dir = os.path.expanduser('~/.local/share/icons/hicolor/512x512/apps')
    os.makedirs(icon_dir, exist_ok=True)
    icon_path = os.path.join(icon_dir, 'isolation-bytes.png')
    try:
        download_with_progress(f'{BASE_URL}/static/icon-512.png', icon_path)
    except Exception:
        pass

    apps_dir = os.path.dirname(LINUX_DESKTOP)
    os.makedirs(apps_dir, exist_ok=True)
    with open(LINUX_DESKTOP, 'w') as f:
        f.write(f'''[Desktop Entry]
Type=Application
Name={APP_NAME}
Comment=Isolation Bytes Antivirus - Web-based security dashboard
Exec={LINUX_BIN}
Icon=isolation-bytes
Terminal=false
Categories=Security;Network;Utility;
StartupNotify=true
StartupWMClass=isolation-bytes
''')
    rollback.track('.desktop entry', lambda: os.remove(LINUX_DESKTOP) if os.path.isfile(LINUX_DESKTOP) else None)

    # Desktop shortcut — try multiple desktop paths
    desktop_paths = [
        os.path.expanduser('~/Desktop'),
        os.path.expanduser('~/桌面'),  # Chinese locale
    ]
    for desktop in desktop_paths:
        if os.path.isdir(desktop):
            dst = os.path.join(desktop, 'isolation-bytes.desktop')
            shutil.copy2(LINUX_DESKTOP, dst)
            os.chmod(dst, 0o755)
            log(f'Desktop shortcut created: {dst}')
            break

    if shutil.which('update-desktop-database'):
        subprocess.run(['update-desktop-database', apps_dir], capture_output=True, check=False)

    # Pin to taskbar/dock for common desktop environments
    # GNOME
    if shutil.which('gsettings'):
        try:
            result = subprocess.run(
                ['gsettings', 'get', 'org.gnome.shell', 'favorite-apps'],
                capture_output=True, text=True, check=False)
            if 'isolation-bytes' not in result.stdout:
                current = result.stdout.strip()
                if current == '@as []':
                    new_favs = "['isolation-bytes.desktop']"
                else:
                    new_favs = current.rstrip("']") + ", 'isolation-bytes.desktop']"
                subprocess.run(
                    ['gsettings', 'set', 'org.gnome.shell', 'favorite-apps', new_favs],
                    capture_output=True, check=False)
                log('Pinned to GNOME dash.')
        except Exception:
            pass

    # Auto-start on login
    autostart_dir = os.path.dirname(LINUX_AUTOSTART)
    os.makedirs(autostart_dir, exist_ok=True)
    with open(LINUX_AUTOSTART, 'w') as f:
        f.write(f'''[Desktop Entry]
Type=Application
Name={APP_NAME}
Exec={LINUX_BIN}
Icon=isolation-bytes
Terminal=false
X-GNOME-Autostart-enabled=true
Categories=Security;Network;
''')
    log(f'Auto-start entry created: {LINUX_AUTOSTART}')

    log(f'Installation complete! Log: {LOG_FILE}', 'OK')
    return True

def linux_launch():
    if not os.path.isfile(LINUX_BIN):
        log('Isolation Bytes is not installed. Run with --install first.', 'ERROR')
        # Fallback: open website in browser
        log('Opening website in browser as fallback...', 'WARN')
        ensure_browser()
        url = os.environ.get('ISOLATION_BYTES_URL', f'{BASE_URL}/')
        import webbrowser
        webbrowser.open(url)
        return True
    try:
        subprocess.Popen([LINUX_BIN])
        log('Launched.', 'OK')
        return True
    except Exception as e:
        log(f'Launch failed: {e}', 'WARN')
        # Fallback: open website in browser
        ensure_browser()
        url = os.environ.get('ISOLATION_BYTES_URL', f'{BASE_URL}/')
        import webbrowser
        webbrowser.open(url)
        return True

def linux_uninstall():
    for p in [LINUX_BIN, LINUX_DESKTOP, LINUX_AUTOSTART,
              os.path.expanduser('~/Desktop/isolation-bytes.desktop')]:
        if os.path.isfile(p):
            os.remove(p)
    log('Uninstalled.', 'OK')
    return True

def linux_repair():
    log('Repairing installation...')
    for p in [LINUX_BIN, LINUX_DESKTOP, LINUX_AUTOSTART]:
        if os.path.isfile(p):
            os.remove(p)
    return linux_install()

# ─── Dispatch ───────────────────────────────────────────────────────────

def get_platform():
    if IS_WINDOWS:
        return 'windows'
    elif IS_MACOS:
        return 'macos'
    elif IS_LINUX:
        return 'linux'
    return 'unknown'

def status():
    pf = get_platform()
    if pf == 'windows':
        return windows_status()
    elif pf == 'macos':
        return macos_status()
    elif pf == 'linux':
        return linux_status()
    return {'installed': False, 'error': f'Unsupported platform: {SYSTEM}'}

def install():
    pf = get_platform()
    if pf == 'windows':
        return windows_install()
    elif pf == 'macos':
        return macos_install()
    elif pf == 'linux':
        return linux_install()
    log(f'Unsupported platform: {SYSTEM}', 'ERROR')
    return False

def launch():
    pf = get_platform()
    if pf == 'windows':
        return windows_launch()
    elif pf == 'macos':
        return macos_launch()
    elif pf == 'linux':
        return linux_launch()
    url = os.environ.get('ISOLATION_BYTES_URL', f'{BASE_URL}/')
    import webbrowser
    webbrowser.open(url)
    log(f'Opened {url} in default browser.', 'OK')
    return True

def uninstall():
    pf = get_platform()
    if pf == 'windows':
        return windows_uninstall()
    elif pf == 'macos':
        return macos_uninstall()
    elif pf == 'linux':
        return linux_uninstall()
    log(f'Unsupported platform: {SYSTEM}', 'ERROR')
    return False

def repair():
    pf = get_platform()
    if pf == 'windows':
        return windows_repair()
    elif pf == 'macos':
        return macos_repair()
    elif pf == 'linux':
        return linux_repair()
    log(f'Unsupported platform: {SYSTEM}', 'ERROR')
    return False

def update():
    """Check for and install updates."""
    log('Checking for updates...')
    update_info = check_for_updates()
    if not update_info.get('update_available'):
        log('Already up to date.', 'OK')
        return True
    if not SILENT:
        if not confirm(f'Update to {update_info["latest"]}?'):
            log('Update cancelled.')
            return False
    log(f'Updating from {update_info["current"]} to {update_info["latest"]}...')
    result = install()
    if result:
        log(f'Updated to {update_info["latest"]}!', 'OK')
    return result

# ─── Main ───────────────────────────────────────────────────────────────

def main():
    args = sys.argv[1:]
    action = 'launch'

    if '--install' in args:
        action = 'install'
    elif '--launch' in args:
        action = 'launch'
    elif '--uninstall' in args:
        action = 'uninstall'
    elif '--status' in args:
        action = 'status'
    elif '--update' in args:
        action = 'update'
    elif '--repair' in args:
        action = 'repair'
    elif '--version' in args:
        print(f'Isolation Bytes Universal Launcher v{LAUNCHER_VERSION}')
        return
    elif '--help' in args or '-h' in args:
        print(__doc__)
        return

    pf = get_platform()
    log(f'Isolation Bytes Universal Launcher v{LAUNCHER_VERSION}')
    log(f'Platform: {pf} ({SYSTEM} {ARCH})')
    log(f'Log file: {LOG_FILE}')

    if action == 'status':
        info = status()
        log(f'Installed: {info.get("installed", False)}')
        if info.get('version'):
            log(f'Version: {info["version"]}')
        if info.get('path'):
            log(f'Path: {info["path"]}')
        if info.get('aumid'):
            log(f'AUMID: {info["aumid"]}')
        # Also check for updates
        if not OFFLINE:
            update_info = check_for_updates()
            if update_info.get('update_available'):
                log(f'Update available: {update_info["latest"]}', 'OK')
        return

    if action == 'install':
        install()
        return

    if action == 'uninstall':
        if not SILENT:
            if not confirm('Uninstall Isolation Bytes?'):
                log('Uninstall cancelled.')
                return
        uninstall()
        return

    if action == 'repair':
        repair()
        return

    if action == 'update':
        update()
        return

    if action == 'launch':
        info = status()
        if not info.get('installed'):
            log('Not installed — installing everything first...')
            result = install()
            if result is False:
                log('Install failed. Opening website as fallback...', 'WARN')
                ensure_browser()
                url = os.environ.get('ISOLATION_BYTES_URL', f'{BASE_URL}/')
                import webbrowser
                webbrowser.open(url)
                return
            if result is None:
                return  # elevated re-launch
        launch()
        return

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        log('\nCancelled by user.', 'WARN')
        rollback.rollback()
        sys.exit(1)
    except Exception as e:
        log(f'Unexpected error: {e}', 'ERROR')
        log(f'Log file: {LOG_FILE}', 'ERROR')
        logging.exception('Unhandled exception')
        rollback.rollback()
        sys.exit(1)
