"""Auto-updater do IronDome.

Busca manifesto no site Vercel (/version.json), baixa o ZIP da nova versão,
preserva dados do usuário (data/, _snapshots/, certs) e substitui os
arquivos de código. Spawn de restart.sh faz o restart depois.
"""
import json
import os
import shutil
import ssl
import sys
import tempfile
import threading
import time
import urllib.request
import zipfile

import version

MANIFEST_URL = 'https://irondome-site.vercel.app/version.json'
APP_DIR = os.path.dirname(os.path.abspath(__file__))
SNAPSHOT_DIR = os.path.join(APP_DIR, '_snapshots')
# Pastas/arquivos que NUNCA são sobrescritos por update — dados do usuário
PRESERVE = {'data', '_snapshots', 'cert.pem', 'key.pem', '__pycache__'}


def _semver_tuple(v: str):
    try:
        return tuple(int(p) for p in v.split('.')[:3])
    except Exception:
        return (0, 0, 0)


def fetch_manifest(timeout=5) -> dict:
    """Baixa o manifesto version.json do site."""
    ctx = ssl.create_default_context()
    req = urllib.request.Request(MANIFEST_URL, headers={'Cache-Control': 'no-cache'})
    with urllib.request.urlopen(req, context=ctx, timeout=timeout) as r:
        return json.loads(r.read())


def check_update() -> dict:
    """Retorna status: current, latest, available, zip_url, changelog_url."""
    current = version.VERSION
    try:
        m = fetch_manifest()
        latest = str(m.get('latest', current))
        available = _semver_tuple(latest) > _semver_tuple(current)
        base = 'https://irondome-site.vercel.app'
        zip_url = m.get('zip_url') or '/irondome-latest.zip'
        if not zip_url.startswith('http'): zip_url = base + zip_url
        chg_url = m.get('changelog_url') or '/CHANGELOG.md'
        if not chg_url.startswith('http'): chg_url = base + chg_url
        return {
            'current': current, 'latest': latest, 'available': available,
            'zip_url': zip_url, 'changelog_url': chg_url,
        }
    except Exception as e:
        return {'current': current, 'error': str(e)[:200]}


def _safe_copy_tree(src, dst, skip_names):
    """Copia src → dst pulando nomes listados em skip_names no topo do src."""
    for name in os.listdir(src):
        if name in skip_names:
            continue
        s = os.path.join(src, name)
        d = os.path.join(dst, name)
        if os.path.isdir(s):
            if os.path.exists(d):
                shutil.rmtree(d)
            shutil.copytree(s, d)
        else:
            shutil.copy2(s, d)


def install_update() -> dict:
    """Baixa o ZIP do site, extrai em tmp, faz snapshot de rollback e copia
    os arquivos novos. Preserva data/, _snapshots/, certs.
    Retorna {ok, message, from_version, to_version, snapshot}."""
    info = check_update()
    if not info.get('available'):
        return {'ok': False, 'message': 'Sem atualização disponível', **info}
    url = info['zip_url']
    from_v = info['current']
    to_v = info['latest']

    # 1) Snapshot de rollback
    os.makedirs(SNAPSHOT_DIR, exist_ok=True)
    snap = os.path.join(SNAPSHOT_DIR, f'pre-update-v{from_v}-{int(time.time())}')
    os.makedirs(snap, exist_ok=True)
    try:
        for name in os.listdir(APP_DIR):
            if name in PRESERVE:
                continue
            s = os.path.join(APP_DIR, name)
            d = os.path.join(snap, name)
            if os.path.isdir(s):
                shutil.copytree(s, d, dirs_exist_ok=True)
            else:
                shutil.copy2(s, d)
    except Exception as e:
        return {'ok': False, 'message': f'falha ao snapshotar: {e}'}

    # 2) Baixa o ZIP
    ctx = ssl.create_default_context()
    tmp_dir = tempfile.mkdtemp(prefix='irondome-update-')
    zip_path = os.path.join(tmp_dir, 'update.zip')
    try:
        with urllib.request.urlopen(url, context=ctx, timeout=30) as r:
            with open(zip_path, 'wb') as f:
                shutil.copyfileobj(r, f)
    except Exception as e:
        return {'ok': False, 'message': f'falha download: {e}', 'from_version': from_v, 'to_version': to_v}

    # 3) Extrai
    extract_dir = os.path.join(tmp_dir, 'extracted')
    try:
        with zipfile.ZipFile(zip_path, 'r') as zf:
            zf.extractall(extract_dir)
    except Exception as e:
        return {'ok': False, 'message': f'falha extrair: {e}'}

    # O ZIP contém uma pasta "irondome/" com os arquivos. Detecta.
    entries = os.listdir(extract_dir)
    src_root = extract_dir
    if len(entries) == 1 and os.path.isdir(os.path.join(extract_dir, entries[0])):
        src_root = os.path.join(extract_dir, entries[0])

    # 4) Copia por cima, pulando PRESERVE
    try:
        _safe_copy_tree(src_root, APP_DIR, PRESERVE)
    except Exception as e:
        return {'ok': False, 'message': f'falha copiar: {e}', 'snapshot': snap}

    # 5) Cleanup tmp
    try: shutil.rmtree(tmp_dir)
    except Exception: pass

    return {
        'ok': True,
        'message': f'Atualizado v{from_v} → v{to_v}',
        'from_version': from_v,
        'to_version': to_v,
        'snapshot': snap,
    }


def schedule_restart(delay_s: float = 1.0):
    """Spawn restart.sh em background e agenda sys.exit. Restart.sh mata
    os processos antigos e sobe os novos."""
    def _do_restart():
        time.sleep(delay_s)
        script = os.path.join(APP_DIR, 'restart.sh')
        if os.path.exists(script):
            os.system(f'nohup bash "{script}" > /tmp/irondome-restart.log 2>&1 &')
        os._exit(0)
    threading.Thread(target=_do_restart, daemon=True).start()
