"""
OpenAver Windows 打包腳本
在 WSL/Linux 環境下打包出 Windows 可用的 ZIP

使用方式：
    python build.py

原理：
    1. 下載 Windows 嵌入式 Python
    2. 用 pip download 下載 Windows wheel 檔案
    3. 解壓 wheel 到 site-packages
    4. 打包成 ZIP
"""
import os
import sys
import shutil
import zipfile
import urllib.request
import subprocess
import tempfile
from pathlib import Path

# ============ 配置 ============

PYTHON_VERSION = "3.12.4"
PYTHON_EMBED_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip"

# 專案結構
PROJECT_ROOT = Path(__file__).parent
BUILD_DIR = PROJECT_ROOT / "build"
DIST_DIR = PROJECT_ROOT / "dist"
CACHE_DIR = PROJECT_ROOT / ".build_cache"  # 緩存目錄（不會被清理）

# 需要複製的專案目錄/檔案
COPY_ITEMS = [
    "web",
    "core",
    "windows",
    "locales",
    "maker_mapping.json",
]

# 主要套件（會自動解析依賴）
PACKAGES = [
    "fastapi",
    "uvicorn[standard]",
    "jinja2",
    "python-multipart",
    "requests",
    "beautifulsoup4",
    "lxml",
    "curl_cffi",
    "websockets",
    "pillow",
    "pywebview",
    "httpx",  # Required for FastAPI TestClient and async HTTP
]

# 打包時排除的套件（測試/開發工具，不影響運行）
EXCLUDE_PACKAGES = {
    # 測試工具
    'pytest', 'pytest-asyncio', 'pytest-mock', 'pytest-cov',
    'pytest-playwright',
    'coverage', 'pluggy', 'iniconfig',
    'playwright',

    # 類型檢查工具
    'mypy', 'mypy-extensions', 'typed-ast', 'types-requests',
    'types-beautifulsoup4', 'types-html5lib',

    # 開發工具
    'pip', 'setuptools', 'wheel', 'twine', 'build',

    # 文檔工具
    'docutils', 'pygments', 'readme-renderer',

    # 未使用
    'langdetect',
}


# ============ 工具函數 ============

def download_file(url: str, dest: Path) -> None:
    """下載檔案"""
    print(f"  下載: {url}")
    urllib.request.urlretrieve(url, dest)
    print(f"  完成: {dest.name}")


def extract_zip(zip_path: Path, dest_dir: Path) -> None:
    """解壓 ZIP"""
    print(f"  解壓: {zip_path.name}")
    with zipfile.ZipFile(zip_path, 'r') as zf:
        zf.extractall(dest_dir)


def extract_wheel(wheel_path: Path, dest_dir: Path) -> None:
    """解壓 wheel 檔案到目標目錄"""
    with zipfile.ZipFile(wheel_path, 'r') as zf:
        for member in zf.namelist():
            # 跳過 .dist-info 以外的 metadata
            if member.endswith('/'):
                continue
            # 解壓到目標目錄
            zf.extract(member, dest_dir)


def extract_tar_gz(tar_path: Path, dest_dir: Path) -> None:
    """解壓 tar.gz 原始碼套件到目標目錄（只複製 Python 模組）"""
    import tarfile
    with tarfile.open(tar_path, 'r:gz') as tf:
        # 找出套件目錄（通常是 package_name-version/package_name/）
        members = tf.getnames()
        # 找出實際的 Python 套件目錄
        for member in members:
            parts = member.split('/')
            if len(parts) >= 2 and not parts[1].endswith('.egg-info') and not parts[1].startswith('.'):
                # 可能是套件目錄
                pkg_name = parts[1]
                if any(m.startswith(f"{parts[0]}/{pkg_name}/") and m.endswith('.py') for m in members):
                    # 確認是 Python 套件
                    pkg_prefix = f"{parts[0]}/{pkg_name}/"
                    for m in tf.getmembers():
                        if m.name.startswith(pkg_prefix):
                            # 調整路徑：移除頂層目錄
                            m.name = m.name[len(parts[0]) + 1:]
                            tf.extract(m, dest_dir)
                    return


# ============ 打包步驟 ============

def clean_build():
    """清理舊的建置目錄"""
    print("\n[1/6] 清理舊建置...")
    if BUILD_DIR.exists():
        shutil.rmtree(BUILD_DIR)
    BUILD_DIR.mkdir(parents=True)
    # 清 source __pycache__ 避免 stale .pyc：若 Edit 同字數同秒寫入，Python
    # import 時 .pyc header (mtime+size) 與 .py 完全 match 會直接信任 cache，
    # 導致 build.py `from core.version import VERSION` 拿到舊版號當 zip filename
    # （內容是新的，只是檔名錯）。一次曾遇 0.8.8→0.8.9 同字數翻車。
    for pycache in PROJECT_ROOT.rglob("__pycache__"):
        # 只清 source 目錄下的 __pycache__，跳 venv / build / dist / node_modules
        rel = pycache.relative_to(PROJECT_ROOT)
        if rel.parts[0] in ("venv", "build", "dist", "node_modules", ".git"):
            continue
        shutil.rmtree(pycache, ignore_errors=True)


def download_embedded_python():
    """下載嵌入式 Python（使用緩存）"""
    print("\n[2/6] 準備嵌入式 Python...")

    python_dir = BUILD_DIR / "OpenAver" / "python"
    python_dir.mkdir(parents=True)

    # 檢查緩存
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    cached_zip = CACHE_DIR / f"python-{PYTHON_VERSION}-embed-amd64.zip"

    if cached_zip.exists():
        print(f"  使用緩存: {cached_zip.name}")
    else:
        print("  下載中（首次會較慢）...")
        download_file(PYTHON_EMBED_URL, cached_zip)

    # 從緩存解壓
    extract_zip(cached_zip, python_dir)

    # 修改 _pth 檔案以啟用 site-packages
    pth_files = list(python_dir.glob("python*._pth"))
    if not pth_files:
        raise RuntimeError("找不到 ._pth 檔案")

    pth_file = pth_files[0]
    pth_name = pth_file.stem  # e.g., "python312"

    pth_content = f"""{pth_name}.zip
.
Lib/site-packages
../app
import site
"""
    pth_file.write_text(pth_content)
    print(f"  已修改: {pth_file.name}")

    return python_dir


def get_all_dependencies():
    """從現有 venv 獲取完整依賴列表（排除測試/開發工具）"""
    result = subprocess.run(
        [sys.executable, "-m", "pip", "freeze"],
        capture_output=True, text=True
    )
    deps = []
    excluded = []
    for line in result.stdout.strip().split('\n'):
        if '==' in line:
            pkg_name = line.split('==')[0].strip()
            # 標準化套件名稱（pip 用 - 和 _ 可互換）
            pkg_normalized = pkg_name.lower().replace('_', '-')
            if pkg_normalized in EXCLUDE_PACKAGES:
                excluded.append(pkg_name)
            else:
                deps.append(line.strip())

    if excluded:
        print(f"  排除 {len(excluded)} 個測試/開發套件: {', '.join(excluded[:5])}{'...' if len(excluded) > 5 else ''}")

    return deps


def download_and_install_packages(python_dir: Path):
    """下載 Windows wheel 並解壓到 site-packages（使用緩存）"""
    print("\n[3/6] 準備依賴套件...")

    site_packages = python_dir / "Lib" / "site-packages"
    site_packages.mkdir(parents=True, exist_ok=True)

    # 使用緩存目錄存放 wheel
    wheels_dir = CACHE_DIR / "wheels"
    wheels_dir.mkdir(parents=True, exist_ok=True)

    # 從現有 venv 獲取所有已安裝的套件（帶版本號，如 "pydantic==2.11.0"）
    all_deps = get_all_dependencies()
    # 加入 pywebview（可能不在 venv 中）
    dep_names = [d.split('==')[0].lower() for d in all_deps]
    if "pywebview" not in dep_names:
        all_deps.append("pywebview")

    # 額外依賴（手動補充 Windows 專用套件）
    extra_deps = [
        "bottle", "proxy-tools", "clr_loader", "pythonnet",
        "win32-setctime", "colorama",
    ]
    all_deps.extend(extra_deps)

    # 建立需要的套件版本映射（套件名 → 完整 spec）
    needed = {}
    for dep in all_deps:
        name = dep.split('==')[0].lower().replace('_', '-')
        needed[name] = dep

    # ⚠️ 清掉 cache 中版本不匹配的舊 wheel（避免混版）
    # 這段不是多餘邏輯！PyPI 上游版本更新時，cache 裡的舊 pydantic-core
    # 會跟新 pydantic 混版，導致 Windows ZIP 執行時 SystemError。
    # 詳見 CLAUDE.md「Windows 打包 Wheel Cache Gotcha」
    stale_count = 0
    for f in list(wheels_dir.glob("*.*")):
        cached_name = f.stem.split('-')[0].lower().replace('_', '-')
        if cached_name in needed and '==' in needed[cached_name]:
            expected_ver = needed[cached_name].split('==')[1]
            # wheel 檔名格式: name-version-...
            parts = f.stem.split('-')
            if len(parts) >= 2 and parts[1] != expected_ver:
                f.unlink()
                stale_count += 1
    if stale_count:
        print(f"  清除 {stale_count} 個版本不匹配的舊 wheel")

    # 檢查已緩存的套件（名稱級，舊版已清除所以不會混版）
    cached_files = set(f.stem.split('-')[0].lower().replace('_', '-') for f in wheels_dir.glob("*.*"))
    to_download = [dep for dep in all_deps if dep.split('==')[0].lower().replace('_', '-') not in cached_files]

    if to_download:
        print(f"  需下載 {len(to_download)} 個新套件（已緩存 {len(all_deps) - len(to_download)} 個）")
        for pkg in to_download:
            # 嘗試下載 Windows wheel
            pip_cmd = [
                sys.executable, "-m", "pip", "download",
                "--dest", str(wheels_dir),
                "--platform", "win_amd64",
                "--python-version", "3.12",
                "--only-binary", ":all:",
                "--no-deps",
                pkg,
            ]
            result = subprocess.run(pip_cmd, capture_output=True, text=True)
            if result.returncode != 0:
                # 嘗試不限平台（純 Python 套件）
                pip_cmd = [
                    sys.executable, "-m", "pip", "download",
                    "--dest", str(wheels_dir),
                    "--no-deps",
                    pkg,
                ]
                subprocess.run(pip_cmd, capture_output=True, text=True)
    else:
        print(f"  全部 {len(all_deps)} 個套件已緩存")

    # 解壓所有套件到 site-packages
    print("\n[4/6] 安裝套件到 site-packages...")
    wheel_files = list(wheels_dir.glob("*.whl"))
    tar_files = list(wheels_dir.glob("*.tar.gz"))
    print(f"  找到 {len(wheel_files)} 個 wheel, {len(tar_files)} 個 tar.gz")

    for wheel_file in wheel_files:
        pkg_name = wheel_file.stem.split('-')[0].lower().replace('_', '-')
        if pkg_name in EXCLUDE_PACKAGES:
            print(f"  跳過（排除）: {wheel_file.name}")
            continue
        print(f"  安裝: {wheel_file.name}")
        extract_wheel(wheel_file, site_packages)

    for tar_file in tar_files:
        pkg_name = tar_file.name.split('-')[0].lower().replace('_', '-')
        if pkg_name in EXCLUDE_PACKAGES:
            print(f"  跳過（排除）: {tar_file.name}")
            continue
        print(f"  安裝: {tar_file.name}")
        extract_tar_gz(tar_file, site_packages)

    # 保留緩存（不清理 wheels_dir）


def copy_project_files():
    """複製專案檔案"""
    print("\n[5/6] 複製專案檔案...")

    app_dir = BUILD_DIR / "OpenAver" / "app"
    app_dir.mkdir(parents=True)

    for item in COPY_ITEMS:
        src = PROJECT_ROOT / item
        dst = app_dir / item

        if src.is_dir():
            print(f"  複製目錄: {item}")
            shutil.copytree(src, dst, ignore=shutil.ignore_patterns(
                "__pycache__", "*.pyc", ".git", ".gitignore", "config.json"
            ))
        elif src.is_file():
            print(f"  複製檔案: {item}")
            shutil.copy2(src, dst)

    # 複製 config.default.json（預設設定範本）
    # 注意：不複製 config.json，讓目標環境保留自己的設定
    config_default_src = PROJECT_ROOT / "web" / "config.default.json"
    config_default_dst = app_dir / "web" / "config.default.json"
    if config_default_src.exists():
        shutil.copy2(config_default_src, config_default_dst)
        print("  複製檔案: config.default.json")

    # 複製範例檔案到根目錄（讓用戶容易找到）
    samples_src = PROJECT_ROOT / "tests" / "samples" / "basic"
    samples_dst = BUILD_DIR / "OpenAver" / "教學檔案"
    if samples_src.exists():
        shutil.copytree(samples_src, samples_dst)
        print("  複製目錄: 教學檔案 (11 個範例)")


def create_launcher_scripts():
    """建立啟動腳本和說明檔（純英文版本，避免 Big5 編碼地雷字問題）"""
    print("  建立啟動腳本...")

    root_dir = BUILD_DIR / "OpenAver"

    # OpenAver.bat - 正常啟動（顯示啟動提示）
    bat_content = '''@echo off
cd /d "%~dp0"
set PYTHONUTF8=1
echo ==============================
echo    OpenAver Starting...
echo ==============================
echo.
start "" "python\\pythonw.exe" "app\\windows\\standalone.py"
ping -n 2 127.0.0.1 >nul
'''

    # OpenAver_Debug.bat - 偵錯模式（顯示控制台）
    # 使用純英文避免 Big5 地雷字問題（誌、誤、訊、回、將、以、下、置、上 等字會導致亂碼）
    debug_bat_content = '''@echo off
echo ======================================
echo    OpenAver Debug Mode
echo ======================================
echo.

REM Force UTF-8 encoding and detailed error output
set PYTHONUTF8=1
set PYTHONUNBUFFERED=1
set PYWEBVIEW_LOG=debug

echo [INFO] Starting OpenAver (Debug Mode)...
echo [INFO] Log location: %USERPROFILE%\\OpenAver\\logs\\debug.log
echo.

cd /d "%~dp0"
"python\\python.exe" "app\\windows\\standalone.py"

if errorlevel 1 (
    echo.
    echo ======================================
    echo [ERROR] Startup failed!
    echo ======================================
    echo.
    echo Please report to GitHub Issues:
    echo 1. Error messages above
    echo 2. Log file: %USERPROFILE%\\OpenAver\\logs\\debug.log
    echo.
)

pause
'''

    readme_en = '''===============================================
  OpenAver Windows Setup Guide
===============================================

Option 1: One-Line Install (Recommended)

  Open PowerShell (search "PowerShell" -> Enter), paste:

  irm https://raw.githubusercontent.com/slive777/OpenAver/main/install.ps1 | iex

  After install, double-click the OpenAver shortcut on your desktop.

===============================================

Option 2: Manual Install

  !! IMPORTANT: Use 7-Zip or WinRAR to extract !!
  Windows built-in extraction keeps "Mark of the Web",
  which blocks the app from running.

[Step 1] Extract ZIP
  - Right-click ZIP -> "Extract All..." (use 7-Zip or WinRAR for best results)

[Step 2] Open Command Prompt in the folder
  - Shift + right-click inside the OpenAver folder
  - Select "Open PowerShell window here" or "Open command window here"

[Step 3] Launch
  OpenAver.bat

  If you already extracted with Windows built-in:
  Right-click the ZIP -> Properties -> check "Unblock" -> OK,
  then re-extract.

[Requirements]
- Windows 10/11 64-bit
- Microsoft Edge WebView2 Runtime
  https://go.microsoft.com/fwlink/p/?LinkId=2124703
- Internet connection (required on first run to fetch metadata)

[Upgrading]
- Delete %USERPROFILE%\\OpenAver\\python\\ before extracting a new version
- Your settings and logs are preserved automatically

[Startup Scripts]

OpenAver.bat       — Normal launch (runs in background)
OpenAver_Debug.bat — Debug mode, logs to Command Prompt and %USERPROFILE%\\OpenAver\\logs\\debug.log

[Troubleshooting]

If the app won't start, use OpenAver_Debug.bat:
  1. Double-click OpenAver_Debug.bat
  2. A Command Prompt window shows live logs; also saved to %USERPROFILE%\\OpenAver\\logs\\debug.log
  3. Attach the log content to your GitHub Issue

[Notes]
- First launch may take a moment
- Config: app\\web\\config.json
  (Full path example: C:\\Users\\YourName\\OpenAver\\app\\web\\config.json)
- Logs: %USERPROFILE%\\OpenAver\\logs\\debug.log

[Report Issues]
GitHub: https://github.com/slive777/OpenAver/issues
Telegram: https://t.me/+J-U2l96gv0FjZTBl
'''

    readme_zh = '''===============================================
  OpenAver Windows 安裝指南
===============================================

方法一：一行指令安裝（推薦）

  打開 PowerShell（搜尋 PowerShell → Enter），貼上：

  irm https://raw.githubusercontent.com/slive777/OpenAver/main/install.ps1 | iex

  安裝完成後雙擊桌面上的 OpenAver 捷徑啟動。

===============================================

方法二：手動安裝

  !! 重要：請使用 7-Zip 或 WinRAR 解壓 !!
  Windows 內建解壓縮會保留「網路標記」(Mark of the Web)，
  導致程式無法正常執行。

[步驟 1] 解壓 ZIP
  - 對 ZIP 按右鍵 → 「解壓縮全部...」（建議使用 7-Zip 或 WinRAR）

[步驟 2] 在資料夾中開啟命令提示字元
  - Shift + 右鍵點擊 OpenAver 資料夾內部空白處
  - 選擇「在此開啟 PowerShell 視窗」或「在此開啟命令視窗」

[步驟 3] 啟動程式
  OpenAver.bat

  如果已經用內建解壓縮：
  對 ZIP 按右鍵 → 內容 → 勾選「解除封鎖」→ 確定，
  再重新解壓。

[系統需求]
- Windows 10/11 64-bit
- Microsoft Edge WebView2 Runtime
  https://go.microsoft.com/fwlink/p/?LinkId=2124703
- 網路連線（首次執行需連線外部服務取得資料）

[升級注意]
- 升級前請先刪除 %USERPROFILE%\\OpenAver\\python\\ 資料夾
- 設定檔和記錄檔會自動保留

[啟動腳本說明]

OpenAver.bat       — 正常啟動（程式在背景運行）
OpenAver_Debug.bat — 調試模式，命令提示字元顯示完整日誌，同時輸出到 %USERPROFILE%\\OpenAver\\logs\\debug.log

[故障排除]

如果程式無法啟動，請使用 OpenAver_Debug.bat 查看詳細日誌：
  1. 雙擊 OpenAver_Debug.bat
  2. 命令提示字元視窗會即時顯示日誌，同時寫入 %USERPROFILE%\\OpenAver\\logs\\debug.log
  3. 將日誌內容附加到 GitHub Issue

[注意事項]
- 首次啟動可能較慢
- 設定檔：app\\web\\config.json
  （完整路徑範例：C:\\Users\\你的帳號\\OpenAver\\app\\web\\config.json）
- 記錄檔：%USERPROFILE%\\OpenAver\\logs\\debug.log

[回報問題]
GitHub: https://github.com/slive777/OpenAver/issues
Telegram: https://t.me/+J-U2l96gv0FjZTBl
'''

    (root_dir / "OpenAver.bat").write_text(bat_content, encoding='ascii')
    (root_dir / "OpenAver_Debug.bat").write_text(debug_bat_content, encoding='ascii')
    (root_dir / "README.txt").write_text(readme_en, encoding='utf-8')
    (root_dir / "README_zh.txt").write_text(readme_zh, encoding='utf-8')

    print("  Created: OpenAver.bat, OpenAver_Debug.bat, README.txt, README_zh.txt")


def get_directory_size(path):
    """計算目錄大小"""
    total = sum(f.stat().st_size for f in Path(path).rglob('*') if f.is_file())
    return total


def optimize_package():
    """優化打包體積：刪除 .dist-info、清理 __pycache__"""
    print("\n[5.5/6] 優化打包體積...")

    app_dir = BUILD_DIR / "OpenAver"
    size_before = get_directory_size(app_dir)

    # 1. 刪除 .dist-info 資料夾
    dist_info_count = 0
    dist_info_size = 0
    for dist_info in app_dir.rglob("*.dist-info"):
        if dist_info.is_dir():
            size = sum(f.stat().st_size for f in dist_info.rglob('*') if f.is_file())
            dist_info_size += size
            shutil.rmtree(dist_info)
            dist_info_count += 1

    if dist_info_count > 0:
        print(f"  刪除 {dist_info_count} 個 .dist-info 資料夾，節省 {dist_info_size / 1024 / 1024:.2f} MB")

    # 2. 清理 __pycache__ 資料夾
    pycache_count = 0
    pycache_size = 0
    for pycache in app_dir.rglob("__pycache__"):
        if pycache.is_dir():
            size = sum(f.stat().st_size for f in pycache.rglob('*') if f.is_file())
            pycache_size += size
            shutil.rmtree(pycache)
            pycache_count += 1

    if pycache_count > 0:
        print(f"  刪除 {pycache_count} 個 __pycache__ 資料夾，節省 {pycache_size / 1024 / 1024:.2f} MB")

    # 3. 刪除 .egg-info 資料夾
    egg_info_count = 0
    egg_info_size = 0
    for egg_info in app_dir.rglob("*.egg-info"):
        if egg_info.is_dir():
            size = sum(f.stat().st_size for f in egg_info.rglob('*') if f.is_file())
            egg_info_size += size
            shutil.rmtree(egg_info)
            egg_info_count += 1

    if egg_info_count > 0:
        print(f"  刪除 {egg_info_count} 個 .egg-info 資料夾，節省 {egg_info_size / 1024 / 1024:.2f} MB")

    # 統計優化結果
    size_after = get_directory_size(app_dir)
    saved = size_before - size_after
    print(f"  體積優化: {size_before / 1024 / 1024:.1f} MB → {size_after / 1024 / 1024:.1f} MB (節省 {saved / 1024 / 1024:.1f} MB)")


def create_zip_package():
    """打包成 ZIP"""
    print("\n[6/6] 打包成 ZIP...")

    # 讀取版本號
    import sys
    sys.path.insert(0, str(PROJECT_ROOT))
    from core.version import VERSION

    DIST_DIR.mkdir(parents=True, exist_ok=True)

    zip_name = f"OpenAver-v{VERSION}-Windows-x64"
    zip_path = DIST_DIR / f"{zip_name}.zip"

    # 刪除舊的 ZIP
    if zip_path.exists():
        zip_path.unlink()

    # 建立 ZIP
    shutil.make_archive(
        str(DIST_DIR / zip_name),
        'zip',
        BUILD_DIR,
        "OpenAver"
    )

    # 計算檔案大小
    size_mb = zip_path.stat().st_size / (1024 * 1024)
    print(f"  完成: {zip_path.name} ({size_mb:.1f} MB)")

    return zip_path


def main():
    """主程序"""
    print("=" * 50)
    print("OpenAver Windows 打包工具")
    print("=" * 50)

    try:
        clean_build()
        python_dir = download_embedded_python()
        download_and_install_packages(python_dir)
        copy_project_files()
        create_launcher_scripts()
        optimize_package()
        zip_path = create_zip_package()

        print("\n" + "=" * 50)
        print("打包完成！")
        print(f"輸出檔案: {zip_path}")
        print("=" * 50)

    except Exception as e:
        print(f"\n錯誤: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()
