#!/usr/bin/env python3
"""課前檢查：看這台電腦能裝哪些地端 AI，輸出建議（JSON＋白話）。只用 Python 標準函式庫。
用法: python check_machine.py [--json out.json]
Mac 已在 M1 Max 實測；Windows 分支照文件寫，尚未在 Windows 實機驗證。
"""
import json, os, platform, shutil, subprocess, sys, argparse
from pathlib import Path

GB = 1024 ** 3

def ram_gb():
    if sys.platform == 'darwin':
        return int(subprocess.run(['sysctl', '-n', 'hw.memsize'], capture_output=True, text=True).stdout) / GB
    if os.name == 'nt':
        import ctypes
        class MS(ctypes.Structure):
            _fields_ = [('dwLength', ctypes.c_ulong), ('dwMemoryLoad', ctypes.c_ulong), ('ullTotalPhys', ctypes.c_ulonglong),
                        ('ullAvailPhys', ctypes.c_ulonglong), ('ullTotalPageFile', ctypes.c_ulonglong), ('ullAvailPageFile', ctypes.c_ulonglong),
                        ('ullTotalVirtual', ctypes.c_ulonglong), ('ullAvailVirtual', ctypes.c_ulonglong), ('sullAvailExtendedVirtual', ctypes.c_ulonglong)]
        m = MS(); m.dwLength = ctypes.sizeof(MS); ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(m))
        return m.ullTotalPhys / GB
    return os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') / GB

def gpu():
    if sys.platform == 'darwin':
        chip = subprocess.run(['sysctl', '-n', 'machdep.cpu.brand_string'], capture_output=True, text=True).stdout.strip()
        return {'kind': 'apple' if platform.machine() == 'arm64' else 'intel-mac', 'name': chip, 'vram_gb': None}
    if shutil.which('nvidia-smi'):
        out = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'], capture_output=True, text=True).stdout.strip().splitlines()
        if out:
            name, mem = out[0].rsplit(',', 1)
            return {'kind': 'nvidia', 'name': name.strip(), 'vram_gb': round(int(mem) / 1024, 1)}
    return {'kind': 'none', 'name': '', 'vram_gb': None}

def main():
    ap = argparse.ArgumentParser(); ap.add_argument('--json'); a = ap.parse_args()
    home = Path.home()
    free = shutil.disk_usage(home).free / GB
    ram = ram_gb(); g = gpu()
    tools = {t: bool(shutil.which(t)) for t in ('uv', 'ffmpeg', 'ollama', 'whisper-cli', 'git', 'swift')}
    plan, notes = [], []
    # 語音：Breeze q5_k 1.1GB，跑起來約 2.1GB 記憶體
    if free >= 5:
        plan.append('speech')
        if g['kind'] == 'apple': notes.append('語音：whisper.cpp＋Metal，每分鐘音檔約 8–10 秒')
        elif g['kind'] == 'nvidia': notes.append('語音：whisper.cpp CUDA 版（Windows 實機未驗證）')
        else: notes.append('語音：沒有 GPU，改用 faster-whisper int8（CPU），每分鐘音檔約 20–45 秒，未在 Windows 驗證')
    # 文字：Qwen3.5 9B / Gemma 4 12B 約 7.6–7.7GB，執行峰值約 9.4GB（16K 上下文）
    if ram >= 15 and free >= 15:
        plan.append('text-12b')
        notes.append('文字：gemma4:12b（或 qwen3.5:9b），' + ('上下文 32K，兩小時會議可一次整理' if ram >= 30 else '上下文 16K，超過約 1 小時的會議要切段'))
    elif ram >= 7.5 and free >= 8:
        plan.append('text-small'); notes.append('文字：記憶體不足 16GB，改用小模型（qwen3.5:4b，未實測）')
    else:
        notes.append('文字：記憶體或硬碟不足，課堂上改用雲端模型示範')
    # 圖片：Z-Image Turbo q4，768 峰值 11.7GB（mflux）／7.1GB（sd.cpp）；1024 峰值 18.3GB（mflux）
    if g['kind'] == 'apple' and ram >= 15 and free >= 12:
        plan.append('image-mac'); notes.append('圖片：sd.cpp（記憶體 7GB 左右）或 mflux（11.7GB，較快）；24GB 以上才開 1024')
    elif g['kind'] == 'nvidia' and (g['vram_gb'] or 0) >= 8 and free >= 12:
        plan.append('image-win'); notes.append('圖片：sd.cpp CUDA／Vulkan 版（Windows 實機未驗證）')
    else:
        notes.append('圖片：這台跑不動地端生圖，課堂上用雲端示範')
    need = {'speech': 3, 'text-12b': 9, 'text-small': 4, 'image-mac': 8, 'image-win': 8}
    total = sum(need[p] for p in plan) + 6  # 6GB 給 uv 環境與工具
    res = {'os': f'{platform.system()} {platform.release()}', 'arch': platform.machine(), 'ram_gb': round(ram, 1),
           'free_disk_gb': round(free, 1), 'gpu': g, 'tools': tools, 'plan': plan, 'download_gb': total,
           'disk_ok': free - total >= 10, 'notes': notes}
    if a.json: json.dump(res, open(a.json, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
    print(f"這台：{res['os']}／{res['arch']}／記憶體 {res['ram_gb']}GB／硬碟剩 {res['free_disk_gb']}GB／GPU {g['kind']} {g['name']}")
    print(f"建議安裝：{', '.join(plan) or '無'}；預計下載約 {total}GB，裝完剩 {round(free - total, 1)}GB" + ('' if res['disk_ok'] else '（不足 10GB，先清空間）'))
    for n in notes: print('  - ' + n)
    print('  缺的工具：' + (', '.join(t for t, ok in tools.items() if not ok) or '無'))

if __name__ == '__main__':
    main()
