using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
using UnityEditor.PackageManager;
using UnityEngine;

namespace XeSoft.BuildDeltaEvidence
{
    internal sealed class BuildDeltaEvidenceExporter : IPostprocessBuildWithReport
    {
        private const string ExporterVersion = "0.1.0";
        private const string SchemaVersion = "bdg-summary-v0.1";
        private const int MaximumPackages = 250;

        private static readonly ContractField[] ContractFields =
        {
            new ContractField("artifactFormat", "XESOFT_BDG_ARTIFACT_FORMAT"),
            new ContractField("scriptingBackend", "XESOFT_BDG_SCRIPTING_BACKEND"),
            new ContractField("configuration", "XESOFT_BDG_CONFIGURATION"),
            new ContractField("architecture", "XESOFT_BDG_ARCHITECTURE"),
            new ContractField("strippingLevel", "XESOFT_BDG_STRIPPING_LEVEL"),
            new ContractField("compression", "XESOFT_BDG_COMPRESSION"),
            new ContractField("buildEntry", "XESOFT_BDG_BUILD_ENTRY"),
            new ContractField("runnerProfile", "XESOFT_BDG_RUNNER_PROFILE")
        };

        public int callbackOrder
        {
            get { return int.MaxValue; }
        }

        public void OnPostprocessBuild(BuildReport report)
        {
            try
            {
                Export(report);
            }
            catch (Exception exception)
            {
                Debug.LogWarning("[XeSoft Build Delta Evidence] Export skipped: " + exception.Message);
            }
        }

        private static void Export(BuildReport report)
        {
            if (report == null)
            {
                throw new InvalidOperationException("Unity did not provide a BuildReport.");
            }

            string revision = RequiredEnvironmentValue("XESOFT_BDG_REVISION");
            Dictionary<string, string> contract = ReadContract();
            List<PackagePair> packages = ReadPackages();
            string json = BuildJson(report, revision, contract, packages);
            string outputDirectory = Environment.GetEnvironmentVariable("XESOFT_BDG_OUTPUT_DIR");

            if (string.IsNullOrWhiteSpace(outputDirectory))
            {
                outputDirectory = Path.Combine("Library", "XeSoft", "BuildDelta");
            }

            string absoluteDirectory = Path.GetFullPath(outputDirectory);
            Directory.CreateDirectory(absoluteDirectory);
            string outputPath = Path.Combine(absoluteDirectory, SafeFileName(revision) + ".json");
            File.WriteAllText(outputPath, json, new UTF8Encoding(false));
            Debug.Log("[XeSoft Build Delta Evidence] Wrote local summary: " + outputPath);
        }

        private static Dictionary<string, string> ReadContract()
        {
            Dictionary<string, string> contract = new Dictionary<string, string>(StringComparer.Ordinal);
            for (int index = 0; index < ContractFields.Length; index += 1)
            {
                ContractField field = ContractFields[index];
                contract.Add(field.JsonName, RequiredEnvironmentValue(field.EnvironmentName));
            }

            return contract;
        }

        private static string RequiredEnvironmentValue(string name)
        {
            string value = Environment.GetEnvironmentVariable(name);
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new InvalidOperationException("Required environment variable " + name + " is missing.");
            }

            string trimmed = value.Trim();
            if (trimmed.Length > 160)
            {
                throw new InvalidOperationException("Environment variable " + name + " must be 160 characters or fewer.");
            }

            return trimmed;
        }

        private static List<PackagePair> ReadPackages()
        {
            PackageInfo[] registered = PackageInfo.GetAllRegisteredPackages();
            if (registered == null)
            {
                throw new InvalidOperationException("Unity Package Manager returned no registered-package inventory.");
            }

            if (registered.Length > MaximumPackages)
            {
                throw new InvalidOperationException("Registered package count exceeds the 250-entry evaluator ceiling.");
            }

            List<PackagePair> packages = new List<PackagePair>();
            HashSet<string> names = new HashSet<string>(StringComparer.Ordinal);
            for (int index = 0; index < registered.Length; index += 1)
            {
                PackageInfo package = registered[index];
                if (package == null || string.IsNullOrWhiteSpace(package.name) || string.IsNullOrWhiteSpace(package.version))
                {
                    throw new InvalidOperationException("Every registered package must expose a non-empty name and version.");
                }

                string name = package.name.Trim();
                string version = package.version.Trim();
                if (name.Length > 180 || version.Length > 120)
                {
                    throw new InvalidOperationException("A registered package name or version exceeds the evaluator limit.");
                }

                if (!names.Add(name))
                {
                    throw new InvalidOperationException("Unity returned a duplicate registered package name: " + name);
                }

                packages.Add(new PackagePair(name, version));
            }

            packages.Sort(delegate(PackagePair left, PackagePair right)
            {
                return string.CompareOrdinal(left.Name, right.Name);
            });
            return packages;
        }

        private static string BuildJson(
            BuildReport report,
            string revision,
            Dictionary<string, string> contract,
            List<PackagePair> packages)
        {
            BuildSummary summary = report.summary;
            if (summary.result != BuildResult.Succeeded)
            {
                throw new InvalidOperationException("Player build did not succeed; no evidence file was written.");
            }

            StringBuilder json = new StringBuilder(2048);
            json.AppendLine("{");
            AppendStringProperty(json, 1, "schemaVersion", SchemaVersion, true);
            AppendStringProperty(json, 1, "revision", revision, true);
            json.AppendLine("  \"metadata\": {");
            AppendStringProperty(json, 2, "unityVersion", Application.unityVersion, true);
            AppendStringProperty(json, 2, "buildTarget", summary.platform.ToString(), true);
            for (int index = 0; index < ContractFields.Length; index += 1)
            {
                ContractField field = ContractFields[index];
                AppendStringProperty(json, 2, field.JsonName, contract[field.JsonName], index < ContractFields.Length - 1);
            }
            json.AppendLine("  },");
            json.AppendLine("  \"metrics\": {");
            AppendNumberProperty(json, 2, "totalBuildBytes", summary.totalSize.ToString(CultureInfo.InvariantCulture), true);
            AppendNumberProperty(json, 2, "buildDurationSeconds", summary.totalTime.TotalSeconds.ToString("0.###", CultureInfo.InvariantCulture), false);
            json.AppendLine("  },");
            json.AppendLine("  \"dependencies\": {");
            for (int index = 0; index < packages.Count; index += 1)
            {
                PackagePair package = packages[index];
                AppendStringProperty(json, 2, package.Name, package.Version, index < packages.Count - 1);
            }
            json.AppendLine("  },");
            json.AppendLine("  \"evidence\": {");
            AppendStringProperty(json, 2, "exporterVersion", ExporterVersion, true);
            AppendStringProperty(json, 2, "scope", "BuildSummary totals, explicit contract labels, and registered package name/version pairs only", true);
            AppendStringProperty(json, 2, "optionalMetrics", "not_measured", true);
            AppendStringProperty(json, 2, "privacy", "No output paths, project paths, logs, source, assets, credentials, personal data, or player data are included", false);
            json.AppendLine("  }");
            json.AppendLine("}");
            return json.ToString();
        }

        private static void AppendStringProperty(StringBuilder json, int indent, string name, string value, bool comma)
        {
            json.Append(' ', indent * 2);
            json.Append(JsonString(name));
            json.Append(": ");
            json.Append(JsonString(value));
            if (comma)
            {
                json.Append(',');
            }
            json.AppendLine();
        }

        private static void AppendNumberProperty(StringBuilder json, int indent, string name, string value, bool comma)
        {
            json.Append(' ', indent * 2);
            json.Append(JsonString(name));
            json.Append(": ");
            json.Append(value);
            if (comma)
            {
                json.Append(',');
            }
            json.AppendLine();
        }

        private static string JsonString(string value)
        {
            StringBuilder escaped = new StringBuilder(value.Length + 2);
            escaped.Append('"');
            for (int index = 0; index < value.Length; index += 1)
            {
                char character = value[index];
                switch (character)
                {
                    case '"': escaped.Append("\\\""); break;
                    case '\\': escaped.Append("\\\\"); break;
                    case '\b': escaped.Append("\\b"); break;
                    case '\f': escaped.Append("\\f"); break;
                    case '\n': escaped.Append("\\n"); break;
                    case '\r': escaped.Append("\\r"); break;
                    case '\t': escaped.Append("\\t"); break;
                    default:
                        if (character < 0x20)
                        {
                            escaped.Append("\\u");
                            escaped.Append(((int)character).ToString("x4", CultureInfo.InvariantCulture));
                        }
                        else
                        {
                            escaped.Append(character);
                        }
                        break;
                }
            }
            escaped.Append('"');
            return escaped.ToString();
        }

        private static string SafeFileName(string revision)
        {
            char[] invalid = Path.GetInvalidFileNameChars();
            StringBuilder safe = new StringBuilder(revision.Length);
            for (int index = 0; index < revision.Length; index += 1)
            {
                char character = revision[index];
                bool isInvalid = character == '/' || character == '\\' || Array.IndexOf(invalid, character) >= 0;
                safe.Append(isInvalid ? '_' : character);
            }
            return safe.ToString();
        }

        private sealed class ContractField
        {
            internal ContractField(string jsonName, string environmentName)
            {
                JsonName = jsonName;
                EnvironmentName = environmentName;
            }

            internal string JsonName { get; private set; }
            internal string EnvironmentName { get; private set; }
        }

        private sealed class PackagePair
        {
            internal PackagePair(string name, string version)
            {
                Name = name;
                Version = version;
            }

            internal string Name { get; private set; }
            internal string Version { get; private set; }
        }
    }
}
