# -*- coding: utf-8 -*-
"""
仿 Cursor / VS Code 的无感增量更新模块（Tkinter 入口用）

【打包前提】采用 PyInstaller 文件夹模式 (-D)。安装目录含 ServerManager.exe 与 _internal 等依赖。
【增量范围】.patch 仅针对 ServerManager.exe：在 TEMP 合成 ServerManager_new.exe 并校验 SHA256；
           替换时只覆盖安装目录下的 ServerManager.exe，绝不修改 _internal 或其它文件。
【临时清理】替换脚本会清理 TEMP 中的 .patch 与 ServerManager_new.exe，不留下垃圾。

依赖：bsdiff4, hashlib, requests, threading, os, sys, subprocess, tkinter。
远端 version.json 支持两种键名：delta 或 delta_updates（兼容旧配置）。
"""

import hashlib
import json
import os
import subprocess
import sys
import threading
import tkinter as tk
from pathlib import Path
from tkinter import messagebox
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from urllib.parse import urlparse

try:
    import requests
    _HAS_REQUESTS = True
except ImportError:
    _HAS_REQUESTS = False

try:
    import bsdiff4
    _HAS_BSDIFF = True
except ImportError:
    _HAS_BSDIFF = False

# 远端 version.json 地址
VERSION_JSON_URL = "http://172.18.180.94:3000/api/file?path=server_manager/build/output/version.json"
REQUEST_TIMEOUT = 15
DOWNLOAD_TIMEOUT = 120


def clean_up_old_version():
    """
    启动时调用：删除安装目录（或当前 exe 所在目录）下以 .old 结尾的残留文件，
    即上一次「重命名大法」更新退下来的旧程序，静默删除不报错。
    """
    if getattr(sys, "frozen", False):
        install_dir = os.path.dirname(os.path.abspath(sys.executable))
    else:
        install_dir = os.path.dirname(os.path.abspath(__file__)) or os.getcwd()
    try:
        for name in os.listdir(install_dir) or []:
            if name.endswith(".old"):
                path = os.path.join(install_dir, name)
                if os.path.isfile(path):
                    try:
                        os.remove(path)
                    except Exception:
                        pass
    except Exception:
        pass


def _write_update_log(install_dir: str, message: str) -> None:
    """向安装目录下的 logs/update_launcher.log 写入，便于确认是否走了 mini_updater。"""
    try:
        from datetime import datetime
        log_dir = os.path.join(install_dir, "logs")
        os.makedirs(log_dir, exist_ok=True)
        log_path = os.path.join(log_dir, "update_launcher.log")
        with open(log_path, "a", encoding="utf-8") as f:
            f.write("[%s] %s\n" % (datetime.now().isoformat(), message))
    except Exception:
        pass


def apply_update_and_restart(
    new_exe_path: str,
    target_exe_name: str = None,
) -> None:
    """
    独立进程外更新：拉起 mini_updater.exe，由其等待本进程退出后替换 exe 并以正确 cwd 重启。
    若 mini_updater.exe 不存在则回退到“死循环重试”的 .bat。
    调用后本进程会立即 os._exit(0)，不会返回。
    """
    new_exe_path = os.path.abspath(new_exe_path)
    current_exe_path = os.path.abspath(sys.executable)
    if not os.path.isfile(new_exe_path):
        return
    install_dir = os.path.normpath(os.path.dirname(current_exe_path))
    target_name = (target_exe_name or os.path.basename(current_exe_path) or "ServerManager.exe").strip() or "ServerManager.exe"

    # 优先使用安装目录下的 mini_updater.exe（与 ServerManager.exe 同目录），必须用绝对路径
    mini_updater_exe = os.path.normpath(os.path.join(install_dir, "mini_updater.exe"))
    if sys.platform == "win32" and os.path.isfile(mini_updater_exe):
        try:
            pid = os.getpid()
            creationflags = 0x00000008  # DETACHED_PROCESS
            subprocess.Popen(
                [
                    mini_updater_exe,
                    "--pid", str(pid),
                    "--install-dir", install_dir,
                    "--new-exe-path", new_exe_path,
                    "--target-exe-name", target_name,
                ],
                cwd=install_dir,
                close_fds=True,
                creationflags=creationflags,
                shell=False,
            )
            _write_update_log(install_dir, "已拉起 mini_updater.exe (pid=%s)" % pid)
            os._exit(0)
            return
        except Exception as e:
            _write_update_log(install_dir, "拉起 mini_updater.exe 失败: %s，改用 .bat" % e)
    else:
        _write_update_log(
            install_dir,
            "mini_updater.exe 未找到 (路径: %s)，使用 .bat 回退" % mini_updater_exe,
        )

    # 回退：.bat 死循环重试（仅 Windows）或 shell 脚本（Unix）
    temp_dir = os.environ.get("TEMP") or os.path.expandvars("%TEMP%")
    bat_path = os.path.join(temp_dir, "apply_update_%s.bat" % os.getpid())
    bat_lines = [
        "@echo off",
        ":retry",
        'move /y "%s" "%s" >nul 2>&1' % (new_exe_path, current_exe_path),
        "if errorlevel 1 (",
        "  ping 127.0.0.1 -n 2 >nul",
        "  goto retry",
        ")",
        'cd /d "%s"' % install_dir,
        'start "" "%s"' % current_exe_path,
        'del "%~f0"',
    ]
    try:
        with open(bat_path, "w", encoding="utf-8") as f:
            f.write("\r\n".join(bat_lines))
        if sys.platform == "win32":
            subprocess.Popen(
                ["cmd", "/c", bat_path],
                creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
                shell=False,
            )
        else:
            script = (
                "while ! mv -f '%s' '%s' 2>/dev/null; do sleep 1; done; cd '%s' && exec '%s'"
                % (new_exe_path, current_exe_path, install_dir, current_exe_path)
            )
            subprocess.Popen(["sh", "-c", script], shell=False)
    except Exception:
        pass
    os._exit(0)


def _parse_version(v: str):
    """'x.y.z' -> (x, y, z)"""
    try:
        parts = v.strip().split(".")
        return (
            int(parts[0]) if len(parts) > 0 else 0,
            int(parts[1]) if len(parts) > 1 else 0,
            int(parts[2]) if len(parts) > 2 else 0,
        )
    except (ValueError, IndexError):
        return (0, 0, 0)


def _version_less(a: str, b: str) -> bool:
    """True 表示 远端 b > 本地 a"""
    return _parse_version(a) < _parse_version(b)


def _get_delta_map(manifest: dict) -> dict:
    """兼容 delta 与 delta_updates 两种键名"""
    d = manifest.get("delta") or manifest.get("delta_updates") or {}
    return d if isinstance(d, dict) else {}


class SeamlessUpdater:
    """
    隐形增量更新：后台守护线程静默准备，就绪后主线程弹一次「重启以更新」，
    用户确认后极速替换 ServerManager.exe（或静默安装完整包）并退出。
    """

    def __init__(self, root: tk.Tk, local_version: str, version_url: str = None):
        self.root = root
        self.local_version = (local_version or "").strip()
        self.version_url = (version_url or VERSION_JSON_URL).strip()
        self._ready_delta_path = None   # TEMP 下 ServerManager_new.exe 路径（增量就绪）
        self._ready_full_path = None    # TEMP 下 Setup.exe 路径（全量就绪）
        self._ready_remote_version = None
        self._lock = threading.Lock()

    def _fetch_manifest(self) -> dict:
        """请求远端 version.json，失败返回空 dict"""
        try:
            if _HAS_REQUESTS:
                r = requests.get(
                    self.version_url,
                    headers={"User-Agent": "ServerManager-Updater/1.0"},
                    timeout=REQUEST_TIMEOUT,
                )
                r.raise_for_status()
                return r.json() if isinstance(r.json(), dict) else {}
            req = Request(self.version_url, headers={"User-Agent": "ServerManager-Updater/1.0"})
            with urlopen(req, timeout=REQUEST_TIMEOUT) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                return data if isinstance(data, dict) else {}
        except (URLError, HTTPError, OSError, json.JSONDecodeError, Exception):
            return {}

    def _download_to_path(self, url: str, dest_path: str) -> bool:
        """静默下载 url 到 dest_path，无 UI。"""
        dest_path = Path(dest_path)
        try:
            dest_path.parent.mkdir(parents=True, exist_ok=True)
            if _HAS_REQUESTS:
                r = requests.get(
                    url,
                    headers={"User-Agent": "ServerManager-Updater/1.0"},
                    stream=True,
                    timeout=DOWNLOAD_TIMEOUT,
                )
                r.raise_for_status()
                with open(dest_path, "wb") as f:
                    for chunk in r.iter_content(chunk_size=65536):
                        if chunk:
                            f.write(chunk)
            else:
                req = Request(url, headers={"User-Agent": "ServerManager-Updater/1.0"})
                with urlopen(req, timeout=DOWNLOAD_TIMEOUT) as resp:
                    dest_path.write_bytes(resp.read())
            return dest_path.is_file()
        except Exception:
            return False

    def _try_delta_path(self, manifest: dict) -> bool:
        """
        若存在当前版本的 delta：在 TEMP 静默下载 .patch -> bsdiff4 合成 ServerManager_new.exe -> 校验 SHA256。
        仅针对 ServerManager.exe，不涉及 _internal。合成后删除 TEMP 中的 .patch。
        成功则设置 self._ready_delta_path 并返回 True；任何一步失败返回 False。
        """
        delta_map = _get_delta_map(manifest)
        delta = delta_map.get(self.local_version)
        if not isinstance(delta, dict):
            return False
        patch_url = (delta.get("patch_url") or "").strip()
        new_exe_sha256 = (delta.get("new_exe_sha256") or "").strip().lower()
        if not patch_url or not new_exe_sha256 or len(new_exe_sha256) != 64:
            return False
        if not _HAS_BSDIFF or not getattr(sys, "frozen", False):
            return False

        temp_dir = os.environ.get("TEMP") or os.path.expandvars("%TEMP%")
        if not temp_dir or not os.path.isdir(temp_dir):
            return False
        patch_path = os.path.join(temp_dir, "ServerManager_update.patch")
        new_exe_path = os.path.join(temp_dir, "ServerManager_new.exe")

        if not self._download_to_path(patch_url, patch_path):
            return False
        if not os.path.isfile(patch_path):
            return False

        try:
            with open(sys.executable, "rb") as f:
                old_data = f.read()
            with open(patch_path, "rb") as f:
                patch_data = f.read()
            new_data = bsdiff4.patch(old_data, patch_data)
            got_sha = hashlib.sha256(new_data).hexdigest().lower()
            if got_sha != new_exe_sha256:
                return False
            with open(new_exe_path, "wb") as f:
                f.write(new_data)
            with self._lock:
                self._ready_delta_path = new_exe_path
            return True
        except Exception:
            return False
        finally:
            try:
                if os.path.isfile(patch_path):
                    os.remove(patch_path)
            except Exception:
                pass

    def _try_full_path(self, manifest: dict) -> bool:
        """静默下载 full_installer_url 到 TEMP，成功则设置 _ready_full_path。"""
        full_url = (
            (manifest.get("full_installer_url") or manifest.get("download_url") or "").strip()
        )
        if not full_url:
            return False
        temp_dir = os.environ.get("TEMP") or os.path.expandvars("%TEMP%")
        if not temp_dir or not os.path.isdir(temp_dir):
            return False
        name = os.path.basename(urlparse(full_url).path) or "ServerManager_Setup.exe"
        local_path = os.path.join(temp_dir, name)
        if not self._download_to_path(full_url, local_path):
            return False
        if not os.path.isfile(local_path):
            return False
        with self._lock:
            self._ready_full_path = local_path
        return True

    def _worker(self):
        """后台线程：检测 -> 优先增量静默准备，失败则全量静默准备 -> 就绪后主线程弹窗"""
        manifest = self._fetch_manifest()
        if not manifest:
            return
        remote_version = (manifest.get("version") or "").strip()
        if not remote_version or not _version_less(self.local_version, remote_version):
            return

        with self._lock:
            self._ready_remote_version = remote_version
            self._ready_delta_path = None
            self._ready_full_path = None

        # 优先增量：静默下载 + 合成 + 校验
        if self._try_delta_path(manifest):
            self.root.after(0, self._show_restart_dialog)
            return
        # 兜底：静默下载完整安装包
        if self._try_full_path(manifest):
            self.root.after(0, self._show_restart_dialog)
            return

    def _show_restart_dialog(self):
        """主线程：仅当有 _ready_delta_path 或 _ready_full_path 时弹出「退出并重启」"""
        with self._lock:
            remote = self._ready_remote_version or "?"
            delta_path = self._ready_delta_path
            full_path = self._ready_full_path
        if not delta_path and not full_path:
            return
        msg = "新版本 v{} 已经准备就绪。点击「是」退出并重启以应用更新。".format(remote)
        if not messagebox.askyesno("更新就绪", msg, default="yes"):
            return
        if delta_path and os.path.isfile(delta_path):
            self._apply_delta_and_exit(delta_path)
        elif full_path and os.path.isfile(full_path):
            self._run_silent_install_and_exit(full_path)

    def _apply_delta_and_exit(self, temp_new_exe_path: str):
        """增量路径：优先调用独立进程外更新器 mini_updater.exe，否则回退到 .bat 重试。"""
        apply_update_and_restart(temp_new_exe_path, target_exe_name=os.path.basename(sys.executable))

    def _run_silent_install_and_exit(self, setup_exe_path: str):
        """全量路径：静默参数启动 Setup.exe，主进程立即退出"""
        try:
            subprocess.Popen(
                [
                    setup_exe_path,
                    "/VERYSILENT",
                    "/SUPPRESSMSGBOXES",
                    "/FORCECLOSEAPPLICATIONS",
                ],
                shell=False,
            )
        except Exception:
            pass
        os._exit(0)

    def start_background_worker(self):
        """启动后台守护线程（静默检测 + 静默准备），不阻塞主线程。"""
        t = threading.Thread(target=self._worker, daemon=True)
        t.start()


# ---------------------------------------------------------------------------
# entrypoints/tk_update_demo.py 调用示例（软件启动时调用）
# ---------------------------------------------------------------------------
"""
# 在 entrypoints/tk_update_demo.py 中（Tkinter 入口）：

import tkinter as tk
from scripts.update.tk_updater import SeamlessUpdater, VERSION_JSON_URL

# 从 version.json 或常量读取本地版本号
LOCAL_VERSION = "1.1.8"  # 需与 version.json / 安装包一致

def main():
    root = tk.Tk()
    root.withdraw()  # 若主界面不是 Tk，可先隐藏，仅用其 messagebox/after

    # 启动无感更新：后台静默准备，就绪后弹「重启以更新」
    SeamlessUpdater(root, LOCAL_VERSION, VERSION_JSON_URL).start_background_worker()

    # 你的主界面（Tk 或其它）
    root.deiconify()
    root.mainloop()

if __name__ == "__main__":
    main()
"""


# ---------------------------------------------------------------------------
# 开发者：如何生成 .patch 文件（发版流程）
# ---------------------------------------------------------------------------
"""
【重要】采用 PyInstaller 文件夹模式 (-D)。增量仅针对 ServerManager.exe，不包含 _internal；
       用户端替换时只覆盖 ServerManager.exe，绝不修改 _internal 或其它文件。

1. 打包：使用 PyInstaller 文件夹模式生成当前版本：
   pyinstaller -D -w -n ServerManager entrypoints/main.py
   得到 dist/ServerManager/ServerManager.exe。

2. 保存上一版本的 ServerManager.exe（仅此单文件，无需 _internal）：
   将上一发版时的 ServerManager.exe 保存为 build/output/ServerManager_1.0.2.exe（版本号与 version.json 中 delta 的键一致）。

3. 生成差分补丁（需安装 bsdiff4：pip install bsdiff4）：
   import bsdiff4
   old = open('build/output/ServerManager_1.0.2.exe', 'rb').read()
   new = open('dist/ServerManager/ServerManager.exe', 'rb').read()
   patch = bsdiff4.diff(old, new)
   open('build/output/patches/v1.0.2_to_v1.0.3.patch', 'wb').write(patch)

4. 计算新 exe 的 SHA256 并写入 version.json：
   import hashlib
   new_sha256 = hashlib.sha256(new).hexdigest().lower()
   # 将 new_sha256 填入 version.json 的 delta["1.0.2"].new_exe_sha256

5. 上传 build/output/patches/*.patch 与 version.json 到服务器；用户端即可通过增量路径静默更新。
"""
