import sys
import os
import io
import uuid
import zipfile


class CombinedVersion:
    def __init__(self, *args):
        if len(args) == 4:
            self.platform = args[0]
            self.packVersion = args[1]
            self.startVersion = args[2]
            self.endVersion = args[3]
        else:
            platform, packVersion, startVersion, endVersion = self._get_sys_args()
            self.platform = platform
            self.packVersion = packVersion
            self.startVersion = startVersion
            self.endVersion = endVersion

        self.check_pack_temp_version()
        self.check_hotfix_temp_version()
        self.get_temp_version_name()

        self.packFileListName = "FileList0.csv"
        self.luaAndConfigPackFileListName = "FileList6100000.csv"
        self.versionFileName = ("Temp" if self.UseTemphotfix else "") + "Version.csv"
        self.mainlandFileListName = "FileListCombine.csv"
        self.cmpConfig = "Config/LauncherConfig"

        self.hotfixRoots = []
        self.hotfixRoots.append("Hotfix/" + self.platform + "/")
        self.hotfixRoots.append("Hotfix/" + self.platform + "Low/")
        self.hotfixRoots.append("Hotfix/" + self.platform + "ETC2Low/")
        self.hotfixRoots.append("Hotfix/" + self.platform + "High/")
        self.hotfixRoots.append("Hotfix/" + self.platform + "ASTCMiddle/")
        self.hotfixRoots.append("Hotfix/" + self.platform + "ASTCLow/")

        self.packRoots = []
        self.packRoots.append("Pack/" + self.platform + "/PV" + self.packVersion + "/")
        self.packRoots.append("Pack/" + self.platform + "Low/PV" + self.packVersion + "/")
        self.packRoots.append("Pack/" + self.platform + "ETC2Low/PV" + self.packVersion + "/")
        self.packRoots.append("Pack/" + self.platform + "High/PV" + self.packVersion + "/")
        self.packRoots.append("Pack/" + self.platform + "ASTCMiddle/PV" + self.packVersion + "/")
        self.packRoots.append("Pack/" + self.platform + "ASTCLow/PV" + self.packVersion + "/")

        self.dstRoots = []
        self.dstRoots.append("VersionCombined/" + self.platform + "/")
        self.dstRoots.append("VersionCombined/" + self.platform + "Low/")
        self.dstRoots.append("VersionCombined/" + self.platform + "ETC2Low/")
        self.dstRoots.append("VersionCombined/" + self.platform + "High/")
        self.dstRoots.append("VersionCombined/" + self.platform + "ASTCMiddle/")
        self.dstRoots.append("VersionCombined/" + self.platform + "ASTCLow/")

        self._zip_ok_count = 0
        self._zip_fail_count = 0
        self._zip_fail_paths = []
        self._total_txt_bytes = 0
        self._total_zip_bytes = 0

    def generate(self):
        self._try_make_dirs()
        self._iterate_and_generate()
        self._print_zip_summary()
        # CI 通过 exit code 判定构建成功失败，任何 LV 失败就阻断后续 CDN 同步
        if self._zip_fail_count > 0:
            sys.exit(1)

    def check_pack_temp_version(self):
        ps = self.packVersion.split(".")
        if len(ps) == 3:
            self.TempPackVersion = 0
        else:
            self.TempPackVersion = int(ps[3])
            self.packVersion = ps[0] + "." + ps[1] + "." + ps[2] + "/Temp" + str(self.TempPackVersion)
            print("TempPackVersion: " + str(self.TempPackVersion), "packVersion: " + self.packVersion)
    
    def check_hotfix_temp_version(self):
        intVersion = int(self.endVersion)
        self.UseTemphotfix = (self.endVersion - intVersion) > 0.01
        self.endVersion = intVersion
        if self.UseTemphotfix:
            print("UseTemphotfix: " + str(self.UseTemphotfix), "endVersion: " + str(self.endVersion))

    def get_temp_version_name(self):
        if self.TempPackVersion > 0 or self.UseTemphotfix:
            self.SaveVersionName = "/Temp" + str(self.TempPackVersion) + "Version.txt"
            self.SaveVersionZipName = "/Temp" + str(self.TempPackVersion) + "Version.zip"
            print("SaveVersionName: " + self.SaveVersionName)
        else:
            self.SaveVersionName = "/Version.txt"
            self.SaveVersionZipName = "/Version.zip"

    @staticmethod
    def _get_sys_args():
        # 读取命令行
        platform = "Android"
        if len(sys.argv) > 1:
            platform = sys.argv[1]

        packVersion = "6.2.0"
        if len(sys.argv) > 2:
            packVersion = sys.argv[2]

        startVersion = 26
        if len(sys.argv) > 3:
            startVersion = int(sys.argv[3])

        endVersion = 27
        if len(sys.argv) > 4:
            endVersion = float(sys.argv[4])
        return platform, packVersion, startVersion, endVersion

    def _try_make_dirs(self):
        # 遍历dstRoots，如果不存在目录则创建
        for dstRoot in self.dstRoots:
            if not os.path.exists(dstRoot):
                os.makedirs(dstRoot)

    @staticmethod
    def _read_file_without_bom(file_path):
        content = ""
        if os.path.exists(file_path):
            # 以字符串读取文件
            with io.open(file_path, 'r', encoding="utf-8") as file:
                content = file.read()
        else:
            print("!!!!!! File not found: " + file_path)
            # return content

        # 判断是否有BOM
        if content and content[0] == '\ufeff':
            # content(有个BOM)
            content = content[1:]
        return content

    @staticmethod
    def _read_file(file_path):
        content = ""
        if os.path.exists(file_path):
            # 以字符串读取文件
            with io.open(file_path, 'r', encoding="utf-8") as file:
                content = file.read()
        return content

    def _iterate_and_generate(self):
        # 按索引遍历hotfixRoots
        for i in range(len(self.hotfixRoots)):
            hotfix_root = self.hotfixRoots[i]
            packRoot = self.packRoots[i]
            dstRoot = self.dstRoots[i]

            if not os.path.exists(hotfix_root):
                continue
            # 遍历hotfixRoot目录下的所有文件夹
            for hotfixLvDir in os.listdir(hotfix_root):
                # 如果字符串hotfixDir包含LV
                if hotfixLvDir.find("LV") != -1:
                    # 以.为分隔符，取第二位
                    strVersion = hotfixLvDir.split(".")[1]
                    # 转换为int
                    intVersion = int(strVersion)
                    if self.startVersion <= intVersion <= self.endVersion:
                        dstDir = dstRoot + hotfixLvDir
                        hotfixDir = hotfix_root + hotfixLvDir
                        if not os.path.exists(dstDir):
                            os.makedirs(dstDir)

                        # 读取Version.txt
                        versionFilePath = hotfixDir + "/" + self.versionFileName
                        # 读取FileList0.csv
                        packFilePath = packRoot + self.packFileListName
                        # 读取FileListCombine.csv文件
                        mainlandFileListPath = packRoot + self.mainlandFileListName

                        if not os.path.exists(versionFilePath) or not os.path.exists(packFilePath) or not os.path.exists(mainlandFileListPath):
                            print("Required files not found in hotfix directory: " + hotfixDir)
                            continue

                        contentVersion = self._read_file_without_bom(versionFilePath)
                        contentPack = self._read_file_without_bom(packFilePath)
                        contentMainland = self._read_file_without_bom(mainlandFileListPath)

                        # 读取Config里的LauncherConfig系列文件
                        cmpConfigPath = self.cmpConfig + ".json"
                        contentCmpConfig = self._read_file(cmpConfigPath)

                        # 旧包没有LuaAndConfig分包时不追加第5段，保持旧客户端自然兼容
                        luaAndConfigPackFilePath = packRoot + self.luaAndConfigPackFileListName
                        contentParts = [contentVersion, contentPack, contentMainland, contentCmpConfig]
                        if os.path.exists(luaAndConfigPackFilePath):
                            contentParts.append(self._read_file_without_bom(luaAndConfigPackFilePath))

                        # 拼接contentCombined
                        contentCombined = "===\n".join(contentParts)

                        # 将contentCombined写入dstFilePath
                        # newline='' 禁止 \n → \r\n 翻译，保证 Windows 本地跑出来的 txt 跟 Linux CI 一致，也跟 zip 内 entry 字节一致
                        dstFilePath = dstDir + self.SaveVersionName
                        with io.open(dstFilePath, 'w', encoding="utf-8", newline='') as file:
                            file.write(contentCombined)

                        # 同步生成同目录的 zip 包，供客户端启动时下载并解压
                        dstZipFilePath = dstDir + self.SaveVersionZipName
                        if self._write_zip_atomic(dstZipFilePath, contentCombined):
                            self._zip_ok_count += 1
                            try:
                                self._total_txt_bytes += os.path.getsize(dstFilePath)
                                self._total_zip_bytes += os.path.getsize(dstZipFilePath)
                            except Exception as e:
                                print("!!!!!! Failed to stat output sizes: " + str(e))
                        else:
                            self._zip_fail_count += 1
                            self._zip_fail_paths.append(dstZipFilePath)

    @staticmethod
    def _write_zip_atomic(zip_path, text_content):
        # 原子写入：先写带进程唯一后缀的 .tmp 再 os.replace 覆盖目标，避免半成品和并发写碰撞；
        # 失败时清理 .tmp 并删掉旧的 zip，避免 CDN 同步上"新 txt + 老 zip"造成内容错位
        tmp_path = zip_path + "." + uuid.uuid4().hex[:8] + ".tmp"
        try:
            # entry 名固定 "Version.txt"，命中客户端 ZipInputStream 的 fast-path
            with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=6) as zf:
                zf.writestr("Version.txt", text_content.encode('utf-8'))
            os.replace(tmp_path, zip_path)
            return True
        except Exception as e:
            print("!!!!!! Zip generation failed for " + zip_path + ": " + str(e))
            if os.path.exists(tmp_path):
                try:
                    os.remove(tmp_path)
                except Exception as e2:
                    print("!!!!!! Failed to remove tmp file " + tmp_path + ": " + str(e2))
            if os.path.exists(zip_path):
                try:
                    os.remove(zip_path)
                except Exception as e3:
                    print("!!!!!! Failed to remove stale zip " + zip_path + ": " + str(e3))
            return False

    def _print_zip_summary(self):
        total = self._zip_ok_count + self._zip_fail_count
        print("[Summary] Zip generated: " + str(self._zip_ok_count) + " ok / "
              + str(self._zip_fail_count) + " failed / " + str(total) + " total")
        if self._total_txt_bytes > 0:
            ratio = self._total_txt_bytes / self._total_zip_bytes if self._total_zip_bytes > 0 else 0.0
            print("[Summary] Total txt: " + self._format_size(self._total_txt_bytes)
                  + " / Total zip: " + self._format_size(self._total_zip_bytes)
                  + " / Avg ratio: " + ("%.2fx" % ratio))
        if self._zip_fail_paths:
            print("[Summary] Failed zip paths:")
            for p in self._zip_fail_paths:
                print("  - " + p)

    @staticmethod
    def _format_size(num_bytes):
        if num_bytes >= 1024 * 1024:
            return "%.2f MB" % (num_bytes / 1024.0 / 1024.0)
        if num_bytes >= 1024:
            return "%.2f KB" % (num_bytes / 1024.0)
        return str(num_bytes) + " B"


if __name__ == '__main__':
    CombinedVersion().generate()
