using System.IO;
using System.IO.Compression;
using System.Text;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.Logging;
using PCL.Core.Utils.Codecs;
using PCL.Core.Utils.OS;
using PCL.Core.Utils.Secret;

namespace PCL;

internal sealed class CrashReportExporter
{
    private const string ReportFolderName = "Report";

    private const string LaunchScriptFileName = "启动脚本.bat";
    private const string RawOutputFileName = "游戏崩溃前的输出.txt";
    private const string LauncherLogFileName = "PCL CE 启动器日志.txt";
    private const string EnvironmentFileName = "环境与启动信息.txt";
    private const string ModInfoFileName = "模组列表.txt";

    public void Export(
        CrashAnalysisContext context,
        string targetZipPath,
        IEnumerable<string>? extraFiles)
    {
        var targetFolder = Basics.GetParentPathOrEmpty(targetZipPath);
        Directory.CreateDirectory(targetFolder);

        if (File.Exists(targetZipPath))
            File.Delete(targetZipPath);

        ModBase.FeedbackInfo();

        var reportFolder = Path.Combine(context.TempFolder, ReportFolderName);

        if (Directory.Exists(reportFolder))
            CrashFileIo.DeleteDirectory(reportFolder);

        Directory.CreateDirectory(reportFolder);

        try
        {
            foreach (var outputFile in _CollectOutputFiles(context, extraFiles))
                _CopyFileToReport(reportFolder, outputFile);

            _WriteEnvironmentInfo(reportFolder);
            _WriteModInfo(reportFolder, context.Instance);

            ZipFile.CreateFromDirectory(reportFolder, targetZipPath);
        }
        finally
        {
            CrashFileIo.DeleteDirectory(reportFolder);
        }
    }

    private static void _CopyFileToReport(
        string reportFolder,
        string outputFile)
    {
        if (!File.Exists(outputFile))
            return;

        var fileName = _GetExportFileName(outputFile, out var fileEncoding);

        fileEncoding ??= EncodingDetector.DetectEncoding(CrashFileIo.ReadBytes(outputFile));

        var fileContent = CrashFileIo.ReadText(outputFile, fileEncoding);
        fileContent = _SanitizeFileContent(fileContent, fileName);

        CrashFileIo.WriteText(
            Path.Combine(reportFolder, fileName),
            fileContent,
            fileEncoding);

        LogWrapper.Info("Crash", $"导出文件：{fileName}，编码：{fileEncoding.HeaderName}");
    }

    private static string _GetExportFileName(
        string outputFile,
        out Encoding? fileEncoding)
    {
        fileEncoding = null;

        var fileName = Path.GetFileName(outputFile);

        switch (fileName)
        {
            case "LatestLaunch.bat":
                return LaunchScriptFileName;

            case "RawOutput.log":
                fileEncoding = Encoding.UTF8;
                return RawOutputFileName;
        }

        var currentLogFile = LogWrapper.CurrentLogger.CurrentLogFiles.LastOrDefault();
        var currentLogFileName = currentLogFile is null ? null : CrashText.AfterLast(currentLogFile, @"\");

        if (currentLogFileName != fileName) return fileName;

        fileEncoding = Encoding.UTF8;
        return LauncherLogFileName;
    }

    private static IReadOnlyList<string> _CollectOutputFiles(
        CrashAnalysisContext context,
        IEnumerable<string>? extraFiles)
    {
        if (extraFiles is not null)
            context.OutputFiles.AddRange(extraFiles);

        return context.OutputFiles;
    }

    private static string _SanitizeFileContent(
        string fileContent,
        string fileName)
    {
        var tokenMask = fileName == LaunchScriptFileName ? 'F' : '*';

        fileContent = McLogFilter.FilterAccessToken(fileContent, tokenMask);
        return McLogFilter.FilterUserName(fileContent, '*');
    }

    private static void _WriteEnvironmentInfo(string reportFolder)
    {
        var launcherLog = CrashText.BeforeFirst(
            CrashText.AfterLast(_ReadReportFile(reportFolder, LauncherLogFileName), "[Launch] ~ 基础参数 ~"),
            "开始 Minecraft 日志监控");

        var launchScript = _ReadReportFile(reportFolder, LaunchScriptFileName);

        var envInfo = new StringBuilder();

        _AppendLauncherInfo(envInfo);
        _AppendProfileInfo(envInfo, launcherLog);
        _AppendInstanceInfo(envInfo, launcherLog, launchScript);
        _AppendEnvironmentInfo(envInfo, launcherLog);

        CrashFileIo.WriteText(
            Path.Combine(reportFolder, EnvironmentFileName),
            envInfo.ToString(),
            Encoding.UTF8);
    }

    private static void _AppendLauncherInfo(StringBuilder builder)
    {
        builder.AppendLine(Lang.Text("Crash.Report.Environment.LauncherVersion", Basics.VersionName));
        builder.AppendLine(Lang.Text("Crash.Report.Environment.LauncherId", Identify.LauncherId));
        builder.AppendLine();
    }

    private static void _AppendProfileInfo(
        StringBuilder builder,
        string launcherLog)
    {
        builder.AppendLine(Lang.Text("Crash.Report.Environment.ProfileSection"));
        builder.AppendLine(
            Lang.Text(
                "Crash.Report.Environment.ProfileName",
                _ExtractLauncherValue(launcherLog, "玩家用户名："),
                _ExtractLauncherValue(launcherLog, "验证方式：")));
        builder.AppendLine();
    }

    private static void _AppendInstanceInfo(
        StringBuilder builder,
        string launcherLog,
        string launchScript)
    {
        builder.AppendLine(Lang.Text("Crash.Report.Environment.InstanceSection"));

        builder.AppendLine(
            Lang.Text(
                "Crash.Report.Environment.SelectedJava",
                _ExtractLauncherValue(launcherLog, "Java 信息：")));

        builder.AppendLine(
            Lang.Text(
                "Crash.Report.Environment.Log4j2NoLookups",
                !launchScript.Contains("-Dlog4j2.formatMsgNoLookups=false", StringComparison.OrdinalIgnoreCase)));

        builder.AppendLine(
            Lang.Text(
                "Crash.Report.Environment.MinecraftFolder",
                _ExtractLauncherValue(launcherLog, "MC 文件夹：")));

        builder.AppendLine();
    }

    private static void _AppendEnvironmentInfo(
        StringBuilder builder,
        string launcherLog)
    {
        builder.AppendLine(Lang.Text("Crash.Report.Environment.EnvironmentSection"));

        builder.AppendLine(
            Lang.Text(
                "Crash.Report.Environment.OperatingSystem",
                SystemInfo.OSInfo,
                !SystemInfo.Is32BitSystem,
                SystemInfo.IsArm64System));

        builder.AppendLine(
            Lang.Text(
                "Crash.Report.Environment.Cpu",
                HardwareInfo.CPUName));

        builder.AppendLine(
            Lang.Text(
                "Crash.Report.Environment.MemoryAllocation",
                _ExtractLauncherValue(launcherLog, "分配的内存："),
                Lang.Number(HardwareInfo.SystemMemorySize / 1024d, "N2"),
                Lang.Number(HardwareInfo.SystemMemorySize, "N0")));

        for (var i = 0; i < HardwareInfo.GPUs.Count; i++)
        {
            var gpu = HardwareInfo.GPUs[i];

            builder.AppendLine(
                Lang.Text(
                    "Crash.Report.Environment.Gpu",
                    i,
                    gpu.Name,
                    _FormatGpuMemory(gpu.Memory),
                    gpu.DriverVersion));
        }
    }

    private static string _ExtractLauncherValue(
        string launcherLog,
        string key)
    {
        return CrashText.Between(launcherLog, key, "[")
            .TrimEnd('[')
            .Trim();
    }

    private static string _FormatGpuMemory(long memory)
    {
        return memory >= 4095L
            ? ">= " + memory
            : memory.ToString();
    }

    private static string _ReadReportFile(
        string reportFolder,
        string fileName)
    {
        var filePath = Path.Combine(reportFolder, fileName);

        return File.Exists(filePath)
            ? CrashFileIo.ReadText(filePath)
            : "";
    }

    private static void _WriteModInfo(string reportFolder, McInstance? instance)
    {
        if (instance is null)
            return;

        try
        {
            var modsFolderName = ModLocalComp.GetPathNameByCompType(ModComp.CompType.Mod);
            var modsFolder = instance.Info.HasLabyMod
                ? Path.Combine(instance.PathIndie, "labymod-neo", "fabric", instance.Info.VanillaName, modsFolderName)
                : Path.Combine(instance.PathIndie, modsFolderName);

            if (!Directory.Exists(modsFolder))
                return;

            // 老 Forge（Drop < 130）的启用 Mod 位于 mods/<版本名> 子目录，需一并扫描
            var scanFolders = new List<string> { modsFolder };
            if (instance.Info.HasForge && instance.Info.Drop < 130)
            {
                var versionSubFolder = Path.Combine(modsFolder, instance.Info.VanillaName);
                if (Directory.Exists(versionSubFolder))
                    scanFolders.Add(versionSubFolder);
            }

            var activeMods = new List<ModLocalComp.LocalCompFile>();
            foreach (var folder in scanFolders)
                foreach (var file in Directory.GetFiles(folder))
                {
                    if (!ModLocalComp.LocalCompFile.IsModFile(file)
                        || file.EndsWith(".disabled", StringComparison.OrdinalIgnoreCase)
                        || file.EndsWith(".old", StringComparison.OrdinalIgnoreCase))
                        continue;
                    var mod = new ModLocalComp.LocalCompFile(file);
                    mod.Load();
                    if (mod.State == ModLocalComp.LocalCompFile.LocalFileStatus.Fine)
                        activeMods.Add(mod);
                }

            activeMods = activeMods
                .OrderBy(m => m.Name ?? m.FileName, StringComparer.OrdinalIgnoreCase)
                .ToList();

            var sb = new StringBuilder();

            var modsByModId = new Dictionary<string, List<ModLocalComp.LocalCompFile>>(StringComparer.OrdinalIgnoreCase);
            foreach (var mod in activeMods)
            {
                if (string.IsNullOrEmpty(mod.ModId))
                    continue;
                if (!modsByModId.TryGetValue(mod.ModId, out var list))
                    modsByModId[mod.ModId] = list = new List<ModLocalComp.LocalCompFile>();
                list.Add(mod);
            }

            var duplicates = new List<string>();
            foreach (var host in activeMods)
                foreach (var embedded in _FlattenEmbedded(host.EmbeddedMods))
                {
                    if (string.IsNullOrEmpty(embedded.ModId)
                        || !modsByModId.TryGetValue(embedded.ModId, out var matches))
                        continue;
                    foreach (var other in matches)
                    {
                        if (ReferenceEquals(other, host))
                            continue;
                        duplicates.Add(Lang.Text("Crash.Report.JarInJarMod.DuplicateEntry", other.Name, other.FileName, host.Name, embedded.Name ?? embedded.ModId));
                    }
                }

            sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateSection"));
            if (duplicates.Count == 0)
                sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateNone"));
            else
            {
                sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateDescription"));
                sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.DuplicateHeuristic"));
                foreach (var line in duplicates.Distinct())
                    sb.AppendLine("\t|-> " + line);
            }

            sb.AppendLine().AppendLine("----------------------------").AppendLine();

            sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.ModListSection"));
            sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.ModListDirectory", modsFolder));
            sb.AppendLine("|-> mods");
            foreach (var mod in activeMods)
            {
                var line = "|  |-> " + (mod.Name ?? mod.FileName);
                if (!string.IsNullOrWhiteSpace(mod.Version))
                    line += $" ({mod.Version})";
                if (mod.Name != mod.FileName)
                    line += $" [{mod.FileName}]";
                sb.AppendLine(line);
            }

            sb.AppendLine().AppendLine("----------------------------").AppendLine();

            sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.JarInJarSection"));
            var hasJij = false;
            foreach (var mod in activeMods)
            {
                if (!mod.EmbeddedMods.Any())
                    continue;
                hasJij = true;
                sb.AppendLine(mod.Name ?? mod.FileName);
                _AppendEmbeddedMods(sb, mod.EmbeddedMods, 1);
                sb.AppendLine();
            }

            if (!hasJij)
                sb.AppendLine(Lang.Text("Crash.Report.JarInJarMod.JarInJarNone"));

            CrashFileIo.WriteText(Path.Combine(reportFolder, ModInfoFileName), sb.ToString(), Encoding.UTF8);
            LogWrapper.Info("Crash", "已导出模组列表及 Jar-in-Jar 信息");
        }
        catch (Exception ex)
        {
            LogWrapper.Warn(ex, "Crash", "导出模组信息失败");
        }
    }

    private static IEnumerable<ModLocalComp.LocalCompFile> _FlattenEmbedded(List<ModLocalComp.LocalCompFile> mods)
    {
        foreach (var mod in mods)
        {
            yield return mod;
            if (mod.EmbeddedMods.Any())
                foreach (var child in _FlattenEmbedded(mod.EmbeddedMods))
                    yield return child;
        }
    }

    private static void _AppendEmbeddedMods(StringBuilder builder, List<ModLocalComp.LocalCompFile> mods, int depth)
    {
        var indent = new string('\t', depth);
        foreach (var mod in mods)
        {
            var line = indent + "|-> " + (mod.Name ?? mod.ModId ?? "?");
            if (!string.IsNullOrWhiteSpace(mod.Version))
                line += $" ({mod.Version})";
            builder.AppendLine(line);
            if (mod.EmbeddedMods.Any())
                _AppendEmbeddedMods(builder, mod.EmbeddedMods, depth + 1);
        }
    }
}