"""GitHub Releases auto-updater.

Checks for new versions via GitHub API, downloads and launches the installer.
Results are cached for 24 hours to avoid hammering the API.
"""

from __future__ import annotations

import json
import logging
import subprocess
import sys
import tempfile
import threading
import time
from collections.abc import Callable
from pathlib import Path

from packaging.version import Version

from src import __version__

log = logging.getLogger(__name__)

_CACHE_TTL = 86400  # 24 hours
_API_TIMEOUT = 5  # seconds — intentionally short: GitHub blocked by GFW, fast-fail prevents startup hang


class UpdateChecker:
    """Non-blocking GitHub release checker with 24-hour cache."""

    def __init__(
        self,
        github_repo: str,
        cache_dir: Path,
        on_update_available: Callable[[str, str], None] | None = None,
    ) -> None:
        """
        Args:
            github_repo: "owner/repo" string (e.g. "yourname/muravey-china")
            cache_dir: Directory to store the update check cache file.
            on_update_available: Called with (new_version, download_url) when update found.
        """
        self._repo = github_repo.strip()
        self._cache_path = cache_dir / "update_check.json"
        self._on_update = on_update_available
        self._current = Version(__version__)

    @property
    def _api_url(self) -> str:
        return f"https://api.github.com/repos/{self._repo}/releases/latest"

    def check_async(self, force: bool = False) -> None:
        """Start background update check. Result delivered via callback."""
        if not self._repo or "/" not in self._repo:
            log.debug("No valid GitHub repo configured — skipping update check")
            return
        thread = threading.Thread(
            target=self._check, args=(force,), daemon=True, name="update-check"
        )
        thread.start()

    def check_sync(self) -> dict | None:
        """Synchronous check — returns result dict or None."""
        return self._do_check()

    # ------------------------------------------------------------------ #
    # Internal
    # ------------------------------------------------------------------ #

    def _check(self, force: bool) -> None:
        """Background thread entry."""
        if not force and self._cache_valid():
            log.debug("Update check skipped (cache fresh)")
            return
        result = self._do_check()
        if result and result.get("update_available") and self._on_update:
            self._on_update(result["latest_version"], result.get("download_url", ""))

    def _cache_valid(self) -> bool:
        """Return True if cache exists and is younger than TTL."""
        if not self._cache_path.exists():
            return False
        try:
            data = json.loads(self._cache_path.read_text())
            return time.time() - data.get("timestamp", 0) < _CACHE_TTL
        except (json.JSONDecodeError, OSError, KeyError, TypeError, ValueError):
            return False

    def _do_check(self) -> dict | None:
        """Fetch latest release from GitHub API.

        Note: GitHub API is blocked in China (GFW). This will timeout
        silently — update checking is non-critical.
        """
        try:
            import urllib.request
            from ..utils.proxied_urllib import proxied_urlopen

            req = urllib.request.Request(
                self._api_url,
                headers={"User-Agent": f"MuraveyChina/{__version__}"},
            )
            # Short timeout: don't block startup behind GFW
            with proxied_urlopen(req, timeout=_API_TIMEOUT) as resp:
                data = json.loads(resp.read())

            tag = data.get("tag_name", "").lstrip("v")
            if not tag:
                return None

            latest = Version(tag)
            update_available = latest > self._current

            # Find platform-specific asset
            download_url = self._find_asset_url(data.get("assets", []))

            result = {
                "current_version": str(self._current),
                "latest_version": tag,
                "update_available": update_available,
                "download_url": download_url,
                "release_url": data.get("html_url", ""),
                "timestamp": time.time(),
            }

            # Save cache
            try:
                self._cache_path.parent.mkdir(parents=True, exist_ok=True)
                self._cache_path.write_text(json.dumps(result))
            except Exception as exc:
                log.debug("Failed to write update cache: %s", exc)

            if update_available:
                log.info("Update available: v%s → v%s", self._current, tag)
            else:
                log.debug("No update (current: v%s, latest: v%s)", self._current, tag)

            return result

        except Exception as exc:
            log.warning("Update check failed: %s", exc)
            return None

    def _find_asset_url(self, assets: list[dict]) -> str:
        """Find the download URL for the current platform."""
        if sys.platform == "win32":
            suffixes = (".exe", "-windows.zip", "-win.zip")
        elif sys.platform == "darwin":
            suffixes = (".dmg", "-mac.zip", "-macos.zip")
        else:
            suffixes = (".tar.gz", ".zip")

        for asset in assets:
            name = asset.get("name", "").lower()
            if any(name.endswith(s) for s in suffixes):
                return asset.get("browser_download_url", "")

        # Fallback: return the first asset
        return assets[0].get("browser_download_url", "") if assets else ""


def download_and_install(
    download_url: str, on_progress: Callable[[int], None] | None = None
) -> bool:
    """Download the update and launch the installer.

    Args:
        download_url: Direct download URL for the release asset.
        on_progress: Called with percentage (0-100) during download.

    Returns:
        True if installer was launched, False on error.
    """
    if not download_url:
        log.error("No download URL provided")
        return False

    suffix = Path(download_url).suffix or ".bin"
    try:
        with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
            tmp_path = Path(tmp.name)

        log.info("Downloading update from %s → %s", download_url, tmp_path)

        import urllib.request
        from src.utils.proxied_urllib import proxied_urlopen

        req = urllib.request.Request(
            download_url,
            headers={"User-Agent": f"MuraveyChina/{__version__}"},
        )
        with proxied_urlopen(req, timeout=300) as resp:
            total = int(resp.headers.get("Content-Length", 0))
            downloaded = 0
            chunk_size = 65536

            with open(tmp_path, "wb") as f:
                while True:
                    chunk = resp.read(chunk_size)
                    if not chunk:
                        break
                    f.write(chunk)
                    downloaded += len(chunk)
                    if on_progress and total:
                        on_progress(int(downloaded * 100 / total))

        log.info("Download complete: %s (%.1f MB)", tmp_path, tmp_path.stat().st_size / 1e6)

        # Launch installer / open DMG
        if sys.platform == "win32":
            subprocess.Popen([str(tmp_path)], shell=False)
        elif sys.platform == "darwin":
            subprocess.Popen(["open", str(tmp_path)])
        else:
            subprocess.Popen(["xdg-open", str(tmp_path)])

        return True

    except Exception as exc:
        log.error("Download/install failed: %s", exc)
        return False
