#!/usr/bin/env python3
"""
Gold AutoTrade — App Updater (GUI backend integration)

Imported by gui_backend.py. Manages update lifecycle:
  check → download/stage → promote (make live) → rollback
"""

from __future__ import annotations

import json
import os
import shutil
import tempfile
import time
from pathlib import Path

ROOT = Path(__file__).resolve().parent
VERSION_FILE = ROOT / "VERSION"
DATA_DIR = ROOT / "data" / "bridge" / "state"
STATE_FILE = DATA_DIR / "update_state.json"
BACKUP_DIR = ROOT.parent  # .backup_vX.Y.Z directories go next to the app root
DEFAULT_MANIFEST_URL = "https://license.huipin199.com/api/update/manifest"


# ── State helpers ──────────────────────────────────────────────────────

def _read_state() -> dict:
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text())
        except Exception:
            pass
    return {}


def _write_state_file(state: dict) -> None:
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2))


def write_state(patch: dict) -> None:
    """Merge patch into the update state file (called by backend)."""
    state = _read_state()
    state.update(patch)
    state["updated_at"] = int(time.time())
    _write_state_file(state)


# ── Version info ───────────────────────────────────────────────────────

def load_display_version() -> str:
    """Return the VERSION file contents (e.g. '1.0.0-trial')."""
    if VERSION_FILE.exists():
        return VERSION_FILE.read_text().strip()
    return "0.0.0"


def get_current_release() -> dict:
    """Return info about the currently active release."""
    ver = load_display_version()
    return {
        "version": ver,
        "path": str(ROOT),
        "vcs_ref": "",
        "installed_at": _read_state().get("installed_at", ""),
    }


def get_candidate_release() -> dict:
    """Return info about the staged candidate (if any)."""
    state = _read_state()
    candidate = state.get("candidate", {})
    if not candidate:
        return {}
    return {
        "version": candidate.get("version", ""),
        "path": candidate.get("path", ""),
        "vcs_ref": "",
        "stage_timestamp": candidate.get("stage_timestamp", ""),
    }


def load_update_state() -> dict:
    """Return the full update lifecycle state for the UI."""
    state = _read_state()
    return {
        "status": state.get("status", "idle"),
        "target_version": state.get("target_version", ""),
        "error": state.get("error", ""),
        "last_check": state.get("last_check", ""),
        "last_update": state.get("last_update", ""),
        "blockers": state.get("blockers", []),
        "safe_to_switch": state.get("safe_to_switch", True),
    }


# ── Manifest resolution ────────────────────────────────────────────────

def resolve_manifest_source(manifest: str | None) -> str:
    """Resolve the manifest source: explicit URL/path → config → default."""
    if manifest:
        return manifest
    try:
        import config_manager
        cfg = config_manager.load_app_config()
        url = cfg.get("update_manifest_url", "").strip()
        if url:
            return url
    except Exception:
        pass
    return DEFAULT_MANIFEST_URL


# ── Manifest fetch ─────────────────────────────────────────────────────

def _fetch_manifest(url: str) -> dict | None:
    """Fetch manifest from URL or local file."""
    if url.startswith("http://") or url.startswith("https://"):
        import urllib.request
        try:
            req = urllib.request.Request(url)
            with urllib.request.urlopen(req, timeout=15) as resp:
                return json.loads(resp.read().decode())
        except Exception:
            return None
    else:
        path = Path(url)
        if path.exists():
            try:
                return json.loads(path.read_text())
            except Exception:
                return None
        return None


def _version_newer(a: str, b: str) -> bool:
    def _parse(v: str) -> list:
        parts = v.split("-")[0].split(".")
        return [int(p) for p in parts if p.isdigit()]
    return _parse(a) > _parse(b)


def _sha256_file(path: Path) -> str:
    import hashlib
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()


# ── Quality gates ──────────────────────────────────────────────────────

def _check_quality_gates() -> tuple[list[str], bool]:
    """Check if it's safe to apply/promote an update. Returns (blockers, safe)."""
    blockers = []
    try:
        # Check for open positions
        pos_file = ROOT / "data" / "execution_state.json"
        if pos_file.exists():
            es = json.loads(pos_file.read_text())
            positions = es.get("open_positions", es.get("positions", []))
            if positions:
                blockers.append("open_positions")
    except Exception:
        pass

    try:
        # Check for pending review signals
        review_dir = ROOT / "logs" / "review_pending"
        if review_dir.exists() and list(review_dir.glob("*.review.json")):
            blockers.append("pending_review_signals")
    except Exception:
        pass

    return blockers, len(blockers) == 0


# ── Check ──────────────────────────────────────────────────────────────

def run_check(manifest_source: str | None = None) -> dict:
    """Check for updates. Returns status dict for the API response."""
    source = resolve_manifest_source(manifest_source)
    manifest = _fetch_manifest(source)
    cur = load_display_version()

    state = _read_state()
    state["last_check"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    state["manifest_source"] = source

    if manifest is None:
        state["status"] = "no_manifest"
        state["error"] = "Cannot reach update server"
        _write_state_file(state)
        return {
            "status": "no_manifest",
            "current_version": cur,
            "target_version": "",
            "update_available": False,
            "quality": {"blockers": [], "safe_to_switch": True},
        }

    trial_pkg = manifest.get("packages", {}).get("trial", {})
    latest = trial_pkg.get("version", manifest.get("version", "0.0.0"))
    changelog = trial_pkg.get("changelog", manifest.get("changelog", ""))
    download_url = trial_pkg.get("url", "")
    file_size = trial_pkg.get("size", 0)
    expected_sha = trial_pkg.get("sha256", "")

    if not _version_newer(latest, cur):
        state["status"] = "up_to_date"
        _write_state_file(state)
        return {
            "status": "up_to_date",
            "current_version": cur,
            "target_version": latest,
            "update_available": False,
            "changelog": changelog,
            "quality": {"blockers": [], "safe_to_switch": True},
        }

    blockers, safe = _check_quality_gates()
    state["status"] = "update_available"
    state["target_version"] = latest
    state["changelog"] = changelog
    state["download_url"] = download_url
    state["file_size"] = file_size
    state["expected_sha256"] = expected_sha
    state["blockers"] = blockers
    state["safe_to_switch"] = safe
    _write_state_file(state)

    return {
        "status": "update_available",
        "current_version": cur,
        "target_version": latest,
        "update_available": True,
        "changelog": changelog,
        "file_size": file_size,
        "quality": {"blockers": blockers, "safe_to_switch": safe},
    }


# ── Apply (stage candidate) ────────────────────────────────────────────

def run_auto(manifest_source: str | None = None, force: bool = False) -> dict:
    """Download and stage the candidate release. Does NOT make it live."""
    source = resolve_manifest_source(manifest_source)
    manifest = _fetch_manifest(source)

    if manifest is None:
        write_state({"status": "no_manifest", "error": "Cannot reach update server"})
        return {"status": "no_manifest", "error": "Cannot reach update server"}

    cur = load_display_version()
    trial_pkg = manifest.get("packages", {}).get("trial", {})
    latest = trial_pkg.get("version", manifest.get("version", "0.0.0"))
    download_path = trial_pkg.get("url", "")
    file_size = trial_pkg.get("size", 0)
    expected_sha = trial_pkg.get("sha256", "")

    if not _version_newer(latest, cur):
        write_state({"status": "up_to_date"})
        return {"status": "up_to_date", "message": f"Already at v{cur}"}

    blockers, safe = _check_quality_gates()
    if not safe and not force:
        write_state({"status": "blocked", "blockers": blockers, "safe_to_switch": False})
        return {"status": "blocked", "blockers": blockers, "message": "Quality gates block update"}

    write_state({"status": "downloading", "target_version": latest})

    # Build full download URL
    if download_path and not download_path.startswith("http"):
        download_full = f"https://huipin199.com{download_path}"
    else:
        download_full = download_path

    if not download_full:
        write_state({"status": "apply_failed", "error": "No download URL in manifest"})
        return {"status": "apply_failed", "error": "No download URL"}

    # Download to temp
    tmpdir = Path(tempfile.mkdtemp(prefix="gat_update_"))
    zip_path = tmpdir / f"update-v{latest}.zip"
    try:
        import urllib.request
        urllib.request.urlretrieve(download_full, str(zip_path))
    except Exception as e:
        shutil.rmtree(tmpdir, ignore_errors=True)
        write_state({"status": "apply_failed", "error": f"Download failed: {e}"})
        return {"status": "apply_failed", "error": f"Download failed: {e}"}

    # Verify SHA256
    if expected_sha:
        actual = _sha256_file(zip_path)
        if actual != expected_sha:
            shutil.rmtree(tmpdir, ignore_errors=True)
            write_state({"status": "apply_failed", "error": "SHA256 mismatch"})
            return {"status": "apply_failed", "error": "SHA256 mismatch"}

    # Extract
    extract_dir = tmpdir / "extracted"
    extract_dir.mkdir()
    import zipfile
    with zipfile.ZipFile(zip_path, "r") as zf:
        zf.extractall(str(extract_dir))

    # Find extracted root
    extracted_items = list(extract_dir.iterdir())
    if len(extracted_items) != 1 or not extracted_items[0].is_dir():
        shutil.rmtree(tmpdir, ignore_errors=True)
        write_state({"status": "apply_failed", "error": "Unexpected zip structure"})
        return {"status": "apply_failed", "error": "Unexpected zip structure"}

    candidate_root = extracted_items[0]

    # Record candidate
    candidate_info = {
        "version": latest,
        "path": str(candidate_root),
        "stage_timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "changelog": manifest.get("changelog", trial_pkg.get("changelog", "")),
        "tmpdir": str(tmpdir),
    }
    write_state({
        "status": "candidate_ready",
        "target_version": latest,
        "candidate": candidate_info,
        "safe_to_switch": safe,
    })

    return {
        "status": "candidate_ready",
        "version": latest,
        "message": f"Candidate v{latest} staged. Use promote to make it live.",
    }


# ── Promote candidate → current ────────────────────────────────────────

def promote_candidate(force: bool = False) -> dict:
    """Make the staged candidate the active release."""
    state = _read_state()
    candidate = state.get("candidate", {})
    if not candidate:
        return {"status": "no_candidate", "error": "No candidate staged"}

    candidate_ver = candidate.get("version", "")
    candidate_path = candidate.get("path", "")

    if not candidate_path or not Path(candidate_path).exists():
        return {"status": "promotion_failed", "error": "Candidate path missing"}

    blockers, safe = _check_quality_gates()
    if not safe and not force:
        return {"status": "promotion_blocked", "blockers": blockers, "message": "Quality gates block promotion"}

    # Create backup of current
    cur_ver = load_display_version()
    backup_dir = BACKUP_DIR / f".backup_v{cur_ver}"
    if not backup_dir.exists():
        try:
            shutil.copytree(
                ROOT, backup_dir, symlinks=True,
                ignore=lambda d, f: [x for x in f if x in ('__pycache__', '.venv', 'node_modules', 'logs', 'data', 'dist')]
            )
            state["backup_dir"] = str(backup_dir)
        except Exception:
            pass

    # Copy candidate over current
    try:
        _copy_over(Path(candidate_path), ROOT)
    except Exception as e:
        return {"status": "promotion_failed", "error": f"Copy failed: {e}"}

    # Update VERSION
    VERSION_FILE.write_text(candidate_ver)

    # Update history
    history = state.get("history", [])
    history.append({
        "version": candidate_ver,
        "from_version": cur_ver,
        "promoted_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "backup_dir": str(backup_dir),
    })
    if len(history) > 10:
        history = history[-10:]

    state["status"] = "installed"
    state["installed_at"] = history[-1]["promoted_at"]
    state["history"] = history
    state["candidate"] = {}
    state["target_version"] = ""
    state["blockers"] = []
    state["safe_to_switch"] = True

    # Cleanup temp
    tmpdir = candidate.get("tmpdir", "")
    if tmpdir:
        shutil.rmtree(Path(tmpdir), ignore_errors=True)

    _write_state_file(state)
    return {"status": "promoted", "version": candidate_ver, "backup_dir": str(backup_dir)}


# ── Rollback ───────────────────────────────────────────────────────────

def rollback_to_previous() -> dict:
    """Revert to the previous version from backup."""
    state = _read_state()
    history = state.get("history", [])
    if not history:
        return {"status": "no_backup", "error": "No previous version to roll back to"}

    last = history[-1]
    backup_dir = Path(last.get("backup_dir", ""))
    if not backup_dir.exists():
        return {"status": "no_backup", "error": f"Backup not found: {backup_dir}"}

    # Restore from backup
    try:
        _copy_over(backup_dir, ROOT)
    except Exception as e:
        return {"status": "rollback_failed", "error": f"Restore failed: {e}"}

    rollback_ver = last.get("from_version", "0.0.0")
    VERSION_FILE.write_text(rollback_ver)

    state["status"] = "rolled_back"
    state["target_version"] = ""
    state["candidate"] = {}
    state["history"] = history[:-1]
    _write_state_file(state)

    return {"status": "rolled_back", "version": rollback_ver, "from_version": last.get("version", "")}


# ── Internal helpers ───────────────────────────────────────────────────

def _copy_over(src: Path, dst: Path) -> None:
    """Copy src contents over dst, overwriting existing files."""
    for item in src.iterdir():
        target = dst / item.name
        if item.is_dir():
            if target.exists() and not target.is_dir():
                target.unlink()
            target.mkdir(exist_ok=True)
            _copy_over(item, target)
        else:
            shutil.copy2(item, target)
