"""
CARL Control Panel - Auto-Update System
Checks GitHub releases and manages self-updates.
"""
import os
import sys
import json
import tempfile
import subprocess
import threading
import urllib.request
import urllib.error
from pathlib import Path

# Version - bump this with each release
APP_VERSION = "1.0.29"

# GitHub repo details
GITHUB_OWNER = "ChristopherKahler"
GITHUB_REPO = "claude-code-carl"
GITHUB_API_URL = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest"

# PyPI package name
PYPI_PACKAGE = "carl-control-panel"
PYPI_API_URL = f"https://pypi.org/pypi/{PYPI_PACKAGE}/json"

# Detect if running from pip install (not frozen exe)
IS_PIP_INSTALL = not getattr(sys, 'frozen', False)

# Update state
_update_info = {
    'checking': False,
    'available': False,
    'downloading': False,
    'download_progress': 0,
    'ready_to_install': False,
    'error': None,
    'latest_version': None,
    'download_url': None,
    'release_notes': None,
    'downloaded_path': None,
    'is_pip': IS_PIP_INSTALL,
    'pip_command': None
}

_update_lock = threading.Lock()


def get_version():
    """Get current app version."""
    return APP_VERSION


def get_update_status():
    """Get current update status."""
    with _update_lock:
        return dict(_update_info)


def _set_update_status(**kwargs):
    """Thread-safe update status setter."""
    with _update_lock:
        _update_info.update(kwargs)


def _compare_versions(v1, v2):
    """
    Compare version strings. Returns:
    -1 if v1 < v2
     0 if v1 == v2
     1 if v1 > v2
    """
    def normalize(v):
        return [int(x) for x in v.lstrip('v').split('.')]

    try:
        n1, n2 = normalize(v1), normalize(v2)
        # Pad to same length
        while len(n1) < len(n2):
            n1.append(0)
        while len(n2) < len(n1):
            n2.append(0)

        for a, b in zip(n1, n2):
            if a < b:
                return -1
            if a > b:
                return 1
        return 0
    except (ValueError, AttributeError):
        return 0


def _check_pypi_updates():
    """
    Check PyPI for latest version (for pip installs).
    Returns dict with update info.
    """
    try:
        req = urllib.request.Request(
            PYPI_API_URL,
            headers={'User-Agent': f'CARL-Control-Panel/{APP_VERSION}'}
        )

        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode('utf-8'))

        latest_version = data.get('info', {}).get('version', '')

        if not latest_version:
            _set_update_status(checking=False, error='Could not determine latest PyPI version')
            return get_update_status()

        is_newer = _compare_versions(latest_version, APP_VERSION) > 0
        pip_cmd = f"pip install --upgrade {PYPI_PACKAGE}"

        _set_update_status(
            checking=False,
            available=is_newer,
            latest_version=latest_version,
            download_url=None,
            release_notes='',
            is_pip=True,
            pip_command=pip_cmd if is_newer else None,
            error=None
        )

    except urllib.error.HTTPError as e:
        _set_update_status(checking=False, error=f'PyPI API error: {e.code}')
    except urllib.error.URLError as e:
        _set_update_status(checking=False, error=f'Network error: {e.reason}')
    except json.JSONDecodeError:
        _set_update_status(checking=False, error='Invalid response from PyPI')
    except Exception as e:
        _set_update_status(checking=False, error=str(e))

    return get_update_status()


def _check_github_updates():
    """
    Check GitHub releases for updates (for standalone exe).
    Returns dict with update info.
    """
    try:
        req = urllib.request.Request(
            GITHUB_API_URL,
            headers={
                'User-Agent': f'CARL-Control-Panel/{APP_VERSION}',
                'Accept': 'application/vnd.github.v3+json'
            }
        )

        with urllib.request.urlopen(req, timeout=10) as response:
            data = json.loads(response.read().decode('utf-8'))

        latest_version = data.get('tag_name', '').lstrip('v')

        if not latest_version:
            _set_update_status(checking=False, error='Could not determine latest version')
            return get_update_status()

        # Find the right asset for this platform
        assets = data.get('assets', [])
        download_url = None

        if sys.platform == 'win32':
            for asset in assets:
                name = asset.get('name', '').lower()
                if name.endswith('.exe') or 'windows' in name:
                    download_url = asset.get('browser_download_url')
                    break
        elif sys.platform == 'darwin':
            for asset in assets:
                name = asset.get('name', '').lower()
                if 'macos' in name or 'darwin' in name:
                    download_url = asset.get('browser_download_url')
                    break

        is_newer = _compare_versions(latest_version, APP_VERSION) > 0

        _set_update_status(
            checking=False,
            available=is_newer,
            latest_version=latest_version,
            download_url=download_url,
            release_notes=data.get('body', ''),
            is_pip=False,
            pip_command=None,
            error=None if download_url or not is_newer else 'No compatible download found'
        )

    except urllib.error.HTTPError as e:
        if e.code == 404:
            _set_update_status(checking=False, error='No releases found')
        else:
            _set_update_status(checking=False, error=f'GitHub API error: {e.code}')
    except urllib.error.URLError as e:
        _set_update_status(checking=False, error=f'Network error: {e.reason}')
    except json.JSONDecodeError:
        _set_update_status(checking=False, error='Invalid response from GitHub')
    except Exception as e:
        _set_update_status(checking=False, error=str(e))

    return get_update_status()


def check_for_updates(force=False):
    """
    Check for updates from PyPI (pip installs) or GitHub (standalone exe).
    Returns dict with update info.
    """
    status = get_update_status()
    if status['checking'] and not force:
        return status

    _set_update_status(checking=True, error=None)

    if IS_PIP_INSTALL:
        return _check_pypi_updates()
    else:
        return _check_github_updates()


def check_for_updates_async(callback=None):
    """
    Check for updates in background thread.
    Optional callback receives the update status dict.
    """
    def _check():
        result = check_for_updates()
        if callback:
            callback(result)

    thread = threading.Thread(target=_check, daemon=True)
    thread.start()
    return thread


def download_update(progress_callback=None):
    """
    Download the latest update to a temp location.
    Returns path to downloaded file or None on error.
    """
    status = get_update_status()

    if not status['available'] or not status['download_url']:
        _set_update_status(error='No update available to download')
        return None

    if status['downloading']:
        return None

    _set_update_status(downloading=True, download_progress=0, error=None)

    try:
        url = status['download_url']

        # Determine filename from URL
        filename = url.split('/')[-1]
        if not filename:
            filename = 'CARL Control Panel.exe' if sys.platform == 'win32' else 'CARL Control Panel'

        # Download to temp directory
        temp_dir = tempfile.gettempdir()
        download_path = os.path.join(temp_dir, f'carl_update_{filename}')

        # Create request
        req = urllib.request.Request(
            url,
            headers={'User-Agent': f'CARL-Control-Panel/{APP_VERSION}'}
        )

        with urllib.request.urlopen(req, timeout=300) as response:
            total_size = int(response.headers.get('Content-Length', 0))
            downloaded = 0
            chunk_size = 8192

            with open(download_path, 'wb') as f:
                while True:
                    chunk = response.read(chunk_size)
                    if not chunk:
                        break
                    f.write(chunk)
                    downloaded += len(chunk)

                    if total_size > 0:
                        progress = int((downloaded / total_size) * 100)
                        _set_update_status(download_progress=progress)
                        if progress_callback:
                            progress_callback(progress)

        _set_update_status(
            downloading=False,
            download_progress=100,
            ready_to_install=True,
            downloaded_path=download_path
        )

        return download_path

    except Exception as e:
        _set_update_status(
            downloading=False,
            error=f'Download failed: {e}'
        )
        return None


def download_update_async(progress_callback=None, complete_callback=None):
    """
    Download update in background thread.
    """
    def _download():
        result = download_update(progress_callback)
        if complete_callback:
            complete_callback(result)

    thread = threading.Thread(target=_download, daemon=True)
    thread.start()
    return thread


def apply_update():
    """
    Apply the downloaded update.
    On Windows: Creates a batch script to swap files after app exits.
    Returns dict with success status.
    """
    status = get_update_status()

    if not status['ready_to_install'] or not status['downloaded_path']:
        return {'success': False, 'error': 'No update ready to install'}

    downloaded_path = status['downloaded_path']

    if not os.path.exists(downloaded_path):
        return {'success': False, 'error': 'Downloaded file not found'}

    try:
        if sys.platform == 'win32':
            return _apply_update_windows(downloaded_path)
        elif sys.platform == 'darwin':
            return _apply_update_macos(downloaded_path)
        else:
            return _apply_update_linux(downloaded_path)
    except Exception as e:
        return {'success': False, 'error': str(e)}


def _apply_update_windows(downloaded_path):
    """
    Windows update strategy:
    1. Create a batch script that:
       - Waits for current process to exit
       - Backs up old exe
       - Copies new exe in place
       - Restarts the app
       - Cleans up
    2. Launch the script
    3. Exit the current app
    """
    if getattr(sys, 'frozen', False):
        current_exe = sys.executable
    else:
        # Running from source - can't self-update
        return {'success': False, 'error': 'Cannot self-update when running from source'}

    current_dir = os.path.dirname(current_exe)
    exe_name = os.path.basename(current_exe)
    backup_path = os.path.join(current_dir, f'{exe_name}.bak')

    # Create update batch script
    script_path = os.path.join(tempfile.gettempdir(), 'carl_update.bat')

    script_content = f'''@echo off
echo CARL Control Panel Updater
echo Waiting for application to close...

:: Wait for the main process to exit (up to 30 seconds)
set /a count=0
:waitloop
tasklist /FI "PID eq {os.getpid()}" 2>NUL | find /I "{os.getpid()}" >NUL
if not errorlevel 1 (
    set /a count+=1
    if %count% GEQ 30 (
        echo Timeout waiting for app to close
        goto :error
    )
    timeout /t 1 /nobreak >NUL
    goto :waitloop
)

echo Application closed. Applying update...

:: Backup old version
if exist "{current_exe}" (
    if exist "{backup_path}" del /f "{backup_path}"
    move /y "{current_exe}" "{backup_path}"
    if errorlevel 1 (
        echo Failed to backup old version
        goto :error
    )
)

:: Copy new version
copy /y "{downloaded_path}" "{current_exe}"
if errorlevel 1 (
    echo Failed to copy new version
    :: Try to restore backup
    if exist "{backup_path}" move /y "{backup_path}" "{current_exe}"
    goto :error
)

echo Update successful! Starting new version...

:: Clean up
del /f "{downloaded_path}" 2>NUL
del /f "{backup_path}" 2>NUL

:: Start the new version
start "" "{current_exe}"

:: Delete this script (delayed)
(goto) 2>nul & del "%~f0"
exit /b 0

:error
echo Update failed!
pause
exit /b 1
'''

    with open(script_path, 'w') as f:
        f.write(script_content)

    # Launch the update script (hidden window)
    subprocess.Popen(
        ['cmd', '/c', 'start', '/min', '', script_path],
        shell=False,
        creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
    )

    return {
        'success': True,
        'message': 'Update script launched. The application will restart automatically.',
        'action': 'exit_app'
    }


def _apply_update_macos(downloaded_path):
    """
    macOS update strategy - similar to Windows but with shell script.
    """
    if getattr(sys, 'frozen', False):
        current_exe = sys.executable
    else:
        return {'success': False, 'error': 'Cannot self-update when running from source'}

    current_dir = os.path.dirname(current_exe)
    exe_name = os.path.basename(current_exe)
    backup_path = os.path.join(current_dir, f'{exe_name}.bak')

    script_path = os.path.join(tempfile.gettempdir(), 'carl_update.sh')

    script_content = f'''#!/bin/bash
echo "CARL Control Panel Updater"
echo "Waiting for application to close..."

# Wait for process to exit
for i in {{1..30}}; do
    if ! kill -0 {os.getpid()} 2>/dev/null; then
        break
    fi
    sleep 1
done

echo "Applying update..."

# Backup and replace
if [ -f "{current_exe}" ]; then
    mv "{current_exe}" "{backup_path}" || exit 1
fi

cp "{downloaded_path}" "{current_exe}" || {{
    mv "{backup_path}" "{current_exe}"
    exit 1
}}

chmod +x "{current_exe}"

echo "Update successful! Starting new version..."

# Clean up
rm -f "{downloaded_path}"
rm -f "{backup_path}"

# Start new version
open "{current_exe}"

# Delete this script
rm -f "$0"
'''

    with open(script_path, 'w') as f:
        f.write(script_content)

    os.chmod(script_path, 0o755)
    subprocess.Popen(['bash', script_path], start_new_session=True)

    return {
        'success': True,
        'message': 'Update script launched. The application will restart automatically.',
        'action': 'exit_app'
    }


def _apply_update_linux(downloaded_path):
    """Linux update - same as macOS."""
    return _apply_update_macos(downloaded_path)


def reset_update_state():
    """Reset update state (e.g., after dismissing notification)."""
    _set_update_status(
        checking=False,
        available=False,
        downloading=False,
        download_progress=0,
        ready_to_install=False,
        error=None,
        latest_version=None,
        download_url=None,
        release_notes=None,
        downloaded_path=None
    )
