using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using Unity.Cinemachine;
using UnityEditor;
using UnityEditor.Recorder.Timeline;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.SceneManagement;
using UnityEngine.Timeline;

namespace Streamingle.Editor
{
    public static class AICameraTimelinePreviewImporter
    {
        public enum CurveSimplificationPreset
        {
            Exact,
            Balanced,
            Editable
        }

        internal const string OriginalDirectorName = "Timeline";
        internal const string PreviewDirectorName = "Timeline_AI_Preview";
        internal const string PreviewCameraName = "AI_CameraPreview";
        internal const string GeneratedAnimationTrackName =
            "AI Generated Camera Motion";
        internal const string GeneratedAnimationTrackPrefix =
            "AI Camera Motion";
        internal const string GeneratedCinemachineTrackName =
            "AI Generated Cinemachine";
        private const string PreviewVirtualCameraName = "GeneratedVirtualCamera";
        private const string PreviewAssetFolderName = "AI_CameraPreview";
        private const string FinalAssetFolderName = "AI_CameraFinal";
        private const string ShotAnimationFolderName = "Shots";
        private const string CameraPrefabFolderName = "Cameras";
        private const string PreviewGenerationProvenanceName =
            "AI Camera Preview Generation Provenance";
        private const double CutBoundaryGuardSeconds = 0.000001;
        private const int DirectionChangeDetectionFlankFrames = 5;
        // Matches the worker's 0.35-second braking radius at 60 fps.
        private const int DirectionChangePreserveRadiusFrames = 21;
        private const float DirectionChangeMinimumSpeedMetersPerSecond = 0.04f;
        // Preserve the generated braking envelope for every concentrated turn
        // covered by the worker policy (40 degrees and above), not only an
        // almost complete reversal.  Linear curve simplification may otherwise
        // collapse a mild 40--55 or clear 60/90-degree slowdown into a kink.
        private const float DirectionChangeMaximumCosine = 0.76604444f;
        private const double PositionCurveAuditTimeToleranceSeconds = 0.000000001;
        private const float PositionCurveAuditErrorEpsilonMeters = 0.0001f;
        private const int PositionCurveAuditLocalWindowRadiusSamples = 2;
        private const float PositionCurveAuditSpeedRatio = 1.5f;
        private const float PositionCurveAuditSpeedDeltaMetersPerSecond = 0.25f;
        private const float PositionCurveAuditSpeedFloorMetersPerSecond = 0.5f;
        private const float PositionCurveAuditAccelerationRatio = 2f;
        private const float PositionCurveAuditAccelerationDeltaMetersPerSecondSquared =
            5f;
        private const float PositionCurveAuditAccelerationFloorMetersPerSecondSquared =
            8f;
        private const float PositionCurveAuditJerkRatio = 3f;
        private const float PositionCurveAuditJerkDeltaMetersPerSecondCubed = 150f;
        private const float PositionCurveAuditJerkFloorMetersPerSecondCubed = 250f;
        private const float PositionCurveAuditMotionNoiseDeadbandMeters = 0.0005f;
        private const float PositionCurveAuditMotionRetentionMinimumMeters = 0.002f;
        private const float PositionCurveAuditMotionRetentionRatio = 0.65f;
        private const float RotationCurveAuditAngleErrorEpsilonDegrees = 0.01f;
        private const float RotationCurveAuditSpeedRatio = 1.15f;
        private const float RotationCurveAuditSpeedDeltaDegreesPerSecond = 2f;
        private const float RotationCurveAuditSpeedFloorDegreesPerSecond = 15f;
        private const float RotationCurveAuditAccelerationRatio = 1.25f;
        private const float RotationCurveAuditAccelerationDeltaDegreesPerSecondSquared =
            60f;
        private const float RotationCurveAuditAccelerationFloorDegreesPerSecondSquared =
            240f;
        // Reconstructing a continuous quaternion path from 60 Hz samples can
        // legitimately reveal a higher finite between-frame peak. These limits
        // still reject the former C1 seam spikes (2.3x-3.8x on shots 13/66/68)
        // without treating bounded C2 interpolation as a regression.
        private const float RotationCurveAuditJerkRatio = 1.85f;
        private const float RotationCurveAuditJerkDeltaDegreesPerSecondCubed = 1200f;
        private const float RotationCurveAuditJerkFloorDegreesPerSecondCubed = 1800f;
        private const float RotationCurveAuditMotionNoiseDeadbandDegrees = 0.02f;
        private const float RotationCurveAuditMotionRetentionMinimumDegrees = 0.15f;
        private const float RotationCurveAuditMotionRetentionRatio = 0.75f;
        private const int CurveAuditPrimarySamplesPerSecond = 240;
        private const int CurveAuditConvergenceSamplesPerSecond = 480;
        private const float CurveAuditConvergenceRatio = 1.35f;
        private const double CurveAuditLocalWindowSeconds = 0.125;
        private const float CurveC1AbsoluteTolerance = 0.0001f;
        // AnimationCurve stores key times, values and tangents as floats. At a
        // 60 Hz knot this is below the acceleration represented by one float
        // ULP at ordinary stage scale, while still rejecting a visible C1-only
        // corner by several orders of magnitude.
        private const float CurveC2AbsoluteTolerance = 0.05f;
        private const float CurveC2RelativeTolerance = 0.005f;
        private const float PositionCurveAuditConvergenceSpeedFloor = 0.1f;
        private const float PositionCurveAuditConvergenceAccelerationFloor = 2f;
        // Third finite differences amplify AnimationCurve's float evaluation
        // noise by dt^-3. The analytic C2 seam audit remains the primary guard;
        // these floors reject rate-divergent impulses above the numeric floor.
        private const float PositionCurveAuditConvergenceJerkFloor = 700f;
        private const float RotationCurveAuditConvergenceSpeedFloor = 5f;
        private const float RotationCurveAuditConvergenceAccelerationFloor = 120f;
        private const float RotationCurveAuditConvergenceJerkFloor = 3600f;
        public const CurveSimplificationPreset DefaultCurveSimplificationPreset =
            CurveSimplificationPreset.Balanced;

        /// <summary>
        /// Assets and scene camera root produced by <see cref="SaveFinalForEditor"/>.
        /// The caller is responsible for saving the destination scene when ready.
        /// </summary>
        public sealed class FinalizedPreviewResult
        {
            internal FinalizedPreviewResult(
                string assetFolderPath,
                TimelineAsset timeline,
                AnimationClip[] animationClips,
                GameObject cameraPrefab,
                GameObject cameraRoot)
            {
                AssetFolderPath = assetFolderPath;
                Timeline = timeline;
                AnimationClips = animationClips;
                CameraPrefab = cameraPrefab;
                CameraRoot = cameraRoot;
            }

            public string AssetFolderPath { get; }
            public TimelineAsset Timeline { get; }
            public IReadOnlyList<AnimationClip> AnimationClips { get; }
            public GameObject CameraPrefab { get; }
            public GameObject CameraRoot { get; }
        }

        internal readonly struct CurveSimplificationSettings
        {
            public CurveSimplificationSettings(
                float positionErrorMeters,
                float quaternionAngleErrorDegrees,
                float fieldOfViewErrorDegrees,
                float dutchErrorDegrees)
            {
                PositionErrorMeters = positionErrorMeters;
                QuaternionAngleErrorDegrees = quaternionAngleErrorDegrees;
                FieldOfViewErrorDegrees = fieldOfViewErrorDegrees;
                DutchErrorDegrees = dutchErrorDegrees;
            }

            public float PositionErrorMeters { get; }
            public float QuaternionAngleErrorDegrees { get; }
            public float FieldOfViewErrorDegrees { get; }
            public float DutchErrorDegrees { get; }
        }

        internal readonly struct PreviewGenerationProvenanceInfo
        {
            internal PreviewGenerationProvenanceInfo(
                string cwAiRelativeGeneratedFolder,
                string absoluteGeneratedFolder,
                string metadataSha256)
            {
                CwAiRelativeGeneratedFolder =
                    cwAiRelativeGeneratedFolder ?? string.Empty;
                AbsoluteGeneratedFolder = absoluteGeneratedFolder ?? string.Empty;
                MetadataSha256 = metadataSha256 ?? string.Empty;
            }

            internal string CwAiRelativeGeneratedFolder { get; }
            internal string AbsoluteGeneratedFolder { get; }
            internal string MetadataSha256 { get; }
        }

        [MenuItem("Tools/Streamingle/Timeline/Create AI Camera Preview")]
        public static void CreatePreviewFromMenu()
        {
            var generatedDirectory = EditorUtility.OpenFolderPanel(
                "Select generated AI camera directory",
                Directory.GetParent(Application.dataPath)?.FullName ?? string.Empty,
                string.Empty);
            if (string.IsNullOrWhiteSpace(generatedDirectory))
            {
                return;
            }

            try
            {
                var result = CreatePreviewForCli(generatedDirectory);
                Debug.Log(result);
            }
            catch (Exception exception)
            {
                Debug.LogException(exception);
                throw;
            }
        }

        [MenuItem("Tools/Streamingle/Timeline/Remove AI Camera Preview")]
        public static void RemovePreviewFromMenu()
        {
            try
            {
                Debug.Log(RemovePreviewForCli());
            }
            catch (Exception exception)
            {
                Debug.LogException(exception);
                throw;
            }
        }

        /// <summary>
        /// Creates an unsaved scene preview while preserving the original Timeline asset.
        /// New Timeline and AnimationClip assets are stored beside the active scene.
        /// </summary>
        public static string ValidateGeneratedDirectoryForCli(
            string generatedDirectory)
        {
            var generatedDirectoryPath = Path.GetFullPath(generatedDirectory);
            var metadataPath = Path.Combine(generatedDirectoryPath, "metadata.json");
            if (!File.Exists(metadataPath))
            {
                throw new FileNotFoundException(
                    "Generated camera metadata was not found.",
                    metadataPath);
            }

            var metadata = ReadGeneratedCameraMetadata(metadataPath);
            ValidateMetadata(metadata);
            var cameraPath = ResolveGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.worldCameraFile,
                "worldCamera");
            var timePath = ResolveGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.timeFile,
                "time");
            var shotsPath = ResolveOptionalGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.shotsFile,
                "shots");
            ValidateGeneratedOutputHashes(
                metadata,
                cameraPath,
                timePath,
                shotsPath);
            var frames = ReadCameraFrames(cameraPath, metadata.frameCount);
            var times = ReadTimes(timePath, metadata.frameCount);
            ValidateGeneratedCameraData(metadata, frames, times);
            var shots = ReadShotDefinitions(metadata, times, shotsPath);
            return
                $"{metadata.songName}: {metadata.frameCount:N0} frames, " +
                $"{shots.Count:N0} shots at {metadata.sampleRate} FPS";
        }

        public static string CreatePreviewForCli(string generatedDirectory)
        {
            return CreatePreviewForCli(
                generatedDirectory,
                DefaultCurveSimplificationPreset);
        }

        public static string CreatePreviewForCli(
            string generatedDirectory,
            CurveSimplificationPreset curvePreset)
        {
            return CreatePreview(generatedDirectory, curvePreset, null);
        }

        /// <summary>
        /// Creates a preview for exactly <paramref name="sourceDirector"/>.
        /// This overload supports scenes containing several song directors under
        /// a shared TimeLine root without consulting any other director.
        /// </summary>
        public static string CreatePreviewForCli(
            string generatedDirectory,
            CurveSimplificationPreset curvePreset,
            PlayableDirector sourceDirector)
        {
            return CreatePreview(
                generatedDirectory,
                curvePreset,
                RequireSourceDirector(sourceDirector));
        }

        public static string CreatePreviewForCli(
            string generatedDirectory,
            CurveSimplificationPreset curvePreset,
            PlayableDirector sourceDirector,
            string cwAiRoot)
        {
            return CreatePreview(
                generatedDirectory,
                curvePreset,
                RequireSourceDirector(sourceDirector),
                cwAiRoot);
        }

        public static string CreatePreviewForCli(
            string generatedDirectory,
            PlayableDirector sourceDirector)
        {
            return CreatePreviewForCli(
                generatedDirectory,
                DefaultCurveSimplificationPreset,
                sourceDirector);
        }

        public static string CreatePreviewWithPresetForCli(
            string generatedDirectory,
            string curvePreset)
        {
            return CreatePreview(
                generatedDirectory,
                ParseCurveSimplificationPreset(curvePreset),
                null);
        }

        public static string CreatePreviewWithPresetForCli(
            string generatedDirectory,
            string curvePreset,
            PlayableDirector sourceDirector)
        {
            return CreatePreviewForCli(
                generatedDirectory,
                ParseCurveSimplificationPreset(curvePreset),
                sourceDirector);
        }

        private static string CreatePreview(
            string generatedDirectory,
            CurveSimplificationPreset curvePreset,
            PlayableDirector explicitSourceDirector,
            string cwAiRoot = null)
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                throw new InvalidOperationException(
                    "AI camera preview must be created in Edit Mode.");
            }

            var generatedDirectoryPath = Path.GetFullPath(generatedDirectory);
            var metadataPath = Path.Combine(generatedDirectoryPath, "metadata.json");
            if (!File.Exists(metadataPath))
            {
                throw new FileNotFoundException(
                    "Generated camera metadata was not found.",
                    metadataPath);
            }

            var metadata = ReadGeneratedCameraMetadata(metadataPath);
            ValidateMetadata(metadata);
            var cameraPath = ResolveGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.worldCameraFile,
                "worldCamera");
            var timePath = ResolveGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.timeFile,
                "time");
            var shotsPath = ResolveOptionalGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.shotsFile,
                "shots");
            ValidateGeneratedOutputHashes(
                metadata,
                cameraPath,
                timePath,
                shotsPath);
            var frames = ReadCameraFrames(cameraPath, metadata.frameCount);
            var times = ReadTimes(timePath, metadata.frameCount);
            ValidateGeneratedCameraData(metadata, frames, times);
            var shotDefinitions = ReadShotDefinitions(metadata, times, shotsPath);

            var isExplicitSource = explicitSourceDirector != null;
            var scene = isExplicitSource
                ? explicitSourceDirector.gameObject.scene
                : SceneManager.GetActiveScene();
            var originalDirector = explicitSourceDirector ??
                                   FindOriginalDirector(scene);
            var originalTimeline = originalDirector.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    $"'{OriginalDirectorName}' is not bound to a TimelineAsset.");
            var originalTimelinePath = AssetDatabase.GetAssetPath(originalTimeline);
            if (string.IsNullOrWhiteSpace(originalTimelinePath))
            {
                throw new InvalidOperationException(
                    "The original Timeline is not a project asset.");
            }

            var previewDirectorName = isExplicitSource
                ? GetScopedPreviewDirectorName(originalDirector)
                : PreviewDirectorName;
            var previewCameraName = isExplicitSource
                ? GetScopedPreviewCameraName(originalDirector)
                : PreviewCameraName;
            EnsureNoExistingPreview(
                scene,
                previewDirectorName,
                previewCameraName);
            var brain = FindCinemachineBrain(originalDirector, originalTimeline);
            var assetFolder = EnsurePreviewAssetFolder(scene.path);
            if (isExplicitSource)
            {
                assetFolder = EnsureAssetSubfolder(
                    assetFolder,
                    GetDirectorAssetScopeName(originalDirector));
            }
            var shotAnimationFolder = EnsureAssetSubfolder(
                assetFolder,
                ShotAnimationFolderName);
            var copiedTimelinePath = AssetDatabase.GenerateUniqueAssetPath(
                $"{assetFolder}/{originalTimeline.name}_AI_Preview.playable");

            if (!AssetDatabase.CopyAsset(originalTimelinePath, copiedTimelinePath))
            {
                throw new IOException(
                    $"Unable to copy Timeline asset to '{copiedTimelinePath}'.");
            }

            AssetDatabase.ImportAsset(
                copiedTimelinePath,
                ImportAssetOptions.ForceSynchronousImport);
            var copiedTimeline = AssetDatabase.LoadAssetAtPath<TimelineAsset>(
                copiedTimelinePath);
            if (copiedTimeline == null)
            {
                throw new InvalidOperationException(
                    $"Unable to load copied Timeline '{copiedTimelinePath}'.");
            }

            GameObject previewDirectorObject = null;
            GameObject previewCameraObject = null;
            var generatedClips = new List<AnimationClip>();
            var generatedClipPaths = new List<string>();
            var originalDirectorWasActive =
                originalDirector.gameObject.activeSelf;
            try
            {
                previewDirectorObject = InstantiateInactivePreviewCloneForEditor(
                    originalDirector.gameObject,
                    originalDirector.transform.parent);
                Undo.RegisterCreatedObjectUndo(
                    previewDirectorObject,
                    "Create AI Timeline Preview");
                previewDirectorObject.name = previewDirectorName;
                var previewDirector = GetClonedDirectorComponent(
                    originalDirector,
                    previewDirectorObject);
                previewDirector.playableAsset = copiedTimeline;
                CopyTrackBindings(
                    originalDirector,
                    originalTimeline,
                    previewDirector,
                    copiedTimeline);
                SuppressAuthoredCinemachineOutputsForEditor(previewDirector);
                MuteCopiedCameraTracks(previewDirector, copiedTimeline);
                MuteCopiedRecorderTracks(copiedTimeline);

                previewCameraObject = new GameObject(previewCameraName);
                if (previewCameraObject.scene != scene)
                {
                    SceneManager.MoveGameObjectToScene(previewCameraObject, scene);
                }
                Undo.RegisterCreatedObjectUndo(
                    previewCameraObject,
                    "Create AI Camera Preview");
                previewCameraObject.transform.SetPositionAndRotation(
                    Vector3.zero,
                    Quaternion.identity);
                var shotCameras = CreateShotCameras(
                    previewCameraObject.transform,
                    shotDefinitions,
                    frames);

                var animationGroup = copiedTimeline.CreateTrack<GroupTrack>(
                    null,
                    GeneratedAnimationTrackName);
                var positionDenseFallbackCount = 0;
                foreach (var shotCamera in shotCameras)
                {
                    var generatedClip = BuildShotAnimationClip(
                        metadata.songName,
                        metadata.sampleRate,
                        frames,
                        times,
                        shotCamera,
                        curvePreset,
                        out var positionUsedExactFallback,
                        out var positionFallbackReason);
                    if (positionUsedExactFallback)
                    {
                        positionDenseFallbackCount++;
                        Debug.LogWarning(
                            $"AI camera shot {shotCamera.Definition.index:D3} " +
                            $"({shotCamera.Definition.cameraName}) used dense C2 " +
                            $"position curves after the reduced-curve audit: " +
                            $"{positionFallbackReason}");
                    }
                    var animationClipPath = AssetDatabase.GenerateUniqueAssetPath(
                        $"{shotAnimationFolder}/" +
                        $"{shotCamera.Definition.index:D3}_" +
                        $"{SanitizeAssetName(shotCamera.Definition.cameraName)}.anim");
                    AssetDatabase.CreateAsset(generatedClip, animationClipPath);
                    generatedClips.Add(generatedClip);
                    generatedClipPaths.Add(animationClipPath);

                    CreateGeneratedAnimationTrackForCamera(
                        copiedTimeline,
                        animationGroup,
                        previewDirector,
                        shotCamera.Camera.gameObject,
                        generatedClip,
                        shotCamera.Definition.index,
                        shotCamera.Definition.cameraName,
                        shotCamera.Definition.start,
                        shotCamera.Definition.duration);
                }

                var cinemachineTrack = copiedTimeline.CreateTrack<CinemachineTrack>(
                    null,
                    GeneratedCinemachineTrackName);
                foreach (var shotCamera in shotCameras)
                {
                    var shotTimelineClip = cinemachineTrack.CreateDefaultClip();
                    shotTimelineClip.displayName = shotCamera.Definition.cameraName;
                    shotTimelineClip.start = shotCamera.Definition.start;
                    shotTimelineClip.duration =
                        shotCamera.Definition.duration -
                        (shotCamera.Definition.endFrameExclusive < metadata.frameCount
                            ? CutBoundaryGuardSeconds
                            : 0);
                    shotTimelineClip.easeInDuration = 0;
                    shotTimelineClip.easeOutDuration = 0;
                    SetClipExtrapolationNone(shotTimelineClip);
                    var shot = shotTimelineClip.asset as CinemachineShot
                        ?? throw new InvalidOperationException(
                            "Unable to create a Cinemachine shot clip.");
                    shot.DisplayName = shotCamera.Definition.cameraName;
                    var exposedName = new PropertyName(
                        $"AI_Camera_{Guid.NewGuid():N}");
                    shot.VirtualCamera.exposedName = exposedName;
                    shot.VirtualCamera.defaultValue = shotCamera.Camera;
                    previewDirector.SetReferenceValue(
                        exposedName,
                        shotCamera.Camera);
                }

                previewDirector.SetGenericBinding(cinemachineTrack, brain);

                Undo.RecordObject(
                    originalDirector.gameObject,
                    "Activate AI Timeline Preview");
                originalDirector.gameObject.SetActive(false);
                previewDirectorObject.SetActive(true);
                previewDirector.time = 0;
                previewDirector.RebuildGraph();
                previewDirector.Evaluate();

                var provenance = UpsertPreviewGenerationProvenance(
                    copiedTimeline,
                    generatedDirectoryPath,
                    metadataPath,
                    cwAiRoot,
                    false,
                    "Create AI Camera Preview Provenance");

                EditorUtility.SetDirty(copiedTimeline);
                EditorUtility.SetDirty(previewDirector);
                EditorUtility.SetDirty(provenance);
                foreach (var generatedClip in generatedClips)
                {
                    EditorUtility.SetDirty(generatedClip);
                }

                EditorSceneManager.MarkSceneDirty(scene);
                AssetDatabase.SaveAssetIfDirty(copiedTimeline);
                AssetDatabase.SaveAssetIfDirty(provenance);
                foreach (var generatedClip in generatedClips)
                {
                    AssetDatabase.SaveAssetIfDirty(generatedClip);
                }

                Selection.activeGameObject = previewDirectorObject;

                return string.Join(
                    Environment.NewLine,
                    "AI camera Timeline preview created.",
                    $"Song: {metadata.songName}",
                    $"Frames: {metadata.frameCount:N0} at {metadata.sampleRate} FPS",
                    $"Shots: {shotDefinitions.Count:N0}",
                    $"Curve Simplification: {curvePreset}",
                    $"Position Dense C2 Fallbacks: {positionDenseFallbackCount:N0}",
                    $"Preview Director: {GetHierarchyPath(previewDirector.transform)}",
                    $"Preview Camera Root: {GetHierarchyPath(previewCameraObject.transform)}",
                    GetPreviewOutputStatusForEditor(brain),
                    $"Timeline Asset: {copiedTimelinePath}",
                    $"Animation Assets: {generatedClipPaths.Count:N0} " +
                    $"under {shotAnimationFolder}",
                    "Original Timeline asset was not modified.",
                    "The scene remains unsaved so existing user changes are preserved.");
            }
            catch
            {
                if (previewDirectorObject != null)
                {
                    UnityEngine.Object.DestroyImmediate(previewDirectorObject);
                }

                if (previewCameraObject != null)
                {
                    UnityEngine.Object.DestroyImmediate(previewCameraObject);
                }

                foreach (var generatedClipPath in generatedClipPaths)
                {
                    AssetDatabase.DeleteAsset(generatedClipPath);
                }

                AssetDatabase.DeleteAsset(copiedTimelinePath);
                originalDirector.gameObject.SetActive(originalDirectorWasActive);
                throw;
            }
        }

        /// <summary>
        /// Replaces one generated camera animation while preserving every other
        /// camera, Timeline clip, and AnimationClip asset GUID.
        /// The candidate must use exactly the same frame count and shot boundaries.
        /// </summary>
        public static string RegenerateShotForCli(
            string generatedDirectory,
            int shotIndex)
        {
            return RegenerateShot(
                generatedDirectory,
                shotIndex,
                DefaultCurveSimplificationPreset,
                null);
        }

        public static string RegenerateShotForCli(
            string generatedDirectory,
            int shotIndex,
            CurveSimplificationPreset curvePreset)
        {
            return RegenerateShot(
                generatedDirectory,
                shotIndex,
                curvePreset,
                null);
        }

        public static string RegenerateShotForCli(
            string generatedDirectory,
            int shotIndex,
            CurveSimplificationPreset curvePreset,
            PlayableDirector sourceDirector)
        {
            return RegenerateShot(
                generatedDirectory,
                shotIndex,
                curvePreset,
                RequireSourceDirector(sourceDirector));
        }

        public static string RegenerateShotForCli(
            string generatedDirectory,
            int shotIndex,
            CurveSimplificationPreset curvePreset,
            PlayableDirector sourceDirector,
            string cwAiRoot)
        {
            return RegenerateShot(
                generatedDirectory,
                shotIndex,
                curvePreset,
                RequireSourceDirector(sourceDirector),
                cwAiRoot);
        }

        public static string RegenerateShotForCli(
            string generatedDirectory,
            int shotIndex,
            PlayableDirector sourceDirector)
        {
            return RegenerateShotForCli(
                generatedDirectory,
                shotIndex,
                DefaultCurveSimplificationPreset,
                sourceDirector);
        }

        public static string RegenerateShotWithPresetForCli(
            string generatedDirectory,
            int shotIndex,
            string curvePreset)
        {
            return RegenerateShot(
                generatedDirectory,
                shotIndex,
                ParseCurveSimplificationPreset(curvePreset),
                null);
        }

        public static string RegenerateShotWithPresetForCli(
            string generatedDirectory,
            int shotIndex,
            string curvePreset,
            PlayableDirector sourceDirector)
        {
            return RegenerateShotForCli(
                generatedDirectory,
                shotIndex,
                ParseCurveSimplificationPreset(curvePreset),
                sourceDirector);
        }

        private static string RegenerateShot(
            string generatedDirectory,
            int shotIndex,
            CurveSimplificationPreset curvePreset,
            PlayableDirector explicitSourceDirector,
            string cwAiRoot = null)
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                throw new InvalidOperationException(
                    "An AI camera shot must be regenerated in Edit Mode.");
            }

            var generatedDirectoryPath = Path.GetFullPath(generatedDirectory);
            var metadataPath = Path.Combine(generatedDirectoryPath, "metadata.json");
            if (!File.Exists(metadataPath))
            {
                throw new FileNotFoundException(
                    "Generated camera metadata was not found.",
                    metadataPath);
            }

            var metadata = ReadGeneratedCameraMetadata(metadataPath);
            ValidateMetadata(metadata);
            var cameraPath = ResolveGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.worldCameraFile,
                "worldCamera");
            var timePath = ResolveGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.timeFile,
                "time");
            var shotsPath = ResolveOptionalGeneratedPayloadPath(
                generatedDirectoryPath,
                metadata.shotsFile,
                "shots");
            ValidateGeneratedOutputHashes(
                metadata,
                cameraPath,
                timePath,
                shotsPath);
            var frames = ReadCameraFrames(cameraPath, metadata.frameCount);
            var times = ReadTimes(timePath, metadata.frameCount);
            ValidateGeneratedCameraData(metadata, frames, times);
            var shotDefinitions = ReadShotDefinitions(metadata, times, shotsPath);
            if (shotIndex < 0 || shotIndex >= shotDefinitions.Count)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(shotIndex),
                    shotIndex,
                    $"Shot index must be between 0 and {shotDefinitions.Count - 1}.");
            }

            var context = explicitSourceDirector != null
                ? FindPreviewContext(explicitSourceDirector)
                : FindLegacyPreviewContext(SceneManager.GetActiveScene());
            var scene = context.Scene;
            var previewDirector = context.Director;
            var previewRoot = context.CameraRoot;
            var timeline = previewDirector.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    "The AI preview director is not bound to a TimelineAsset.");
            var animationEntries = GetGeneratedAnimationEntries(timeline);
            var animationClips = animationEntries
                .Select(entry => entry.Clip)
                .ToArray();
            var cinemachineTrack = timeline.GetOutputTracks()
                .OfType<CinemachineTrack>()
                .SingleOrDefault(track =>
                    track.name == GeneratedCinemachineTrackName)
                ?? throw new InvalidOperationException(
                    $"Timeline does not contain '{GeneratedCinemachineTrackName}'.");
            var cinemachineClips = cinemachineTrack.GetClips()
                .OrderBy(clip => clip.start)
                .ToArray();

            if (animationClips.Length != shotDefinitions.Count ||
                cinemachineClips.Length != shotDefinitions.Count ||
                previewRoot.transform.childCount != shotDefinitions.Count)
            {
                throw new InvalidDataException(
                    "Candidate shot count does not match the generated Timeline. " +
                    "Run a full generation instead.");
            }

            var tolerance = 0.25 / metadata.sampleRate;
            for (var index = 0; index < shotDefinitions.Count; index++)
            {
                var definition = shotDefinitions[index];
                if (Math.Abs(animationClips[index].start - definition.start) >
                        tolerance ||
                    Math.Abs(animationClips[index].duration - definition.duration) >
                        tolerance ||
                    Math.Abs(cinemachineClips[index].start - definition.start) >
                        tolerance)
                {
                    throw new InvalidDataException(
                        $"Candidate shot {index} has different cut boundaries. " +
                        "Run a full generation instead.");
                }
            }

            var selectedAnimationEntry = animationEntries[shotIndex];
            var selectedTimelineClip = selectedAnimationEntry.Clip;
            var existingClip = selectedTimelineClip.animationClip
                ?? throw new InvalidOperationException(
                    $"Shot {shotIndex} does not reference an AnimationClip.");
            AddPreviewAssetPath(new List<string>(), existingClip);

            var cameraTransform = previewRoot.transform.GetChild(shotIndex);
            var camera = cameraTransform.GetComponent<CinemachineCamera>()
                ?? throw new InvalidOperationException(
                    $"Generated camera {shotIndex} is missing CinemachineCamera.");
            var definitionForReplacement = shotDefinitions[shotIndex];
            var boundAnimator = previewDirector.GetGenericBinding(
                selectedAnimationEntry.Track) as Animator;
            var relativePath = boundAnimator != null &&
                               boundAnimator.gameObject == camera.gameObject
                ? string.Empty
                : cameraTransform.name;
            var replacement = BuildShotAnimationClip(
                metadata.songName,
                metadata.sampleRate,
                frames,
                times,
                new PreviewShotCamera
                {
                    Definition = definitionForReplacement,
                    RelativePath = relativePath,
                    Camera = camera
                },
                curvePreset,
                out var positionUsedExactFallback,
                out var positionFallbackReason);
            if (positionUsedExactFallback)
            {
                Debug.LogWarning(
                    $"AI camera shot {shotIndex:D3} used dense C2 position " +
                    $"curves after the reduced-curve audit: " +
                    $"{positionFallbackReason}");
            }

            var previousDirectorTime = previewDirector.time;
            var undoName = $"Regenerate AI Camera Shot {shotIndex:D3}";
            Undo.IncrementCurrentGroup();
            var undoGroup = Undo.GetCurrentGroup();
            Undo.SetCurrentGroupName(undoName);
            var undoReverted = false;
            try
            {
                Undo.RegisterCompleteObjectUndo(
                    existingClip,
                    undoName);
                Undo.RecordObject(
                    cameraTransform,
                    undoName);
                Undo.RecordObject(
                    camera,
                    undoName);
                Undo.RegisterCompleteObjectUndo(
                    selectedAnimationEntry.Track,
                    undoName);

                var preservedAssetName = existingClip.name;
                EditorUtility.CopySerialized(replacement, existingClip);
                existingClip.name = preservedAssetName;
                UnityEngine.Object.DestroyImmediate(replacement);

                var firstFrame = frames[definitionForReplacement.startFrame];
                cameraTransform.SetLocalPositionAndRotation(
                    firstFrame.Position,
                    firstFrame.Rotation);
                if (string.IsNullOrEmpty(relativePath))
                {
                    ConfigurePerCameraTrackOffset(
                        selectedAnimationEntry.Track,
                        cameraTransform);
                }
                var lens = camera.Lens;
                lens.FieldOfView = firstFrame.FieldOfView;
                lens.Dutch = firstFrame.Dutch;
                camera.Lens = lens;

                selectedTimelineClip.displayName = cameraTransform.name;
                var provenance = UpsertPreviewGenerationProvenance(
                    timeline,
                    generatedDirectoryPath,
                    metadataPath,
                    cwAiRoot,
                    true,
                    undoName);
                EditorUtility.SetDirty(existingClip);
                EditorUtility.SetDirty(provenance);
                EditorUtility.SetDirty(camera);
                EditorUtility.SetDirty(timeline);
                EditorUtility.SetDirty(previewDirector);
                EditorSceneManager.MarkSceneDirty(scene);
                AssetDatabase.SaveAssetIfDirty(existingClip);
                AssetDatabase.SaveAssetIfDirty(provenance);
                AssetDatabase.SaveAssetIfDirty(timeline);

                previewDirector.time = Math.Max(
                    definitionForReplacement.start,
                    Math.Min(
                        previousDirectorTime,
                        definitionForReplacement.end - 1.0 / metadata.sampleRate));
                previewDirector.RebuildGraph();
                previewDirector.Evaluate();
                Selection.activeGameObject = camera.gameObject;
            }
            catch
            {
                undoReverted = true;
                Undo.RevertAllDownToGroup(undoGroup);
                throw;
            }
            finally
            {
                if (replacement != null)
                {
                    UnityEngine.Object.DestroyImmediate(replacement);
                }

                if (!undoReverted)
                {
                    Undo.CollapseUndoOperations(undoGroup);
                }
            }

            return string.Join(
                Environment.NewLine,
                $"AI camera shot {shotIndex:D3} regenerated.",
                $"Camera: {cameraTransform.name}",
                $"Frames: {definitionForReplacement.startFrame:N0}-" +
                $"{definitionForReplacement.endFrameExclusive - 1:N0}",
                $"Curve Simplification: {curvePreset}",
                positionUsedExactFallback
                    ? $"Position Curves: Dense C2 fallback " +
                      $"({positionFallbackReason})"
                    : "Position Curves: requested preset passed motion audit",
                $"Animation Asset: {AssetDatabase.GetAssetPath(existingClip)}",
                "All other generated camera clips were preserved.",
                "The scene remains unsaved.");
        }

        public static string[] GetGeneratedShotNamesForEditor()
        {
            return TryFindLegacyPreviewContext(
                SceneManager.GetActiveScene(),
                out var context)
                ? GetGeneratedShotNames(context.CameraRoot)
                : Array.Empty<string>();
        }

        public static string[] GetGeneratedShotNamesForEditor(
            PlayableDirector sourceDirector)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            return TryFindPreviewContext(sourceDirector, out var context)
                ? GetGeneratedShotNames(context.CameraRoot)
                : Array.Empty<string>();
        }

        private static string[] GetGeneratedShotNames(GameObject previewRoot)
        {
            if (previewRoot == null)
            {
                return Array.Empty<string>();
            }

            return Enumerable.Range(0, previewRoot.transform.childCount)
                .Select(index =>
                    $"{index:D3} | {previewRoot.transform.GetChild(index).name}")
                .ToArray();
        }

        /// <summary>
        /// A generated shot resolved from either the Timeline selection or the
        /// selected generated camera GameObject.
        /// </summary>
        public readonly struct GeneratedShotSelection
        {
            internal GeneratedShotSelection(
                int shotIndex,
                string cameraName,
                double start,
                double duration,
                GameObject cameraObject,
                AnimationClip animationClip)
            {
                ShotIndex = shotIndex;
                CameraName = cameraName;
                Start = start;
                Duration = duration;
                CameraObject = cameraObject;
                AnimationClip = animationClip;
            }

            public int ShotIndex { get; }
            public string CameraName { get; }
            public double Start { get; }
            public double Duration { get; }
            public GameObject CameraObject { get; }
            public AnimationClip AnimationClip { get; }
        }

        /// <summary>
        /// Resolves the camera or clip currently selected by the user. Timeline
        /// clip selection takes priority, followed by an AnimationClip asset and
        /// a generated camera selected in the Hierarchy.
        /// </summary>
        public static bool TryGetSelectedGeneratedShotForEditor(
            PlayableDirector sourceDirector,
            out GeneratedShotSelection selection)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            if (!TryFindPreviewContext(sourceDirector, out var context))
            {
                selection = default;
                return false;
            }

            var timeline = context.Director.playableAsset as TimelineAsset;
            if (timeline == null ||
                !TryResolveGeneratedShotIndexForEditor(
                    timeline,
                    context.CameraRoot,
                    GetTimelineEditorSelectedClip(),
                    Selection.activeGameObject,
                    Selection.activeObject,
                    out var shotIndex))
            {
                selection = default;
                return false;
            }

            var entries = GetGeneratedAnimationEntries(timeline);
            if (shotIndex < 0 ||
                shotIndex >= entries.Length ||
                shotIndex >= context.CameraRoot.transform.childCount)
            {
                selection = default;
                return false;
            }

            var cameraObject = context.CameraRoot.transform
                .GetChild(shotIndex)
                .gameObject;
            var timelineClip = entries[shotIndex].Clip;
            selection = new GeneratedShotSelection(
                shotIndex,
                cameraObject.name,
                timelineClip.start,
                timelineClip.duration,
                cameraObject,
                timelineClip.animationClip);
            return true;
        }

        public static int GetSelectedGeneratedShotIndexForEditor(
            PlayableDirector sourceDirector)
        {
            return TryGetSelectedGeneratedShotForEditor(
                sourceDirector,
                out var selection)
                ? selection.ShotIndex
                : -1;
        }

        public static bool TryGetSelectedGeneratedShotForEditor(
            PlayableDirector sourceDirector,
            out int shotIndex,
            out string cameraName)
        {
            if (TryGetSelectedGeneratedShotForEditor(
                    sourceDirector,
                    out GeneratedShotSelection selection))
            {
                shotIndex = selection.ShotIndex;
                cameraName = selection.CameraName;
                return true;
            }

            shotIndex = -1;
            cameraName = string.Empty;
            return false;
        }

        /// <summary>
        /// Selects a generated camera and seeks the preview director into its
        /// shot. This does not save or otherwise modify the scene.
        /// </summary>
        public static GeneratedShotSelection SelectGeneratedShotForEditor(
            PlayableDirector sourceDirector,
            int shotIndex)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            var context = FindPreviewContext(sourceDirector);
            var timeline = context.Director.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    "The AI preview director is not bound to a TimelineAsset.");
            var entries = GetGeneratedAnimationEntries(timeline);
            if (shotIndex < 0 ||
                shotIndex >= entries.Length ||
                shotIndex >= context.CameraRoot.transform.childCount)
            {
                throw new ArgumentOutOfRangeException(nameof(shotIndex));
            }

            var timelineClip = entries[shotIndex].Clip;
            var cameraObject = context.CameraRoot.transform
                .GetChild(shotIndex)
                .gameObject;
            context.Director.time = timelineClip.start;
            context.Director.RebuildGraph();
            context.Director.Evaluate();
            Selection.activeGameObject = cameraObject;
            return new GeneratedShotSelection(
                shotIndex,
                cameraObject.name,
                timelineClip.start,
                timelineClip.duration,
                cameraObject,
                timelineClip.animationClip);
        }

        public static string RegenerateSelectedShotForCli(
            string generatedDirectory,
            PlayableDirector sourceDirector)
        {
            return RegenerateSelectedShotForCli(
                generatedDirectory,
                DefaultCurveSimplificationPreset,
                sourceDirector);
        }

        public static string RegenerateSelectedShotForCli(
            string generatedDirectory,
            CurveSimplificationPreset curvePreset,
            PlayableDirector sourceDirector)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            if (!TryGetSelectedGeneratedShotForEditor(
                    sourceDirector,
                    out var selection))
            {
                throw new InvalidOperationException(
                    "Select an AI camera or one of its Timeline clips before regenerating.");
            }

            return RegenerateShot(
                generatedDirectory,
                selection.ShotIndex,
                curvePreset,
                sourceDirector);
        }

        internal static bool TryResolveGeneratedShotIndexForEditor(
            TimelineAsset timeline,
            GameObject cameraRoot,
            TimelineClip selectedTimelineClip,
            GameObject selectedGameObject,
            UnityEngine.Object selectedAsset,
            out int shotIndex)
        {
            shotIndex = -1;
            if (timeline == null || cameraRoot == null)
            {
                return false;
            }

            var animationEntries = GetGeneratedAnimationEntries(timeline);
            var cinemachineClips = GetGeneratedCinemachineClips(timeline);
            if (selectedTimelineClip != null)
            {
                shotIndex = Array.FindIndex(
                    animationEntries,
                    entry => ReferenceEquals(entry.Clip, selectedTimelineClip));
                if (shotIndex < 0)
                {
                    shotIndex = Array.FindIndex(
                        cinemachineClips,
                        clip => ReferenceEquals(clip, selectedTimelineClip));
                }

                if (shotIndex >= 0)
                {
                    return true;
                }
            }

            if (selectedAsset != null)
            {
                shotIndex = Array.FindIndex(
                    animationEntries,
                    entry =>
                        entry.Clip.animationClip == selectedAsset ||
                        entry.Clip.asset == selectedAsset);
                if (shotIndex < 0)
                {
                    shotIndex = Array.FindIndex(
                        cinemachineClips,
                        clip => clip.asset == selectedAsset);
                }

                if (shotIndex >= 0)
                {
                    return true;
                }
            }

            var selectedTransform = selectedGameObject != null
                ? selectedGameObject.transform
                : null;
            while (selectedTransform != null &&
                   selectedTransform.parent != cameraRoot.transform)
            {
                selectedTransform = selectedTransform.parent;
            }

            if (selectedTransform == null ||
                selectedTransform.parent != cameraRoot.transform)
            {
                return false;
            }

            shotIndex = selectedTransform.GetSiblingIndex();
            return shotIndex >= 0 &&
                   shotIndex < cameraRoot.transform.childCount &&
                   shotIndex < animationEntries.Length;
        }

        private static TimelineClip GetTimelineEditorSelectedClip()
        {
            try
            {
                var timelineEditorType = Type.GetType(
                    "UnityEditor.Timeline.TimelineEditor, Unity.Timeline.Editor");
                var selectedClipProperty = timelineEditorType?.GetProperty(
                    "selectedClip",
                    BindingFlags.Public | BindingFlags.Static);
                return selectedClipProperty?.GetValue(null) as TimelineClip;
            }
            catch
            {
                // Timeline selection is optional. Hierarchy/asset selection still
                // provides a stable fallback when no Timeline window is open.
                return null;
            }
        }

        public static string RemovePreviewForCli()
        {
            return RemovePreview(null);
        }

        public static string RemovePreviewForCli(
            PlayableDirector sourceDirector)
        {
            return RemovePreview(RequireSourceDirector(sourceDirector));
        }

        private static string RemovePreview(
            PlayableDirector explicitSourceDirector)
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                throw new InvalidOperationException(
                    "AI camera preview must be removed in Edit Mode.");
            }

            var scene = explicitSourceDirector != null
                ? explicitSourceDirector.gameObject.scene
                : SceneManager.GetActiveScene();
            PreviewContext context;
            var found = explicitSourceDirector != null
                ? TryFindPreviewContext(explicitSourceDirector, out context)
                : TryFindLegacyPreviewContext(scene, out context);
            var previewDirector = found ? context.Director : null;
            var previewCamera = found ? context.CameraRoot : null;
            var deletedAssets = new List<string>();
            var scopedAssetFolder = found && explicitSourceDirector != null
                ? $"{GetPreviewAssetFolderPath(scene.path)}/" +
                  GetDirectorAssetScopeName(explicitSourceDirector)
                : string.Empty;

            if (previewDirector != null)
            {
                var timeline = previewDirector.playableAsset as TimelineAsset;
                if (timeline != null)
                {
                    var animationClips = GetGeneratedAnimationEntries(timeline)
                        .Select(entry => entry.Clip.animationClip)
                        .Where(clip => clip != null)
                        .ToArray();
                    foreach (var animationClip in animationClips)
                    {
                        AddPreviewAssetPath(deletedAssets, animationClip);
                    }

                    AddPreviewAssetPath(deletedAssets, timeline);
                }

                UnityEngine.Object.DestroyImmediate(previewDirector.gameObject);
            }

            if (previewCamera != null)
            {
                UnityEngine.Object.DestroyImmediate(previewCamera);
            }

            if (!string.IsNullOrEmpty(scopedAssetFolder) &&
                AssetDatabase.IsValidFolder(scopedAssetFolder))
            {
                if (!scopedAssetFolder.Contains(
                        $"/{PreviewAssetFolderName}/"))
                {
                    throw new InvalidOperationException(
                        $"Refusing to remove non-preview folder " +
                        $"'{scopedAssetFolder}'.");
                }

                if (!AssetDatabase.DeleteAsset(scopedAssetFolder))
                {
                    throw new IOException(
                        $"Unable to remove preview asset folder " +
                        $"'{scopedAssetFolder}'.");
                }
            }
            else
            {
                foreach (var assetPath in deletedAssets.Distinct())
                {
                    AssetDatabase.DeleteAsset(assetPath);
                }
            }

            // A full regeneration removes and recreates the same scoped asset
            // folder in one editor action. Force the deletion into the asset
            // database before GenerateUniqueAssetPath is called again; without
            // this refresh Unity can briefly report the deleted parent as valid
            // and return an empty destination path for the copied Timeline.
            AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);

            var originalDirector = explicitSourceDirector ??
                                   Resources.FindObjectsOfTypeAll<PlayableDirector>()
                                       .FirstOrDefault(director =>
                                           director.gameObject.scene == scene &&
                                           director.name == OriginalDirectorName);
            if (originalDirector != null &&
                (explicitSourceDirector == null || found))
            {
                originalDirector.gameObject.SetActive(true);
                originalDirector.time = 0;
                originalDirector.RebuildGraph();
                originalDirector.Evaluate();
            }

            EditorSceneManager.MarkSceneDirty(scene);
            return $"Removed AI preview objects and {deletedAssets.Count} preview assets.";
        }

        /// <summary>
        /// Copies the selected source director's current AI preview into a durable
        /// final asset set and binds it to <paramref name="destinationDirector"/>.
        /// The active scene is never changed or saved. The source director, source
        /// preview, scene paths, and both director times are left untouched.
        /// </summary>
        public static FinalizedPreviewResult SaveFinalForEditor(
            PlayableDirector sourceDirector,
            PlayableDirector destinationDirector)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            destinationDirector = RequireDestinationDirector(
                sourceDirector,
                destinationDirector);
            return SaveFinalForEditor(
                sourceDirector,
                destinationDirector,
                GetDefaultFinalAssetFolder(
                    sourceDirector,
                    destinationDirector));
        }

        /// <summary>
        /// Same as the two-argument overload, with an explicit Assets-relative
        /// destination folder (for example Assets/MyShow/AI_CameraFinal/SongA).
        /// </summary>
        public static FinalizedPreviewResult SaveFinalForEditor(
            PlayableDirector sourceDirector,
            PlayableDirector destinationDirector,
            string finalAssetFolder)
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                throw new InvalidOperationException(
                    "AI camera final assets must be created in Edit Mode.");
            }

            sourceDirector = RequireSourceDirector(sourceDirector);
            destinationDirector = RequireDestinationDirector(
                sourceDirector,
                destinationDirector);
            var context = FindPreviewContext(sourceDirector);
            var sourceTimeline = context.Director.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    "The AI preview director is not bound to a TimelineAsset.");
            var destinationTimelineBefore =
                destinationDirector.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    "The destination director is not bound to a TimelineAsset.");
            var destinationTimeBefore = destinationDirector.time;
            var destinationStateBefore = CaptureDirectorState(destinationDirector);
            var sourceStateBefore = CaptureDirectorState(sourceDirector);
            var previewStateBefore = CaptureDirectorState(context.Director);
            var previewCameraRootWasDirty =
                EditorUtility.IsDirty(context.CameraRoot);
            var activeSceneBefore = SceneManager.GetActiveScene();
            var normalizedFolder = NormalizeFinalAssetFolder(finalAssetFolder);
            EnsureAssetFolderRecursively(normalizedFolder);
            var shotFolder = EnsureAssetSubfolder(
                normalizedFolder,
                ShotAnimationFolderName);
            var cameraFolder = EnsureAssetSubfolder(
                normalizedFolder,
                CameraPrefabFolderName);

            var createdAssetPaths = new List<string>();
            var finalClips = new List<AnimationClip>();
            TimelineAsset finalTimeline = null;
            GameObject cameraPrefab = null;
            GameObject finalCameraRoot = null;
            AuthoredCinemachineStateSnapshot destinationCinemachineStateBefore =
                null;
            try
            {
                var sourceTimelinePath = AssetDatabase.GetAssetPath(sourceTimeline);
                if (string.IsNullOrWhiteSpace(sourceTimelinePath))
                {
                    throw new InvalidOperationException(
                        "The AI preview Timeline is not a project asset.");
                }

                var finalTimelinePath = AssetDatabase.GenerateUniqueAssetPath(
                    $"{normalizedFolder}/" +
                    $"{SanitizeAssetName(sourceTimeline.name)}_Final.playable");
                if (!AssetDatabase.CopyAsset(sourceTimelinePath, finalTimelinePath))
                {
                    throw new IOException(
                        $"Unable to copy the final Timeline to '{finalTimelinePath}'.");
                }

                createdAssetPaths.Add(finalTimelinePath);
                AssetDatabase.ImportAsset(
                    finalTimelinePath,
                    ImportAssetOptions.ForceSynchronousImport);
                finalTimeline = AssetDatabase.LoadAssetAtPath<TimelineAsset>(
                    finalTimelinePath)
                    ?? throw new InvalidOperationException(
                        $"Unable to load copied Timeline '{finalTimelinePath}'.");
                RemovePreviewGenerationProvenance(finalTimeline);

                CopyGeneratedAnimationAssets(
                    sourceTimeline,
                    finalTimeline,
                    shotFolder,
                    createdAssetPaths,
                    finalClips);

                var cameraPrefabPath = AssetDatabase.GenerateUniqueAssetPath(
                    $"{cameraFolder}/" +
                    $"{SanitizeAssetName(sourceDirector.name)}_AI_Cameras.prefab");
                cameraPrefab = PrefabUtility.SaveAsPrefabAsset(
                    context.CameraRoot,
                    cameraPrefabPath,
                    out var prefabSaved);
                if (!prefabSaved || cameraPrefab == null)
                {
                    throw new IOException(
                        $"Unable to save generated cameras to '{cameraPrefabPath}'.");
                }

                createdAssetPaths.Add(cameraPrefabPath);
                finalCameraRoot = PrefabUtility.InstantiatePrefab(
                    cameraPrefab,
                    destinationDirector.gameObject.scene) as GameObject
                    ?? throw new InvalidOperationException(
                        "Unable to instantiate final cameras in the destination scene.");
                finalCameraRoot.name =
                    $"AI_CameraFinal__{GetDirectorScopeLabel(sourceDirector)}";

                BindFinalTimelineToDestination(
                    destinationDirector,
                    destinationTimelineBefore,
                    finalTimeline,
                    finalCameraRoot);
                destinationCinemachineStateBefore =
                    CaptureAuthoredCinemachineState(destinationDirector);
                SuppressAuthoredCinemachineOutputsForEditor(
                    destinationDirector,
                    false);
                destinationDirector.time = destinationTimeBefore;
                destinationDirector.RebuildGraph();
                destinationDirector.time = destinationTimeBefore;

                EditorUtility.SetDirty(finalTimeline);
                EditorUtility.SetDirty(destinationDirector);
                EditorUtility.SetDirty(finalCameraRoot);
                foreach (var clip in finalClips)
                {
                    EditorUtility.SetDirty(clip);
                    AssetDatabase.SaveAssetIfDirty(clip);
                }

                AssetDatabase.SaveAssetIfDirty(finalTimeline);
                AssetDatabase.SaveAssetIfDirty(cameraPrefab);
                EditorSceneManager.MarkSceneDirty(
                    destinationDirector.gameObject.scene);

                AssertFinalizationSourceWasPreserved(
                    sourceStateBefore,
                    previewStateBefore,
                    context.CameraRoot,
                    previewCameraRootWasDirty,
                    activeSceneBefore);

                return new FinalizedPreviewResult(
                    normalizedFolder,
                    finalTimeline,
                    finalClips.ToArray(),
                    cameraPrefab,
                    finalCameraRoot);
            }
            catch
            {
                RestoreAuthoredCinemachineState(
                    destinationCinemachineStateBefore);
                RestoreDestinationDirectorAfterFinalizationFailure(
                    destinationStateBefore,
                    finalTimeline);
                if (finalCameraRoot != null)
                {
                    UnityEngine.Object.DestroyImmediate(finalCameraRoot);
                }

                for (var index = createdAssetPaths.Count - 1; index >= 0; index--)
                {
                    AssetDatabase.DeleteAsset(createdAssetPaths[index]);
                }

                AssetDatabase.SaveAssets();
                throw;
            }
        }

        private static PlayableDirector RequireDestinationDirector(
            PlayableDirector sourceDirector,
            PlayableDirector destinationDirector)
        {
            if (destinationDirector == null)
            {
                throw new ArgumentNullException(nameof(destinationDirector));
            }

            if (destinationDirector == sourceDirector)
            {
                throw new ArgumentException(
                    "The source director cannot also be the final destination. " +
                    "Use a director in the copied/final scene so the source stays unchanged.",
                    nameof(destinationDirector));
            }

            if (destinationDirector.gameObject.scene ==
                sourceDirector.gameObject.scene)
            {
                throw new ArgumentException(
                    "The final destination director must be in a different loaded " +
                    "scene so the source scene and its dirty state remain untouched.",
                    nameof(destinationDirector));
            }

            var scene = destinationDirector.gameObject.scene;
            if (!scene.IsValid() || !scene.isLoaded)
            {
                throw new InvalidOperationException(
                    "The destination director must belong to a loaded scene.");
            }

            if (destinationDirector.playableAsset is not TimelineAsset)
            {
                throw new InvalidOperationException(
                    "The destination director is not bound to a TimelineAsset.");
            }

            return destinationDirector;
        }

        private static string GetDefaultFinalAssetFolder(
            PlayableDirector sourceDirector,
            PlayableDirector destinationDirector)
        {
            var scenePath = destinationDirector.gameObject.scene.path
                ?.Replace('\\', '/');
            if (string.IsNullOrWhiteSpace(scenePath))
            {
                throw new InvalidOperationException(
                    "The destination scene must be saved under Assets when no " +
                    "explicit final asset folder is supplied.");
            }

            var sceneFolder = Path.GetDirectoryName(scenePath)
                ?.Replace('\\', '/');
            if (string.IsNullOrWhiteSpace(sceneFolder))
            {
                throw new InvalidOperationException(
                    "The destination scene must be saved under Assets when no " +
                    "explicit final asset folder is supplied.");
            }

            return $"{sceneFolder}/{FinalAssetFolderName}/" +
                   SanitizeAssetName(sourceDirector.name);
        }

        internal static string NormalizeFinalAssetFolder(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
            {
                throw new ArgumentException(
                    "A final asset folder under Assets is required.",
                    nameof(path));
            }

            var normalized = path.Trim().Replace('\\', '/').TrimEnd('/');
            if (Path.IsPathRooted(normalized) ||
                normalized.Equals("Assets", StringComparison.OrdinalIgnoreCase) ||
                !normalized.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException(
                    $"Final asset folder must be a dedicated folder under Assets: '{path}'.",
                    nameof(path));
            }

            var segments = normalized.Split('/');
            if (segments.Any(segment =>
                    string.IsNullOrWhiteSpace(segment) ||
                    segment == "." ||
                    segment == ".."))
            {
                throw new ArgumentException(
                    $"Final asset folder contains an invalid segment: '{path}'.",
                    nameof(path));
            }

            var projectRoot = Path.GetDirectoryName(Application.dataPath)
                ?? throw new InvalidOperationException(
                    "Unable to resolve the Unity project root.");
            var fullPath = Path.GetFullPath(Path.Combine(projectRoot, normalized));
            var assetsRoot = Path.GetFullPath(Application.dataPath)
                .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
            if (!fullPath.StartsWith(
                    assetsRoot + Path.DirectorySeparatorChar,
                    StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException(
                    $"Final asset folder resolves outside Assets: '{path}'.",
                    nameof(path));
            }

            return normalized;
        }

        private static void EnsureAssetFolderRecursively(string assetFolder)
        {
            var segments = assetFolder.Split('/');
            var current = segments[0];
            for (var index = 1; index < segments.Length; index++)
            {
                var next = $"{current}/{segments[index]}";
                if (!AssetDatabase.IsValidFolder(next))
                {
                    var guid = AssetDatabase.CreateFolder(current, segments[index]);
                    if (string.IsNullOrWhiteSpace(guid))
                    {
                        throw new IOException(
                            $"Unable to create asset folder '{next}'.");
                    }
                }

                current = next;
            }
        }

        private static void CopyGeneratedAnimationAssets(
            TimelineAsset sourceTimeline,
            TimelineAsset finalTimeline,
            string shotFolder,
            ICollection<string> createdAssetPaths,
            ICollection<AnimationClip> finalClips)
        {
            var sourceClips = GetGeneratedAnimationEntries(sourceTimeline)
                .Select(entry => entry.Clip)
                .ToArray();
            var copiedTimelineClips = GetGeneratedAnimationEntries(finalTimeline)
                .Select(entry => entry.Clip)
                .ToArray();
            if (sourceClips.Length == 0 ||
                sourceClips.Length != copiedTimelineClips.Length)
            {
                throw new InvalidOperationException(
                    "The copied Timeline does not match its generated animation clips.");
            }

            for (var index = 0; index < sourceClips.Length; index++)
            {
                var sourceClip = sourceClips[index].animationClip
                    ?? throw new InvalidOperationException(
                        $"Generated shot {index} has no AnimationClip.");
                AddPreviewAssetPath(new List<string>(), sourceClip);
                var sourceClipPath = AssetDatabase.GetAssetPath(sourceClip);
                var finalClipPath = AssetDatabase.GenerateUniqueAssetPath(
                    $"{shotFolder}/{index:D3}_" +
                    $"{SanitizeAssetName(sourceClip.name)}.anim");
                if (!AssetDatabase.CopyAsset(sourceClipPath, finalClipPath))
                {
                    throw new IOException(
                        $"Unable to copy generated shot to '{finalClipPath}'.");
                }

                createdAssetPaths.Add(finalClipPath);
                AssetDatabase.ImportAsset(
                    finalClipPath,
                    ImportAssetOptions.ForceSynchronousImport);
                var finalClip = AssetDatabase.LoadAssetAtPath<AnimationClip>(
                    finalClipPath)
                    ?? throw new InvalidOperationException(
                        $"Unable to load copied animation '{finalClipPath}'.");
                var animationPlayable =
                    copiedTimelineClips[index].asset as AnimationPlayableAsset
                    ?? throw new InvalidOperationException(
                        $"Final shot {index} is not an AnimationPlayableAsset.");
                animationPlayable.clip = finalClip;
                EditorUtility.SetDirty(animationPlayable);
                finalClips.Add(finalClip);
            }
        }

        private static GeneratedAnimationEntry[] GetGeneratedAnimationEntries(
            TimelineAsset timeline)
        {
            if (timeline == null)
            {
                return Array.Empty<GeneratedAnimationEntry>();
            }

            return timeline.GetOutputTracks()
                .OfType<AnimationTrack>()
                .Where(IsGeneratedAnimationTrack)
                .SelectMany(track => track.GetClips().Select(clip =>
                    new GeneratedAnimationEntry(track, clip)))
                .OrderBy(entry => entry.Clip.start)
                .ThenBy(entry => entry.Clip.displayName)
                .ToArray();
        }

        private static AnimationTrack[] GetGeneratedAnimationTracks(
            TimelineAsset timeline)
        {
            return timeline.GetOutputTracks()
                .OfType<AnimationTrack>()
                .Where(IsGeneratedAnimationTrack)
                .OrderBy(track => track.GetClips()
                    .Select(clip => clip.start)
                    .DefaultIfEmpty(double.MaxValue)
                    .Min())
                .ToArray();
        }

        private static bool IsGeneratedAnimationTrack(AnimationTrack track)
        {
            if (track == null)
            {
                return false;
            }

            if (track.name == GeneratedAnimationTrackName)
            {
                // Legacy previews used one root AnimationTrack with all shots.
                return true;
            }

            return track.parent is GroupTrack group &&
                   group.name == GeneratedAnimationTrackName;
        }

        private static CinemachineTrack GetGeneratedCinemachineTrack(
            TimelineAsset timeline)
        {
            return timeline.GetOutputTracks()
                .OfType<CinemachineTrack>()
                .SingleOrDefault(track =>
                    track.name == GeneratedCinemachineTrackName)
                ?? throw new InvalidOperationException(
                    $"Timeline does not contain '{GeneratedCinemachineTrackName}'.");
        }

        private static TimelineClip[] GetGeneratedCinemachineClips(
            TimelineAsset timeline)
        {
            var track = timeline.GetOutputTracks()
                .OfType<CinemachineTrack>()
                .SingleOrDefault(candidate =>
                    candidate.name == GeneratedCinemachineTrackName);
            return track == null
                ? Array.Empty<TimelineClip>()
                : track.GetClips()
                    .OrderBy(clip => clip.start)
                    .ToArray();
        }

        private static void BindFinalTimelineToDestination(
            PlayableDirector destinationDirector,
            TimelineAsset destinationTimelineBefore,
            TimelineAsset finalTimeline,
            GameObject finalCameraRoot)
        {
            var destinationTracks = destinationTimelineBefore.GetOutputTracks()
                .Where(track => !IsGeneratedCameraTrack(track))
                .ToArray();
            var finalBaseTracks = finalTimeline.GetOutputTracks()
                .Where(track => !IsGeneratedCameraTrack(track))
                .ToArray();
            if (destinationTracks.Length != finalBaseTracks.Length)
            {
                throw new InvalidOperationException(
                    "The destination Timeline structure does not match the preview source.");
            }

            var bindings = new UnityEngine.Object[destinationTracks.Length];
            for (var index = 0; index < destinationTracks.Length; index++)
            {
                if (destinationTracks[index].GetType() != finalBaseTracks[index].GetType() ||
                    destinationTracks[index].name != finalBaseTracks[index].name)
                {
                    throw new InvalidOperationException(
                        $"Destination Timeline track {index} does not match the preview source.");
                }

                bindings[index] =
                    destinationDirector.GetGenericBinding(destinationTracks[index]);
            }

            var brain = FindCinemachineBrain(
                destinationDirector,
                destinationTimelineBefore);
            var animationTracks = GetGeneratedAnimationTracks(finalTimeline);
            var animationEntries = GetGeneratedAnimationEntries(finalTimeline);
            var cinemachineTrack = GetGeneratedCinemachineTrack(finalTimeline);
            var shots = cinemachineTrack.GetClips()
                .OrderBy(clip => clip.start)
                .ToArray();
            if (shots.Length != finalCameraRoot.transform.childCount)
            {
                throw new InvalidOperationException(
                    "Final camera count does not match the generated Cinemachine shots.");
            }

            if (animationEntries.Length != shots.Length)
            {
                throw new InvalidOperationException(
                    "Final camera count does not match the generated animation shots.");
            }

            // PlayableDirector keeps serialized binding keys even after its
            // playableAsset changes. Clear every old key so the Final scene does
            // not retain a hidden dependency on the disposable preview Timeline.
            ClearTimelineBindingsForFinalization(
                destinationDirector,
                destinationTimelineBefore);

            destinationDirector.playableAsset = finalTimeline;
            for (var index = 0; index < finalBaseTracks.Length; index++)
            {
                destinationDirector.SetGenericBinding(
                    finalBaseTracks[index],
                    bindings[index]);
            }

            var usesPerCameraAnimationTracks =
                animationTracks.Length == shots.Length &&
                animationTracks.All(track => track.GetClips().Count() == 1);
            if (usesPerCameraAnimationTracks)
            {
                for (var index = 0; index < animationTracks.Length; index++)
                {
                    var animator = finalCameraRoot.transform.GetChild(index)
                        .GetComponent<Animator>()
                        ?? throw new InvalidOperationException(
                            $"Final camera {index} is missing its Animator.");
                    destinationDirector.SetGenericBinding(
                        animationTracks[index],
                        animator);
                }
            }
            else if (animationTracks.Length == 1)
            {
                // Backward compatibility for previews created before per-camera
                // Animator tracks were introduced.
                var animator = finalCameraRoot.GetComponent<Animator>()
                    ?? throw new InvalidOperationException(
                        "The legacy final camera root is missing its Animator.");
                destinationDirector.SetGenericBinding(
                    animationTracks[0],
                    animator);
            }
            else
            {
                throw new InvalidOperationException(
                    "Generated camera animation tracks do not map one-to-one to shots.");
            }

            destinationDirector.SetGenericBinding(cinemachineTrack, brain);
            for (var index = 0; index < shots.Length; index++)
            {
                var camera = finalCameraRoot.transform.GetChild(index)
                    .GetComponent<CinemachineCamera>()
                    ?? throw new InvalidOperationException(
                        $"Final camera {index} is missing CinemachineCamera.");
                var shot = shots[index].asset as CinemachineShot
                    ?? throw new InvalidOperationException(
                        $"Final shot {index} is not a CinemachineShot.");
                var exposedName = shot.VirtualCamera.exposedName;
                shot.VirtualCamera.defaultValue = camera;
                destinationDirector.SetReferenceValue(exposedName, camera);
                EditorUtility.SetDirty(shot);
            }

        }

        internal static void ClearTimelineBindingsForFinalization(
            PlayableDirector director,
            TimelineAsset timeline)
        {
            if (director == null)
            {
                throw new ArgumentNullException(nameof(director));
            }

            if (timeline == null)
            {
                throw new ArgumentNullException(nameof(timeline));
            }

            foreach (var track in timeline.GetOutputTracks())
            {
                director.ClearGenericBinding(track);
            }
        }

        private static bool IsGeneratedCameraTrack(TrackAsset track)
        {
            return track is AnimationTrack animationTrack
                ? IsGeneratedAnimationTrack(animationTrack)
                : track.name == GeneratedCinemachineTrackName;
        }

        private static DirectorStateSnapshot CaptureDirectorState(
            PlayableDirector director)
        {
            var timeline = director.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    $"Director '{director.name}' is not bound to a TimelineAsset.");
            var tracks = timeline.GetOutputTracks().ToArray();
            var referenceNames = timeline.GetOutputTracks()
                .SelectMany(track => track.GetClips())
                .Select(clip => clip.asset as CinemachineShot)
                .Where(shot => shot != null)
                .Select(shot => shot.VirtualCamera.exposedName)
                .Distinct()
                .ToArray();
            var referenceValues = new UnityEngine.Object[referenceNames.Length];
            var referenceValuesValid = new bool[referenceNames.Length];
            for (var index = 0; index < referenceNames.Length; index++)
            {
                referenceValues[index] = director.GetReferenceValue(
                    referenceNames[index],
                    out referenceValuesValid[index]) as UnityEngine.Object;
            }

            return new DirectorStateSnapshot(
                director,
                timeline,
                director.time,
                director.gameObject.activeSelf,
                director.gameObject.scene.path,
                director.gameObject.scene.isDirty,
                EditorUtility.IsDirty(director),
                EditorUtility.IsDirty(director.gameObject),
                EditorUtility.IsDirty(timeline),
                tracks,
                tracks.Select(director.GetGenericBinding).ToArray(),
                referenceNames,
                referenceValues,
                referenceValuesValid);
        }

        private static void AssertFinalizationSourceWasPreserved(
            DirectorStateSnapshot sourceStateBefore,
            DirectorStateSnapshot previewStateBefore,
            GameObject previewCameraRoot,
            bool previewCameraRootWasDirty,
            Scene activeSceneBefore)
        {
            AssertDirectorStateWasPreserved(sourceStateBefore);
            AssertDirectorStateWasPreserved(previewStateBefore);
            if (EditorUtility.IsDirty(previewCameraRoot) !=
                    previewCameraRootWasDirty ||
                SceneManager.GetActiveScene() != activeSceneBefore)
            {
                throw new InvalidOperationException(
                    "Finalization changed the preview camera or active scene unexpectedly.");
            }
        }

        private static void AssertDirectorStateWasPreserved(
            DirectorStateSnapshot snapshot)
        {
            var director = snapshot.Director;
            var scene = director.gameObject.scene;
            var currentTimeline = director.playableAsset as TimelineAsset;
            var currentTracks = currentTimeline == null
                ? Array.Empty<TrackAsset>()
                : currentTimeline.GetOutputTracks().ToArray();
            var bindingsMatch = currentTracks.Length == snapshot.Tracks.Length;
            for (var index = 0;
                 bindingsMatch && index < currentTracks.Length;
                 index++)
            {
                bindingsMatch = currentTracks[index] == snapshot.Tracks[index] &&
                                director.GetGenericBinding(currentTracks[index]) ==
                                snapshot.Bindings[index];
            }

            if (currentTimeline != snapshot.Timeline ||
                Math.Abs(director.time - snapshot.Time) > double.Epsilon ||
                director.gameObject.activeSelf != snapshot.ActiveSelf ||
                scene.path != snapshot.ScenePath ||
                scene.isDirty != snapshot.SceneWasDirty ||
                EditorUtility.IsDirty(director) != snapshot.DirectorWasDirty ||
                EditorUtility.IsDirty(director.gameObject) !=
                    snapshot.GameObjectWasDirty ||
                EditorUtility.IsDirty(snapshot.Timeline) !=
                    snapshot.TimelineWasDirty ||
                !bindingsMatch)
            {
                throw new InvalidOperationException(
                    $"Finalization changed source state for '{director.name}'.");
            }
        }

        private static void RestoreDestinationDirectorAfterFinalizationFailure(
            DirectorStateSnapshot snapshot,
            TimelineAsset finalTimeline)
        {
            var destinationDirector = snapshot?.Director;
            if (destinationDirector == null)
            {
                return;
            }

            if (finalTimeline != null)
            {
                foreach (var track in finalTimeline.GetOutputTracks())
                {
                    destinationDirector.ClearGenericBinding(track);
                }

                var cinemachineTrack = finalTimeline.GetOutputTracks()
                    .OfType<CinemachineTrack>()
                    .FirstOrDefault(track =>
                        track.name == GeneratedCinemachineTrackName);
                if (cinemachineTrack != null)
                {
                    foreach (var clip in cinemachineTrack.GetClips())
                    {
                        if (clip.asset is CinemachineShot shot)
                        {
                            destinationDirector.ClearReferenceValue(
                                shot.VirtualCamera.exposedName);
                        }
                    }
                }
            }

            destinationDirector.playableAsset = snapshot.Timeline;
            for (var index = 0; index < snapshot.Tracks.Length; index++)
            {
                destinationDirector.SetGenericBinding(
                    snapshot.Tracks[index],
                    snapshot.Bindings[index]);
            }

            for (var index = 0; index < snapshot.ReferenceNames.Length; index++)
            {
                if (snapshot.ReferenceValuesValid[index])
                {
                    destinationDirector.SetReferenceValue(
                        snapshot.ReferenceNames[index],
                        snapshot.ReferenceValues[index]);
                }
                else
                {
                    destinationDirector.ClearReferenceValue(
                        snapshot.ReferenceNames[index]);
                }
            }

            destinationDirector.gameObject.SetActive(snapshot.ActiveSelf);
            destinationDirector.time = snapshot.Time;
            destinationDirector.RebuildGraph();
            destinationDirector.time = snapshot.Time;
        }

        private static List<GeneratedShot> ReadShotDefinitions(
            GeneratedCameraMetadata metadata,
            IReadOnlyList<double> times,
            string shotsPath)
        {
            if (string.IsNullOrWhiteSpace(metadata.shotsFile))
            {
                return new List<GeneratedShot>
                {
                    new()
                    {
                        index = 0,
                        cameraName = PreviewVirtualCameraName,
                        shotType = "continuous",
                        startFrame = 0,
                        endFrameExclusive = metadata.frameCount,
                        start = 0,
                        end = metadata.duration,
                        duration = metadata.duration
                    }
                };
            }

            if (string.IsNullOrWhiteSpace(shotsPath))
            {
                throw new InvalidDataException(
                    "Generated shot payload path was not resolved.");
            }

            var shotFile = JsonUtility.FromJson<GeneratedShotFile>(
                File.ReadAllText(shotsPath));
            if (shotFile?.shots == null || shotFile.shots.Count == 0)
            {
                throw new InvalidDataException(
                    "Generated shot file does not contain any shots.");
            }

            var ordered = shotFile.shots
                .OrderBy(shot => shot.startFrame)
                .ToList();
            var expectedStart = 0;
            for (var index = 0; index < ordered.Count; index++)
            {
                var shot = ordered[index];
                if (shot.startFrame != expectedStart ||
                    shot.endFrameExclusive <= shot.startFrame ||
                    shot.endFrameExclusive > metadata.frameCount)
                {
                    throw new InvalidDataException(
                        $"Shot {index} has an invalid or non-contiguous frame range.");
                }

                shot.index = index;
                shot.cameraName = string.IsNullOrWhiteSpace(shot.cameraName)
                    ? $"Shot_{index:D3}"
                    : shot.cameraName.Replace('/', '_').Replace('\\', '_');
                shot.start =
                    shot.startFrame / (double)metadata.sampleRate;
                shot.end =
                    shot.endFrameExclusive / (double)metadata.sampleRate;
                shot.duration = shot.end - shot.start;
                expectedStart = shot.endFrameExclusive;
            }

            if (expectedStart != metadata.frameCount)
            {
                throw new InvalidDataException(
                    "Generated shots do not cover the full camera timeline.");
            }

            return ordered;
        }

        private static List<PreviewShotCamera> CreateShotCameras(
            Transform cameraRoot,
            IReadOnlyList<GeneratedShot> shotDefinitions,
            IReadOnlyList<GeneratedCameraFrame> frames)
        {
            var result = new List<PreviewShotCamera>(shotDefinitions.Count);
            foreach (var definition in shotDefinitions)
            {
                var cameraObject = new GameObject(definition.cameraName);
                cameraObject.transform.SetParent(cameraRoot, false);
                var camera = cameraObject.AddComponent<CinemachineCamera>();
                var initialFrame = frames[definition.startFrame];
                cameraObject.transform.SetLocalPositionAndRotation(
                    initialFrame.Position,
                    initialFrame.Rotation);
                var lens = camera.Lens;
                lens.FieldOfView = initialFrame.FieldOfView;
                lens.Dutch = initialFrame.Dutch;
                camera.Lens = lens;
                result.Add(new PreviewShotCamera
                {
                    Definition = definition,
                    // Every generated camera is bound to its own AnimationTrack,
                    // so the clip animates the bound object itself rather than a
                    // child path below one shared root Animator.
                    RelativePath = string.Empty,
                    Camera = camera
                });
            }

            return result;
        }

        internal static AnimationTrack CreateGeneratedAnimationTrackForCamera(
            TimelineAsset timeline,
            GroupTrack animationGroup,
            PlayableDirector director,
            GameObject cameraObject,
            AnimationClip animationClip,
            int shotIndex,
            string cameraName,
            double start,
            double duration)
        {
            if (timeline == null)
            {
                throw new ArgumentNullException(nameof(timeline));
            }

            if (animationGroup == null)
            {
                throw new ArgumentNullException(nameof(animationGroup));
            }

            if (director == null)
            {
                throw new ArgumentNullException(nameof(director));
            }

            if (cameraObject == null)
            {
                throw new ArgumentNullException(nameof(cameraObject));
            }

            if (animationClip == null)
            {
                throw new ArgumentNullException(nameof(animationClip));
            }

            if (duration <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(duration));
            }

            var animator = cameraObject.GetComponent<Animator>();
            if (animator == null)
            {
                animator = cameraObject.AddComponent<Animator>();
            }
            animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
            var safeCameraName = string.IsNullOrWhiteSpace(cameraName)
                ? $"Shot_{shotIndex:D3}"
                : cameraName;
            var animationTrack = timeline.CreateTrack<AnimationTrack>(
                animationGroup,
                $"{GeneratedAnimationTrackPrefix} {shotIndex:D3} | " +
                safeCameraName);
            ConfigurePerCameraTrackOffset(
                animationTrack,
                cameraObject.transform);

            var timelineClip = animationTrack.CreateClip(animationClip);
            timelineClip.displayName = safeCameraName;
            timelineClip.start = start;
            timelineClip.duration = duration;
            timelineClip.clipIn = 0;
            timelineClip.easeInDuration = 0;
            timelineClip.easeOutDuration = 0;
            SetClipExtrapolationNone(timelineClip);
            director.SetGenericBinding(animationTrack, animator);
            return animationTrack;
        }

        internal static void ConfigurePerCameraTrackOffset(
            AnimationTrack animationTrack,
            Transform cameraTransform)
        {
            if (animationTrack == null)
            {
                throw new ArgumentNullException(nameof(animationTrack));
            }

            if (cameraTransform == null)
            {
                throw new ArgumentNullException(nameof(cameraTransform));
            }

            // A finite AnimationTrack treats root Transform curves as motion
            // relative to the clip's first sample. Seed the track offset with
            // that first world/local pose so the evaluated camera stays at the
            // generated coordinates instead of collapsing around scene origin.
            animationTrack.trackOffset = TrackOffset.ApplyTransformOffsets;
            animationTrack.position = cameraTransform.localPosition;
            animationTrack.rotation = cameraTransform.localRotation;
            animationTrack.infiniteClipOffsetPosition = Vector3.zero;
            animationTrack.infiniteClipOffsetRotation = Quaternion.identity;
        }

        private static AnimationClip BuildShotAnimationClip(
            string songName,
            int sampleRate,
            IReadOnlyList<GeneratedCameraFrame> frames,
            IReadOnlyList<double> times,
            PreviewShotCamera shotCamera,
            CurveSimplificationPreset curvePreset,
            out bool positionUsedExactFallback,
            out string positionFallbackReason)
        {
            var clip = new AnimationClip
            {
                name =
                    $"{songName}_{shotCamera.Definition.index:D3}_" +
                    $"{shotCamera.Definition.cameraName}",
                frameRate = sampleRate,
                wrapMode = WrapMode.ClampForever
            };

            var start = shotCamera.Definition.startFrame;
            var end = shotCamera.Definition.endFrameExclusive;
            var relativePath = shotCamera.RelativePath;
            var sampleCount = end - start;
            var localTimes = new double[sampleCount];
            var positions = new Vector3[sampleCount];
            var rotations = new Quaternion[sampleCount];
            var fieldOfView = new float[sampleCount];
            var dutch = new float[sampleCount];
            var startTime = times[start];
            for (var index = 0; index < sampleCount; index++)
            {
                var frame = frames[start + index];
                localTimes[index] = times[start + index] - startTime;
                positions[index] = frame.Position;
                rotations[index] = frame.Rotation;
                fieldOfView[index] = frame.FieldOfView;
                dutch[index] = frame.Dutch;
            }

            var simplified = SimplifyCameraCurves(
                localTimes,
                positions,
                rotations,
                fieldOfView,
                dutch,
                curvePreset);
            var positionCurves = BuildPositionCurves(simplified, curvePreset);
            positionUsedExactFallback = positionCurves.UsedExactFallback;
            positionFallbackReason = positionCurves.FallbackReason;
            SetCurve(
                clip,
                relativePath,
                typeof(Transform),
                "m_LocalPosition.x",
                positionCurves.XCurve);
            SetCurve(
                clip,
                relativePath,
                typeof(Transform),
                "m_LocalPosition.y",
                positionCurves.YCurve);
            SetCurve(
                clip,
                relativePath,
                typeof(Transform),
                "m_LocalPosition.z",
                positionCurves.ZCurve);
            var rotationCurves = BuildRotationCurves(simplified, curvePreset);
            SetRotationCurves(clip, relativePath, rotationCurves);
            SetLinearCurve(
                clip,
                relativePath,
                typeof(CinemachineCamera),
                "Lens.FieldOfView",
                BuildKeys(
                    simplified.Times,
                    simplified.FieldOfViewIndices,
                    index => simplified.FieldOfView[index]));
            SetLinearCurve(
                clip,
                relativePath,
                typeof(CinemachineCamera),
                "Lens.Dutch",
                BuildKeys(
                    simplified.Times,
                    simplified.DutchIndices,
                    index => simplified.Dutch[index]));

            clip.EnsureQuaternionContinuity();
            rotationCurves = EnsureAppliedRotationCurveSafety(
                clip,
                relativePath,
                simplified,
                curvePreset,
                rotationCurves);
            if (rotationCurves.UsedDenseFallback)
            {
                Debug.LogWarning(
                    $"AI camera shot {shotCamera.Definition.index:D3} " +
                    $"({shotCamera.Definition.cameraName}) used dense C2 rotation " +
                    $"curves after the final imported-clip audit: " +
                    $"{rotationCurves.FallbackReason}");
            }
            return clip;
        }

        internal static CurveSimplificationSettings
            GetCurveSimplificationSettings(CurveSimplificationPreset preset)
        {
            return preset switch
            {
                CurveSimplificationPreset.Exact =>
                    new CurveSimplificationSettings(0f, 0f, 0f, 0f),
                CurveSimplificationPreset.Balanced =>
                    new CurveSimplificationSettings(0.005f, 0.15f, 0.08f, 0.08f),
                CurveSimplificationPreset.Editable =>
                    new CurveSimplificationSettings(0.02f, 0.5f, 0.25f, 0.25f),
                _ => throw new ArgumentOutOfRangeException(nameof(preset), preset, null)
            };
        }

        internal static SimplifiedCameraCurves SimplifyCameraCurves(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> positions,
            IReadOnlyList<Quaternion> rotations,
            IReadOnlyList<float> fieldOfView,
            IReadOnlyList<float> dutch,
            CurveSimplificationPreset preset)
        {
            ValidateCurveSampleCounts(
                times,
                positions,
                rotations,
                fieldOfView,
                dutch);
            var settings = GetCurveSimplificationSettings(preset);
            var continuousRotations = MakeQuaternionSequenceContinuous(rotations);

            return new SimplifiedCameraCurves(
                times.ToArray(),
                positions.ToArray(),
                continuousRotations,
                fieldOfView.ToArray(),
                dutch.ToArray(),
                SimplifyVectorIndices(
                    times,
                    positions,
                    settings.PositionErrorMeters),
                SimplifyQuaternionIndices(
                    times,
                    continuousRotations,
                    settings.QuaternionAngleErrorDegrees),
                SimplifyScalarIndices(
                    times,
                    fieldOfView,
                    settings.FieldOfViewErrorDegrees),
                SimplifyScalarIndices(
                    times,
                    dutch,
                    settings.DutchErrorDegrees));
        }

        internal static PositionCurveBuildResult BuildPositionCurves(
            SimplifiedCameraCurves simplified,
            CurveSimplificationPreset preset)
        {
            if (simplified == null)
            {
                throw new ArgumentNullException(nameof(simplified));
            }

            PositionCurveBuildResult result;
            var settings = GetCurveSimplificationSettings(preset);
            if (preset == CurveSimplificationPreset.Exact ||
                simplified.Times.Length < 2)
            {
                result = BuildVelocityAwarePositionCurves(
                    simplified.Times,
                    simplified.Positions,
                    CreateAllSampleIndices(simplified.Times.Length),
                    false,
                    string.Empty);
                var exactAudit = AuditPositionCurves(
                    simplified.Times,
                    simplified.Positions,
                    result.XCurve,
                    result.YCurve,
                    result.ZCurve,
                    settings.PositionErrorMeters);
                if (!exactAudit.Passed)
                {
                    throw new InvalidOperationException(
                        "Exact dense C2 position curves failed their final " +
                        "raw-derived high-rate motion audit: " +
                        exactAudit.FailureReason);
                }
            }
            else
            {
                var candidate = BuildVelocityAwarePositionCurves(
                    simplified.Times,
                    simplified.Positions,
                    simplified.PositionIndices);
                var audit = AuditPositionCurves(
                    simplified.Times,
                    simplified.Positions,
                    candidate.XCurve,
                    candidate.YCurve,
                    candidate.ZCurve,
                    settings.PositionErrorMeters);
                if (audit.Passed)
                {
                    result = candidate;
                }
                else if (TryBuildRefinedPositionCurves(
                             simplified.Times,
                             simplified.Positions,
                             simplified.PositionIndices,
                             settings.PositionErrorMeters,
                             out var refined))
                {
                    result = refined;
                }
                else
                {
                    result = BuildAuditedDensePositionFallback(
                        simplified.Times,
                        simplified.Positions,
                        settings.PositionErrorMeters,
                        audit.FailureReason);
                }
            }

            simplified.SetPositionCurveDiagnostics(
                result.UsedExactFallback,
                result.FallbackReason);
            return result;
        }

        internal static PositionCurveAuditResult AuditPositionCurves(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> positions,
            AnimationCurve xCurve,
            AnimationCurve yCurve,
            AnimationCurve zCurve,
            float maximumPositionErrorMeters)
        {
            if (times == null || positions == null)
            {
                throw new ArgumentNullException(nameof(times));
            }

            if (xCurve == null || yCurve == null || zCurve == null)
            {
                throw new ArgumentNullException(nameof(xCurve));
            }

            if (times.Count == 0 || positions.Count != times.Count)
            {
                throw new ArgumentException(
                    "Position audit samples must have the same non-zero length.");
            }

            var allowedPositionError =
                maximumPositionErrorMeters + PositionCurveAuditErrorEpsilonMeters;
            var evaluatedAtSourceTimes = EvaluatePositionCurves(
                times,
                xCurve,
                yCurve,
                zCurve);
            for (var index = 0; index < times.Count; index++)
            {
                var error = Vector3.Distance(
                    positions[index],
                    evaluatedAtSourceTimes[index]);
                if (!IsFinite(error) || error > allowedPositionError)
                {
                    return PositionCurveAuditResult.Fail(
                        $"source-sample position error {error:F6} m exceeded " +
                        $"{allowedPositionError:F6} m at frame {index}");
                }
            }

            if (TryDescribeC2CurveFailure(xCurve, "position x", out var c2Failure) ||
                TryDescribeC2CurveFailure(yCurve, "position y", out c2Failure) ||
                TryDescribeC2CurveFailure(zCurve, "position z", out c2Failure))
            {
                return PositionCurveAuditResult.Fail(c2Failure);
            }

            var primaryAuditTimes = BuildUniformCurveAuditTimes(
                times,
                CurveAuditPrimarySamplesPerSecond);
            var convergenceAuditTimes = BuildUniformCurveAuditTimes(
                times,
                CurveAuditConvergenceSamplesPerSecond);
            var evaluatedPrimary = EvaluatePositionCurves(
                primaryAuditTimes,
                xCurve,
                yCurve,
                zCurve);
            var evaluatedConvergence = EvaluatePositionCurves(
                convergenceAuditTimes,
                xCurve,
                yCurve,
                zCurve);
            var sourceMotion = MeasurePositionMotion(times, positions);
            var primaryMotion = MeasurePositionMotion(
                primaryAuditTimes,
                evaluatedPrimary);
            var convergenceMotion = MeasurePositionMotion(
                convergenceAuditTimes,
                evaluatedConvergence);
            if (TryDescribeTimeWindowPositionMotionRegression(
                    "240 Hz raw-derived contract",
                    sourceMotion,
                    primaryMotion,
                    out var rawContractFailure))
            {
                return PositionCurveAuditResult.Fail(rawContractFailure);
            }

            if (TryDescribePositionSamplingConvergence(
                    primaryMotion,
                    convergenceMotion,
                    out var convergenceFailure))
            {
                return PositionCurveAuditResult.Fail(convergenceFailure);
            }

            if (TryDescribeMotionRetentionRegression(
                    "240 Hz",
                    positions,
                    evaluatedPrimary,
                    out var retentionFailure))
            {
                return PositionCurveAuditResult.Fail(retentionFailure);
            }

            return PositionCurveAuditResult.Pass();
        }

        private static PositionCurveBuildResult BuildVelocityAwarePositionCurves(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> positions,
            IReadOnlyList<int> indices,
            bool usedExactFallback = false,
            string fallbackReason = "")
        {
            return new PositionCurveBuildResult(
                BuildC2SplineCurve(
                    times,
                    positions.Select(value => value.x).ToArray(),
                    indices),
                BuildC2SplineCurve(
                    times,
                    positions.Select(value => value.y).ToArray(),
                    indices),
                BuildC2SplineCurve(
                    times,
                    positions.Select(value => value.z).ToArray(),
                    indices),
                usedExactFallback,
                fallbackReason);
        }

        private static PositionCurveBuildResult BuildAuditedDensePositionFallback(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> sourcePositions,
            float maximumPositionErrorMeters,
            string initialFailureReason)
        {
            var allIndices = CreateAllSampleIndices(times.Count);
            var exactDense = BuildVelocityAwarePositionCurves(
                times,
                sourcePositions,
                allIndices,
                true,
                initialFailureReason);
            var lastAudit = AuditPositionCurves(
                times,
                sourcePositions,
                exactDense.XCurve,
                exactDense.YCurve,
                exactDense.ZCurve,
                maximumPositionErrorMeters);
            if (lastAudit.Passed)
            {
                return exactDense;
            }

            var exactFailureReason = lastAudit.FailureReason;

            var smoothingPassCounts = new[] { 1, 2, 4, 8, 16, 32, 64 };
            foreach (var passCount in smoothingPassCounts)
            {
                var smoothed = SmoothPositionsWithinErrorBudget(
                    sourcePositions,
                    maximumPositionErrorMeters * 0.9f,
                    passCount);
                var candidate = BuildVelocityAwarePositionCurves(
                    times,
                    smoothed,
                    allIndices,
                    true,
                    initialFailureReason +
                    $"; bounded C2 smoothing passes={passCount}");
                lastAudit = AuditPositionCurves(
                    times,
                    sourcePositions,
                    candidate.XCurve,
                    candidate.YCurve,
                    candidate.ZCurve,
                    maximumPositionErrorMeters);
                if (lastAudit.Passed)
                {
                    return candidate;
                }
            }

            throw new InvalidOperationException(
                "Dense C2 position fallback failed its final jerk-bounded " +
                $"motion audit (exact: {exactFailureReason}; final: " +
                $"{lastAudit.FailureReason})");
        }

        private static bool TryBuildRefinedPositionCurves(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> sourcePositions,
            IReadOnlyList<int> initialIndices,
            float maximumPositionErrorMeters,
            out PositionCurveBuildResult result)
        {
            const int maximumAddedKeys = 64;
            var indices = new SortedSet<int>(initialIndices);
            var allowedError = maximumPositionErrorMeters +
                               PositionCurveAuditErrorEpsilonMeters;
            for (var iteration = 0;
                 iteration <= maximumAddedKeys && indices.Count < times.Count;
                 iteration++)
            {
                var candidate = BuildVelocityAwarePositionCurves(
                    times,
                    sourcePositions,
                    indices.ToArray());
                var evaluated = EvaluatePositionCurves(
                    times,
                    candidate.XCurve,
                    candidate.YCurve,
                    candidate.ZCurve);
                var worstIndex = -1;
                var worstError = 0f;
                for (var index = 0; index < times.Count; index++)
                {
                    var error = Vector3.Distance(
                        sourcePositions[index],
                        evaluated[index]);
                    if (error > worstError)
                    {
                        worstError = error;
                        worstIndex = index;
                    }
                }

                if (worstError <= allowedError)
                {
                    var audit = AuditPositionCurves(
                        times,
                        sourcePositions,
                        candidate.XCurve,
                        candidate.YCurve,
                        candidate.ZCurve,
                        maximumPositionErrorMeters);
                    if (audit.Passed)
                    {
                        result = candidate;
                        return true;
                    }

                    break;
                }

                if (worstIndex < 0 || !indices.Add(worstIndex))
                {
                    break;
                }
            }

            result = default;
            return false;
        }

        internal static RotationCurveBuildResult BuildRotationCurves(
            SimplifiedCameraCurves simplified,
            CurveSimplificationPreset preset)
        {
            if (simplified == null)
            {
                throw new ArgumentNullException(nameof(simplified));
            }

            var allIndices = CreateAllSampleIndices(simplified.Times.Length);
            IReadOnlyList<int> requestedIndices =
                preset == CurveSimplificationPreset.Exact ||
                simplified.Times.Length < 2
                    ? allIndices
                    : simplified.RotationIndices;
            var candidate = BuildVelocityAwareRotationCurves(
                simplified.Times,
                simplified.Rotations,
                requestedIndices,
                false,
                string.Empty);
            var allowedAngleError = GetCurveSimplificationSettings(preset)
                .QuaternionAngleErrorDegrees;
            var audit = AuditRotationCurves(
                simplified.Times,
                simplified.Rotations,
                candidate.XCurve,
                candidate.YCurve,
                candidate.ZCurve,
                candidate.WCurve,
                allowedAngleError);

            RotationCurveBuildResult result;
            if (audit.Passed)
            {
                result = candidate;
            }
            else if (preset != CurveSimplificationPreset.Exact &&
                     TryBuildRefinedRotationCurves(
                         simplified.Times,
                         simplified.Rotations,
                         requestedIndices,
                         allowedAngleError,
                         out var refined))
            {
                result = refined;
            }
            else if (preset != CurveSimplificationPreset.Exact)
            {
                result = BuildAuditedDenseRotationFallback(
                    simplified.Times,
                    simplified.Rotations,
                    allowedAngleError,
                    audit.FailureReason);
            }
            else
            {
                throw new InvalidOperationException(
                    "Dense C2 rotation curves failed their final angular " +
                    $"motion audit: {audit.FailureReason}");
            }

            simplified.SetRotationCurveDiagnostics(
                result.UsedDenseFallback,
                result.FallbackReason);
            return result;
        }

        internal static RotationCurveAuditResult AuditRotationCurves(
            IReadOnlyList<double> times,
            IReadOnlyList<Quaternion> rotations,
            AnimationCurve xCurve,
            AnimationCurve yCurve,
            AnimationCurve zCurve,
            AnimationCurve wCurve,
            float maximumAngleErrorDegrees)
        {
            if (times == null || rotations == null)
            {
                throw new ArgumentNullException(nameof(times));
            }

            if (xCurve == null || yCurve == null ||
                zCurve == null || wCurve == null)
            {
                throw new ArgumentNullException(nameof(xCurve));
            }

            if (times.Count == 0 || rotations.Count != times.Count)
            {
                throw new ArgumentException(
                    "Rotation audit samples must have the same non-zero length.");
            }

            var continuousRotations = MakeQuaternionSequenceContinuous(rotations);
            if (!TryEvaluateRotationCurves(
                    times,
                    xCurve,
                    yCurve,
                    zCurve,
                    wCurve,
                    out var evaluatedAtSourceTimes,
                    out var evaluationFailure))
            {
                return RotationCurveAuditResult.Fail(evaluationFailure);
            }

            var allowedAngleError =
                maximumAngleErrorDegrees +
                RotationCurveAuditAngleErrorEpsilonDegrees;
            for (var index = 0; index < times.Count; index++)
            {
                var error = Quaternion.Angle(
                    continuousRotations[index],
                    evaluatedAtSourceTimes[index]);
                if (!IsFinite(error) || error > allowedAngleError)
                {
                    return RotationCurveAuditResult.Fail(
                        $"source-sample angle error {error:F4} deg exceeded " +
                        $"{allowedAngleError:F4} deg at frame {index}");
                }
            }

            if (TryDescribeC2CurveFailure(xCurve, "rotation x", out var c2Failure) ||
                TryDescribeC2CurveFailure(yCurve, "rotation y", out c2Failure) ||
                TryDescribeC2CurveFailure(zCurve, "rotation z", out c2Failure) ||
                TryDescribeC2CurveFailure(wCurve, "rotation w", out c2Failure))
            {
                return RotationCurveAuditResult.Fail(c2Failure);
            }

            var primaryAuditTimes = BuildUniformCurveAuditTimes(
                times,
                CurveAuditPrimarySamplesPerSecond);
            var convergenceAuditTimes = BuildUniformCurveAuditTimes(
                times,
                CurveAuditConvergenceSamplesPerSecond);
            if (!TryEvaluateRotationCurves(
                    primaryAuditTimes,
                    xCurve,
                    yCurve,
                    zCurve,
                    wCurve,
                    out var evaluatedPrimary,
                    out evaluationFailure))
            {
                return RotationCurveAuditResult.Fail(evaluationFailure);
            }

            if (!TryEvaluateRotationCurves(
                    convergenceAuditTimes,
                    xCurve,
                    yCurve,
                    zCurve,
                    wCurve,
                    out var evaluatedConvergence,
                    out evaluationFailure))
            {
                return RotationCurveAuditResult.Fail(evaluationFailure);
            }

            var sourceMotion = MeasureAngularMotion(times, continuousRotations);
            var primaryMotion = MeasureAngularMotion(
                primaryAuditTimes,
                evaluatedPrimary);
            var convergenceMotion = MeasureAngularMotion(
                convergenceAuditTimes,
                evaluatedConvergence);
            if (TryDescribeTimeWindowAngularMotionRegression(
                    "240 Hz raw-derived contract",
                    sourceMotion,
                    primaryMotion,
                    out var rawContractFailure))
            {
                return RotationCurveAuditResult.Fail(rawContractFailure);
            }

            if (TryDescribeAngularSamplingConvergence(
                    primaryMotion,
                    convergenceMotion,
                    out var convergenceFailure))
            {
                return RotationCurveAuditResult.Fail(convergenceFailure);
            }

            if (TryDescribeAngularMotionRetentionRegression(
                    "240 Hz",
                    continuousRotations,
                    evaluatedPrimary,
                    out var retentionFailure))
            {
                return RotationCurveAuditResult.Fail(retentionFailure);
            }

            return RotationCurveAuditResult.Pass();
        }

        private static RotationCurveBuildResult BuildVelocityAwareRotationCurves(
            IReadOnlyList<double> times,
            IReadOnlyList<Quaternion> rotations,
            IReadOnlyList<int> indices,
            bool usedDenseFallback,
            string fallbackReason)
        {
            var continuousRotations = MakeQuaternionSequenceContinuous(rotations);
            return new RotationCurveBuildResult(
                BuildC2SplineCurve(
                    times,
                    continuousRotations.Select(value => value.x).ToArray(),
                    indices),
                BuildC2SplineCurve(
                    times,
                    continuousRotations.Select(value => value.y).ToArray(),
                    indices),
                BuildC2SplineCurve(
                    times,
                    continuousRotations.Select(value => value.z).ToArray(),
                    indices),
                BuildC2SplineCurve(
                    times,
                    continuousRotations.Select(value => value.w).ToArray(),
                    indices),
                usedDenseFallback,
                fallbackReason);
        }

        private static RotationCurveBuildResult BuildAuditedDenseRotationFallback(
            IReadOnlyList<double> times,
            IReadOnlyList<Quaternion> sourceRotations,
            float maximumAngleErrorDegrees,
            string initialFailureReason)
        {
            var continuousSource = MakeQuaternionSequenceContinuous(sourceRotations);
            var allIndices = CreateAllSampleIndices(times.Count);
            var exactDense = BuildVelocityAwareRotationCurves(
                times,
                continuousSource,
                allIndices,
                true,
                initialFailureReason);
            var lastAudit = AuditRotationCurves(
                times,
                continuousSource,
                exactDense.XCurve,
                exactDense.YCurve,
                exactDense.ZCurve,
                exactDense.WCurve,
                maximumAngleErrorDegrees);
            if (lastAudit.Passed)
            {
                return exactDense;
            }

            var exactFailureReason = lastAudit.FailureReason;

            var smoothingPassCounts = new[] { 1, 2, 4, 8, 16, 32, 64 };
            foreach (var passCount in smoothingPassCounts)
            {
                var smoothed = SmoothRotationsWithinErrorBudget(
                    continuousSource,
                    maximumAngleErrorDegrees * 0.65f,
                    passCount);
                var candidate = BuildVelocityAwareRotationCurves(
                    times,
                    smoothed,
                    allIndices,
                    true,
                    initialFailureReason +
                    $"; bounded C2 smoothing passes={passCount}");
                lastAudit = AuditRotationCurves(
                    times,
                    continuousSource,
                    candidate.XCurve,
                    candidate.YCurve,
                    candidate.ZCurve,
                    candidate.WCurve,
                    maximumAngleErrorDegrees);
                if (lastAudit.Passed)
                {
                    return candidate;
                }
            }

            throw new InvalidOperationException(
                "Dense C2 rotation fallback failed its final angular " +
                $"jerk-bounded audit (exact: {exactFailureReason}; final: " +
                $"{lastAudit.FailureReason})");
        }

        private static bool TryBuildRefinedRotationCurves(
            IReadOnlyList<double> times,
            IReadOnlyList<Quaternion> sourceRotations,
            IReadOnlyList<int> initialIndices,
            float maximumAngleErrorDegrees,
            out RotationCurveBuildResult result)
        {
            const int maximumAddedKeys = 64;
            var continuousSource = MakeQuaternionSequenceContinuous(sourceRotations);
            var indices = new SortedSet<int>(initialIndices);
            var allowedError = maximumAngleErrorDegrees +
                               RotationCurveAuditAngleErrorEpsilonDegrees;
            for (var iteration = 0;
                 iteration <= maximumAddedKeys && indices.Count < times.Count;
                 iteration++)
            {
                var candidate = BuildVelocityAwareRotationCurves(
                    times,
                    continuousSource,
                    indices.ToArray(),
                    false,
                    string.Empty);
                var evaluated = EvaluateRotationCurves(times, candidate);
                var worstIndex = -1;
                var worstError = 0f;
                for (var index = 0; index < times.Count; index++)
                {
                    var error = Quaternion.Angle(
                        continuousSource[index],
                        evaluated[index]);
                    if (error > worstError)
                    {
                        worstError = error;
                        worstIndex = index;
                    }
                }

                if (worstError <= allowedError)
                {
                    var audit = AuditRotationCurves(
                        times,
                        continuousSource,
                        candidate.XCurve,
                        candidate.YCurve,
                        candidate.ZCurve,
                        candidate.WCurve,
                        maximumAngleErrorDegrees);
                    if (audit.Passed)
                    {
                        result = candidate;
                        return true;
                    }

                    break;
                }

                if (worstIndex < 0 || !indices.Add(worstIndex))
                {
                    break;
                }
            }

            result = default;
            return false;
        }

        private static RotationCurveBuildResult EnsureAppliedRotationCurveSafety(
            AnimationClip clip,
            string relativePath,
            SimplifiedCameraCurves simplified,
            CurveSimplificationPreset preset,
            RotationCurveBuildResult requested)
        {
            var applied = ReadAppliedRotationCurves(
                clip,
                relativePath,
                requested.UsedDenseFallback,
                requested.FallbackReason);
            var allowedAngleError = GetCurveSimplificationSettings(preset)
                .QuaternionAngleErrorDegrees;
            var audit = AuditRotationCurves(
                simplified.Times,
                simplified.Rotations,
                applied.XCurve,
                applied.YCurve,
                applied.ZCurve,
                applied.WCurve,
                allowedAngleError);
            if (audit.Passed)
            {
                return applied;
            }

            var fallbackReason =
                $"post-EnsureQuaternionContinuity clip audit failed: " +
                $"{audit.FailureReason}";
            var fallback = BuildAuditedDenseRotationFallback(
                simplified.Times,
                simplified.Rotations,
                allowedAngleError,
                string.IsNullOrEmpty(requested.FallbackReason)
                    ? fallbackReason
                    : $"{requested.FallbackReason}; {fallbackReason}");

            // The source quaternions were already sign-normalized before the C2
            // spline was built.  If Unity's legacy continuity pass changed the
            // audited curve tangents, overwrite it with the dense C2 contract
            // and validate the exact curves that remain stored in the clip.
            SetRotationCurves(clip, relativePath, fallback);
            applied = ReadAppliedRotationCurves(
                clip,
                relativePath,
                true,
                fallback.FallbackReason);
            var fallbackAudit = AuditRotationCurves(
                simplified.Times,
                simplified.Rotations,
                applied.XCurve,
                applied.YCurve,
                applied.ZCurve,
                applied.WCurve,
                allowedAngleError);
            if (!fallbackAudit.Passed)
            {
                throw new InvalidOperationException(
                    "The final AnimationClip rotation curves failed the dense " +
                    $"C2 safety contract: {fallbackAudit.FailureReason}");
            }

            simplified.SetRotationCurveDiagnostics(true, applied.FallbackReason);
            return applied;
        }

        private static void SetRotationCurves(
            AnimationClip clip,
            string relativePath,
            RotationCurveBuildResult curves)
        {
            SetCurve(
                clip,
                relativePath,
                typeof(Transform),
                "m_LocalRotation.x",
                curves.XCurve);
            SetCurve(
                clip,
                relativePath,
                typeof(Transform),
                "m_LocalRotation.y",
                curves.YCurve);
            SetCurve(
                clip,
                relativePath,
                typeof(Transform),
                "m_LocalRotation.z",
                curves.ZCurve);
            SetCurve(
                clip,
                relativePath,
                typeof(Transform),
                "m_LocalRotation.w",
                curves.WCurve);
        }

        private static RotationCurveBuildResult ReadAppliedRotationCurves(
            AnimationClip clip,
            string relativePath,
            bool usedDenseFallback,
            string fallbackReason)
        {
            return new RotationCurveBuildResult(
                GetCurve(
                    clip,
                    relativePath,
                    typeof(Transform),
                    "m_LocalRotation.x"),
                GetCurve(
                    clip,
                    relativePath,
                    typeof(Transform),
                    "m_LocalRotation.y"),
                GetCurve(
                    clip,
                    relativePath,
                    typeof(Transform),
                    "m_LocalRotation.z"),
                GetCurve(
                    clip,
                    relativePath,
                    typeof(Transform),
                    "m_LocalRotation.w"),
                usedDenseFallback,
                fallbackReason);
        }

        private static Vector3[] SmoothPositionsWithinErrorBudget(
            IReadOnlyList<Vector3> source,
            float maximumDeviationMeters,
            int passCount)
        {
            var original = source.ToArray();
            var current = original.ToArray();
            if (current.Length < 3 || maximumDeviationMeters <= 0f)
            {
                return current;
            }

            for (var pass = 0; pass < passCount; pass++)
            {
                var filteredValues = current.ToArray();
                var maximumFilteredDeviation = 0f;
                for (var index = 1; index + 1 < current.Length; index++)
                {
                    var filtered =
                        GetClamped(current, index - 2) * (1f / 16f) +
                        GetClamped(current, index - 1) * (4f / 16f) +
                        current[index] * (6f / 16f) +
                        GetClamped(current, index + 1) * (4f / 16f) +
                        GetClamped(current, index + 2) * (1f / 16f);
                    var endpointWeight = GetEndpointSmoothingWeight(
                        index,
                        current.Length);
                    filteredValues[index] = Vector3.LerpUnclamped(
                        original[index],
                        filtered,
                        endpointWeight);
                    maximumFilteredDeviation = Mathf.Max(
                        maximumFilteredDeviation,
                        Vector3.Distance(
                            original[index],
                            filteredValues[index]));
                }

                var budgetScale = maximumFilteredDeviation <=
                                  maximumDeviationMeters
                    ? 1f
                    : maximumDeviationMeters / maximumFilteredDeviation;
                var next = original.ToArray();
                for (var index = 1; index + 1 < current.Length; index++)
                {
                    next[index] = Vector3.LerpUnclamped(
                        original[index],
                        filteredValues[index],
                        budgetScale);
                }
                next[0] = original[0];
                next[next.Length - 1] = original[original.Length - 1];
                current = next;
            }

            return current;
        }

        private static Quaternion[] SmoothRotationsWithinErrorBudget(
            IReadOnlyList<Quaternion> source,
            float maximumDeviationDegrees,
            int passCount)
        {
            var original = MakeQuaternionSequenceContinuous(source);
            var current = original.ToArray();
            if (current.Length < 3 || maximumDeviationDegrees <= 0f)
            {
                return current;
            }

            for (var pass = 0; pass < passCount; pass++)
            {
                var filteredValues = current.ToArray();
                var maximumFilteredDeviation = 0f;
                for (var index = 1; index + 1 < current.Length; index++)
                {
                    var reference = current[index];
                    var weighted = Vector4.zero;
                    AccumulateQuaternion(
                        ref weighted,
                        GetClamped(current, index - 2),
                        reference,
                        1f);
                    AccumulateQuaternion(
                        ref weighted,
                        GetClamped(current, index - 1),
                        reference,
                        4f);
                    AccumulateQuaternion(
                        ref weighted,
                        reference,
                        reference,
                        6f);
                    AccumulateQuaternion(
                        ref weighted,
                        GetClamped(current, index + 1),
                        reference,
                        4f);
                    AccumulateQuaternion(
                        ref weighted,
                        GetClamped(current, index + 2),
                        reference,
                        1f);
                    var filtered = NormalizeQuaternion(new Quaternion(
                        weighted.x,
                        weighted.y,
                        weighted.z,
                        weighted.w));
                    if (Quaternion.Dot(original[index], filtered) < 0f)
                    {
                        filtered = NegateQuaternion(filtered);
                    }

                    filteredValues[index] = Quaternion.Slerp(
                        original[index],
                        filtered,
                        GetEndpointSmoothingWeight(index, current.Length));
                    maximumFilteredDeviation = Mathf.Max(
                        maximumFilteredDeviation,
                        Quaternion.Angle(
                            original[index],
                            filteredValues[index]));
                }

                var budgetScale = maximumFilteredDeviation <=
                                  maximumDeviationDegrees
                    ? 1f
                    : maximumDeviationDegrees / maximumFilteredDeviation;
                var next = original.ToArray();
                for (var index = 1; index + 1 < current.Length; index++)
                {
                    next[index] = Quaternion.Slerp(
                        original[index],
                        filteredValues[index],
                        budgetScale);
                }
                next[0] = original[0];
                next[next.Length - 1] = original[original.Length - 1];
                current = MakeQuaternionSequenceContinuous(next);
            }

            return current;
        }

        private static float GetEndpointSmoothingWeight(int index, int count)
        {
            const float fadeFrames = 8f;
            var distanceFromEndpoint = Mathf.Min(index, count - 1 - index);
            var t = Mathf.Clamp01(distanceFromEndpoint / fadeFrames);
            // Quintic smootherstep makes both velocity and acceleration of the
            // smoothing displacement vanish at the preserved shot endpoints.
            return t * t * t * (t * (t * 6f - 15f) + 10f);
        }

        private static T GetClamped<T>(IReadOnlyList<T> values, int index)
        {
            return values[Mathf.Clamp(index, 0, values.Count - 1)];
        }

        private static void AccumulateQuaternion(
            ref Vector4 total,
            Quaternion value,
            Quaternion reference,
            float weight)
        {
            if (Quaternion.Dot(reference, value) < 0f)
            {
                value = NegateQuaternion(value);
            }

            total.x += value.x * weight;
            total.y += value.y * weight;
            total.z += value.z * weight;
            total.w += value.w * weight;
        }

        private static AnimationCurve BuildC2SplineCurve(
            IReadOnlyList<double> times,
            IReadOnlyList<float> values,
            IReadOnlyList<int> indices)
        {
            if (indices.Count == 1)
            {
                var sampleIndex = indices[0];
                return new AnimationCurve(
                    new Keyframe((float)times[sampleIndex], values[sampleIndex], 0f, 0f));
            }

            var keyCount = indices.Count;
            var keyTimes = new double[keyCount];
            var keyValues = new double[keyCount];
            for (var keyIndex = 0; keyIndex < keyCount; keyIndex++)
            {
                var sampleIndex = indices[keyIndex];
                // Solve against the exact float data that AnimationCurve will
                // store. Solving against the original doubles and rounding only
                // at the end creates measurable acceleration seams at 60 Hz.
                keyTimes[keyIndex] = (float)times[sampleIndex];
                keyValues[keyIndex] = values[sampleIndex];
            }

            var secondDerivatives = SolveC2SplineSecondDerivatives(
                keyTimes,
                keyValues);
            var tangents = CalculateC2SplineTangents(
                keyTimes,
                keyValues,
                secondDerivatives);
            var keys = new Keyframe[indices.Count];
            for (var keyIndex = 0; keyIndex < indices.Count; keyIndex++)
            {
                keys[keyIndex] = new Keyframe(
                    (float)keyTimes[keyIndex],
                    (float)keyValues[keyIndex],
                    tangents[keyIndex],
                    tangents[keyIndex]);
            }

            return new AnimationCurve(keys);
        }

        private static double[] SolveC2SplineSecondDerivatives(
            IReadOnlyList<double> times,
            IReadOnlyList<double> values)
        {
            var count = values.Count;
            var secondDerivatives = new double[count];
            if (count <= 2)
            {
                return secondDerivatives;
            }

            if (count == 3)
            {
                var leftDuration = times[1] - times[0];
                var rightDuration = times[2] - times[1];
                var acceleration = 2.0 *
                    ((values[2] - values[1]) / rightDuration -
                     (values[1] - values[0]) / leftDuration) /
                    (leftDuration + rightDuration);
                secondDerivatives[0] = acceleration;
                secondDerivatives[1] = acceleration;
                secondDerivatives[2] = acceleration;
                return secondDerivatives;
            }

            var interiorCount = count - 2;
            var lower = new double[interiorCount];
            var diagonal = new double[interiorCount];
            var upper = new double[interiorCount];
            var rightHandSide = new double[interiorCount];
            for (var interior = 0; interior < interiorCount; interior++)
            {
                var index = interior + 1;
                var leftDuration = times[index] - times[index - 1];
                var rightDuration = times[index + 1] - times[index];
                lower[interior] = leftDuration;
                diagonal[interior] = 2.0 * (leftDuration + rightDuration);
                upper[interior] = rightDuration;
                rightHandSide[interior] = 6.0 *
                    ((values[index + 1] - values[index]) / rightDuration -
                     (values[index] - values[index - 1]) / leftDuration);
            }

            // Not-a-knot boundaries make jerk continuous at the first and last
            // interior key. Unlike a natural spline, this does not inject an
            // artificial endpoint acceleration reset into every camera shot.
            var firstDuration = times[1] - times[0];
            var secondDuration = times[2] - times[1];
            lower[0] = 0.0;
            diagonal[0] =
                3.0 * firstDuration +
                2.0 * secondDuration +
                firstDuration * firstDuration / secondDuration;
            upper[0] =
                secondDuration -
                firstDuration * firstDuration / secondDuration;

            var previousDuration =
                times[count - 2] - times[count - 3];
            var lastDuration =
                times[count - 1] - times[count - 2];
            lower[interiorCount - 1] =
                previousDuration -
                lastDuration * lastDuration / previousDuration;
            diagonal[interiorCount - 1] =
                2.0 * previousDuration +
                3.0 * lastDuration +
                lastDuration * lastDuration / previousDuration;
            upper[interiorCount - 1] = 0.0;

            for (var index = 1; index < interiorCount; index++)
            {
                var scale = lower[index] / diagonal[index - 1];
                diagonal[index] -= scale * upper[index - 1];
                rightHandSide[index] -= scale * rightHandSide[index - 1];
            }

            secondDerivatives[count - 2] =
                rightHandSide[interiorCount - 1] /
                diagonal[interiorCount - 1];
            for (var interior = interiorCount - 2; interior >= 0; interior--)
            {
                secondDerivatives[interior + 1] =
                    (rightHandSide[interior] -
                     upper[interior] * secondDerivatives[interior + 2]) /
                    diagonal[interior];
            }

            secondDerivatives[0] =
                (1.0 + firstDuration / secondDuration) *
                secondDerivatives[1] -
                firstDuration / secondDuration * secondDerivatives[2];
            secondDerivatives[count - 1] =
                (1.0 + lastDuration / previousDuration) *
                secondDerivatives[count - 2] -
                lastDuration / previousDuration *
                secondDerivatives[count - 3];

            return secondDerivatives;
        }

        private static float[] CalculateC2SplineTangents(
            IReadOnlyList<double> times,
            IReadOnlyList<double> values,
            IReadOnlyList<double> secondDerivatives)
        {
            var count = values.Count;
            var tangents = new float[count];
            for (var segment = 0; segment < count - 1; segment++)
            {
                var duration = times[segment + 1] - times[segment];
                var secant = (values[segment + 1] - values[segment]) / duration;
                var leftTangent = secant -
                    duration *
                    (2.0 * secondDerivatives[segment] +
                     secondDerivatives[segment + 1]) /
                    6.0;
                var rightTangent = secant +
                    duration *
                    (secondDerivatives[segment] +
                     2.0 * secondDerivatives[segment + 1]) /
                    6.0;
                if (segment == 0)
                {
                    tangents[0] = (float)leftTangent;
                }

                tangents[segment + 1] = (float)rightTangent;
            }

            return tangents;
        }

        private static int[] CreateAllSampleIndices(int count)
        {
            return Enumerable.Range(0, count).ToArray();
        }

        internal static List<double> BuildPositionCurveAuditTimes(
            IReadOnlyList<double> sourceTimes)
        {
            if (sourceTimes == null)
            {
                throw new ArgumentNullException(nameof(sourceTimes));
            }

            if (sourceTimes.Count == 0)
            {
                throw new ArgumentException(
                    "Position audit requires at least one source time.",
                    nameof(sourceTimes));
            }

            var result = new List<double>(sourceTimes.Count * 2 - 1);
            for (var index = 0; index < sourceTimes.Count; index++)
            {
                var sourceTime = sourceTimes[index];
                if (double.IsNaN(sourceTime) || double.IsInfinity(sourceTime))
                {
                    throw new ArgumentException(
                        "Position audit source times must be finite.",
                        nameof(sourceTimes));
                }

                AppendPositionCurveAuditTime(result, sourceTime);
                if (index + 1 >= sourceTimes.Count)
                {
                    continue;
                }

                var nextTime = sourceTimes[index + 1];
                var duration = nextTime - sourceTime;
                if (duration < -PositionCurveAuditTimeToleranceSeconds)
                {
                    throw new ArgumentException(
                        "Position audit source times must be ascending.",
                        nameof(sourceTimes));
                }

                if (duration > PositionCurveAuditTimeToleranceSeconds &&
                    (float)sourceTime != (float)nextTime)
                {
                    AppendPositionCurveAuditTime(
                        result,
                        sourceTime + duration * 0.5);
                }
            }

            return result;
        }

        private static void AppendPositionCurveAuditTime(
            ICollection<double> result,
            double time)
        {
            if (result.Count == 0)
            {
                result.Add(time);
                return;
            }

            var previous = result.Last();
            if (time - previous <= PositionCurveAuditTimeToleranceSeconds ||
                (float)time == (float)previous)
            {
                return;
            }

            result.Add(time);
        }

        internal static List<double> BuildUniformCurveAuditTimes(
            IReadOnlyList<double> sourceTimes,
            int samplesPerSecond)
        {
            if (sourceTimes == null)
            {
                throw new ArgumentNullException(nameof(sourceTimes));
            }

            if (sourceTimes.Count == 0)
            {
                throw new ArgumentException(
                    "Curve audit requires at least one source time.",
                    nameof(sourceTimes));
            }

            if (samplesPerSecond <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(samplesPerSecond));
            }

            var start = sourceTimes[0];
            var end = sourceTimes[sourceTimes.Count - 1];
            var duration = end - start;
            if (duration <= PositionCurveAuditTimeToleranceSeconds)
            {
                return new List<double> { (double)(float)start };
            }

            var intervalCount = Math.Max(
                1,
                (int)Math.Ceiling(duration * samplesPerSecond));
            var result = new List<double>(intervalCount + 1);
            for (var index = 0; index <= intervalCount; index++)
            {
                var time = (double)(float)(
                    start + duration * index / intervalCount);
                if (result.Count == 0 || time > result[result.Count - 1])
                {
                    result.Add(time);
                }
            }

            return result;
        }

        internal static bool TryDescribeC2CurveFailure(
            AnimationCurve curve,
            string label,
            out string failureReason)
        {
            if (curve == null)
            {
                throw new ArgumentNullException(nameof(curve));
            }

            var keys = curve.keys;
            for (var index = 0; index < keys.Length; index++)
            {
                if (!IsFinite(keys[index].inTangent) ||
                    !IsFinite(keys[index].outTangent))
                {
                    failureReason =
                        $"{label} key {index} has a non-finite tangent";
                    return true;
                }

                if (Mathf.Abs(keys[index].inTangent - keys[index].outTangent) >
                    CurveC1AbsoluteTolerance)
                {
                    failureReason =
                        $"{label} key {index} is not C1: in/out velocity " +
                        $"{keys[index].inTangent:F6}/" +
                        $"{keys[index].outTangent:F6}";
                    return true;
                }
            }

            for (var index = 1; index + 1 < keys.Length; index++)
            {
                var leftAcceleration = CalculateHermiteSecondDerivativeAtRight(
                    keys[index - 1],
                    keys[index]);
                var rightAcceleration = CalculateHermiteSecondDerivativeAtLeft(
                    keys[index],
                    keys[index + 1]);
                var scale = Mathf.Max(
                    Mathf.Abs(leftAcceleration),
                    Mathf.Abs(rightAcceleration));
                var allowed = Mathf.Max(
                    CurveC2AbsoluteTolerance,
                    scale * CurveC2RelativeTolerance);
                if (!IsFinite(leftAcceleration) ||
                    !IsFinite(rightAcceleration) ||
                    Mathf.Abs(leftAcceleration - rightAcceleration) > allowed)
                {
                    failureReason =
                        $"{label} key {index} is not C2: left/right " +
                        $"acceleration {leftAcceleration:F4}/" +
                        $"{rightAcceleration:F4}; allowed delta {allowed:F4}";
                    return true;
                }
            }

            failureReason = string.Empty;
            return false;
        }

        private static float CalculateHermiteSecondDerivativeAtLeft(
            Keyframe left,
            Keyframe right)
        {
            var duration = right.time - left.time;
            var displacement = right.value - left.value;
            return 6f * displacement / (duration * duration) -
                   (4f * left.outTangent + 2f * right.inTangent) / duration;
        }

        private static float CalculateHermiteSecondDerivativeAtRight(
            Keyframe left,
            Keyframe right)
        {
            var duration = right.time - left.time;
            var displacement = right.value - left.value;
            return -6f * displacement / (duration * duration) +
                   (2f * left.outTangent + 4f * right.inTangent) / duration;
        }

        private static Vector3[] EvaluatePositionCurves(
            IReadOnlyList<double> times,
            AnimationCurve xCurve,
            AnimationCurve yCurve,
            AnimationCurve zCurve)
        {
            var result = new Vector3[times.Count];
            for (var index = 0; index < times.Count; index++)
            {
                var time = (float)times[index];
                result[index] = new Vector3(
                    xCurve.Evaluate(time),
                    yCurve.Evaluate(time),
                    zCurve.Evaluate(time));
            }

            return result;
        }

        private static PositionMotionSeries MeasurePositionMotion(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> positions)
        {
            if (times.Count < 2)
            {
                return PositionMotionSeries.Empty;
            }

            var velocities = new Vector3[times.Count - 1];
            var velocityTimes = new double[velocities.Length];
            var speeds = new float[velocities.Length];
            for (var index = 0; index < velocities.Length; index++)
            {
                var duration = times[index + 1] - times[index];
                velocities[index] =
                    (positions[index + 1] - positions[index]) / (float)duration;
                velocityTimes[index] = (times[index + 1] + times[index]) * 0.5;
                speeds[index] = velocities[index].magnitude;
            }

            if (velocities.Length < 2)
            {
                return new PositionMotionSeries(
                    new PositionMetricSeries(velocityTimes, speeds),
                    PositionMetricSeries.Empty,
                    PositionMetricSeries.Empty);
            }

            var accelerations = new Vector3[velocities.Length - 1];
            var accelerationTimes = new double[accelerations.Length];
            var accelerationMagnitudes = new float[accelerations.Length];
            for (var index = 0; index < accelerations.Length; index++)
            {
                var duration = velocityTimes[index + 1] - velocityTimes[index];
                accelerations[index] =
                    (velocities[index + 1] - velocities[index]) / (float)duration;
                accelerationTimes[index] =
                    (velocityTimes[index + 1] + velocityTimes[index]) * 0.5;
                accelerationMagnitudes[index] = accelerations[index].magnitude;
            }

            var jerkTimes = new double[Math.Max(0, accelerations.Length - 1)];
            var jerkMagnitudes = new float[jerkTimes.Length];
            for (var index = 0; index < accelerations.Length - 1; index++)
            {
                var duration =
                    accelerationTimes[index + 1] - accelerationTimes[index];
                var jerk =
                    (accelerations[index + 1] - accelerations[index]) /
                    (float)duration;
                jerkTimes[index] =
                    (accelerationTimes[index + 1] + accelerationTimes[index]) *
                    0.5;
                jerkMagnitudes[index] = jerk.magnitude;
            }

            return new PositionMotionSeries(
                new PositionMetricSeries(velocityTimes, speeds),
                new PositionMetricSeries(
                    accelerationTimes,
                    accelerationMagnitudes),
                new PositionMetricSeries(jerkTimes, jerkMagnitudes));
        }

        internal static Quaternion[] EvaluateRotationCurves(
            IReadOnlyList<double> times,
            RotationCurveBuildResult curves)
        {
            if (curves == null)
            {
                throw new ArgumentNullException(nameof(curves));
            }

            if (!TryEvaluateRotationCurves(
                    times,
                    curves.XCurve,
                    curves.YCurve,
                    curves.ZCurve,
                    curves.WCurve,
                    out var result,
                    out var failureReason))
            {
                throw new InvalidOperationException(failureReason);
            }

            return result;
        }

        private static bool TryEvaluateRotationCurves(
            IReadOnlyList<double> times,
            AnimationCurve xCurve,
            AnimationCurve yCurve,
            AnimationCurve zCurve,
            AnimationCurve wCurve,
            out Quaternion[] rotations,
            out string failureReason)
        {
            rotations = new Quaternion[times.Count];
            for (var index = 0; index < times.Count; index++)
            {
                var time = (float)times[index];
                var rotation = new Quaternion(
                    xCurve.Evaluate(time),
                    yCurve.Evaluate(time),
                    zCurve.Evaluate(time),
                    wCurve.Evaluate(time));
                if (!IsFinite(rotation.x) ||
                    !IsFinite(rotation.y) ||
                    !IsFinite(rotation.z) ||
                    !IsFinite(rotation.w))
                {
                    failureReason =
                        $"rotation curve evaluated to a non-finite quaternion " +
                        $"at {times[index]:F4} s";
                    return false;
                }

                var squaredMagnitude =
                    rotation.x * rotation.x +
                    rotation.y * rotation.y +
                    rotation.z * rotation.z +
                    rotation.w * rotation.w;
                if (squaredMagnitude < 0.000000000001f)
                {
                    failureReason =
                        $"rotation curve evaluated to a zero quaternion at " +
                        $"{times[index]:F4} s";
                    return false;
                }

                rotation = NormalizeQuaternion(rotation);
                if (index > 0 && Quaternion.Dot(rotations[index - 1], rotation) < 0f)
                {
                    rotation = NegateQuaternion(rotation);
                }

                rotations[index] = rotation;
            }

            failureReason = string.Empty;
            return true;
        }

        private static PositionMotionSeries MeasureAngularMotion(
            IReadOnlyList<double> times,
            IReadOnlyList<Quaternion> rotations)
        {
            if (times.Count < 2)
            {
                return PositionMotionSeries.Empty;
            }

            var continuousRotations = MakeQuaternionSequenceContinuous(rotations);
            var velocities = new Vector3[times.Count - 1];
            var velocityTimes = new double[velocities.Length];
            var speeds = new float[velocities.Length];
            for (var index = 0; index < velocities.Length; index++)
            {
                var duration = times[index + 1] - times[index];
                velocities[index] = CalculateAngularVelocityDegrees(
                    continuousRotations[index],
                    continuousRotations[index + 1],
                    duration);
                velocityTimes[index] = (times[index + 1] + times[index]) * 0.5;
                speeds[index] = velocities[index].magnitude;
            }

            if (velocities.Length < 2)
            {
                return new PositionMotionSeries(
                    new PositionMetricSeries(velocityTimes, speeds),
                    PositionMetricSeries.Empty,
                    PositionMetricSeries.Empty);
            }

            var accelerations = new Vector3[velocities.Length - 1];
            var accelerationTimes = new double[accelerations.Length];
            var accelerationMagnitudes = new float[accelerations.Length];
            for (var index = 0; index < accelerations.Length; index++)
            {
                var duration = velocityTimes[index + 1] - velocityTimes[index];
                accelerations[index] =
                    (velocities[index + 1] - velocities[index]) / (float)duration;
                accelerationTimes[index] =
                    (velocityTimes[index + 1] + velocityTimes[index]) * 0.5;
                accelerationMagnitudes[index] = accelerations[index].magnitude;
            }

            var jerkTimes = new double[Math.Max(0, accelerations.Length - 1)];
            var jerkMagnitudes = new float[jerkTimes.Length];
            for (var index = 0; index < accelerations.Length - 1; index++)
            {
                var duration =
                    accelerationTimes[index + 1] - accelerationTimes[index];
                var jerk =
                    (accelerations[index + 1] - accelerations[index]) /
                    (float)duration;
                jerkTimes[index] =
                    (accelerationTimes[index + 1] + accelerationTimes[index]) *
                    0.5;
                jerkMagnitudes[index] = jerk.magnitude;
            }

            return new PositionMotionSeries(
                new PositionMetricSeries(velocityTimes, speeds),
                new PositionMetricSeries(
                    accelerationTimes,
                    accelerationMagnitudes),
                new PositionMetricSeries(jerkTimes, jerkMagnitudes));
        }

        private static Vector3 CalculateAngularVelocityDegrees(
            Quaternion start,
            Quaternion end,
            double duration)
        {
            if (duration <= double.Epsilon)
            {
                return Vector3.zero;
            }

            var delta = NormalizeQuaternion(end * Quaternion.Inverse(start));
            if (delta.w < 0f)
            {
                delta = NegateQuaternion(delta);
            }

            var sineHalfAngle = Mathf.Sqrt(
                delta.x * delta.x +
                delta.y * delta.y +
                delta.z * delta.z);
            if (sineHalfAngle < 0.0000001f)
            {
                return Vector3.zero;
            }

            var halfAngle = Mathf.Atan2(
                sineHalfAngle,
                Mathf.Clamp(delta.w, -1f, 1f));
            var degreesPerSecond =
                2f * halfAngle * Mathf.Rad2Deg / (float)duration;
            return new Vector3(delta.x, delta.y, delta.z) /
                   sineHalfAngle * degreesPerSecond;
        }

        private static bool TryDescribeTimeWindowPositionMotionRegression(
            string sampleLabel,
            PositionMotionSeries reference,
            PositionMotionSeries candidate,
            out string failureReason)
        {
            if (TryDescribeTimeWindowMetricRegression(
                    sampleLabel,
                    "speed",
                    "m/s",
                    reference.Speed,
                    candidate.Speed,
                    CurveAuditLocalWindowSeconds,
                    PositionCurveAuditSpeedRatio,
                    PositionCurveAuditSpeedDeltaMetersPerSecond,
                    PositionCurveAuditSpeedFloorMetersPerSecond,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeTimeWindowMetricRegression(
                    sampleLabel,
                    "acceleration",
                    "m/s^2",
                    reference.Acceleration,
                    candidate.Acceleration,
                    CurveAuditLocalWindowSeconds,
                    PositionCurveAuditAccelerationRatio,
                    PositionCurveAuditAccelerationDeltaMetersPerSecondSquared,
                    PositionCurveAuditAccelerationFloorMetersPerSecondSquared,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeTimeWindowMetricRegression(
                sampleLabel,
                "jerk",
                "m/s^3",
                reference.Jerk,
                candidate.Jerk,
                CurveAuditLocalWindowSeconds,
                PositionCurveAuditJerkRatio,
                PositionCurveAuditJerkDeltaMetersPerSecondCubed,
                PositionCurveAuditJerkFloorMetersPerSecondCubed,
                out failureReason);
        }

        private static bool TryDescribePositionSamplingConvergence(
            PositionMotionSeries primary,
            PositionMotionSeries convergence,
            out string failureReason)
        {
            var window = 1.0 / 60.0;
            if (TryDescribeTimeWindowMetricRegression(
                    "240/480 Hz convergence",
                    "speed",
                    "m/s",
                    primary.Speed,
                    convergence.Speed,
                    window,
                    CurveAuditConvergenceRatio,
                    0f,
                    PositionCurveAuditConvergenceSpeedFloor,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeTimeWindowMetricRegression(
                    "240/480 Hz convergence",
                    "acceleration",
                    "m/s^2",
                    primary.Acceleration,
                    convergence.Acceleration,
                    window,
                    CurveAuditConvergenceRatio,
                    0f,
                    PositionCurveAuditConvergenceAccelerationFloor,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeTimeWindowMetricRegression(
                "240/480 Hz convergence",
                "jerk",
                "m/s^3",
                primary.Jerk,
                convergence.Jerk,
                window,
                CurveAuditConvergenceRatio,
                0f,
                PositionCurveAuditConvergenceJerkFloor,
                out failureReason);
        }

        private static bool TryDescribeTimeWindowAngularMotionRegression(
            string sampleLabel,
            PositionMotionSeries reference,
            PositionMotionSeries candidate,
            out string failureReason)
        {
            if (TryDescribeTimeWindowMetricRegression(
                    sampleLabel,
                    "angular speed",
                    "deg/s",
                    reference.Speed,
                    candidate.Speed,
                    CurveAuditLocalWindowSeconds,
                    RotationCurveAuditSpeedRatio,
                    RotationCurveAuditSpeedDeltaDegreesPerSecond,
                    RotationCurveAuditSpeedFloorDegreesPerSecond,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeTimeWindowMetricRegression(
                    sampleLabel,
                    "angular acceleration",
                    "deg/s^2",
                    reference.Acceleration,
                    candidate.Acceleration,
                    CurveAuditLocalWindowSeconds,
                    RotationCurveAuditAccelerationRatio,
                    RotationCurveAuditAccelerationDeltaDegreesPerSecondSquared,
                    RotationCurveAuditAccelerationFloorDegreesPerSecondSquared,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeTimeWindowMetricRegression(
                sampleLabel,
                "angular jerk",
                "deg/s^3",
                reference.Jerk,
                candidate.Jerk,
                CurveAuditLocalWindowSeconds,
                RotationCurveAuditJerkRatio,
                RotationCurveAuditJerkDeltaDegreesPerSecondCubed,
                RotationCurveAuditJerkFloorDegreesPerSecondCubed,
                out failureReason);
        }

        private static bool TryDescribeAngularSamplingConvergence(
            PositionMotionSeries primary,
            PositionMotionSeries convergence,
            out string failureReason)
        {
            var window = 1.0 / 60.0;
            if (TryDescribeTimeWindowMetricRegression(
                    "240/480 Hz convergence",
                    "angular speed",
                    "deg/s",
                    primary.Speed,
                    convergence.Speed,
                    window,
                    CurveAuditConvergenceRatio,
                    0f,
                    RotationCurveAuditConvergenceSpeedFloor,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeTimeWindowMetricRegression(
                    "240/480 Hz convergence",
                    "angular acceleration",
                    "deg/s^2",
                    primary.Acceleration,
                    convergence.Acceleration,
                    window,
                    CurveAuditConvergenceRatio,
                    0f,
                    RotationCurveAuditConvergenceAccelerationFloor,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeTimeWindowMetricRegression(
                "240/480 Hz convergence",
                "angular jerk",
                "deg/s^3",
                primary.Jerk,
                convergence.Jerk,
                window,
                CurveAuditConvergenceRatio,
                0f,
                RotationCurveAuditConvergenceJerkFloor,
                out failureReason);
        }

        private static bool TryDescribeTimeWindowMetricRegression(
            string sampleLabel,
            string metricLabel,
            string unit,
            PositionMetricSeries reference,
            PositionMetricSeries candidate,
            double windowSeconds,
            float ratio,
            float absoluteDelta,
            float absoluteFloor,
            out string failureReason)
        {
            if (candidate.Values.Length == 0)
            {
                failureReason = string.Empty;
                return false;
            }

            var firstReference = 0;
            var pastLastReference = 0;
            for (var candidateIndex = 0;
                 candidateIndex < candidate.Values.Length;
                 candidateIndex++)
            {
                var time = candidate.Times[candidateIndex];
                while (firstReference < reference.Times.Length &&
                       reference.Times[firstReference] < time - windowSeconds)
                {
                    firstReference++;
                }

                if (pastLastReference < firstReference)
                {
                    pastLastReference = firstReference;
                }

                while (pastLastReference < reference.Times.Length &&
                       reference.Times[pastLastReference] <= time + windowSeconds)
                {
                    pastLastReference++;
                }

                var referenceMaximum = 0f;
                for (var referenceIndex = firstReference;
                     referenceIndex < pastLastReference;
                     referenceIndex++)
                {
                    referenceMaximum = Mathf.Max(
                        referenceMaximum,
                        reference.Values[referenceIndex]);
                }

                var allowed = Mathf.Max(
                    absoluteFloor,
                    Mathf.Max(
                        referenceMaximum * ratio,
                        referenceMaximum + absoluteDelta));
                var candidateValue = candidate.Values[candidateIndex];
                if (IsFinite(candidateValue) && candidateValue <= allowed)
                {
                    continue;
                }

                failureReason =
                    $"{sampleLabel} {metricLabel} regressed near {time:F4} s " +
                    $"from local raw {referenceMaximum:F3} to " +
                    $"{candidateValue:F3} {unit}; allowed {allowed:F3} {unit}";
                return true;
            }

            failureReason = string.Empty;
            return false;
        }

        private static bool TryDescribeAlignedAngularMotionRegression(
            string sampleLabel,
            PositionMotionSeries reference,
            PositionMotionSeries candidate,
            out string failureReason)
        {
            if (TryDescribeAlignedMetricRegression(
                    sampleLabel,
                    "angular speed",
                    "deg/s",
                    reference.Speed,
                    candidate.Speed,
                    RotationCurveAuditSpeedRatio,
                    RotationCurveAuditSpeedDeltaDegreesPerSecond,
                    RotationCurveAuditSpeedFloorDegreesPerSecond,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeAlignedMetricRegression(
                    sampleLabel,
                    "angular acceleration",
                    "deg/s^2",
                    reference.Acceleration,
                    candidate.Acceleration,
                    RotationCurveAuditAccelerationRatio,
                    RotationCurveAuditAccelerationDeltaDegreesPerSecondSquared,
                    RotationCurveAuditAccelerationFloorDegreesPerSecondSquared,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeAlignedMetricRegression(
                sampleLabel,
                "angular jerk",
                "deg/s^3",
                reference.Jerk,
                candidate.Jerk,
                RotationCurveAuditJerkRatio,
                RotationCurveAuditJerkDeltaDegreesPerSecondCubed,
                RotationCurveAuditJerkFloorDegreesPerSecondCubed,
                out failureReason);
        }

        private static bool TryDescribeAngularMotionRegression(
            string sampleLabel,
            PositionMotionMetrics reference,
            PositionMotionMetrics candidate,
            out string failureReason)
        {
            if (TryDescribeMetricRegression(
                    sampleLabel,
                    "angular speed",
                    "deg/s",
                    reference.MaximumSpeed,
                    candidate.MaximumSpeed,
                    RotationCurveAuditSpeedRatio,
                    RotationCurveAuditSpeedDeltaDegreesPerSecond,
                    RotationCurveAuditSpeedFloorDegreesPerSecond,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeMetricRegression(
                    sampleLabel,
                    "angular acceleration",
                    "deg/s^2",
                    reference.MaximumAcceleration,
                    candidate.MaximumAcceleration,
                    RotationCurveAuditAccelerationRatio,
                    RotationCurveAuditAccelerationDeltaDegreesPerSecondSquared,
                    RotationCurveAuditAccelerationFloorDegreesPerSecondSquared,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeMetricRegression(
                sampleLabel,
                "angular jerk",
                "deg/s^3",
                reference.MaximumJerk,
                candidate.MaximumJerk,
                RotationCurveAuditJerkRatio,
                RotationCurveAuditJerkDeltaDegreesPerSecondCubed,
                RotationCurveAuditJerkFloorDegreesPerSecondCubed,
                out failureReason);
        }

        private static bool TryDescribeAngularMotionRetentionRegression(
            string sampleLabel,
            IReadOnlyList<Quaternion> reference,
            IReadOnlyList<Quaternion> candidate,
            out string failureReason)
        {
            var referencePathLength = MeasureMeaningfulAngularPathLength(reference);
            if (referencePathLength < RotationCurveAuditMotionRetentionMinimumDegrees)
            {
                failureReason = string.Empty;
                return false;
            }

            var candidatePathLength = MeasureMeaningfulAngularPathLength(candidate);
            var requiredPathLength = Math.Max(
                0f,
                referencePathLength * RotationCurveAuditMotionRetentionRatio -
                RotationCurveAuditMotionNoiseDeadbandDegrees);
            if (IsFinite(candidatePathLength) &&
                candidatePathLength >= requiredPathLength)
            {
                failureReason = string.Empty;
                return false;
            }

            failureReason =
                $"{sampleLabel} meaningful angular motion retention fell from " +
                $"{referencePathLength:F3} deg to {candidatePathLength:F3} deg; " +
                $"required {requiredPathLength:F3} deg after the " +
                $"{RotationCurveAuditMotionNoiseDeadbandDegrees:F3} deg noise " +
                "deadband";
            return true;
        }

        private static float MeasureMeaningfulAngularPathLength(
            IReadOnlyList<Quaternion> rotations)
        {
            if (rotations.Count < 2)
            {
                return 0f;
            }

            var continuousRotations = MakeQuaternionSequenceContinuous(rotations);
            var pathLength = 0f;
            var anchor = continuousRotations[0];
            for (var index = 1; index < continuousRotations.Length; index++)
            {
                var angle = Quaternion.Angle(anchor, continuousRotations[index]);
                if (angle < RotationCurveAuditMotionNoiseDeadbandDegrees)
                {
                    continue;
                }

                pathLength += angle;
                anchor = continuousRotations[index];
            }

            return pathLength;
        }

        internal static bool TryDescribeAlignedMotionRegression(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> referencePositions,
            IReadOnlyList<Vector3> candidatePositions,
            out string failureReason)
        {
            if (times == null || referencePositions == null ||
                candidatePositions == null)
            {
                throw new ArgumentNullException(nameof(times));
            }

            if (times.Count != referencePositions.Count ||
                times.Count != candidatePositions.Count)
            {
                throw new ArgumentException(
                    "Aligned motion samples must have matching lengths.");
            }

            return TryDescribeAlignedMotionRegression(
                "aligned-test-grid",
                MeasurePositionMotion(times, referencePositions),
                MeasurePositionMotion(times, candidatePositions),
                out failureReason);
        }

        private static bool TryDescribeAlignedMotionRegression(
            string sampleLabel,
            PositionMotionSeries reference,
            PositionMotionSeries candidate,
            out string failureReason)
        {
            if (TryDescribeAlignedMetricRegression(
                    sampleLabel,
                    "speed",
                    "m/s",
                    reference.Speed,
                    candidate.Speed,
                    PositionCurveAuditSpeedRatio,
                    PositionCurveAuditSpeedDeltaMetersPerSecond,
                    PositionCurveAuditSpeedFloorMetersPerSecond,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeAlignedMetricRegression(
                    sampleLabel,
                    "acceleration",
                    "m/s^2",
                    reference.Acceleration,
                    candidate.Acceleration,
                    PositionCurveAuditAccelerationRatio,
                    PositionCurveAuditAccelerationDeltaMetersPerSecondSquared,
                    PositionCurveAuditAccelerationFloorMetersPerSecondSquared,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeAlignedMetricRegression(
                sampleLabel,
                "jerk",
                "m/s^3",
                reference.Jerk,
                candidate.Jerk,
                PositionCurveAuditJerkRatio,
                PositionCurveAuditJerkDeltaMetersPerSecondCubed,
                PositionCurveAuditJerkFloorMetersPerSecondCubed,
                out failureReason);
        }

        private static bool TryDescribeAlignedMetricRegression(
            string sampleLabel,
            string metricLabel,
            string unit,
            PositionMetricSeries reference,
            PositionMetricSeries candidate,
            float ratio,
            float absoluteDelta,
            float absoluteFloor,
            out string failureReason)
        {
            if (reference.Values.Length != candidate.Values.Length)
            {
                failureReason =
                    $"{sampleLabel} {metricLabel} audit grids were not aligned";
                return true;
            }

            var radius = PositionCurveAuditLocalWindowRadiusSamples;
            for (var center = 0; center < candidate.Values.Length; center++)
            {
                var start = Math.Max(0, center - radius);
                var end = Math.Min(candidate.Values.Length - 1, center + radius);
                var referenceWindowMaximum = 0f;
                var candidateWindowMaximum = 0f;
                for (var index = start; index <= end; index++)
                {
                    referenceWindowMaximum = Mathf.Max(
                        referenceWindowMaximum,
                        reference.Values[index]);
                    candidateWindowMaximum = Mathf.Max(
                        candidateWindowMaximum,
                        candidate.Values[index]);
                }

                var allowed = Mathf.Max(
                    absoluteFloor,
                    Mathf.Max(
                        referenceWindowMaximum * ratio,
                        referenceWindowMaximum + absoluteDelta));
                if (IsFinite(candidateWindowMaximum) &&
                    candidateWindowMaximum <= allowed)
                {
                    continue;
                }

                var sampleTime = candidate.Times.Length > center
                    ? candidate.Times[center]
                    : 0.0;
                failureReason =
                    $"{sampleLabel} windowed {metricLabel} regressed near " +
                    $"{sampleTime:F4} s from {referenceWindowMaximum:F3} to " +
                    $"{candidateWindowMaximum:F3} {unit}; allowed " +
                    $"{allowed:F3} {unit}";
                return true;
            }

            failureReason = string.Empty;
            return false;
        }

        private static bool TryDescribeMotionRetentionRegression(
            string sampleLabel,
            IReadOnlyList<Vector3> reference,
            IReadOnlyList<Vector3> candidate,
            out string failureReason)
        {
            var referencePathLength = MeasureMeaningfulPathLength(reference);
            if (referencePathLength < PositionCurveAuditMotionRetentionMinimumMeters)
            {
                failureReason = string.Empty;
                return false;
            }

            var candidatePathLength = MeasureMeaningfulPathLength(candidate);
            var requiredPathLength = Math.Max(
                0f,
                referencePathLength * PositionCurveAuditMotionRetentionRatio -
                PositionCurveAuditMotionNoiseDeadbandMeters);
            if (IsFinite(candidatePathLength) &&
                candidatePathLength >= requiredPathLength)
            {
                failureReason = string.Empty;
                return false;
            }

            failureReason =
                $"{sampleLabel} meaningful motion retention fell from " +
                $"{referencePathLength:F4} m to {candidatePathLength:F4} m; " +
                $"required {requiredPathLength:F4} m after the " +
                $"{PositionCurveAuditMotionNoiseDeadbandMeters:F4} m noise " +
                "deadband";
            return true;
        }

        private static float MeasureMeaningfulPathLength(
            IReadOnlyList<Vector3> positions)
        {
            if (positions.Count < 2)
            {
                return 0f;
            }

            var pathLength = 0f;
            var anchor = positions[0];
            for (var index = 1; index < positions.Count; index++)
            {
                var distance = Vector3.Distance(anchor, positions[index]);
                if (distance < PositionCurveAuditMotionNoiseDeadbandMeters)
                {
                    continue;
                }

                pathLength += distance;
                anchor = positions[index];
            }

            return pathLength;
        }

        private static bool TryDescribeMotionRegression(
            string sampleLabel,
            PositionMotionMetrics reference,
            PositionMotionMetrics candidate,
            out string failureReason)
        {
            if (TryDescribeMetricRegression(
                    sampleLabel,
                    "speed",
                    "m/s",
                    reference.MaximumSpeed,
                    candidate.MaximumSpeed,
                    PositionCurveAuditSpeedRatio,
                    PositionCurveAuditSpeedDeltaMetersPerSecond,
                    PositionCurveAuditSpeedFloorMetersPerSecond,
                    out failureReason))
            {
                return true;
            }

            if (TryDescribeMetricRegression(
                    sampleLabel,
                    "acceleration",
                    "m/s^2",
                    reference.MaximumAcceleration,
                    candidate.MaximumAcceleration,
                    PositionCurveAuditAccelerationRatio,
                    PositionCurveAuditAccelerationDeltaMetersPerSecondSquared,
                    PositionCurveAuditAccelerationFloorMetersPerSecondSquared,
                    out failureReason))
            {
                return true;
            }

            return TryDescribeMetricRegression(
                sampleLabel,
                "jerk",
                "m/s^3",
                reference.MaximumJerk,
                candidate.MaximumJerk,
                PositionCurveAuditJerkRatio,
                PositionCurveAuditJerkDeltaMetersPerSecondCubed,
                PositionCurveAuditJerkFloorMetersPerSecondCubed,
                out failureReason);
        }

        private static bool TryDescribeMetricRegression(
            string sampleLabel,
            string metricLabel,
            string unit,
            float reference,
            float candidate,
            float ratio,
            float absoluteDelta,
            float absoluteFloor,
            out string failureReason)
        {
            var allowed = Mathf.Max(
                absoluteFloor,
                Mathf.Max(reference * ratio, reference + absoluteDelta));
            if (IsFinite(candidate) && candidate <= allowed)
            {
                failureReason = string.Empty;
                return false;
            }

            failureReason =
                $"{sampleLabel} {metricLabel} regressed from {reference:F3} " +
                $"to {candidate:F3} {unit}; allowed {allowed:F3} {unit}";
            return true;
        }

        private static Keyframe[] BuildKeys(
            IReadOnlyList<double> times,
            IReadOnlyList<int> indices,
            Func<int, float> selector)
        {
            var keys = new Keyframe[indices.Count];
            for (var keyIndex = 0; keyIndex < indices.Count; keyIndex++)
            {
                var sampleIndex = indices[keyIndex];
                keys[keyIndex] = new Keyframe(
                    (float)times[sampleIndex],
                    selector(sampleIndex));
            }

            return keys;
        }

        private static int[] SimplifyVectorIndices(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> values,
            float maximumError)
        {
            var required = CreateEndpointSet(values.Count);
            AddScalarExtrema(values.Count, index => values[index].x, required);
            AddScalarExtrema(values.Count, index => values[index].y, required);
            AddScalarExtrema(values.Count, index => values[index].z, required);
            AddDirectionChangeNeighborhoods(times, values, required);
            return SimplifyIndices(
                times,
                maximumError,
                required,
                (index, left, right, blend) => Vector3.Distance(
                    values[index],
                    Vector3.LerpUnclamped(values[left], values[right], blend)));
        }

        private static int[] SimplifyQuaternionIndices(
            IReadOnlyList<double> times,
            IReadOnlyList<Quaternion> values,
            float maximumAngleError)
        {
            var required = CreateEndpointSet(values.Count);
            AddScalarExtrema(values.Count, index => values[index].x, required);
            AddScalarExtrema(values.Count, index => values[index].y, required);
            AddScalarExtrema(values.Count, index => values[index].z, required);
            AddScalarExtrema(values.Count, index => values[index].w, required);
            return SimplifyIndices(
                times,
                maximumAngleError,
                required,
                (index, left, right, blend) => Quaternion.Angle(
                    values[index],
                    NormalizedQuaternionLerp(values[left], values[right], blend)));
        }

        private static int[] SimplifyScalarIndices(
            IReadOnlyList<double> times,
            IReadOnlyList<float> values,
            float maximumError)
        {
            var required = CreateEndpointSet(values.Count);
            AddScalarExtrema(values.Count, index => values[index], required);
            return SimplifyIndices(
                times,
                maximumError,
                required,
                (index, left, right, blend) => Mathf.Abs(
                    values[index] -
                    Mathf.LerpUnclamped(values[left], values[right], blend)));
        }

        private static int[] SimplifyIndices(
            IReadOnlyList<double> times,
            float maximumError,
            ISet<int> requiredIndices,
            Func<int, int, int, float, float> measureError)
        {
            if (times.Count <= 1 || maximumError <= 0f)
            {
                return Enumerable.Range(0, times.Count).ToArray();
            }

            var selected = new SortedSet<int>(requiredIndices);
            var anchors = selected.ToArray();
            var pending = new Stack<CurveSegment>();
            for (var anchorIndex = 1; anchorIndex < anchors.Length; anchorIndex++)
            {
                pending.Push(new CurveSegment(
                    anchors[anchorIndex - 1],
                    anchors[anchorIndex]));
            }

            while (pending.Count > 0)
            {
                var segment = pending.Pop();
                if (segment.End - segment.Start <= 1)
                {
                    continue;
                }

                var duration = times[segment.End] - times[segment.Start];
                var greatestError = -1f;
                var greatestErrorIndex = -1;
                for (var index = segment.Start + 1; index < segment.End; index++)
                {
                    var blend = duration > double.Epsilon
                        ? (float)((times[index] - times[segment.Start]) / duration)
                        : (index - segment.Start) /
                          (float)(segment.End - segment.Start);
                    var error = measureError(
                        index,
                        segment.Start,
                        segment.End,
                        blend);
                    if (error > greatestError)
                    {
                        greatestError = error;
                        greatestErrorIndex = index;
                    }
                }

                if (greatestErrorIndex < 0 || greatestError <= maximumError)
                {
                    continue;
                }

                selected.Add(greatestErrorIndex);
                pending.Push(new CurveSegment(segment.Start, greatestErrorIndex));
                pending.Push(new CurveSegment(greatestErrorIndex, segment.End));
            }

            return selected.ToArray();
        }

        private static HashSet<int> CreateEndpointSet(int count)
        {
            var result = new HashSet<int> { 0 };
            if (count > 1)
            {
                result.Add(count - 1);
            }

            return result;
        }

        private static void AddScalarExtrema(
            int count,
            Func<int, float> selector,
            ISet<int> indices)
        {
            var minimumIndex = 0;
            var maximumIndex = 0;
            for (var index = 1; index < count; index++)
            {
                if (selector(index) < selector(minimumIndex))
                {
                    minimumIndex = index;
                }

                if (selector(index) > selector(maximumIndex))
                {
                    maximumIndex = index;
                }
            }

            indices.Add(minimumIndex);
            indices.Add(maximumIndex);
        }

        private static void AddDirectionChangeNeighborhoods(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> values,
            ISet<int> indices)
        {
            var flank = DirectionChangeDetectionFlankFrames;
            if (values.Count < flank * 2 + 1)
            {
                return;
            }

            for (var center = flank; center + flank < values.Count; center++)
            {
                var beforeDuration = times[center] - times[center - flank];
                var afterDuration = times[center + flank] - times[center];
                if (beforeDuration <= double.Epsilon ||
                    afterDuration <= double.Epsilon)
                {
                    continue;
                }

                var beforeVelocity =
                    (values[center] - values[center - flank]) /
                    (float)beforeDuration;
                var afterVelocity =
                    (values[center + flank] - values[center]) /
                    (float)afterDuration;
                var beforeSpeed = beforeVelocity.magnitude;
                var afterSpeed = afterVelocity.magnitude;
                if (Mathf.Min(beforeSpeed, afterSpeed) <
                    DirectionChangeMinimumSpeedMetersPerSecond)
                {
                    continue;
                }

                var cosine = Vector3.Dot(beforeVelocity, afterVelocity) /
                             (beforeSpeed * afterSpeed);
                if (cosine > DirectionChangeMaximumCosine)
                {
                    continue;
                }

                var start = Mathf.Max(
                    0,
                    center - DirectionChangePreserveRadiusFrames);
                var end = Mathf.Min(
                    values.Count - 1,
                    center + DirectionChangePreserveRadiusFrames);
                for (var index = start; index <= end; index++)
                {
                    indices.Add(index);
                }
            }
        }

        private static Quaternion[] MakeQuaternionSequenceContinuous(
            IReadOnlyList<Quaternion> rotations)
        {
            var result = new Quaternion[rotations.Count];
            for (var index = 0; index < rotations.Count; index++)
            {
                var rotation = NormalizeQuaternion(rotations[index]);
                if (index > 0 && Quaternion.Dot(result[index - 1], rotation) < 0f)
                {
                    rotation = NegateQuaternion(rotation);
                }

                result[index] = rotation;
            }

            return result;
        }

        private static Quaternion NormalizedQuaternionLerp(
            Quaternion left,
            Quaternion right,
            float blend)
        {
            return NormalizeQuaternion(new Quaternion(
                Mathf.LerpUnclamped(left.x, right.x, blend),
                Mathf.LerpUnclamped(left.y, right.y, blend),
                Mathf.LerpUnclamped(left.z, right.z, blend),
                Mathf.LerpUnclamped(left.w, right.w, blend)));
        }

        private static Quaternion NormalizeQuaternion(Quaternion value)
        {
            var magnitude = Mathf.Sqrt(
                value.x * value.x +
                value.y * value.y +
                value.z * value.z +
                value.w * value.w);
            if (magnitude < 0.000001f)
            {
                throw new ArgumentException("A camera rotation quaternion is zero.");
            }

            var inverseMagnitude = 1f / magnitude;
            return new Quaternion(
                value.x * inverseMagnitude,
                value.y * inverseMagnitude,
                value.z * inverseMagnitude,
                value.w * inverseMagnitude);
        }

        private static Quaternion NegateQuaternion(Quaternion value)
        {
            return new Quaternion(-value.x, -value.y, -value.z, -value.w);
        }

        private static void ValidateCurveSampleCounts(
            IReadOnlyList<double> times,
            IReadOnlyList<Vector3> positions,
            IReadOnlyList<Quaternion> rotations,
            IReadOnlyList<float> fieldOfView,
            IReadOnlyList<float> dutch)
        {
            if (times == null ||
                positions == null ||
                rotations == null ||
                fieldOfView == null ||
                dutch == null)
            {
                throw new ArgumentNullException(nameof(times));
            }

            if (times.Count == 0 ||
                positions.Count != times.Count ||
                rotations.Count != times.Count ||
                fieldOfView.Count != times.Count ||
                dutch.Count != times.Count)
            {
                throw new ArgumentException(
                    "Camera curve sample arrays must have the same non-zero length.");
            }

            for (var index = 1; index < times.Count; index++)
            {
                if (times[index] <= times[index - 1])
                {
                    throw new ArgumentException(
                        "Camera curve sample times must be strictly increasing.");
                }
            }
        }

        private static CurveSimplificationPreset ParseCurveSimplificationPreset(
            string value)
        {
            if (!Enum.TryParse(value?.Trim(), true, out CurveSimplificationPreset preset) ||
                !Enum.IsDefined(typeof(CurveSimplificationPreset), preset))
            {
                throw new ArgumentException(
                    "Curve preset must be Exact, Balanced, or Editable.",
                    nameof(value));
            }

            return preset;
        }

        private static void SetLinearCurve(
            AnimationClip clip,
            string relativePath,
            Type componentType,
            string propertyName,
            Keyframe[] keys)
        {
            SetCurve(
                clip,
                relativePath,
                componentType,
                propertyName,
                BuildLinearCurve(keys));
        }

        private static AnimationCurve BuildLinearCurve(Keyframe[] keys)
        {
            var curve = new AnimationCurve(keys);
            for (var index = 0; index < curve.length; index++)
            {
                AnimationUtility.SetKeyLeftTangentMode(
                    curve,
                    index,
                    AnimationUtility.TangentMode.Linear);
                AnimationUtility.SetKeyRightTangentMode(
                    curve,
                    index,
                    AnimationUtility.TangentMode.Linear);
            }

            return curve;
        }

        private static void SetCurve(
            AnimationClip clip,
            string relativePath,
            Type componentType,
            string propertyName,
            AnimationCurve curve)
        {
            AnimationUtility.SetEditorCurve(
                clip,
                EditorCurveBinding.FloatCurve(
                    relativePath,
                    componentType,
                propertyName),
                curve);
        }

        private static AnimationCurve GetCurve(
            AnimationClip clip,
            string relativePath,
            Type componentType,
            string propertyName)
        {
            return AnimationUtility.GetEditorCurve(
                clip,
                EditorCurveBinding.FloatCurve(
                    relativePath,
                    componentType,
                    propertyName));
        }

        private static void SetClipExtrapolationNone(TimelineClip clip)
        {
            SetNonPublicTimelineClipProperty(
                clip,
                nameof(TimelineClip.preExtrapolationMode),
                TimelineClip.ClipExtrapolation.None);
            SetNonPublicTimelineClipProperty(
                clip,
                nameof(TimelineClip.postExtrapolationMode),
                TimelineClip.ClipExtrapolation.None);
        }

        private static void SetNonPublicTimelineClipProperty(
            TimelineClip clip,
            string propertyName,
            TimelineClip.ClipExtrapolation value)
        {
            var property = typeof(TimelineClip).GetProperty(
                propertyName,
                BindingFlags.Instance |
                BindingFlags.Public |
                BindingFlags.NonPublic);
            var setter = property?.GetSetMethod(true)
                ?? throw new MissingMethodException(
                    typeof(TimelineClip).FullName,
                    propertyName);
            setter.Invoke(clip, new object[] { value });
        }

        private static void AddPreviewAssetPath(
            ICollection<string> paths,
            UnityEngine.Object asset)
        {
            if (asset == null)
            {
                return;
            }

            var path = AssetDatabase.GetAssetPath(asset).Replace('\\', '/');
            if (!path.Contains($"/{PreviewAssetFolderName}/"))
            {
                throw new InvalidOperationException(
                    $"Refusing to remove non-preview asset '{path}'.");
            }

            paths.Add(path);
        }

        private static GeneratedCameraFrame[] ReadCameraFrames(
            string path,
            int frameCount)
        {
            const int valuesPerFrame = 9;
            var expectedBytes = (long)frameCount * valuesPerFrame * sizeof(float);
            var file = new FileInfo(path);
            var actualBytes = file.Exists ? file.Length : -1;
            if (actualBytes != expectedBytes)
            {
                throw new InvalidDataException(
                    $"Generated camera size is {actualBytes:N0} bytes; " +
                    $"expected {expectedBytes:N0}.");
            }

            var result = new GeneratedCameraFrame[frameCount];
            using var reader = new BinaryReader(new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                1024 * 1024,
                FileOptions.SequentialScan));
            for (var index = 0; index < frameCount; index++)
            {
                result[index] = new GeneratedCameraFrame
                {
                    Position = new Vector3(
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle()),
                    Rotation = new Quaternion(
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle(),
                        reader.ReadSingle()),
                    FieldOfView = reader.ReadSingle(),
                    Dutch = reader.ReadSingle()
                };
            }

            return result;
        }

        private static double[] ReadTimes(string path, int frameCount)
        {
            var expectedBytes = (long)frameCount * sizeof(double);
            var file = new FileInfo(path);
            var actualBytes = file.Exists ? file.Length : -1;
            if (actualBytes != expectedBytes)
            {
                throw new InvalidDataException(
                    $"Generated time size is {actualBytes:N0} bytes; " +
                    $"expected {expectedBytes:N0}.");
            }

            var result = new double[frameCount];
            using var reader = new BinaryReader(new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                1024 * 1024,
                FileOptions.SequentialScan));
            for (var index = 0; index < frameCount; index++)
            {
                result[index] = reader.ReadDouble();
            }

            return result;
        }

        private static PlayableDirector FindOriginalDirector(Scene scene)
        {
            return Resources.FindObjectsOfTypeAll<PlayableDirector>()
                .Where(director =>
                    director.gameObject.scene == scene &&
                    director.name == OriginalDirectorName)
                .SingleOrDefault()
                ?? throw new InvalidOperationException(
                    $"Scene '{scene.name}' does not contain one active " +
                    $"'{OriginalDirectorName}' PlayableDirector.");
        }

        private static PlayableDirector RequireSourceDirector(
            PlayableDirector sourceDirector)
        {
            if (sourceDirector == null)
            {
                throw new ArgumentNullException(nameof(sourceDirector));
            }

            var scene = sourceDirector.gameObject.scene;
            if (!scene.IsValid() || !scene.isLoaded)
            {
                throw new InvalidOperationException(
                    $"Source director '{sourceDirector.name}' must belong to a loaded scene.");
            }

            if (sourceDirector.playableAsset is not TimelineAsset)
            {
                throw new InvalidOperationException(
                    $"Source director '{sourceDirector.name}' is not bound to a TimelineAsset.");
            }

            return sourceDirector;
        }

        internal static string GetScopedPreviewDirectorName(
            PlayableDirector sourceDirector)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            return $"{PreviewDirectorName}__{GetDirectorScopeLabel(sourceDirector)}";
        }

        internal static bool IsPreviewDirectorForEditor(
            PlayableDirector director)
        {
            if (director == null ||
                !(director.name == PreviewDirectorName ||
                  director.name.StartsWith(
                      PreviewDirectorName + "__",
                      StringComparison.Ordinal)))
            {
                return false;
            }

            return director.playableAsset is TimelineAsset timeline &&
                   timeline.GetOutputTracks().Any(track =>
                       track is AnimationTrack animationTrack &&
                       IsGeneratedAnimationTrack(animationTrack) ||
                       track.name == GeneratedCinemachineTrackName);
        }

        internal static string GetScopedPreviewCameraName(
            PlayableDirector sourceDirector)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            return $"{PreviewCameraName}__{GetDirectorScopeLabel(sourceDirector)}";
        }

        private static string GetDirectorAssetScopeName(
            PlayableDirector sourceDirector)
        {
            return $"Source_{GetDirectorScopeLabel(sourceDirector)}";
        }

        private static string GetDirectorScopeLabel(
            PlayableDirector sourceDirector)
        {
            return $"{SanitizeAssetName(sourceDirector.name)}_" +
                   $"{ComputeStableHash(GetDirectorScopeSeed(sourceDirector)):X12}";
        }

        private static string GetDirectorScopeSeed(
            PlayableDirector sourceDirector)
        {
            var scene = sourceDirector.gameObject.scene;
            var siblingDirectors =
                sourceDirector.gameObject.GetComponents<PlayableDirector>();
            var componentIndex = Array.IndexOf(
                siblingDirectors,
                sourceDirector);
            var timelinePath = AssetDatabase.GetAssetPath(
                sourceDirector.playableAsset);
            var timelineGuid = string.IsNullOrWhiteSpace(timelinePath)
                ? string.Empty
                : AssetDatabase.AssetPathToGUID(timelinePath);
            // Scene-object instance IDs change after every domain reload, and
            // AssetDatabase local-file IDs are not consistently available for
            // unsaved scene state. Derive the scope from serialized hierarchy
            // order instead so the same preview remains discoverable.
            return $"{scene.path}|{scene.name}|" +
                   $"{GetStableHierarchyKey(sourceDirector.transform)}|" +
                   $"director:{componentIndex}|timeline:{timelineGuid}";
        }

        private static ulong ComputeStableHash(string value)
        {
            const ulong offset = 14695981039346656037UL;
            const ulong prime = 1099511628211UL;
            var hash = offset;
            foreach (var character in value ?? string.Empty)
            {
                hash ^= character;
                hash *= prime;
            }

            return hash & 0xFFFFFFFFFFFFUL;
        }

        private static bool TryFindPreviewContext(
            PlayableDirector sourceDirector,
            out PreviewContext context)
        {
            var scene = sourceDirector.gameObject.scene;
            var expectedDirectorName = GetScopedPreviewDirectorName(sourceDirector);
            var expectedCameraName = GetScopedPreviewCameraName(sourceDirector);
            var previewDirector = Resources.FindObjectsOfTypeAll<PlayableDirector>()
                .FirstOrDefault(director =>
                    director.gameObject.scene == scene &&
                    director.name == expectedDirectorName);
            if (previewDirector == null)
            {
                if (sourceDirector.name == OriginalDirectorName)
                {
                    return TryFindLegacyPreviewContext(scene, out context);
                }

                context = default;
                return false;
            }

            context = CreatePreviewContext(
                scene,
                previewDirector,
                expectedCameraName);
            return true;
        }

        private static PreviewContext FindPreviewContext(
            PlayableDirector sourceDirector)
        {
            if (TryFindPreviewContext(sourceDirector, out var context))
            {
                return context;
            }

            throw new InvalidOperationException(
                $"Scene '{sourceDirector.gameObject.scene.name}' does not contain " +
                $"an AI preview for '{GetHierarchyPath(sourceDirector.transform)}'.");
        }

        private static bool TryFindLegacyPreviewContext(
            Scene scene,
            out PreviewContext context)
        {
            var previewDirector = Resources.FindObjectsOfTypeAll<PlayableDirector>()
                .FirstOrDefault(director =>
                    director.gameObject.scene == scene &&
                    director.name == PreviewDirectorName);
            if (previewDirector == null)
            {
                context = default;
                return false;
            }

            context = CreatePreviewContext(
                scene,
                previewDirector,
                PreviewCameraName);
            return true;
        }

        private static PreviewContext FindLegacyPreviewContext(Scene scene)
        {
            if (TryFindLegacyPreviewContext(scene, out var context))
            {
                return context;
            }

            throw new InvalidOperationException(
                $"Scene '{scene.name}' does not contain '{PreviewDirectorName}'.");
        }

        private static PreviewContext CreatePreviewContext(
            Scene scene,
            PlayableDirector previewDirector,
            string fallbackCameraName)
        {
            var timeline = previewDirector.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    $"Preview director '{previewDirector.name}' is not bound to a TimelineAsset.");
            var animationTrack = GetGeneratedAnimationTracks(timeline)
                .FirstOrDefault();
            var animator = animationTrack == null
                ? null
                : previewDirector.GetGenericBinding(animationTrack) as Animator;
            var boundCameraRoot = animator != null &&
                                  animator.gameObject.scene == scene
                ? animator.GetComponent<CinemachineCamera>() != null &&
                  animator.transform.parent != null
                    ? animator.transform.parent.gameObject
                    : animator.gameObject
                : null;
            var cameraRoot = boundCameraRoot != null
                ? boundCameraRoot
                : Resources.FindObjectsOfTypeAll<GameObject>()
                    .FirstOrDefault(gameObject =>
                        gameObject.scene == scene &&
                        gameObject.name == fallbackCameraName);
            if (cameraRoot == null)
            {
                throw new InvalidOperationException(
                    $"Preview '{previewDirector.name}' has no bound generated camera root.");
            }

            return new PreviewContext(scene, previewDirector, cameraRoot);
        }

        private static PlayableDirector GetClonedDirectorComponent(
            PlayableDirector sourceDirector,
            GameObject clonedGameObject)
        {
            var sourceComponents =
                sourceDirector.gameObject.GetComponents<PlayableDirector>();
            var clonedComponents =
                clonedGameObject.GetComponents<PlayableDirector>();
            var index = Array.IndexOf(sourceComponents, sourceDirector);
            if (index < 0 || index >= clonedComponents.Length)
            {
                throw new InvalidOperationException(
                    "Unable to resolve the selected PlayableDirector on its preview clone.");
            }

            return clonedComponents[index];
        }

        internal static GameObject InstantiateInactivePreviewCloneForEditor(
            GameObject sourceObject,
            Transform destinationParent)
        {
            if (sourceObject == null)
            {
                throw new ArgumentNullException(nameof(sourceObject));
            }

            var sourceScene = sourceObject.scene;
            if (!sourceScene.IsValid() || !sourceScene.isLoaded)
            {
                throw new InvalidOperationException(
                    "The source Timeline object must belong to a loaded scene.");
            }

            var stagingObject = new GameObject(
                "__CWAI_InactivePreviewCloneStaging");
            stagingObject.SetActive(false);
            try
            {
                if (stagingObject.scene != sourceScene)
                {
                    SceneManager.MoveGameObjectToScene(stagingObject, sourceScene);
                }

                var clone = UnityEngine.Object.Instantiate(
                    sourceObject,
                    stagingObject.transform,
                    true);
                clone.SetActive(false);
                clone.transform.SetParent(destinationParent, true);
                return clone;
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(stagingObject);
            }
        }

        private static void EnsureNoExistingPreview(
            Scene scene,
            string previewDirectorName,
            string previewCameraName)
        {
            var existing = Resources.FindObjectsOfTypeAll<GameObject>()
                .FirstOrDefault(gameObject =>
                    gameObject.scene == scene &&
                    (gameObject.name == previewDirectorName ||
                     gameObject.name == previewCameraName));
            if (existing != null)
            {
                throw new InvalidOperationException(
                    $"Preview object '{existing.name}' already exists. " +
                    "Remove or rename it before generating another preview.");
            }
        }

        private static CinemachineBrain FindCinemachineBrain(
            PlayableDirector director,
            TimelineAsset timeline)
        {
            if (director == null)
            {
                throw new ArgumentNullException(nameof(director));
            }

            if (timeline == null)
            {
                throw new ArgumentNullException(nameof(timeline));
            }

            var scene = director.gameObject.scene;
            if (!scene.IsValid() || !scene.isLoaded)
            {
                throw new InvalidOperationException(
                    "The Timeline director must belong to a loaded scene.");
            }

            var sceneBrains = scene.GetRootGameObjects()
                .SelectMany(root =>
                    root.GetComponentsInChildren<CinemachineBrain>(true))
                .ToArray();
            return ResolveCinemachineBrainForEditor(
                director,
                timeline,
                sceneBrains);
        }

        internal static CinemachineBrain ResolveCinemachineBrainForEditor(
            PlayableDirector director,
            TimelineAsset timeline,
            IReadOnlyList<CinemachineBrain> sceneBrains)
        {
            if (director == null)
            {
                throw new ArgumentNullException(nameof(director));
            }

            if (timeline == null)
            {
                throw new ArgumentNullException(nameof(timeline));
            }

            if (sceneBrains == null)
            {
                throw new ArgumentNullException(nameof(sceneBrains));
            }

            var scene = director.gameObject.scene;
            var directlyBoundBrains = GetBoundCinemachineBrains(
                director,
                timeline);
            if (directlyBoundBrains.Length > 1)
            {
                throw new InvalidOperationException(
                    "The Timeline has Cinemachine Tracks bound to multiple " +
                    "CinemachineBrain components. Bind every camera track to one Brain.");
            }

            if (directlyBoundBrains.Length == 1)
            {
                return ValidateExplicitCinemachineBrain(
                    directlyBoundBrains[0],
                    scene);
            }

            var descendantBoundBrains = director.gameObject
                .GetComponentsInChildren<PlayableDirector>(true)
                .Where(descendant =>
                    descendant != null &&
                    descendant != director &&
                    descendant.gameObject.scene == scene &&
                    descendant.playableAsset is TimelineAsset)
                .SelectMany(descendant => GetBoundCinemachineBrains(
                    descendant,
                    (TimelineAsset)descendant.playableAsset))
                .Distinct()
                .ToArray();
            if (descendantBoundBrains.Length > 1)
            {
                throw new InvalidOperationException(
                    "Descendant PlayableDirectors under the selected Timeline object " +
                    "have Cinemachine Tracks bound to multiple CinemachineBrain " +
                    "components. Bind them to one Brain or bind the selected Timeline " +
                    "directly.");
            }

            if (descendantBoundBrains.Length == 1)
            {
                return ValidateExplicitCinemachineBrain(
                    descendantBoundBrains[0],
                    scene);
            }

            var usableBrains = sceneBrains
                .Where(brain => IsUsableCinemachineBrain(brain, scene))
                .Distinct()
                .ToArray();
            var mainCameraBrains = usableBrains
                .Where(brain =>
                {
                    var camera = brain.OutputCamera;
                    return camera != null && camera.CompareTag("MainCamera");
                })
                .ToArray();
            if (mainCameraBrains.Length == 1)
            {
                return mainCameraBrains[0];
            }

            if (mainCameraBrains.Length > 1)
            {
                throw new InvalidOperationException(
                    "Multiple enabled MainCamera-tagged Cameras have a " +
                    "CinemachineBrain in the Timeline scene. Keep exactly one Main Camera " +
                    "or bind a Cinemachine Track explicitly.");
            }

            if (usableBrains.Length == 1)
            {
                return usableBrains[0];
            }

            if (usableBrains.Length == 0)
            {
                throw new InvalidOperationException(
                    "No enabled CinemachineBrain with an enabled Camera was found in " +
                    "the Timeline scene. Add a CinemachineBrain to the Main Camera or " +
                    "bind a Cinemachine Track explicitly.");
            }

            throw new InvalidOperationException(
                "Multiple enabled CinemachineBrain components were found in the " +
                "Timeline scene and none belongs to a unique Main Camera. Tag exactly " +
                "one Camera as MainCamera or bind a Cinemachine Track explicitly.");
        }

        private static CinemachineBrain[] GetBoundCinemachineBrains(
            PlayableDirector director,
            TimelineAsset timeline)
        {
            return timeline.GetOutputTracks()
                .OfType<CinemachineTrack>()
                .Select(track =>
                    director.GetGenericBinding(track) as CinemachineBrain)
                .Where(brain => brain != null)
                .Distinct()
                .ToArray();
        }

        private static CinemachineBrain ValidateExplicitCinemachineBrain(
            CinemachineBrain brain,
            Scene scene)
        {
            if (brain.gameObject.scene != scene)
            {
                throw new InvalidOperationException(
                    "The CinemachineBrain explicitly bound to a Timeline must belong " +
                    "to the same scene as the selected Timeline director.");
            }

            var camera = brain.OutputCamera;
            if (camera == null)
            {
                throw new InvalidOperationException(
                    "The CinemachineBrain explicitly bound to a Timeline must have an " +
                    "output Camera component on its controlled GameObject.");
            }

            if (camera.gameObject.scene != scene)
            {
                throw new InvalidOperationException(
                    "The CinemachineBrain output Camera explicitly bound to a Timeline " +
                    "must belong to the same scene as the selected Timeline director.");
            }

            if (!brain.isActiveAndEnabled || !camera.isActiveAndEnabled)
            {
                Debug.LogWarning(
                    "The CinemachineBrain explicitly bound to a Timeline or its " +
                    "Camera is disabled. Camera AI will use the configured Brain " +
                    "without enabling either component automatically.",
                    brain);
            }

            return brain;
        }

        internal static string GetPreviewOutputStatusForEditor(
            CinemachineBrain brain)
        {
            if (brain == null)
            {
                throw new ArgumentNullException(nameof(brain));
            }

            var camera = brain.OutputCamera;
            var cameraName = camera != null
                ? camera.gameObject.name
                : brain.gameObject.name;
            if (camera != null &&
                brain.isActiveAndEnabled &&
                camera.isActiveAndEnabled)
            {
                return $"Preview Output: configured '{cameraName}' CinemachineBrain " +
                       "and Camera are active.";
            }

            return $"Preview Output: configured '{cameraName}' CinemachineBrain or " +
                   "Camera is disabled; enable it to see AI camera playback.";
        }

        private static bool IsUsableCinemachineBrain(
            CinemachineBrain brain,
            Scene scene)
        {
            if (brain == null ||
                brain.gameObject.scene != scene ||
                !brain.isActiveAndEnabled)
            {
                return false;
            }

            var camera = brain.OutputCamera;
            return camera != null &&
                   camera.gameObject.scene == scene &&
                   camera.isActiveAndEnabled;
        }

        internal static void SuppressAuthoredCinemachineOutputsForEditor(
            PlayableDirector selectedDirector,
            bool recordUndo = true)
        {
            if (selectedDirector == null)
            {
                throw new ArgumentNullException(nameof(selectedDirector));
            }

            var directorObject = selectedDirector.gameObject;
            var descendantDirectors = directorObject
                .GetComponentsInChildren<PlayableDirector>(true)
                .Where(descendant =>
                    descendant != null &&
                    descendant != selectedDirector &&
                    descendant.playableAsset is TimelineAsset)
                .ToArray();
            foreach (var descendantDirector in descendantDirectors)
            {
                var descendantTimeline =
                    (TimelineAsset)descendantDirector.playableAsset;
                var cinemachineTracks = descendantTimeline.GetOutputTracks()
                    .OfType<CinemachineTrack>()
                    .ToArray();
                if (cinemachineTracks.Length == 0)
                {
                    continue;
                }

                if (recordUndo)
                {
                    Undo.RecordObject(
                        descendantDirector,
                        "Disconnect Authored Cameras From AI Preview");
                }

                foreach (var cinemachineTrack in cinemachineTracks)
                {
                    descendantDirector.ClearGenericBinding(cinemachineTrack);
                }

                RebuildDirectorGraphPreservingPlayback(descendantDirector);

                EditorUtility.SetDirty(descendantDirector);
            }

            foreach (var authoredCamera in directorObject
                         .GetComponentsInChildren<CinemachineVirtualCameraBase>(true))
            {
                if (authoredCamera == null || !authoredCamera.enabled)
                {
                    continue;
                }

                if (recordUndo)
                {
                    Undo.RecordObject(
                        authoredCamera,
                        "Disable Authored Cameras In AI Output");
                }

                authoredCamera.enabled = false;
                EditorUtility.SetDirty(authoredCamera);
            }
        }

        internal static AuthoredCinemachineStateSnapshot
            CaptureAuthoredCinemachineState(PlayableDirector selectedDirector)
        {
            if (selectedDirector == null)
            {
                throw new ArgumentNullException(nameof(selectedDirector));
            }

            var directorObject = selectedDirector.gameObject;
            var directorStates = directorObject
                .GetComponentsInChildren<PlayableDirector>(true)
                .Where(descendant =>
                    descendant != null &&
                    descendant != selectedDirector &&
                    descendant.playableAsset is TimelineAsset)
                .Select(descendant =>
                {
                    var timeline = (TimelineAsset)descendant.playableAsset;
                    var tracks = timeline.GetOutputTracks()
                        .OfType<CinemachineTrack>()
                        .ToArray();
                    return new AuthoredCinemachineDirectorState(
                        descendant,
                        tracks,
                        tracks.Select(descendant.GetGenericBinding).ToArray());
                })
                .Where(state => state.Tracks.Length > 0)
                .ToArray();
            var cameraStates = directorObject
                .GetComponentsInChildren<CinemachineVirtualCameraBase>(true)
                .Where(camera => camera != null)
                .Select(camera => new AuthoredVirtualCameraState(
                    camera,
                    camera.enabled))
                .ToArray();
            return new AuthoredCinemachineStateSnapshot(
                directorStates,
                cameraStates);
        }

        internal static void RestoreAuthoredCinemachineState(
            AuthoredCinemachineStateSnapshot snapshot)
        {
            if (snapshot == null)
            {
                return;
            }

            foreach (var cameraState in snapshot.CameraStates)
            {
                if (cameraState.Camera == null)
                {
                    continue;
                }

                cameraState.Camera.enabled = cameraState.Enabled;
                EditorUtility.SetDirty(cameraState.Camera);
            }

            foreach (var directorState in snapshot.DirectorStates)
            {
                var director = directorState.Director;
                if (director == null)
                {
                    continue;
                }

                for (var index = 0; index < directorState.Tracks.Length; index++)
                {
                    var track = directorState.Tracks[index];
                    if (track == null)
                    {
                        continue;
                    }

                    var binding = directorState.Bindings[index];
                    if (binding != null)
                    {
                        director.SetGenericBinding(track, binding);
                    }
                    else
                    {
                        director.ClearGenericBinding(track);
                    }
                }

                RebuildDirectorGraphPreservingPlayback(director);

                EditorUtility.SetDirty(director);
            }
        }

        private static void RebuildDirectorGraphPreservingPlayback(
            PlayableDirector director)
        {
            if (!director.playableGraph.IsValid())
            {
                return;
            }

            var time = director.time;
            var wasPlaying = director.state == PlayState.Playing;
            director.Stop();
            director.RebuildGraph();
            director.time = time;
            if (wasPlaying)
            {
                director.Play();
                director.time = time;
            }
            else
            {
                director.Evaluate();
                director.time = time;
            }
        }

        private static void CopyTrackBindings(
            PlayableDirector originalDirector,
            TimelineAsset originalTimeline,
            PlayableDirector copiedDirector,
            TimelineAsset copiedTimeline)
        {
            var originalTracks = originalTimeline.GetOutputTracks().ToArray();
            var copiedTracks = copiedTimeline.GetOutputTracks().ToArray();
            if (originalTracks.Length != copiedTracks.Length)
            {
                throw new InvalidOperationException(
                    "The copied Timeline track structure does not match the original.");
            }

            for (var index = 0; index < originalTracks.Length; index++)
            {
                copiedDirector.SetGenericBinding(
                    copiedTracks[index],
                    originalDirector.GetGenericBinding(originalTracks[index]));
            }
        }

        private static void MuteCopiedCameraTracks(
            PlayableDirector director,
            TimelineAsset timeline)
        {
            foreach (var track in timeline.GetOutputTracks())
            {
                if (track is CinemachineTrack)
                {
                    track.muted = true;
                    continue;
                }

                if (track is not AnimationTrack)
                {
                    continue;
                }

                var animator = director.GetGenericBinding(track) as Animator;
                if (animator == null)
                {
                    continue;
                }

                var path = GetHierarchyPath(animator.transform);
                if (animator.name.StartsWith("Cam", StringComparison.Ordinal) ||
                    path.Contains("/Cam/"))
                {
                    track.muted = true;
                }
            }
        }

        internal static int MuteCopiedRecorderTracks(TimelineAsset timeline)
        {
            if (timeline == null)
            {
                throw new ArgumentNullException(nameof(timeline));
            }

            var mutedCount = 0;
            foreach (var track in EnumerateTracks(timeline))
            {
                if (track is not RecorderTrack || track.muted)
                {
                    continue;
                }

                track.muted = true;
                mutedCount++;
            }

            return mutedCount;
        }

        private static IEnumerable<TrackAsset> EnumerateTracks(
            TimelineAsset timeline)
        {
            foreach (var rootTrack in timeline.GetRootTracks())
            {
                yield return rootTrack;
                foreach (var childTrack in EnumerateChildTracks(rootTrack))
                {
                    yield return childTrack;
                }
            }
        }

        private static IEnumerable<TrackAsset> EnumerateChildTracks(
            TrackAsset parent)
        {
            foreach (var child in parent.GetChildTracks())
            {
                yield return child;
                foreach (var descendant in EnumerateChildTracks(child))
                {
                    yield return descendant;
                }
            }
        }

        internal static bool TryGetPreviewGenerationProvenanceForEditor(
            PlayableDirector sourceDirector,
            out PreviewGenerationProvenanceInfo provenance)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            if (!TryFindPreviewContext(sourceDirector, out var context) ||
                context.Director.playableAsset is not TimelineAsset timeline)
            {
                provenance = default;
                return false;
            }

            return TryGetPreviewGenerationProvenance(
                timeline,
                out provenance);
        }

        internal static bool TryGetPreviewTimelineForEditor(
            PlayableDirector sourceDirector,
            out TimelineAsset timeline)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            if (!TryFindPreviewContext(sourceDirector, out var context) ||
                context.Director.playableAsset is not TimelineAsset value)
            {
                timeline = null;
                return false;
            }

            timeline = value;
            return true;
        }

        internal static void EnsurePreviewGenerationProvenanceForEditor(
            PlayableDirector sourceDirector,
            string generatedDirectory,
            string cwAiRoot)
        {
            sourceDirector = RequireSourceDirector(sourceDirector);
            var context = FindPreviewContext(sourceDirector);
            var timeline = context.Director.playableAsset as TimelineAsset
                ?? throw new InvalidOperationException(
                    "The AI preview director is not bound to a TimelineAsset.");
            if (FindPreviewGenerationProvenance(timeline) != null)
            {
                return;
            }

            var generatedDirectoryPath = Path.GetFullPath(generatedDirectory);
            var metadataPath = Path.Combine(
                generatedDirectoryPath,
                "metadata.json");
            var created = UpsertPreviewGenerationProvenance(
                timeline,
                generatedDirectoryPath,
                metadataPath,
                cwAiRoot,
                false,
                "Migrate AI Camera Preview Provenance");
            AssetDatabase.SaveAssetIfDirty(created);
            AssetDatabase.SaveAssetIfDirty(timeline);
        }

        internal static bool TryGetPreviewGenerationProvenanceForTests(
            TimelineAsset timeline,
            out PreviewGenerationProvenanceInfo provenance)
        {
            return TryGetPreviewGenerationProvenance(
                timeline,
                out provenance);
        }

        internal static AICameraPreviewGenerationProvenance
            UpsertPreviewGenerationProvenanceForTests(
                TimelineAsset timeline,
                string generatedDirectory,
                string cwAiRoot,
                bool recordUndo)
        {
            var generatedDirectoryPath = Path.GetFullPath(generatedDirectory);
            return UpsertPreviewGenerationProvenance(
                timeline,
                generatedDirectoryPath,
                Path.Combine(generatedDirectoryPath, "metadata.json"),
                cwAiRoot,
                recordUndo,
                "Update AI Camera Preview Provenance");
        }

        internal static void RemovePreviewGenerationProvenanceForTests(
            TimelineAsset timeline)
        {
            RemovePreviewGenerationProvenance(timeline);
        }

        private static bool TryGetPreviewGenerationProvenance(
            TimelineAsset timeline,
            out PreviewGenerationProvenanceInfo provenance)
        {
            var asset = FindPreviewGenerationProvenance(timeline);
            if (asset == null)
            {
                provenance = default;
                return false;
            }

            if (!string.Equals(
                    asset.SchemaVersion,
                    AICameraPreviewGenerationProvenance.CurrentSchemaVersion,
                    StringComparison.Ordinal))
            {
                throw new InvalidDataException(
                    $"Unsupported AI preview provenance schema: " +
                    $"{asset.SchemaVersion}");
            }

            provenance = new PreviewGenerationProvenanceInfo(
                asset.CwAiRelativeGeneratedFolder,
                asset.AbsoluteGeneratedFolder,
                asset.MetadataSha256);
            return true;
        }

        private static AICameraPreviewGenerationProvenance
            UpsertPreviewGenerationProvenance(
                TimelineAsset timeline,
                string generatedDirectoryPath,
                string metadataPath,
                string cwAiRoot,
                bool recordUndo,
                string undoName)
        {
            if (timeline == null)
            {
                throw new ArgumentNullException(nameof(timeline));
            }

            var timelinePath = AssetDatabase.GetAssetPath(timeline);
            if (string.IsNullOrWhiteSpace(timelinePath))
            {
                throw new InvalidOperationException(
                    "AI preview provenance requires a persistent Timeline asset.");
            }

            if (!File.Exists(metadataPath))
            {
                throw new FileNotFoundException(
                    "Generated camera metadata was not found.",
                    metadataPath);
            }

            var fullGeneratedDirectory = Path.GetFullPath(
                generatedDirectoryPath);
            var metadataSha256 = ComputeSha256(metadataPath);
            var relativeGeneratedDirectory =
                BuildCwAiRelativeGeneratedDirectory(
                    fullGeneratedDirectory,
                    cwAiRoot);
            var provenance = FindPreviewGenerationProvenance(timeline);
            if (provenance == null)
            {
                provenance = ScriptableObject.CreateInstance<
                    AICameraPreviewGenerationProvenance>();
                provenance.name = PreviewGenerationProvenanceName;
                provenance.hideFlags =
                    HideFlags.HideInHierarchy | HideFlags.HideInInspector;
                AssetDatabase.AddObjectToAsset(provenance, timeline);
                if (recordUndo)
                {
                    Undo.RegisterCreatedObjectUndo(provenance, undoName);
                }
            }
            else
            {
                if (recordUndo)
                {
                    Undo.RegisterCompleteObjectUndo(provenance, undoName);
                }

                provenance.hideFlags =
                    HideFlags.HideInHierarchy | HideFlags.HideInInspector;
            }

            provenance.SetValues(
                relativeGeneratedDirectory,
                fullGeneratedDirectory,
                metadataSha256);
            EditorUtility.SetDirty(provenance);
            EditorUtility.SetDirty(timeline);
            return provenance;
        }

        private static AICameraPreviewGenerationProvenance
            FindPreviewGenerationProvenance(TimelineAsset timeline)
        {
            if (timeline == null)
            {
                return null;
            }

            var timelinePath = AssetDatabase.GetAssetPath(timeline);
            if (string.IsNullOrWhiteSpace(timelinePath))
            {
                return null;
            }

            var matches = AssetDatabase.LoadAllAssetsAtPath(timelinePath)
                .OfType<AICameraPreviewGenerationProvenance>()
                .ToArray();
            if (matches.Length > 1)
            {
                throw new InvalidDataException(
                    "AI preview Timeline contains duplicate generation provenance.");
            }

            return matches.SingleOrDefault();
        }

        private static string BuildCwAiRelativeGeneratedDirectory(
            string generatedDirectoryPath,
            string cwAiRoot)
        {
            var resolvedRoot = ResolveCwAiRootForProvenance(
                generatedDirectoryPath,
                cwAiRoot);
            if (string.IsNullOrWhiteSpace(resolvedRoot))
            {
                return string.Empty;
            }

            var fullRoot = Path.GetFullPath(resolvedRoot)
                .TrimEnd(
                    Path.DirectorySeparatorChar,
                    Path.AltDirectorySeparatorChar);
            var fullGenerated = Path.GetFullPath(generatedDirectoryPath);
            var relative = Path.GetRelativePath(fullRoot, fullGenerated);
            if (string.IsNullOrWhiteSpace(relative) ||
                Path.IsPathRooted(relative) ||
                relative.Equals("..", StringComparison.Ordinal) ||
                relative.StartsWith(
                    ".." + Path.DirectorySeparatorChar,
                    StringComparison.Ordinal) ||
                relative.StartsWith(
                    ".." + Path.AltDirectorySeparatorChar,
                    StringComparison.Ordinal))
            {
                return string.Empty;
            }

            return relative
                .Replace(Path.DirectorySeparatorChar, '/')
                .Replace(Path.AltDirectorySeparatorChar, '/');
        }

        private static string ResolveCwAiRootForProvenance(
            string generatedDirectoryPath,
            string cwAiRoot)
        {
            if (!string.IsNullOrWhiteSpace(cwAiRoot))
            {
                return Path.GetFullPath(cwAiRoot);
            }

            for (var current = new DirectoryInfo(generatedDirectoryPath);
                 current != null;
                 current = current.Parent)
            {
                if (File.Exists(Path.Combine(
                        current.FullName,
                        "MachineLearning",
                        "CameraDirector",
                        "generate_hybrid.py")))
                {
                    return current.FullName;
                }
            }

            return string.Empty;
        }

        private static void RemovePreviewGenerationProvenance(
            TimelineAsset timeline)
        {
            if (timeline == null)
            {
                return;
            }

            var timelinePath = AssetDatabase.GetAssetPath(timeline);
            if (string.IsNullOrWhiteSpace(timelinePath))
            {
                return;
            }

            var assets = AssetDatabase.LoadAllAssetsAtPath(timelinePath)
                .OfType<AICameraPreviewGenerationProvenance>()
                .ToArray();
            foreach (var provenance in assets)
            {
                UnityEngine.Object.DestroyImmediate(provenance, true);
            }

            if (assets.Length > 0)
            {
                EditorUtility.SetDirty(timeline);
                AssetDatabase.SaveAssetIfDirty(timeline);
                AssetDatabase.ImportAsset(
                    timelinePath,
                    ImportAssetOptions.ForceSynchronousImport);
            }
        }

        private static string EnsurePreviewAssetFolder(string scenePath)
        {
            var previewFolder = GetPreviewAssetFolderPath(scenePath);
            var sceneFolder = Path.GetDirectoryName(previewFolder)
                ?.Replace('\\', '/');
            if (!AssetDatabase.IsValidFolder(previewFolder))
            {
                AssetDatabase.CreateFolder(sceneFolder, PreviewAssetFolderName);
            }

            return previewFolder;
        }

        private static string GetPreviewAssetFolderPath(string scenePath)
        {
            var sceneFolder = Path.GetDirectoryName(scenePath)
                ?.Replace('\\', '/');
            if (string.IsNullOrWhiteSpace(sceneFolder))
            {
                throw new InvalidOperationException(
                    "The active scene must be saved under Assets.");
            }

            return $"{sceneFolder}/{PreviewAssetFolderName}";
        }

        private static string EnsureAssetSubfolder(
            string parentFolder,
            string folderName)
        {
            var path = $"{parentFolder}/{folderName}";
            if (!AssetDatabase.IsValidFolder(path))
            {
                AssetDatabase.CreateFolder(parentFolder, folderName);
            }

            return path;
        }

        private static string SanitizeAssetName(string value)
        {
            var invalid = Path.GetInvalidFileNameChars();
            var characters = string.IsNullOrWhiteSpace(value)
                ? "Shot".ToCharArray()
                : value.Select(character =>
                        invalid.Contains(character) ||
                        character == '/' ||
                        character == '\\'
                            ? '_'
                            : character)
                    .ToArray();
            return new string(characters);
        }

        private static void ValidateGeneratedCameraData(
            GeneratedCameraMetadata metadata,
            IReadOnlyList<GeneratedCameraFrame> frames,
            IReadOnlyList<double> times)
        {
            var expectedDuration = metadata.frameCount /
                                   (double)metadata.sampleRate;
            if (Math.Abs(metadata.duration - expectedDuration) >
                0.5 / metadata.sampleRate)
            {
                throw new InvalidDataException(
                    "Generated camera duration does not match frame count and FPS.");
            }

            for (var index = 0; index < times.Count; index++)
            {
                var expectedTime = index / (double)metadata.sampleRate;
                if (double.IsNaN(times[index]) ||
                    double.IsInfinity(times[index]) ||
                    Math.Abs(times[index] - expectedTime) > 0.000001)
                {
                    throw new InvalidDataException(
                        $"Generated camera time is invalid at frame {index}.");
                }

                var frame = frames[index];
                if (!IsFinite(frame.Position.x) ||
                    !IsFinite(frame.Position.y) ||
                    !IsFinite(frame.Position.z) ||
                    !IsFinite(frame.Rotation.x) ||
                    !IsFinite(frame.Rotation.y) ||
                    !IsFinite(frame.Rotation.z) ||
                    !IsFinite(frame.Rotation.w) ||
                    !IsFinite(frame.FieldOfView) ||
                    !IsFinite(frame.Dutch) ||
                    frame.FieldOfView <= 0 ||
                    frame.FieldOfView >= 180 ||
                    frame.Rotation.x * frame.Rotation.x +
                    frame.Rotation.y * frame.Rotation.y +
                    frame.Rotation.z * frame.Rotation.z +
                    frame.Rotation.w * frame.Rotation.w < 0.000001f)
                {
                    throw new InvalidDataException(
                        $"Generated camera contains invalid values at frame {index}.");
                }
            }
        }

        private static bool IsFinite(float value)
        {
            return !float.IsNaN(value) && !float.IsInfinity(value);
        }

        private static void ValidateMetadata(GeneratedCameraMetadata metadata)
        {
            if (metadata == null ||
                metadata.frameCount <= 0 ||
                metadata.sampleRate <= 0 ||
                metadata.duration <= 0 ||
                string.IsNullOrWhiteSpace(metadata.worldCameraFile) ||
                string.IsNullOrWhiteSpace(metadata.timeFile))
            {
                throw new InvalidDataException(
                    "Generated camera metadata is incomplete.");
            }

            if (metadata.lookAtUsed)
            {
                throw new InvalidDataException(
                    "LookAt-based generated cameras are not supported.");
            }
        }

        private static string ResolveOptionalGeneratedPayloadPath(
            string generatedDirectory,
            string declaredPath,
            string payloadName)
        {
            return string.IsNullOrWhiteSpace(declaredPath)
                ? null
                : ResolveGeneratedPayloadPath(
                    generatedDirectory,
                    declaredPath,
                    payloadName);
        }

        private static string ResolveGeneratedPayloadPath(
            string generatedDirectory,
            string declaredPath,
            string payloadName)
        {
            if (string.IsNullOrWhiteSpace(generatedDirectory))
            {
                throw new ArgumentException(
                    "Generated directory is required.",
                    nameof(generatedDirectory));
            }

            if (string.IsNullOrWhiteSpace(declaredPath))
            {
                throw new InvalidDataException(
                    $"metadata path for {payloadName} is missing.");
            }

            if (Path.IsPathRooted(declaredPath))
            {
                throw new InvalidDataException(
                    $"metadata path for {payloadName} must be relative to the " +
                    "generated directory.");
            }

            var segments = declaredPath.Split(
                new[] { '/', '\\' },
                StringSplitOptions.None);
            var invalidFileNameCharacters = Path.GetInvalidFileNameChars();
            if (segments.Length == 0 ||
                segments.Any(segment =>
                    string.IsNullOrWhiteSpace(segment) ||
                    segment == "." ||
                    segment == ".." ||
                    segment.IndexOfAny(invalidFileNameCharacters) >= 0))
            {
                throw new InvalidDataException(
                    $"metadata path for {payloadName} contains an invalid segment.");
            }

            var pathComparison = Path.DirectorySeparatorChar == '\\'
                ? StringComparison.OrdinalIgnoreCase
                : StringComparison.Ordinal;
            var rootPath = Path.GetFullPath(generatedDirectory);
            var fileSystemRoot = Path.GetPathRoot(rootPath);
            if (!string.Equals(rootPath, fileSystemRoot, pathComparison))
            {
                rootPath = rootPath.TrimEnd(
                    Path.DirectorySeparatorChar,
                    Path.AltDirectorySeparatorChar);
            }
            if (!Directory.Exists(rootPath))
            {
                throw new DirectoryNotFoundException(
                    $"Generated directory was not found: {rootPath}");
            }

            var rootAttributes = File.GetAttributes(rootPath);
            if ((rootAttributes & FileAttributes.Directory) == 0 ||
                (rootAttributes & FileAttributes.ReparsePoint) != 0)
            {
                throw new InvalidDataException(
                    "Generated directory must be a physical directory, not a " +
                    "symlink, junction, or other reparse point.");
            }

            var fullPath = Path.GetFullPath(
                Path.Combine(rootPath, string.Join(
                    Path.DirectorySeparatorChar.ToString(),
                    segments)));
            var rootPrefix = rootPath.EndsWith(
                    Path.DirectorySeparatorChar.ToString(),
                    StringComparison.Ordinal) ||
                rootPath.EndsWith(
                    Path.AltDirectorySeparatorChar.ToString(),
                    StringComparison.Ordinal)
                    ? rootPath
                    : rootPath + Path.DirectorySeparatorChar;
            if (!fullPath.StartsWith(rootPrefix, pathComparison))
            {
                throw new InvalidDataException(
                    $"metadata path for {payloadName} resolves outside the " +
                    "generated directory.");
            }

            var currentPath = rootPath;
            for (var index = 0; index < segments.Length; index++)
            {
                currentPath = Path.Combine(currentPath, segments[index]);
                if (!File.Exists(currentPath) && !Directory.Exists(currentPath))
                {
                    throw new FileNotFoundException(
                        $"Generated {payloadName} payload was not found.",
                        currentPath);
                }

                var attributes = File.GetAttributes(currentPath);
                if ((attributes & FileAttributes.ReparsePoint) != 0)
                {
                    throw new InvalidDataException(
                        $"metadata path for {payloadName} crosses a symlink, " +
                        "junction, or other reparse point.");
                }

                var isLast = index == segments.Length - 1;
                var isDirectory =
                    (attributes & FileAttributes.Directory) != 0;
                if ((!isLast && !isDirectory) ||
                    (isLast &&
                     (isDirectory ||
                      (attributes & FileAttributes.Device) != 0)))
                {
                    throw new InvalidDataException(
                        $"Generated {payloadName} payload must resolve to a " +
                        "regular file.");
                }
            }

            if (!string.Equals(currentPath, fullPath, pathComparison))
            {
                throw new InvalidDataException(
                    $"metadata path for {payloadName} did not resolve canonically.");
            }

            return fullPath;
        }

        private static GeneratedCameraMetadata ReadGeneratedCameraMetadata(
            string metadataPath)
        {
            var json = File.ReadAllText(metadataPath);
            var outputSha256Declared =
                TryGetTopLevelJsonPropertyValueStart(
                    json,
                    "outputSha256",
                    out var outputSha256ValueStart);
            if (outputSha256Declared &&
                (outputSha256ValueStart >= json.Length ||
                 json[outputSha256ValueStart] != '{'))
            {
                throw new InvalidDataException(
                    "metadata.outputSha256 must be a JSON object.");
            }

            var metadata = new GeneratedCameraMetadata();
            JsonUtility.FromJsonOverwrite(json, metadata);
            metadata.OutputSha256Declared = outputSha256Declared;
            if (!outputSha256Declared)
            {
                // JsonUtility eagerly constructs nested serializable objects even
                // when the property is absent. Preserve the legacy schema signal.
                metadata.outputSha256 = null;
            }
            else if (metadata.outputSha256 == null)
            {
                throw new InvalidDataException(
                    "metadata.outputSha256 must be a JSON object.");
            }

            return metadata;
        }

        private static bool TryGetTopLevelJsonPropertyValueStart(
            string json,
            string propertyName,
            out int valueStart)
        {
            valueStart = -1;
            if (string.IsNullOrEmpty(json) ||
                string.IsNullOrEmpty(propertyName))
            {
                return false;
            }

            var objectDepth = 0;
            for (var index = 0; index < json.Length; index++)
            {
                var character = json[index];
                if (character == '{')
                {
                    objectDepth++;
                    continue;
                }

                if (character == '}')
                {
                    objectDepth--;
                    continue;
                }

                if (character != '"')
                {
                    continue;
                }

                var stringStart = index + 1;
                var escaped = false;
                var stringEnd = stringStart;
                for (; stringEnd < json.Length; stringEnd++)
                {
                    var stringCharacter = json[stringEnd];
                    if (escaped)
                    {
                        escaped = false;
                        continue;
                    }

                    if (stringCharacter == '\\')
                    {
                        escaped = true;
                        continue;
                    }

                    if (stringCharacter == '"')
                    {
                        break;
                    }
                }

                if (stringEnd >= json.Length)
                {
                    return false;
                }

                index = stringEnd;
                if (objectDepth != 1 ||
                    stringEnd - stringStart != propertyName.Length ||
                    string.CompareOrdinal(
                        json,
                        stringStart,
                        propertyName,
                        0,
                        propertyName.Length) != 0)
                {
                    continue;
                }

                var separator = stringEnd + 1;
                while (separator < json.Length &&
                       char.IsWhiteSpace(json[separator]))
                {
                    separator++;
                }

                if (separator >= json.Length || json[separator] != ':')
                {
                    continue;
                }

                valueStart = separator + 1;
                while (valueStart < json.Length &&
                       char.IsWhiteSpace(json[valueStart]))
                {
                    valueStart++;
                }

                return true;
            }

            return false;
        }

        private static void ValidateGeneratedOutputHashes(
            GeneratedCameraMetadata metadata,
            string cameraPath,
            string timePath,
            string shotsPath)
        {
            if (!metadata.OutputSha256Declared)
            {
                // Legacy Native and Hybrid outputs predate payload hashes.
                return;
            }

            if (metadata.outputSha256 == null)
            {
                throw new InvalidDataException(
                    "metadata.outputSha256 must be a JSON object.");
            }

            ValidateGeneratedOutputHash(
                "worldCamera",
                metadata.outputSha256.worldCamera,
                cameraPath);
            ValidateGeneratedOutputHash(
                "time",
                metadata.outputSha256.time,
                timePath);

            if (string.IsNullOrWhiteSpace(metadata.shotsFile))
            {
                if (!string.IsNullOrWhiteSpace(metadata.outputSha256.shots))
                {
                    throw new InvalidDataException(
                        "metadata.outputSha256.shots is present but metadata.shotsFile " +
                        "is empty.");
                }

                return;
            }

            ValidateGeneratedOutputHash(
                "shots",
                metadata.outputSha256.shots,
                shotsPath);
        }

        private static void ValidateGeneratedOutputHash(
            string payloadName,
            string expectedSha256,
            string path)
        {
            if (string.IsNullOrWhiteSpace(expectedSha256))
            {
                throw new InvalidDataException(
                    $"metadata.outputSha256.{payloadName} is missing.");
            }

            var normalizedExpected = expectedSha256.Trim();
            if (normalizedExpected.Length != 64 ||
                normalizedExpected.Any(character => !Uri.IsHexDigit(character)))
            {
                throw new InvalidDataException(
                    $"metadata.outputSha256.{payloadName} is not a valid SHA-256 value.");
            }

            if (!File.Exists(path))
            {
                throw new FileNotFoundException(
                    $"Generated {payloadName} payload was not found.",
                    path);
            }

            var actualSha256 = ComputeSha256(path);
            if (!string.Equals(
                    normalizedExpected,
                    actualSha256,
                    StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidDataException(
                    $"Generated {payloadName} payload SHA-256 mismatch. " +
                    $"Expected {normalizedExpected}; actual {actualSha256}.");
            }
        }

        private static string ComputeSha256(string path)
        {
            using var algorithm = SHA256.Create();
            using var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                1024 * 1024,
                FileOptions.SequentialScan);
            return string.Concat(
                algorithm.ComputeHash(stream)
                    .Select(value => value.ToString("x2")));
        }

        private static string GetHierarchyPath(Transform transform)
        {
            var names = new List<string>();
            while (transform != null)
            {
                names.Add(transform.name);
                transform = transform.parent;
            }

            names.Reverse();
            return string.Join("/", names);
        }

        private static string GetStableHierarchyKey(Transform transform)
        {
            var segments = new List<string>();
            while (transform != null)
            {
                // Names alone are ambiguous when sibling objects intentionally
                // share a name. Serialized sibling order survives reloads while
                // Unity instance IDs do not.
                segments.Add($"{transform.name}[{transform.GetSiblingIndex()}]");
                transform = transform.parent;
            }

            segments.Reverse();
            return string.Join("/", segments);
        }

        private sealed class GeneratedCameraFrame
        {
            public Vector3 Position;
            public Quaternion Rotation;
            public float FieldOfView;
            public float Dutch;
        }

        private readonly struct CurveSegment
        {
            public CurveSegment(int start, int end)
            {
                Start = start;
                End = end;
            }

            public int Start { get; }
            public int End { get; }
        }

        internal sealed class PositionCurveBuildResult
        {
            internal PositionCurveBuildResult(
                AnimationCurve xCurve,
                AnimationCurve yCurve,
                AnimationCurve zCurve,
                bool usedExactFallback,
                string fallbackReason)
            {
                XCurve = xCurve ?? throw new ArgumentNullException(nameof(xCurve));
                YCurve = yCurve ?? throw new ArgumentNullException(nameof(yCurve));
                ZCurve = zCurve ?? throw new ArgumentNullException(nameof(zCurve));
                UsedExactFallback = usedExactFallback;
                FallbackReason = fallbackReason ?? string.Empty;
            }

            internal AnimationCurve XCurve { get; }
            internal AnimationCurve YCurve { get; }
            internal AnimationCurve ZCurve { get; }
            internal bool UsedExactFallback { get; }
            internal string FallbackReason { get; }
        }

        internal sealed class RotationCurveBuildResult
        {
            internal RotationCurveBuildResult(
                AnimationCurve xCurve,
                AnimationCurve yCurve,
                AnimationCurve zCurve,
                AnimationCurve wCurve,
                bool usedDenseFallback,
                string fallbackReason)
            {
                XCurve = xCurve ?? throw new ArgumentNullException(nameof(xCurve));
                YCurve = yCurve ?? throw new ArgumentNullException(nameof(yCurve));
                ZCurve = zCurve ?? throw new ArgumentNullException(nameof(zCurve));
                WCurve = wCurve ?? throw new ArgumentNullException(nameof(wCurve));
                UsedDenseFallback = usedDenseFallback;
                FallbackReason = fallbackReason ?? string.Empty;
            }

            internal AnimationCurve XCurve { get; }
            internal AnimationCurve YCurve { get; }
            internal AnimationCurve ZCurve { get; }
            internal AnimationCurve WCurve { get; }
            internal bool UsedDenseFallback { get; }
            internal string FallbackReason { get; }
        }

        internal readonly struct PositionCurveAuditResult
        {
            private PositionCurveAuditResult(bool passed, string failureReason)
            {
                Passed = passed;
                FailureReason = failureReason ?? string.Empty;
            }

            internal bool Passed { get; }
            internal string FailureReason { get; }

            internal static PositionCurveAuditResult Pass()
            {
                return new PositionCurveAuditResult(true, string.Empty);
            }

            internal static PositionCurveAuditResult Fail(string failureReason)
            {
                return new PositionCurveAuditResult(false, failureReason);
            }
        }

        internal readonly struct RotationCurveAuditResult
        {
            private RotationCurveAuditResult(bool passed, string failureReason)
            {
                Passed = passed;
                FailureReason = failureReason ?? string.Empty;
            }

            internal bool Passed { get; }
            internal string FailureReason { get; }

            internal static RotationCurveAuditResult Pass()
            {
                return new RotationCurveAuditResult(true, string.Empty);
            }

            internal static RotationCurveAuditResult Fail(string failureReason)
            {
                return new RotationCurveAuditResult(false, failureReason);
            }
        }

        private readonly struct PositionMetricSeries
        {
            internal static readonly PositionMetricSeries Empty =
                new PositionMetricSeries(
                    Array.Empty<double>(),
                    Array.Empty<float>());

            internal PositionMetricSeries(double[] times, float[] values)
            {
                Times = times ?? Array.Empty<double>();
                Values = values ?? Array.Empty<float>();
                Maximum = Values.Length == 0 ? 0f : Values.Max();
            }

            internal double[] Times { get; }
            internal float[] Values { get; }
            internal float Maximum { get; }
        }

        private readonly struct PositionMotionSeries
        {
            internal static readonly PositionMotionSeries Empty =
                new PositionMotionSeries(
                    PositionMetricSeries.Empty,
                    PositionMetricSeries.Empty,
                    PositionMetricSeries.Empty);

            internal PositionMotionSeries(
                PositionMetricSeries speed,
                PositionMetricSeries acceleration,
                PositionMetricSeries jerk)
            {
                Speed = speed;
                Acceleration = acceleration;
                Jerk = jerk;
            }

            internal PositionMetricSeries Speed { get; }
            internal PositionMetricSeries Acceleration { get; }
            internal PositionMetricSeries Jerk { get; }
            internal PositionMotionMetrics Metrics =>
                new PositionMotionMetrics(
                    Speed.Maximum,
                    Acceleration.Maximum,
                    Jerk.Maximum);
        }

        private readonly struct PositionMotionMetrics
        {
            internal PositionMotionMetrics(
                float maximumSpeed,
                float maximumAcceleration,
                float maximumJerk)
            {
                MaximumSpeed = maximumSpeed;
                MaximumAcceleration = maximumAcceleration;
                MaximumJerk = maximumJerk;
            }

            internal float MaximumSpeed { get; }
            internal float MaximumAcceleration { get; }
            internal float MaximumJerk { get; }
        }

        internal sealed class SimplifiedCameraCurves
        {
            public SimplifiedCameraCurves(
                double[] times,
                Vector3[] positions,
                Quaternion[] rotations,
                float[] fieldOfView,
                float[] dutch,
                int[] positionIndices,
                int[] rotationIndices,
                int[] fieldOfViewIndices,
                int[] dutchIndices)
            {
                Times = times;
                Positions = positions;
                Rotations = rotations;
                FieldOfView = fieldOfView;
                Dutch = dutch;
                PositionIndices = positionIndices;
                RotationIndices = rotationIndices;
                FieldOfViewIndices = fieldOfViewIndices;
                DutchIndices = dutchIndices;
            }

            public double[] Times { get; }
            public Vector3[] Positions { get; }
            public Quaternion[] Rotations { get; }
            public float[] FieldOfView { get; }
            public float[] Dutch { get; }
            public int[] PositionIndices { get; }
            public int[] RotationIndices { get; }
            public int[] FieldOfViewIndices { get; }
            public int[] DutchIndices { get; }
            internal bool PositionUsedExactFallback { get; private set; }
            internal string PositionFallbackReason { get; private set; } =
                string.Empty;
            internal bool RotationUsedDenseFallback { get; private set; }
            internal string RotationFallbackReason { get; private set; } =
                string.Empty;

            internal void SetPositionCurveDiagnostics(
                bool usedExactFallback,
                string fallbackReason)
            {
                PositionUsedExactFallback = usedExactFallback;
                PositionFallbackReason = fallbackReason ?? string.Empty;
            }

            internal void SetRotationCurveDiagnostics(
                bool usedDenseFallback,
                string fallbackReason)
            {
                RotationUsedDenseFallback = usedDenseFallback;
                RotationFallbackReason = fallbackReason ?? string.Empty;
            }
        }

        private sealed class PreviewShotCamera
        {
            public GeneratedShot Definition;
            public string RelativePath;
            public CinemachineCamera Camera;
        }

        private readonly struct GeneratedAnimationEntry
        {
            public GeneratedAnimationEntry(
                AnimationTrack track,
                TimelineClip clip)
            {
                Track = track;
                Clip = clip;
            }

            public AnimationTrack Track { get; }
            public TimelineClip Clip { get; }
        }

        private readonly struct PreviewContext
        {
            public PreviewContext(
                Scene scene,
                PlayableDirector director,
                GameObject cameraRoot)
            {
                Scene = scene;
                Director = director;
                CameraRoot = cameraRoot;
            }

            public Scene Scene { get; }
            public PlayableDirector Director { get; }
            public GameObject CameraRoot { get; }
        }

        internal sealed class AuthoredCinemachineStateSnapshot
        {
            public AuthoredCinemachineStateSnapshot(
                AuthoredCinemachineDirectorState[] directorStates,
                AuthoredVirtualCameraState[] cameraStates)
            {
                DirectorStates = directorStates;
                CameraStates = cameraStates;
            }

            public AuthoredCinemachineDirectorState[] DirectorStates { get; }
            public AuthoredVirtualCameraState[] CameraStates { get; }
        }

        internal sealed class AuthoredCinemachineDirectorState
        {
            public AuthoredCinemachineDirectorState(
                PlayableDirector director,
                CinemachineTrack[] tracks,
                UnityEngine.Object[] bindings)
            {
                Director = director;
                Tracks = tracks;
                Bindings = bindings;
            }

            public PlayableDirector Director { get; }
            public CinemachineTrack[] Tracks { get; }
            public UnityEngine.Object[] Bindings { get; }
        }

        internal sealed class AuthoredVirtualCameraState
        {
            public AuthoredVirtualCameraState(
                CinemachineVirtualCameraBase camera,
                bool enabled)
            {
                Camera = camera;
                Enabled = enabled;
            }

            public CinemachineVirtualCameraBase Camera { get; }
            public bool Enabled { get; }
        }

        private sealed class DirectorStateSnapshot
        {
            public DirectorStateSnapshot(
                PlayableDirector director,
                TimelineAsset timeline,
                double time,
                bool activeSelf,
                string scenePath,
                bool sceneWasDirty,
                bool directorWasDirty,
                bool gameObjectWasDirty,
                bool timelineWasDirty,
                TrackAsset[] tracks,
                UnityEngine.Object[] bindings,
                PropertyName[] referenceNames,
                UnityEngine.Object[] referenceValues,
                bool[] referenceValuesValid)
            {
                Director = director;
                Timeline = timeline;
                Time = time;
                ActiveSelf = activeSelf;
                ScenePath = scenePath;
                SceneWasDirty = sceneWasDirty;
                DirectorWasDirty = directorWasDirty;
                GameObjectWasDirty = gameObjectWasDirty;
                TimelineWasDirty = timelineWasDirty;
                Tracks = tracks;
                Bindings = bindings;
                ReferenceNames = referenceNames;
                ReferenceValues = referenceValues;
                ReferenceValuesValid = referenceValuesValid;
            }

            public PlayableDirector Director { get; }
            public TimelineAsset Timeline { get; }
            public double Time { get; }
            public bool ActiveSelf { get; }
            public string ScenePath { get; }
            public bool SceneWasDirty { get; }
            public bool DirectorWasDirty { get; }
            public bool GameObjectWasDirty { get; }
            public bool TimelineWasDirty { get; }
            public TrackAsset[] Tracks { get; }
            public UnityEngine.Object[] Bindings { get; }
            public PropertyName[] ReferenceNames { get; }
            public UnityEngine.Object[] ReferenceValues { get; }
            public bool[] ReferenceValuesValid { get; }
        }

        [Serializable]
        private sealed class GeneratedShotFile
        {
            public List<GeneratedShot> shots;
        }

        [Serializable]
        private sealed class GeneratedShot
        {
            public int index;
            public string cameraName;
            public string shotType;
            public int startFrame;
            public int endFrameExclusive;
            public double start;
            public double end;
            public double duration;
            public string sourceSong;
            public int sourceShotIndex;
        }

        [Serializable]
        private sealed class GeneratedCameraMetadata
        {
            public string songName;
            public int sampleRate;
            public int frameCount;
            public double duration;
            public string worldCameraFile;
            public string timeFile;
            public string shotsFile;
            public GeneratedOutputSha256 outputSha256;
            public bool lookAtUsed;
            public bool OutputSha256Declared { get; set; }
        }

        [Serializable]
        private sealed class GeneratedOutputSha256
        {
            public string worldCamera;
            public string time;
            public string shots;
        }
    }
}
