using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Build;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEngine;
using Stopwatch = System.Diagnostics.Stopwatch;

/// <summary>
/// Custom build script for CI that optionally builds Addressables before the player.
/// </summary>
public static class BuildScript {
    private const long LargeAddressablesOutputWarningBytes = 1024L * 1024L * 1024L;
    private const int AddressableLabelFanoutWarningEntries = 25;

    public static string AddressablesUnityMajorMinor {
        get {
            var parts = Application.unityVersion.Split('.');
            return parts.Length >= 2 ? $"{parts[0]}.{parts[1]}" : Application.unityVersion;
        }
    }

    private sealed class AddressableLabelFanout {
        public string label;
        public int entryCount;
        public int packTogetherEntryCount;
        public readonly HashSet<string> groups = new();
    }

    /// <summary>
    /// Builds Addressables content. Call via: -executeMethod BuildScript.BuildAddressables
    /// </summary>
    public static void BuildAddressables() {
        BuildAddressables(GetBoolArg("-addressablesBuildLayout", true));
    }

    private static void BuildAddressables(bool generateLayoutReport) {
        EnsureDotNetSdkEnvironment();
        Debug.Log($"Building Addressables content... generateLayoutReport={generateLayoutReport}");
        DeleteAddressablesBuildReports();
        LogAddressableGroupPackingModes();
        LogAddressableLabelFanout();

        var settings = AddressableAssetSettingsDefaultObject.Settings;
        var resolvedBuildPath = "";
        if (settings != null) {
            var profileId = settings.activeProfileId;
            var buildPath = settings.profileSettings.GetValueByName(profileId, "Remote.BuildPath");
            resolvedBuildPath = ResolveAddressablesProfilePath(settings, profileId, buildPath);
            CleanAddressablesOutput(resolvedBuildPath);
        }

        AddressablesPlayerBuildResult result = null;
        LogTimed("Build Addressables content", () => {
            BuildAddressables(generateLayoutReport, out result);
        });

        if (!string.IsNullOrEmpty(result.Error)) {
            Debug.LogError($"Addressables build failed: {result.Error}");
            EditorApplication.Exit(1);
        }

        Debug.Log($"Addressables build completed. Duration: {result.Duration}s");

        // Log the output location for remote bundles
        if (!string.IsNullOrEmpty(resolvedBuildPath)) {
            Debug.Log($"Addressables bundles built to: {resolvedBuildPath}");
            LogAddressablesOutput(resolvedBuildPath);
            LogAddressablesBuildReports();
            Debug.Log("Upload these bundles to your CDN at the configured Remote.LoadPath");
        }
    }

    /// <summary>
    /// Builds Addressables and then the Windows player.
    /// Call via: -executeMethod BuildScript.BuildWindowsPlayer
    /// </summary>
    public static void BuildWindowsPlayer() {
        var totalTimer = Stopwatch.StartNew();
        var buildAddressables = GetBoolArg("-buildAddressables", true);
        var generateLayoutReport = GetBoolArg("-addressablesBuildLayout", buildAddressables);
        Debug.Log(
                $"Windows player build options: buildAddressables={buildAddressables}, addressablesBuildLayout={generateLayoutReport}");

        LogTimed(
                "Switch build target to StandaloneWindows64",
                () => SwitchBuildTarget(
                        BuildTargetGroup.Standalone,
                        BuildTarget.StandaloneWindows64));
        MaybeBuildAddressables(buildAddressables, generateLayoutReport);

        string buildPath = GetBuildPathArg();
        if (string.IsNullOrEmpty(buildPath)) {
            Debug.LogError("No build path specified. Use -buildPath <path>");
            EditorApplication.Exit(1);
            return;
        }

        Debug.Log($"Building Windows player to: {buildPath}");

        var options = new BuildPlayerOptions {
            scenes = GetEnabledScenes(),
            locationPathName = buildPath,
            target = BuildTarget.StandaloneWindows64,
            options = BuildOptions.None
        };

        BuildReport report = null;
        LogTimed("Build Windows player", () => {
            report = BuildPlayerWithTemporaryStandaloneBackend(
                    options,
                    ScriptingImplementation.Mono2x,
                    "Windows players are cross-built on macOS ARM runners, where Unity 6000 provides Windows Mono support but not Windows IL2CPP support.");
        });

        if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) {
            Debug.LogError($"Build failed: {report.summary.result}");
            EditorApplication.Exit(1);
        }

        totalTimer.Stop();
        Debug.Log(
                $"Build completed successfully! Total Windows player build duration: {totalTimer.Elapsed.TotalSeconds:0.###}s");
    }

    /// <summary>
    /// Builds Addressables and then the macOS player (Intel).
    /// Call via: -executeMethod BuildScript.BuildMacPlayer
    /// </summary>
    public static void BuildMacPlayer() {
        var totalTimer = Stopwatch.StartNew();
        var buildAddressables = GetBoolArg("-buildAddressables", true);
        var generateLayoutReport = GetBoolArg("-addressablesBuildLayout", buildAddressables);
        Debug.Log(
                $"macOS player build options: buildAddressables={buildAddressables}, addressablesBuildLayout={generateLayoutReport}");

        LogTimed(
                "Switch build target to StandaloneOSX",
                () => SwitchBuildTarget(BuildTargetGroup.Standalone, BuildTarget.StandaloneOSX));
        MaybeBuildAddressables(buildAddressables, generateLayoutReport);

        string buildPath = GetBuildPathArg();
        if (string.IsNullOrEmpty(buildPath)) {
            Debug.LogError("No build path specified. Use -buildPath <path>");
            EditorApplication.Exit(1);
            return;
        }

        Debug.Log($"Building macOS player to: {buildPath}");

        // Set architecture to Intel + Apple Silicon (Universal)
        EditorUserBuildSettings.SetPlatformSettings(
                BuildPipeline.GetBuildTargetName(BuildTarget.StandaloneOSX),
                "Architecture",
                ((int)UnityEditor.Build.OSArchitecture.x64ARM64).ToString());

        var options = new BuildPlayerOptions {
            scenes = GetEnabledScenes(),
            locationPathName = buildPath,
            target = BuildTarget.StandaloneOSX,
            options = BuildOptions.None
        };

        BuildReport report = null;
        LogTimed("Build macOS player", () => { report = BuildPipeline.BuildPlayer(options); });

        if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) {
            Debug.LogError($"Build failed: {report.summary.result}");
            EditorApplication.Exit(1);
        }

        totalTimer.Stop();
        Debug.Log(
                $"Build completed successfully! Total macOS player build duration: {totalTimer.Elapsed.TotalSeconds:0.###}s");
    }

    /// <summary>
    /// Builds Addressables only for iOS (no player build).
    /// Explicitly switches to iOS build target first.
    /// Call via: -executeMethod BuildScript.BuildiOSAddressables
    /// </summary>
    public static void BuildiOSAddressables() {
        LogTimed(
                "Switch build target to iOS",
                () => SwitchBuildTarget(BuildTargetGroup.iOS, BuildTarget.iOS));
        BuildAddressables();
    }

    /// <summary>
    /// Builds Addressables and then the iOS player.
    /// Call via: -executeMethod BuildScript.BuildiOSPlayer
    /// Optional: -buildNumber <number> to set CFBundleVersion
    /// </summary>
    public static void BuildiOSPlayer() {
        var totalTimer = Stopwatch.StartNew();
        var buildAddressables = GetBoolArg("-buildAddressables", true);
        var generateLayoutReport = GetBoolArg("-addressablesBuildLayout", buildAddressables);
        LogTimed(
                "Switch build target to iOS",
                () => SwitchBuildTarget(BuildTargetGroup.iOS, BuildTarget.iOS));
        MaybeBuildAddressables(buildAddressables, generateLayoutReport);

        string buildPath = GetBuildPathArg();
        if (string.IsNullOrEmpty(buildPath)) {
            Debug.LogError("No build path specified. Use -buildPath <path>");
            EditorApplication.Exit(1);
            return;
        }

        // Set build number if provided (required for unique TestFlight uploads)
        string buildNumber = GetBuildNumberArg();
        if (!string.IsNullOrEmpty(buildNumber)) {
            PlayerSettings.iOS.buildNumber = buildNumber;
            Debug.Log($"Set iOS build number to: {buildNumber}");
        } else {
            Debug.Log($"Using existing iOS build number: {PlayerSettings.iOS.buildNumber}");
        }

        Debug.Log($"Building iOS player to: {buildPath}");

        var options = new BuildPlayerOptions {
            scenes = GetEnabledScenes(),
            locationPathName = buildPath,
            target = BuildTarget.iOS,
            options = BuildOptions.None
        };

        BuildReport report = null;
        LogTimed("Build iOS player", () => { report = BuildPipeline.BuildPlayer(options); });

        if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) {
            Debug.LogError($"Build failed: {report.summary.result}");
            EditorApplication.Exit(1);
        }

        totalTimer.Stop();
        Debug.Log(
                $"iOS build completed successfully! Total iOS player build duration: {totalTimer.Elapsed.TotalSeconds:0.###}s");
        Debug.Log("Open the Xcode project in the output folder to build and deploy to device.");
    }

    private static void SwitchBuildTarget(BuildTargetGroup group, BuildTarget target) {
        if (EditorUserBuildSettings.activeBuildTarget == target) {
            Debug.Log($"Already on build target {target}");
            return;
        }

        Debug.Log($"Switching to build target {target}...");
        bool success = EditorUserBuildSettings.SwitchActiveBuildTarget(group, target);
        if (!success) {
            Debug.LogError($"Failed to switch to {target}. Is platform support installed?");
            EditorApplication.Exit(1);
        }
        Debug.Log($"Successfully switched to {target}");
    }

    private static string[] GetEnabledScenes() {
        var scenes = new System.Collections.Generic.List<string>();
        foreach (var scene in EditorBuildSettings.scenes) {
            if (scene.enabled) { scenes.Add(scene.path); }
        }
        return scenes.ToArray();
    }

    private static string GetBuildPathArg() {
        var args = System.Environment.GetCommandLineArgs();
        for (int i = 0; i < args.Length - 1; i++) {
            if (args[i] == "-buildPath") { return args[i + 1]; }
        }
        return null;
    }

    private static string GetBuildNumberArg() {
        var args = System.Environment.GetCommandLineArgs();
        for (int i = 0; i < args.Length - 1; i++) {
            if (args[i] == "-buildNumber") { return args[i + 1]; }
        }
        return null;
    }

    private static bool GetBoolArg(string name, bool defaultValue) {
        var args = System.Environment.GetCommandLineArgs();
        for (int i = 0; i < args.Length - 1; i++) {
            if (args[i] != name) { continue; }

            if (bool.TryParse(args[i + 1], out var parsed)) { return parsed; }
            Debug.LogWarning(
                    $"Could not parse boolean value '{args[i + 1]}' for {name}; using default {defaultValue}.");
            return defaultValue;
        }
        return defaultValue;
    }

    private static void MaybeBuildAddressables(bool buildAddressables, bool generateLayoutReport) {
        if (!buildAddressables) {
            Debug.Log("Skipping Addressables build because -buildAddressables false was provided.");
            return;
        }

        BuildAddressables(generateLayoutReport);
    }

    private static void BuildAddressables(
            bool generateLayoutReport,
            out AddressablesPlayerBuildResult result) {
        if (!generateLayoutReport) {
            BuildAddressablesWithStandaloneMono(out result);
            return;
        }

        var originalGenerateBuildLayout = ProjectConfigData.GenerateBuildLayout;
        var originalReportFileFormat = ProjectConfigData.BuildLayoutReportFileFormat;

        ProjectConfigData.GenerateBuildLayout = true;
        ProjectConfigData.BuildLayoutReportFileFormat = ProjectConfigData.ReportFileFormat.JSON;
        try {
            BuildAddressablesWithStandaloneMono(out result);
        } finally {
            ProjectConfigData.GenerateBuildLayout = originalGenerateBuildLayout;
            ProjectConfigData.BuildLayoutReportFileFormat = originalReportFileFormat;
        }
    }

    private static void BuildAddressablesWithStandaloneMono(
            out AddressablesPlayerBuildResult result) {
        if (EditorUserBuildSettings.selectedBuildTargetGroup != BuildTargetGroup.Standalone) {
            AddressableAssetSettings.BuildPlayerContent(out result);
            return;
        }

        var originalBackend = PlayerSettings.GetScriptingBackend(NamedBuildTarget.Standalone);
        if (originalBackend == ScriptingImplementation.Mono2x) {
            AddressableAssetSettings.BuildPlayerContent(out result);
            return;
        }

        Debug.Log(
                $"Temporarily using Mono scripting backend for Standalone Addressables build; will restore {originalBackend} before player build.");
        PlayerSettings.SetScriptingBackend(
                NamedBuildTarget.Standalone,
                ScriptingImplementation.Mono2x);
        try {
            AddressableAssetSettings.BuildPlayerContent(out result);
        } finally {
            PlayerSettings.SetScriptingBackend(NamedBuildTarget.Standalone, originalBackend);
            Debug.Log($"Restored Standalone scripting backend to {originalBackend}.");
        }
    }

    private static void LogTimed(string operation, Action action) {
        var timer = Stopwatch.StartNew();
        Debug.Log($"{operation} started.");
        try {
            action();
        } finally {
            timer.Stop();
            Debug.Log($"{operation} completed in {timer.Elapsed.TotalSeconds:0.###}s.");
        }
    }

    private static void LogAddressablesBuildReports() {
        var reportPath =
                Path.GetFullPath(UnityEngine.AddressableAssets.Addressables.BuildReportPath);
        if (!Directory.Exists(reportPath)) {
            Debug.LogWarning($"Addressables build report path does not exist: {reportPath}");
            return;
        }

        var reports = new List<FileInfo>();
        foreach (var file in Directory
                         .GetFiles(reportPath, "*.json", SearchOption.TopDirectoryOnly)) {
            reports.Add(new FileInfo(file));
        }

        reports.Sort((left, right) => right.LastWriteTimeUtc.CompareTo(left.LastWriteTimeUtc));
        Debug.Log($"Addressables build reports: {reports.Count} JSON files at {reportPath}");

        var reportLimit = Math.Min(5, reports.Count);
        for (var i = 0; i < reportLimit; i++) {
            var report = reports[i];
            Debug.Log(
                    $"Addressables build report #{i + 1}: {FormatBytes(report.Length)} {report.Name}");
        }
    }

    private static void DeleteAddressablesBuildReports() {
        var reportPath =
                Path.GetFullPath(UnityEngine.AddressableAssets.Addressables.BuildReportPath);
        if (!Directory.Exists(reportPath)) { return; }

        var deletedCount = 0;
        foreach (var file in Directory
                         .GetFiles(reportPath, "*.json", SearchOption.TopDirectoryOnly)) {
            File.Delete(file);
            deletedCount++;
        }

        if (deletedCount > 0) {
            Debug.Log(
                    $"Deleted {deletedCount} stale Addressables build report JSON files from {reportPath}");
        }
    }

    private static void CleanAddressablesOutput(string path) {
        if (string.IsNullOrEmpty(path)) {
            Debug.LogWarning("Addressables output path is empty; skipping output cleanup.");
            return;
        }

        var fullPath = Path.GetFullPath(path);
        var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
        if (!fullPath.StartsWith(
                    projectRoot + Path.DirectorySeparatorChar,
                    StringComparison.Ordinal) ||
            fullPath.IndexOf("ServerData", StringComparison.Ordinal) < 0) {
            Debug.LogWarning($"Refusing to clean unexpected Addressables output path: {fullPath}");
            return;
        }

        if (!Directory.Exists(fullPath)) { return; }

        Directory.Delete(fullPath, true);
        Directory.CreateDirectory(fullPath);
        Debug.Log($"Deleted stale Addressables output at {fullPath}");
    }

    private static void LogAddressableGroupPackingModes() {
        var settings = AddressableAssetSettingsDefaultObject.Settings;
        if (settings == null) {
            Debug.LogWarning("AddressableAssetSettingsDefaultObject.Settings is null.");
            return;
        }

        foreach (var group in settings.groups) {
            if (group == null) { continue; }

            var bundledSchema = group.GetSchema<BundledAssetGroupSchema>();
            if (bundledSchema == null) {
                Debug.Log($"Addressables group '{group.Name}' has no BundledAssetGroupSchema.");
                continue;
            }

            Debug.Log(
                    $"Addressables group '{group.Name}' packing mode: {bundledSchema.BundleMode}");
        }
    }

    private static void LogAddressableLabelFanout() {
        var settings = AddressableAssetSettingsDefaultObject.Settings;
        if (settings == null) {
            Debug.LogWarning("AddressableAssetSettingsDefaultObject.Settings is null.");
            return;
        }

        var fanoutByLabel = new Dictionary<string, AddressableLabelFanout>();
        foreach (var group in settings.groups) {
            if (group == null) { continue; }

            var bundledSchema = group.GetSchema<BundledAssetGroupSchema>();
            var groupPacksTogether = bundledSchema != null &&
                                     bundledSchema.BundleMode ==
                                             BundledAssetGroupSchema.BundlePackingMode.PackTogether;

            foreach (var entry in group.entries) {
                foreach (var label in entry.labels) {
                    if (!fanoutByLabel.TryGetValue(label, out var fanout)) {
                        fanout = new AddressableLabelFanout { label = label };
                        fanoutByLabel[label] = fanout;
                    }

                    fanout.entryCount++;
                    if (groupPacksTogether) { fanout.packTogetherEntryCount++; }
                    fanout.groups.Add(group.Name);
                }
            }
        }

        var fanouts = new List<AddressableLabelFanout>(fanoutByLabel.Values);
        fanouts.Sort((left, right) => right.entryCount.CompareTo(left.entryCount));

        Debug.Log($"Addressables label fanout report: {fanouts.Count} labels.");

        var labelLimit = Math.Min(25, fanouts.Count);
        for (var i = 0; i < labelLimit; i++) {
            var fanout = fanouts[i];
            var groupNames = string.Join(", ", fanout.groups);
            var message =
                    $"Addressables label fanout #{i + 1}: '{fanout.label}' references {fanout.entryCount} entries across {fanout.groups.Count} group(s): {groupNames}";
            if (fanout.packTogetherEntryCount > 0) {
                message += $"; {fanout.packTogetherEntryCount} entries are in pack-together groups";
            }

            if (fanout.entryCount >= AddressableLabelFanoutWarningEntries) {
                Debug.LogWarning(
                        message +
                        ". Large label loads can trigger many client bundle downloads; prefer smaller labels or on-demand address loads when this is not intentional.");
            } else {
                Debug.Log(message);
            }
        }
    }

    private static string ResolveAddressablesProfilePath(
            AddressableAssetSettings settings,
            string profileId,
            string profilePath) {
        if (string.IsNullOrEmpty(profilePath)) { return profilePath; }

        var evaluatedPath = settings.profileSettings.EvaluateString(profileId, profilePath);
        return string.IsNullOrEmpty(evaluatedPath) ? profilePath : evaluatedPath;
    }

    private static void LogAddressablesOutput(string path) {
        if (string.IsNullOrEmpty(path)) {
            Debug.LogWarning("Addressables output path is empty; skipping bundle size report.");
            return;
        }

        var fullPath = Path.GetFullPath(path);
        if (!Directory.Exists(fullPath)) {
            Debug.LogWarning(
                    $"Addressables output path does not exist; skipping bundle size report: {fullPath}");
            return;
        }

        var files = new List<FileInfo>();
        foreach (var file in Directory.GetFiles(fullPath, "*", SearchOption.AllDirectories)) {
            files.Add(new FileInfo(file));
        }

        files.Sort((left, right) => right.Length.CompareTo(left.Length));

        long totalBytes = 0;
        foreach (var file in files) { totalBytes += file.Length; }

        Debug.Log(
                $"Addressables output size report: {files.Count} files, {FormatBytes(totalBytes)} total at {fullPath}");

        var largeFiles = 0;
        foreach (var file in files) {
            if (file.Length < LargeAddressablesOutputWarningBytes) { continue; }
            largeFiles++;
        }

        if (largeFiles > 0) {
            Debug.LogWarning(
                    $"Addressables output contains {largeFiles} file(s) at or above {FormatBytes(LargeAddressablesOutputWarningBytes)}. " +
                    "Large bundles increase CDN upload time and client download size; consider splitting the relevant Addressables group or label.");
        }

        var fileLimit = Math.Min(25, files.Count);
        for (var i = 0; i < fileLimit; i++) {
            var file = files[i];
            var relativePath =
                    file.FullName.Substring(fullPath.Length)
                            .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
            Debug.Log(
                    $"Addressables output file #{i + 1}: {FormatBytes(file.Length)} {relativePath}");
        }
    }

    private static string FormatBytes(long bytes) {
        string[] units = { "B", "KiB", "MiB", "GiB" };
        var size = (double)bytes;
        var unitIndex = 0;
        while (size >= 1024 && unitIndex < units.Length - 1) {
            size /= 1024;
            unitIndex++;
        }

        return $"{size:0.##} {units[unitIndex]}";
    }

    private static BuildReport BuildPlayerWithTemporaryStandaloneBackend(
            BuildPlayerOptions options,
            ScriptingImplementation backend,
            string reason) {
        var originalBackend = PlayerSettings.GetScriptingBackend(NamedBuildTarget.Standalone);
        if (originalBackend == backend) { return BuildPipeline.BuildPlayer(options); }

        Debug.Log(
                $"Temporarily using {backend} scripting backend for {options.target} player build. {reason} Will restore {originalBackend} after the build.");
        PlayerSettings.SetScriptingBackend(NamedBuildTarget.Standalone, backend);
        try {
            return BuildPipeline.BuildPlayer(options);
        } finally {
            PlayerSettings.SetScriptingBackend(NamedBuildTarget.Standalone, originalBackend);
            Debug.Log($"Restored Standalone scripting backend to {originalBackend}.");
        }
    }

    private static void EnsureDotNetSdkEnvironment() {
        var dotnetRoot = Environment.GetEnvironmentVariable("DOTNET_ROOT");
        if (string.IsNullOrEmpty(dotnetRoot)) { return; }

        var currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
        var dotnetRootPrefix = dotnetRoot + Path.PathSeparator;
        if (!currentPath.StartsWith(dotnetRootPrefix, StringComparison.Ordinal)) {
            Environment.SetEnvironmentVariable("PATH", dotnetRootPrefix + currentPath);
        }

        var dotnetHostPath = Path.Combine(dotnetRoot, "dotnet");
        Environment.SetEnvironmentVariable("DOTNET_HOST_PATH", dotnetHostPath);
        Debug.Log($"Using .NET SDK from DOTNET_ROOT={dotnetRoot}");
    }
}
