using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using UnityEngine;

namespace Streamingle.Editor
{
    public enum AICameraABRenderCodec
    {
        MP4,
        WEBM
    }

    [Serializable]
    public sealed class AICameraABRenderRequest
    {
        public string candidateADirectory;
        public string candidateBDirectory;
        public string outputRoot;
        public int startFrame;
        public int endFrameExclusive;
        public AICameraABRenderCodec codec = AICameraABRenderCodec.MP4;
        public int width = 1920;
        public int height = 1080;
    }

    [Serializable]
    public sealed class AICameraABRenderManifest
    {
        public string schemaVersion = "1.0";
        public string jobId;
        public string state;
        public string createdUtc;
        public string completedUtc;
        public string error;
        public AICameraABEnvironmentInfo environment;
        public AICameraABSceneInfo scene;
        public AICameraABFrameRange frameRange;
        public AICameraABMovieSettings movie;
        public AICameraABFairnessInfo fairness;
        public List<AICameraABAudioClipInfo> audio = new();
        public List<AICameraABCandidateResult> candidates = new();
    }

    [Serializable]
    public sealed class AICameraABEnvironmentInfo
    {
        public string unityVersion;
        public string recorderVersion;
        public string cwAiVersion;
        public string operatingSystem;
    }

    [Serializable]
    public sealed class AICameraABSceneInfo
    {
        public string scenePath;
        public string sceneGuid;
        public string timelinePath;
        public string timelineGuid;
        public double timelineFrameRate;
    }

    [Serializable]
    public sealed class AICameraABFrameRange
    {
        public int startFrame;
        public int endFrameExclusive;
        public int expectedFrameCount;
        public double startSeconds;
        public double endSecondsExclusive;
    }

    [Serializable]
    public sealed class AICameraABMovieSettings
    {
        public string codec;
        public string encoder;
        public string quality;
        public int width;
        public int height;
        public double aspectRatio;
        public int frameRate;
        public string cameraSource;
        public bool captureAudio;
        public bool captureAlpha;
    }

    [Serializable]
    public sealed class AICameraABFairnessInfo
    {
        public bool sameSampleRate;
        public bool sameFrameCount;
        public bool sameTimeSha256;
        public bool sameSongId;
        public bool sameDatasetId;
        public bool sameTargetAspectRatio;
        public bool sameSceneAndTimeline;
        public string sharedTimeSha256;
        public string sharedSongId;
        public string targetEvaluationGroupId;
    }

    [Serializable]
    public sealed class AICameraABAudioClipInfo
    {
        public string trackName;
        public string clipName;
        public string assetPath;
        public string assetGuid;
        public string assetSha256;
        public double timelineStart;
        public double clipIn;
        public double duration;
    }

    [Serializable]
    public sealed class AICameraABCandidateResult
    {
        public string label;
        public string generatedDirectory;
        public string horizontalFramingPolicy;
        public string metadataSha256;
        public string cameraSha256;
        public string timeSha256;
        public string shotsSha256;
        public string declaredCameraSha256;
        public string declaredTimeSha256;
        public string declaredShotsSha256;
        public string songName;
        public string songId;
        public string datasetId;
        public string targetEvaluationGroupId;
        public double targetAspectRatio;
        public int sampleRate;
        public int frameCount;
        public double duration;
        public string outputBasePath;
        public string outputPath;
        public string status;
        public string startedUtc;
        public string completedUtc;
        public long outputBytes;
        public string outputSha256;
        public string error;
    }

    [Serializable]
    internal sealed class AICameraABRenderJobState
    {
        public string schemaVersion = "1.0";
        public string jobId;
        public string phase;
        public string manifestPath;
        public string runDirectory;
        public int candidateIndex;
        public bool cancelRequested;
        public bool cleanupPending;
        public bool cinemachineSaveDuringPlayStateCaptured;
        public bool cinemachineSaveDuringPlayOriginalEnabled;
        public bool cinemachineSaveDuringPlayStateRestored;
        public int stableOutputChecks;
        public long lastObservedOutputBytes = -1;
        public string finalizationStartedUtc;
        public string error;
        public AICameraABRenderRequest request;
        public AICameraABSceneSetupSnapshot sceneSetup;
    }

    [Serializable]
    internal sealed class AICameraABSceneSetupSnapshot
    {
        public string activeScenePath;
        public List<AICameraABSceneEntry> scenes = new();
    }

    [Serializable]
    internal sealed class AICameraABSceneEntry
    {
        public string path;
        public bool isLoaded;
        public bool isActive;
    }

    [Serializable]
    internal sealed class AICameraABGeneratedCameraMetadata
    {
        public string songName;
        public string songId;
        public string datasetId;
        public string targetEvaluationGroupId;
        public string horizontalFramingPolicy;
        public double targetAspectRatio;
        public double aspectRatioOverride;
        public int sampleRate;
        public int frameCount;
        public double duration;
        public string worldCameraFile;
        public string timeFile;
        public string shotsFile;
        public AICameraABGeneratedOutputSha256 outputSha256;
    }

    [Serializable]
    internal sealed class AICameraABGeneratedOutputSha256
    {
        public string worldCamera;
        public string time;
        public string shots;
    }

    internal sealed class AICameraABCandidateProbe
    {
        public AICameraABGeneratedCameraMetadata Metadata;
        public AICameraABCandidateResult Result;
    }

    internal static class AICameraABRenderUtility
    {
        private static readonly StringComparison PathComparison =
            Application.platform == RuntimePlatform.WindowsEditor
                ? StringComparison.OrdinalIgnoreCase
                : StringComparison.Ordinal;

        internal static string UtcNow()
        {
            return DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture);
        }

        internal static AICameraABRenderCodec ParseCodec(string value)
        {
            if (Enum.TryParse(value, true, out AICameraABRenderCodec codec))
            {
                return codec;
            }

            throw new ArgumentException(
                $"Unsupported movie codec '{value}'. Use MP4 or WEBM.",
                nameof(value));
        }

        internal static string GetExtension(AICameraABRenderCodec codec)
        {
            return codec == AICameraABRenderCodec.MP4 ? "mp4" : "webm";
        }

        internal static int ToInclusiveEndFrame(
            int startFrame,
            int endFrameExclusive,
            int frameCount)
        {
            ValidateFrameRange(startFrame, endFrameExclusive, frameCount);
            return endFrameExclusive - 1;
        }

        internal static void ValidateFrameRange(
            int startFrame,
            int endFrameExclusive,
            int frameCount)
        {
            if (frameCount <= 0 ||
                startFrame < 0 ||
                endFrameExclusive <= startFrame ||
                endFrameExclusive > frameCount)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(endFrameExclusive),
                    $"Frame range [{startFrame}, {endFrameExclusive}) is invalid " +
                    $"for a {frameCount}-frame camera.");
            }
        }

        internal static void ValidateOutputSettings(
            AICameraABRenderCodec codec,
            int width,
            int height)
        {
            if (width <= 0 || height <= 0)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(width),
                    "Movie width and height must both be positive.");
            }

            if (codec == AICameraABRenderCodec.MP4 &&
                (width % 2 != 0 || height % 2 != 0))
            {
                throw new ArgumentException(
                    "MP4/H.264 output requires even width and height.");
            }
        }

        internal static AICameraABCandidateProbe ReadCandidate(
            string label,
            string generatedDirectory)
        {
            if (string.IsNullOrWhiteSpace(generatedDirectory))
            {
                throw new ArgumentException(
                    $"Candidate {label} directory is empty.",
                    nameof(generatedDirectory));
            }

            var canonicalDirectory = Path.GetFullPath(generatedDirectory);
            var metadataPath = ResolveInputFile(
                canonicalDirectory,
                "metadata.json",
                true);
            var metadata = JsonUtility.FromJson<AICameraABGeneratedCameraMetadata>(
                File.ReadAllText(metadataPath));
            if (metadata == null ||
                metadata.sampleRate <= 0 ||
                metadata.frameCount <= 0 ||
                metadata.duration <= 0 ||
                string.IsNullOrWhiteSpace(metadata.worldCameraFile) ||
                string.IsNullOrWhiteSpace(metadata.timeFile))
            {
                throw new InvalidDataException(
                    $"Candidate {label} metadata is incomplete.");
            }

            var cameraPath = ResolveInputFile(
                canonicalDirectory,
                metadata.worldCameraFile,
                true);
            var timePath = ResolveInputFile(
                canonicalDirectory,
                metadata.timeFile,
                true);
            var shotsPath = string.IsNullOrWhiteSpace(metadata.shotsFile)
                ? string.Empty
                : ResolveInputFile(
                    canonicalDirectory,
                    metadata.shotsFile,
                    true);

            var result = new AICameraABCandidateResult
            {
                label = label,
                generatedDirectory = canonicalDirectory,
                horizontalFramingPolicy = metadata.horizontalFramingPolicy,
                metadataSha256 = ComputeSha256(metadataPath),
                cameraSha256 = ComputeSha256(cameraPath),
                timeSha256 = ComputeSha256(timePath),
                shotsSha256 = string.IsNullOrWhiteSpace(shotsPath)
                    ? string.Empty
                    : ComputeSha256(shotsPath),
                declaredCameraSha256 =
                    metadata.outputSha256?.worldCamera ?? string.Empty,
                declaredTimeSha256 =
                    metadata.outputSha256?.time ?? string.Empty,
                declaredShotsSha256 =
                    metadata.outputSha256?.shots ?? string.Empty,
                songName = metadata.songName,
                songId = metadata.songId,
                datasetId = metadata.datasetId,
                targetEvaluationGroupId = metadata.targetEvaluationGroupId,
                targetAspectRatio = GetTargetAspectRatio(metadata),
                sampleRate = metadata.sampleRate,
                frameCount = metadata.frameCount,
                duration = metadata.duration,
                status = "queued"
            };

            ValidateDeclaredSha(
                label,
                "camera",
                result.declaredCameraSha256,
                result.cameraSha256);
            ValidateDeclaredSha(
                label,
                "time",
                result.declaredTimeSha256,
                result.timeSha256);
            ValidateDeclaredSha(
                label,
                "shots",
                result.declaredShotsSha256,
                result.shotsSha256);

            return new AICameraABCandidateProbe
            {
                Metadata = metadata,
                Result = result
            };
        }

        internal static AICameraABFairnessInfo ValidateCandidatePair(
            AICameraABCandidateProbe candidateA,
            AICameraABCandidateProbe candidateB,
            AICameraABRenderRequest request)
        {
            if (candidateA == null || candidateB == null)
            {
                throw new ArgumentNullException(
                    candidateA == null ? nameof(candidateA) : nameof(candidateB));
            }

            var a = candidateA.Result;
            var b = candidateB.Result;
            var sameSongId = string.Equals(
                a.songId ?? string.Empty,
                b.songId ?? string.Empty,
                StringComparison.Ordinal);
            var sameEvaluationGroup = string.Equals(
                a.targetEvaluationGroupId ?? string.Empty,
                b.targetEvaluationGroupId ?? string.Empty,
                StringComparison.Ordinal);
            var sameDatasetId = string.Equals(
                a.datasetId ?? string.Empty,
                b.datasetId ?? string.Empty,
                StringComparison.Ordinal);
            var sameAspect = Math.Abs(
                a.targetAspectRatio - b.targetAspectRatio) < 0.0001;
            var fairness = new AICameraABFairnessInfo
            {
                sameSampleRate = a.sampleRate == b.sampleRate,
                sameFrameCount = a.frameCount == b.frameCount,
                sameTimeSha256 = string.Equals(
                    a.timeSha256,
                    b.timeSha256,
                    StringComparison.OrdinalIgnoreCase),
                sameSongId = sameSongId,
                sameDatasetId = sameDatasetId,
                sameTargetAspectRatio = sameAspect,
                sameSceneAndTimeline = true,
                sharedTimeSha256 = a.timeSha256,
                sharedSongId = a.songId,
                targetEvaluationGroupId = a.targetEvaluationGroupId
            };

            if (!fairness.sameSampleRate ||
                !fairness.sameFrameCount ||
                !fairness.sameTimeSha256 ||
                !sameSongId ||
                !sameDatasetId ||
                !sameEvaluationGroup ||
                !sameAspect)
            {
                throw new InvalidDataException(
                    "A/B candidates must share sample rate, frame count, " +
                    "time.f64, songId, datasetId, evaluation group, and " +
                    "target aspect ratio.");
            }

            ValidateFrameRange(
                request.startFrame,
                request.endFrameExclusive,
                a.frameCount);
            ValidateOutputSettings(request.codec, request.width, request.height);

            if (a.targetAspectRatio > 0)
            {
                var outputAspect = request.width / (double)request.height;
                if (Math.Abs(outputAspect - a.targetAspectRatio) > 0.01)
                {
                    throw new ArgumentException(
                        $"Output aspect ratio {outputAspect:F4} does not match " +
                        $"candidate target aspect {a.targetAspectRatio:F4}.");
                }
            }

            return fairness;
        }

        internal static string ComputeSha256(string path)
        {
            using var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                1024 * 1024,
                FileOptions.SequentialScan);
            using var sha = SHA256.Create();
            return BitConverter.ToString(sha.ComputeHash(stream))
                .Replace("-", string.Empty)
                .ToLowerInvariant();
        }

        internal static string SanitizeFileName(string value)
        {
            var source = string.IsNullOrWhiteSpace(value) ? "candidate" : value;
            var invalid = Path.GetInvalidFileNameChars();
            var builder = new StringBuilder(source.Length);
            foreach (var character in source)
            {
                builder.Append(
                    Array.IndexOf(invalid, character) >= 0 ? '_' : character);
            }

            return builder.ToString().Trim().TrimEnd('.');
        }

        internal static T ReadJson<T>(string path)
        {
            if (!File.Exists(path))
            {
                return default;
            }

            return JsonUtility.FromJson<T>(File.ReadAllText(path));
        }

        internal static void WriteJsonAtomic<T>(string path, T value)
        {
            const int maximumReplaceAttempts = 8;
            var canonicalPath = Path.GetFullPath(path);
            var directory = Path.GetDirectoryName(canonicalPath)
                ?? throw new InvalidOperationException(
                    $"Path '{canonicalPath}' has no parent directory.");
            Directory.CreateDirectory(directory);

            var temporaryPath =
                $"{canonicalPath}.{Guid.NewGuid():N}.tmp";
            try
            {
                File.WriteAllText(
                    temporaryPath,
                    JsonUtility.ToJson(value, true),
                    new UTF8Encoding(false));
                for (var attempt = 0; ; attempt++)
                {
                    try
                    {
                        if (File.Exists(canonicalPath))
                        {
                            File.Replace(temporaryPath, canonicalPath, null);
                        }
                        else
                        {
                            File.Move(temporaryPath, canonicalPath);
                        }

                        return;
                    }
                    catch (IOException)
                        when (attempt + 1 < maximumReplaceAttempts)
                    {
                        Thread.Sleep(10 * (attempt + 1));
                    }
                    catch (UnauthorizedAccessException)
                        when (attempt + 1 < maximumReplaceAttempts)
                    {
                        Thread.Sleep(10 * (attempt + 1));
                    }
                }
            }
            finally
            {
                if (File.Exists(temporaryPath))
                {
                    File.Delete(temporaryPath);
                }
            }
        }

        internal static bool IsTerminalPhase(string phase)
        {
            return phase == AICameraABRenderRunner.PhaseComplete ||
                   phase == AICameraABRenderRunner.PhaseFailed ||
                   phase == AICameraABRenderRunner.PhaseCancelled;
        }

        private static double GetTargetAspectRatio(
            AICameraABGeneratedCameraMetadata metadata)
        {
            return metadata.targetAspectRatio > 0
                ? metadata.targetAspectRatio
                : metadata.aspectRatioOverride;
        }

        private static string ResolveInputFile(
            string rootDirectory,
            string relativePath,
            bool requireFile)
        {
            if (string.IsNullOrWhiteSpace(relativePath) ||
                Path.IsPathRooted(relativePath))
            {
                throw new InvalidDataException(
                    $"Generated input path '{relativePath}' must be relative.");
            }

            var root = Path.GetFullPath(rootDirectory);
            var resolved = Path.GetFullPath(Path.Combine(root, relativePath));
            var prefix = root.TrimEnd(
                    Path.DirectorySeparatorChar,
                    Path.AltDirectorySeparatorChar) +
                Path.DirectorySeparatorChar;
            if (!resolved.StartsWith(prefix, PathComparison))
            {
                throw new InvalidDataException(
                    $"Generated input '{relativePath}' escapes its directory.");
            }

            if (requireFile && !File.Exists(resolved))
            {
                throw new FileNotFoundException(
                    "Generated camera input was not found.",
                    resolved);
            }

            return resolved;
        }

        private static void ValidateDeclaredSha(
            string label,
            string inputName,
            string declared,
            string actual)
        {
            if (string.IsNullOrWhiteSpace(declared))
            {
                return;
            }

            if (string.IsNullOrWhiteSpace(actual) ||
                !string.Equals(
                    declared,
                    actual,
                    StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidDataException(
                    $"Candidate {label} {inputName} SHA256 does not match metadata.");
            }
        }
    }
}
