# https://t.me/ZGQinc

from datetime import datetime, UTC
import os
import json
import time
import requests
import sys
import csv
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm

MAX_WORKERS = 16
MAX_RETRIES = 1000
SAVE_INTERVAL = 50
OUTPUT_DIR = "output"
os.makedirs(OUTPUT_DIR, exist_ok=True)

LOG_FILE = os.path.join(OUTPUT_DIR, "log.txt")
ALL_GAMES_RAW = os.path.join(OUTPUT_DIR, "all_games_list.json")
OUTPUT_JSON = os.path.join(OUTPUT_DIR, "all_games.json")
ALL_GAMES_CSV = os.path.join(OUTPUT_DIR, "all_games.csv")
COMPLETE_MD5_TXT = os.path.join(OUTPUT_DIR, "md5.txt")
SORT_BY_SIZE = os.path.join(OUTPUT_DIR, "sorted_by_size.json")
SORT_BY_TITLE = os.path.join(OUTPUT_DIR, "sorted_by_title.json")
SUMMARY_JSON = os.path.join(OUTPUT_DIR, "game_size_summary.json")
SUMMARY_CSV = os.path.join(OUTPUT_DIR, "game_size_summary.csv")
MAGNET_TXT = os.path.join(OUTPUT_DIR, "magnet_links.txt")

file_lock = threading.Lock()

def log(msg):
    with file_lock:
        with open(LOG_FILE, "a", encoding="utf-8") as f:
            f.write(msg + "\n")
        print(msg)

def complete_md5():
    url = "https://gog-games.to/api/web/complete-md5"
    try:
        resp = requests.get(url, timeout=30)
        resp.raise_for_status()
        with open(COMPLETE_MD5_TXT, "w", encoding="utf-8") as f:
            f.write(resp.text)
    except Exception as e:
        log(f"[错误] 获取完整MD5失败: {e}")

def fetch_game_list():
    url = "https://gog-games.to/api/web/all-games"
    try:
        resp = requests.get(url, timeout=15)
        resp.raise_for_status()
        data = resp.json()
        with open(ALL_GAMES_RAW, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        return data
    except Exception as e:
        log(f"[错误] 获取游戏列表失败: {e}")
        return []

def fetch_game_details(game_id):
    url = f"https://gog-games.to/api/web/query-game/{game_id}"
    attempt = 0
    
    while attempt < MAX_RETRIES:
        try:
            resp = requests.get(url, timeout=15)
            if resp.status_code == 429:
                log(f"[429] Too Many Requests，休息 10 秒... (ID: {game_id})")
                time.sleep(10)
                continue
                
            resp.raise_for_status()
            return resp.json()
            
        except Exception as e:
            attempt += 1
            log(f"[警告] 获取游戏详情失败 ID: {game_id} (第 {attempt}/{MAX_RETRIES} 次): {e}")
            time.sleep(2)
            
    log(f"[彻底失败] 超过最大重试次数 ID: {game_id}")
    return None

def process_single_game(game, existing_map, existing_index):
    game_id = game["id"]
    new_update_time = game.get("last_update")
    old_update_time = existing_map.get(game_id)
    old_game = existing_index.get(game_id)

    if new_update_time is None:
        if old_game:
            return old_game
    elif old_update_time == new_update_time and old_game:
        return old_game

    detail = fetch_game_details(game_id)
    if not detail:
        return None

    game_info = detail.get("game_info", {})
    files = detail.get("files", [])
    for f in files:
        f["size_mib"] = round(f["size"] / 1048576, 2)
    game_info["files"] = files
    return game_info

def generate_csv_from_all_games():
    if not os.path.exists(OUTPUT_JSON):
        print(f"[警告] {OUTPUT_JSON} 不存在，无法生成 CSV。")
        return

    try:
        with open(OUTPUT_JSON, "r", encoding="utf-8") as f:
            games = json.load(f)
    except Exception as e:
        print(f"[错误] 读取 JSON 失败: {e}")
        return

    rows = []
    for game in games:
        timestamp = game.get("release_timestamp")
        if isinstance(timestamp, (int, float)):
            release_date = datetime.fromtimestamp(timestamp, UTC).strftime("%Y-%m-%d")
        else:
            release_date = ""

        base_info = {
            "game_id": game.get("id", ""),
            "slug": game.get("slug", ""),
            "title": game.get("title", ""),
            "developer": game.get("developer", ""),
            "publisher": game.get("publisher", ""),
            "rating": game.get("rating", ""),
            "popularity_ranking": game.get("popularity_ranking", ""),
            "age_rating_18": game.get("age_rating_18", False),
            "release_date": release_date,
            "is_indev": game.get("is_indev", False),
            "last_update": game.get("last_update", ""),
            "gog_url": game.get("gog_url", ""),
            "genres": ", ".join(game["genres"]) if isinstance(game.get("genres"), list) else "",
            "tags": ", ".join(game["tags"]) if isinstance(game.get("tags"), list) else "",
            "image": game.get("image", ""),
            "background": game.get("background", ""),
            "infohash": game.get("infohash", "")
        }

        files = game.get("files", [])
        for f in files:
            rows.append([
                base_info["game_id"],
                base_info["slug"],
                base_info["title"],
                base_info["developer"],
                base_info["publisher"],
                base_info["rating"],
                base_info["popularity_ranking"],
                base_info["age_rating_18"],
                base_info["release_date"],
                base_info["is_indev"],
                base_info["last_update"],
                base_info["gog_url"],
                base_info["genres"],
                base_info["tags"],
                base_info["image"],
                base_info["background"],
                base_info["infohash"],
                f.get("type", ""),
                f.get("name", ""),
                f.get("size_mib", 0)
            ])
            
    with open(ALL_GAMES_CSV, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow([
            "game_id", "slug", "title", "developer", "publisher",
            "rating", "popularity_ranking", "age_rating_18", "release_date",
            "is_indev", "last_update", "gog_url",
            "genres", "tags", "image", "background", "infohash",
            "file_type", "file_name", "size_mib"
        ])
        writer.writerows(rows)

    print(f"[完成] 已生成 CSV：{ALL_GAMES_CSV}")

def fetch_all_games():
    complete_md5()

    existing_data = []
    existing_map = {}
    if os.path.exists(OUTPUT_JSON):
        try:
            with open(OUTPUT_JSON, "r", encoding="utf-8") as f:
                existing_data = json.load(f)
                for g in existing_data:
                    existing_map[g["id"]] = g.get("last_update", "")
        except Exception as e:
            log(f"[错误] 加载已存在JSON失败: {e}")

    all_games = fetch_game_list()
    if not all_games:
        log("[终止] 数据库为空。")
        return

    log(f"[开始] 共 {len(all_games)} 个游戏，已抓取 {len(existing_map)} 个。")

    updated_data = []
    existing_index = {g["id"]: g for g in existing_data}
    save_counter = 0

    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        future_to_game = {
            executor.submit(process_single_game, game, existing_map, existing_index): game 
            for game in all_games
        }

        for future in tqdm(as_completed(future_to_game), total=len(all_games), desc="Checking updates", unit="game"):
            result = future.result()
            if result:
                with file_lock:
                    updated_data.append(result)
                    save_counter += 1
                    
                    if save_counter >= SAVE_INTERVAL:
                        with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
                            json.dump(updated_data, f, ensure_ascii=False, indent=2)
                        save_counter = 0

    with file_lock:
        with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
            json.dump(updated_data, f, ensure_ascii=False, indent=2)

    log(f"[完成] 所有数据已保存至 {OUTPUT_JSON}")
    generate_csv_from_all_games()

def load_data():
    if not os.path.exists(OUTPUT_JSON):
        print(f"[错误] 找不到文件: {OUTPUT_JSON}")
        return []
    with open(OUTPUT_JSON, "r", encoding="utf-8") as f:
        return json.load(f)

def total_file_size(game):
    return sum(f.get("size_mib", 0) for f in game.get("files", []))

def export_magnet(games):
    with open(MAGNET_TXT, "w", encoding="utf-8") as out:
        for game in games:
            if not game.get("infohash"):
                continue
            size = total_file_size(game)
            title = game.get("title", "unknown").replace(" ", "_")
            gid = game.get("id")
            name = f"{gid}_{title}_{size}MiB"
            magnet = f"magnet:?xt=urn:btih:{game['infohash']}&dn={name}"
            out.write(magnet + "\n")
    print(f"[输出] 已生成磁力链接文件：{MAGNET_TXT}")

def sort_menu():
    games = load_data()
    if not games:
        return

    print("\n--- 排序与导出 ---")
    print("1. 按文件总大小排序并导出JSON")
    print("2. 按标题排序并导出JSON")
    print("3. 导出磁力链接")
    print("0. 返回")

    choice = input("请输入序号: ").strip()

    if choice == "1":
        games.sort(key=total_file_size, reverse=True)
        with open(SORT_BY_SIZE, "w", encoding="utf-8") as f:
            json.dump(games, f, ensure_ascii=False, indent=2)
        print(f"[完成] 已按大小排序并导出：{SORT_BY_SIZE}")

    elif choice == "2":
        games.sort(key=lambda x: x.get("title", "").lower())
        with open(SORT_BY_TITLE, "w", encoding="utf-8") as f:
            json.dump(games, f, ensure_ascii=False, indent=2)
        print(f"[完成] 已按标题排序并导出：{SORT_BY_TITLE}")

    elif choice == "3":
        games.sort(key=total_file_size, reverse=True)
        export_magnet(games)

def generate_summary():
    games = load_data()
    if not games:
        return

    result = []
    total_site_size = 0.0
    total_file_count = 0

    for game in games:
        files = game.get("files", [])
        file_count = len(files)
        total_size = sum(f.get("size_mib", 0) for f in files)
        total_site_size += total_size
        total_file_count += file_count
        result.append({
            "id": game.get("id"),
            "title": game.get("title"),
            "file_count": file_count,
            "total_size_mib": round(total_size, 2)
        })

    summary = {
        "total_game_count": len(games),
        "total_file_count": total_file_count,
        "total_site_size_mib": round(total_site_size, 2),
        "total_site_size_tib": round(total_site_size / 1048576, 4),
        "games": sorted(result, key=lambda g: g["total_size_mib"], reverse=True)
    }

    with open(SUMMARY_JSON, "w", encoding="utf-8") as f:
        json.dump(summary, f, ensure_ascii=False, indent=2)
    print(f"[完成] 游戏大小摘要已保存为JSON：{SUMMARY_JSON}")

    with open(SUMMARY_CSV, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(["total_game_count", summary["total_game_count"]])
        writer.writerow(["total_file_count", summary["total_file_count"]])
        writer.writerow(["total_site_size_mib", summary["total_site_size_mib"]])
        writer.writerow(["total_site_size_tib", summary["total_site_size_tib"]])
        writer.writerow([])
        writer.writerow(["id", "title", "file_count", "total_size_mib"])
        for game in summary["games"]:
            writer.writerow([
                game["id"],
                game["title"],
                game["file_count"],
                game["total_size_mib"]
            ])
    print(f"[完成] 游戏大小摘要已保存为CSV：{SUMMARY_CSV}")

def main_menu():
    while True:
        print("\n--- gog-games.to 数据库抓取 ---")
        print("1. 抓取全部游戏数据")
        print("2. 排序与导出")
        print("3. 生成大小摘要")
        print("0. 退出")

        choice = input("请输入序号: ").strip()
        if choice == "1":
            fetch_all_games()
        elif choice == "2":
            sort_menu()
        elif choice == "3":
            generate_summary()
        elif choice == "0":
            print("已退出。")
            break
        else:
            print("[错误] 无效输入，请重试。")

if __name__ == "__main__":
    main_menu()