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

namespace Streamingle.Editor
{
    public static class TimelineCameraBatchExporter
    {
        private const string AuthoredSceneRoot = "Assets/Resourcedata/Character";
        private const string ReportFileName = "yamo_batch_report.json";
        private const string DatasetSchemaVersion = "1.3";

        public static string WriteAuthoredSceneInventoryForCli(string outputPath)
        {
            EnsureEditMode();

            var candidates = DiscoverCandidateScenes();
            var report = new InventoryReport
            {
                schemaVersion = "1.0",
                createdUtc = DateTime.UtcNow.ToString("O"),
                unityVersion = Application.unityVersion,
                assetRoot = AuthoredSceneRoot,
                candidateSceneCount = candidates.Count,
                uniqueTimelineCount = candidates
                    .SelectMany(candidate => candidate.timelines)
                    .Select(timeline => timeline.assetPath)
                    .Distinct(StringComparer.Ordinal)
                    .Count(),
                candidates = candidates
            };

            var fullPath = Path.GetFullPath(outputPath);
            WriteJsonAtomic(fullPath, report);
            return fullPath;
        }

        public static string WriteActiveSceneDiagnosticsForCli(string outputPath)
        {
            EnsureEditMode();

            var scene = SceneManager.GetActiveScene();
            var report = new ActiveSceneDiagnosticReport
            {
                schemaVersion = "1.0",
                createdUtc = DateTime.UtcNow.ToString("O"),
                scenePath = scene.path,
                directors = Resources.FindObjectsOfTypeAll<UnityEngine.Playables.PlayableDirector>()
                    .Where(director =>
                        director.gameObject.scene == scene &&
                        director.playableAsset is TimelineAsset)
                    .OrderBy(director => GetHierarchyPath(director.transform))
                    .Select(BuildDirectorDiagnostic)
                    .ToList()
            };

            var fullPath = Path.GetFullPath(outputPath);
            WriteJsonAtomic(fullPath, report);
            return fullPath;
        }

        public static string ExportAuthoredScenesForCli(
            string outputRoot,
            int startIndex = 0,
            int maxScenes = 0,
            bool requireAudio = false)
        {
            EnsureEditMode();
            EnsureNoDirtyScenes();

            var fullOutputRoot = Path.GetFullPath(outputRoot)
                .TrimEnd(
                    Path.DirectorySeparatorChar,
                    Path.AltDirectorySeparatorChar);
            EnsureSafeOutputRoot(fullOutputRoot);
            Directory.CreateDirectory(fullOutputRoot);
            var reportPath = Path.Combine(fullOutputRoot, ReportFileName);
            var candidates = DiscoverCandidateScenes();
            if (candidates.Count == 0)
            {
                throw new InvalidOperationException(
                    "No authored camera scene candidates were discovered.");
            }

            if (startIndex < 0 || startIndex >= candidates.Count)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(startIndex),
                    startIndex,
                    $"Expected 0 through {candidates.Count - 1}.");
            }

            var firstIndex = startIndex;
            var endExclusive = maxScenes > 0
                ? Math.Min(candidates.Count, firstIndex + maxScenes)
                : candidates.Count;
            if (endExclusive <= firstIndex)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(maxScenes),
                    maxScenes,
                    "The requested range contains no scenes.");
            }

            var report = new BatchReport
            {
                schemaVersion = "1.1",
                startedUtc = DateTime.UtcNow.ToString("O"),
                lastUpdatedUtc = DateTime.UtcNow.ToString("O"),
                state = "running",
                unityVersion = Application.unityVersion,
                assetRoot = AuthoredSceneRoot,
                outputRoot = fullOutputRoot,
                candidateSceneCount = candidates.Count,
                candidateInventoryFingerprint = StableHash(string.Join(
                    "\n",
                    candidates.Select(candidate =>
                        candidate.scenePath + "|" +
                        string.Join(
                            ",",
                            candidate.timelines.Select(timeline =>
                                timeline.assetPath))))),
                startIndex = firstIndex,
                endExclusive = endExclusive,
                requireAudio = requireAudio,
                results = new List<SceneExportResult>()
            };
            WriteJsonAtomic(reportPath, report);

            var originalSetup = EditorSceneManager.GetSceneManagerSetup();
            try
            {
                for (var index = firstIndex; index < endExclusive; index++)
                {
                    var candidate = candidates[index];
                    var result = ExportCandidate(
                        candidate,
                        index,
                        candidates.Count,
                        fullOutputRoot,
                        requireAudio);
                    report.results.Add(result);
                    report.processedSceneCount = report.results.Count;
                    report.lastUpdatedUtc = DateTime.UtcNow.ToString("O");
                    WriteJsonAtomic(reportPath, report);
                }
            }
            catch (Exception exception)
            {
                report.state = "failed";
                report.fatalError = exception.ToString();
                throw;
            }
            finally
            {
                EditorUtility.ClearProgressBar();
                try
                {
                    RestoreSceneSetup(originalSetup);
                }
                catch (Exception exception)
                {
                    report.restoreError = exception.ToString();
                }

                if (!string.Equals(
                        report.state,
                        "failed",
                        StringComparison.Ordinal))
                {
                    var hasSceneErrors = report.results.Any(result =>
                        result.status != "exported" &&
                        result.status != "already_complete" &&
                        result.status != "exported_with_skips" &&
                        result.status != "already_complete_with_skips");
                    var hasSkippedTracks = report.results.Any(result =>
                        result.skippedCameraTrackCount > 0);
                    report.state = string.IsNullOrWhiteSpace(report.restoreError)
                        ? hasSceneErrors
                            ? "completed_with_errors"
                            : hasSkippedTracks
                                ? "completed_with_skips"
                                : "completed"
                        : "completed_with_restore_error";
                }

                report.completedUtc = DateTime.UtcNow.ToString("O");
                report.lastUpdatedUtc = report.completedUtc;
                WriteJsonAtomic(reportPath, report);
            }

            if (!string.Equals(
                    report.state,
                    "completed",
                    StringComparison.Ordinal))
            {
                throw new InvalidOperationException(
                    $"Batch finished with state '{report.state}'. " +
                    $"See '{reportPath}'.");
            }

            return reportPath;
        }

        private static void EnsureSafeOutputRoot(string outputRoot)
        {
            var projectRoot = Directory.GetParent(Application.dataPath)?.FullName
                ?? throw new InvalidOperationException(
                    "Unable to resolve the Unity project root.");
            foreach (var protectedPath in new[]
            {
                projectRoot,
                Path.Combine(projectRoot, "Assets"),
                Path.Combine(projectRoot, "Packages"),
                Path.Combine(projectRoot, "Library")
            })
            {
                var normalizedProtected = Path.GetFullPath(protectedPath)
                    .TrimEnd(
                        Path.DirectorySeparatorChar,
                        Path.AltDirectorySeparatorChar);
                if (string.Equals(
                        outputRoot.TrimEnd(
                            Path.DirectorySeparatorChar,
                            Path.AltDirectorySeparatorChar),
                        normalizedProtected,
                        StringComparison.OrdinalIgnoreCase) ||
                    outputRoot.StartsWith(
                        normalizedProtected + Path.DirectorySeparatorChar,
                        StringComparison.OrdinalIgnoreCase))
                {
                    throw new InvalidOperationException(
                        $"Output root must be outside the Unity project: " +
                        $"'{outputRoot}'.");
                }
            }
        }

        private static SceneExportResult ExportCandidate(
            SceneCandidate candidate,
            int index,
            int candidateCount,
            string outputRoot,
            bool requireAudio)
        {
            var sceneName = Path.GetFileNameWithoutExtension(candidate.scenePath);
            var datasetName =
                $"TimelineCamera_60fps_YAMO_{StableHash(candidate.scenePath)}_" +
                SanitizeFileName(sceneName, 72);
            var datasetPath = Path.Combine(outputRoot, datasetName);
            var manifestPath = Path.Combine(datasetPath, "dataset_manifest.json");
            var result = new SceneExportResult
            {
                candidateIndex = index,
                scenePath = candidate.scenePath,
                timelinePaths = candidate.timelines
                    .Select(timeline => timeline.assetPath)
                    .ToList(),
                outputPath = datasetPath,
                startedUtc = DateTime.UtcNow.ToString("O")
            };

            var stopwatch = System.Diagnostics.Stopwatch.StartNew();
            var workingPath = Path.Combine(
                $"{outputRoot}.working",
                $"{index:D4}_{StableHash(candidate.scenePath)}_" +
                Guid.NewGuid().ToString("N"));
            try
            {
                EditorUtility.DisplayProgressBar(
                    "YAMO Camera Dataset Export",
                    $"{index + 1:N0} / {candidateCount:N0}: {sceneName}",
                    (index + 1) / (float)Math.Max(1, candidateCount));
                Debug.Log(
                    $"[CW-AI] Exporting scene {index + 1}/{candidateCount}: " +
                    candidate.scenePath);

                var openedScene = EditorSceneManager.OpenScene(
                    candidate.scenePath,
                    OpenSceneMode.Single);
                if (!openedScene.IsValid() || !openedScene.isLoaded)
                {
                    throw new InvalidOperationException(
                        $"Unable to load scene '{candidate.scenePath}'.");
                }

                var loadedTimelines = DiscoverLoadedSceneTimelines(openedScene);
                if (loadedTimelines.Count == 0)
                {
                    result.status = "not_exportable";
                    result.error =
                        "The loaded scene has no Cinemachine Timeline with shots.";
                    return result;
                }

                candidate = new SceneCandidate
                {
                    scenePath = candidate.scenePath,
                    timelines = loadedTimelines
                };
                result.timelinePaths = loadedTimelines
                    .Select(timeline => timeline.assetPath)
                    .ToList();

                if (File.Exists(manifestPath))
                {
                    if (ValidateCompleteDataset(
                            datasetPath,
                            candidate,
                            requireAudio,
                            openedScene,
                            out var existingValidationError))
                    {
                        PopulateTrackCounts(datasetPath, result);
                        result.status = result.skippedCameraTrackCount > 0
                            ? "already_complete_with_skips"
                            : "already_complete";
                    }
                    else
                    {
                        result.status = "invalid_existing";
                        result.error = existingValidationError;
                    }

                    return result;
                }

                if (Directory.Exists(datasetPath))
                {
                    result.status = "partial_exists";
                    result.error =
                        "An existing output directory was preserved and not overwritten.";
                    return result;
                }

                TimelineCameraDatasetExporter.ExportActiveScene60FpsToForCli(
                    workingPath,
                    requireAudio,
                    true);
                var workingManifestPath = Path.Combine(
                    workingPath,
                    "dataset_manifest.json");
                if (!File.Exists(workingManifestPath))
                {
                    throw new IOException(
                        $"Export completed without a manifest: '{workingManifestPath}'.");
                }

                if (!ValidateCompleteDataset(
                        workingPath,
                        candidate,
                        requireAudio,
                        openedScene,
                        out var validationError))
                {
                    throw new InvalidDataException(
                        $"Export validation failed: {validationError}");
                }

                Directory.CreateDirectory(outputRoot);
                Directory.Move(workingPath, datasetPath);
                PopulateTrackCounts(datasetPath, result);
                result.status = result.skippedCameraTrackCount > 0
                    ? "exported_with_skips"
                    : "exported";
            }
            catch (Exception exception)
            {
                result.status =
                    exception is InvalidOperationException &&
                    exception.Message.IndexOf(
                        "does not contain a supported camera Timeline",
                        StringComparison.Ordinal) >= 0
                        ? "not_exportable"
                        : "failed";
                result.error = exception.ToString();
                if (Directory.Exists(workingPath))
                {
                    result.partialOutputPath = workingPath;
                }

                Debug.LogError(
                    $"[CW-AI] Scene export failed: {candidate.scenePath}\n{exception}");
            }
            finally
            {
                stopwatch.Stop();
                result.elapsedSeconds = stopwatch.Elapsed.TotalSeconds;
                result.completedUtc = DateTime.UtcNow.ToString("O");
            }

            return result;
        }

        private static void PopulateTrackCounts(
            string datasetPath,
            SceneExportResult result)
        {
            var manifest = JsonUtility.FromJson<DatasetManifestProbe>(
                File.ReadAllText(
                    Path.Combine(datasetPath, "dataset_manifest.json"),
                    Encoding.UTF8));
            result.discoveredCameraTrackCount =
                manifest?.discoveredCameraTrackCount ?? 0;
            result.exportedCameraTrackCount =
                manifest?.exportedCameraTrackCount ?? 0;
            result.skippedCameraTrackCount =
                manifest?.skippedCameraTracks?.Count ?? 0;
        }

        private static bool ValidateCompleteDataset(
            string datasetPath,
            SceneCandidate candidate,
            bool requireAudio,
            Scene loadedScene,
            out string error)
        {
            try
            {
                var manifestPath = Path.Combine(
                    datasetPath,
                    "dataset_manifest.json");
                if (!File.Exists(manifestPath))
                {
                    error = "dataset_manifest.json is missing.";
                    return false;
                }

                var manifest = JsonUtility.FromJson<DatasetManifestProbe>(
                    File.ReadAllText(manifestPath, Encoding.UTF8));
                if (manifest == null)
                {
                    error = "dataset_manifest.json could not be parsed.";
                    return false;
                }

                if (!string.Equals(
                        manifest.schemaVersion,
                        DatasetSchemaVersion,
                        StringComparison.Ordinal))
                {
                    error =
                        $"Unsupported dataset schema '{manifest.schemaVersion}'.";
                    return false;
                }

                if (!string.Equals(
                        manifest.scenePath,
                        candidate.scenePath,
                        StringComparison.Ordinal))
                {
                    error =
                        $"Manifest scene '{manifest.scenePath}' does not match " +
                        $"'{candidate.scenePath}'.";
                    return false;
                }

                var currentFingerprint =
                    TimelineCameraDatasetExporter
                        .ComputeSceneCameraSourceFingerprint(loadedScene);
                if (string.IsNullOrWhiteSpace(manifest.sourceFingerprint) ||
                    !string.Equals(
                        manifest.sourceFingerprint,
                        currentFingerprint,
                        StringComparison.Ordinal))
                {
                    error =
                        "The loaded scene camera source fingerprint does not " +
                        "match the manifest.";
                    return false;
                }

                if (manifest.sampleRate != 60 ||
                    manifest.songs == null ||
                    manifest.songs.Count == 0)
                {
                    error = "Manifest sample rate or song list is invalid.";
                    return false;
                }

                var skippedCount =
                    manifest.skippedCameraTracks?.Count ?? 0;
                if (manifest.discoveredCameraTrackCount <= 0 ||
                    manifest.exportedCameraTrackCount !=
                    manifest.songs.Count ||
                    manifest.discoveredCameraTrackCount !=
                    manifest.exportedCameraTrackCount + skippedCount)
                {
                    error =
                        "Camera track accounting in the manifest is inconsistent.";
                    return false;
                }

                var folderNames = new HashSet<string>(
                    StringComparer.OrdinalIgnoreCase);
                foreach (var song in manifest.songs)
                {
                    if (song == null ||
                        song.frameCount <= 0 ||
                        song.sharedBoneCount <= 0 ||
                        song.shotCount <= 0 ||
                        song.missingCameraFrames != 0 ||
                        song.missingJointSamples != 0 ||
                        song.missingRootSamples != 0 ||
                        string.IsNullOrWhiteSpace(song.cameraTrackName) ||
                        !folderNames.Add(song.folderName) ||
                        !candidate.timelines.Any(timeline =>
                            string.Equals(
                                timeline.assetPath,
                                song.motionTimelineAssetPath,
                                StringComparison.Ordinal)) ||
                        !TryResolveChildPath(
                            datasetPath,
                            song.folderName,
                            out var songPath))
                    {
                        error = "Manifest contains invalid song metadata.";
                        return false;
                    }

                    if (!ValidateFileSize(
                            songPath,
                            song.jointsFile,
                            (long)song.frameCount *
                            song.sharedBoneCount * 3 * sizeof(float),
                            out error) ||
                        !ValidateFileSize(
                            songPath,
                            song.cameraFile,
                            (long)song.frameCount * 9 * sizeof(float),
                            out error) ||
                        !ValidateFileSize(
                            songPath,
                            song.rootFile,
                            (long)song.frameCount * 14 * sizeof(float),
                            out error) ||
                        !ValidateFileSize(
                            songPath,
                            song.shotIndexFile,
                            (long)song.frameCount * sizeof(int),
                            out error) ||
                        !ValidateFileSize(
                            songPath,
                            song.timeFile,
                            (long)song.frameCount * sizeof(double),
                            out error) ||
                        !ValidateRequiredFile(
                            songPath,
                            "metadata.json",
                            out error,
                            true) ||
                        !ValidateRequiredFile(
                            songPath,
                            song.shotsFile,
                            out error,
                            true) ||
                        !ValidateRequiredFile(
                            songPath,
                            song.previewFile,
                            out error,
                            true))
                    {
                        return false;
                    }

                    if (requireAudio &&
                        string.IsNullOrWhiteSpace(song.audioFile))
                    {
                        error = "Required source audio is not embedded.";
                        return false;
                    }

                    if (!string.IsNullOrWhiteSpace(song.audioFile) &&
                        !ValidateRequiredFile(
                            songPath,
                            song.audioFile,
                            out error,
                            true))
                    {
                        return false;
                    }
                }

                error = string.Empty;
                return true;
            }
            catch (Exception exception)
            {
                error = exception.Message;
                return false;
            }
        }

        private static bool ValidateFileSize(
            string directory,
            string relativePath,
            long expectedBytes,
            out string error)
        {
            if (!TryResolveChildPath(
                    directory,
                    relativePath,
                    out var fullPath) ||
                !File.Exists(fullPath))
            {
                error = $"Required file '{relativePath}' is missing.";
                return false;
            }

            var actualBytes = new FileInfo(fullPath).Length;
            if (actualBytes != expectedBytes)
            {
                error =
                    $"File '{relativePath}' has {actualBytes} bytes; " +
                    $"{expectedBytes} were expected.";
                return false;
            }

            error = string.Empty;
            return true;
        }

        private static bool ValidateRequiredFile(
            string directory,
            string relativePath,
            out string error,
            bool requireNonEmpty = false)
        {
            if (!TryResolveChildPath(
                    directory,
                    relativePath,
                    out var fullPath) ||
                !File.Exists(fullPath))
            {
                error = $"Required file '{relativePath}' is missing.";
                return false;
            }

            if (requireNonEmpty && new FileInfo(fullPath).Length == 0)
            {
                error = $"Required file '{relativePath}' is empty.";
                return false;
            }

            error = string.Empty;
            return true;
        }

        private static bool TryResolveChildPath(
            string directory,
            string relativePath,
            out string fullPath)
        {
            fullPath = string.Empty;
            if (string.IsNullOrWhiteSpace(relativePath))
            {
                return false;
            }

            var root = Path.GetFullPath(directory)
                .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) +
                Path.DirectorySeparatorChar;
            var candidate = Path.GetFullPath(Path.Combine(root, relativePath));
            if (!candidate.StartsWith(
                    root,
                    StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            fullPath = candidate;
            return true;
        }

        private static List<SceneCandidate> DiscoverCandidateScenes()
        {
            var timelineByGuid = new Dictionary<string, TimelineInventory>(
                StringComparer.Ordinal);
            foreach (var guid in AssetDatabase.FindAssets(
                "t:TimelineAsset",
                new[] { AuthoredSceneRoot }))
            {
                var assetPath = AssetDatabase.GUIDToAssetPath(guid);
                var timeline = AssetDatabase.LoadAssetAtPath<TimelineAsset>(assetPath);
                var inventory = BuildTimelineInventory(timeline);
                if (inventory == null)
                {
                    continue;
                }

                inventory.assetPath = assetPath;
                timelineByGuid[guid] = inventory;
            }

            var prefabTimelines = BuildPrefabTimelineMap(timelineByGuid);
            var candidates = new List<SceneCandidate>();
            foreach (var guid in AssetDatabase.FindAssets(
                "t:Scene",
                new[] { AuthoredSceneRoot }))
            {
                var scenePath = AssetDatabase.GUIDToAssetPath(guid);
                var timelines = FindPlayableDirectorTimelineGuids(scenePath)
                    .Concat(FindReferencedGuids(scenePath))
                    .Concat(FindSourcePrefabGuids(scenePath)
                        .SelectMany(prefabGuid =>
                            ResolvePrefabTimelineGuids(
                                prefabGuid,
                                prefabTimelines,
                                new HashSet<string>(StringComparer.Ordinal))))
                    .Where(timelineByGuid.ContainsKey)
                    .Select(timelineGuid => timelineByGuid[timelineGuid])
                    .GroupBy(timeline => timeline.assetPath, StringComparer.Ordinal)
                    .Select(group => group.First())
                    .OrderBy(timeline => timeline.assetPath, StringComparer.Ordinal)
                    .ToList();
                if (timelines.Count == 0)
                {
                    continue;
                }

                candidates.Add(new SceneCandidate
                {
                    scenePath = scenePath,
                    timelines = timelines
                });
            }

            return candidates
                .OrderBy(candidate => candidate.scenePath, StringComparer.Ordinal)
                .ToList();
        }

        private static TimelineInventory BuildTimelineInventory(
            TimelineAsset timeline)
        {
            if (timeline == null)
            {
                return null;
            }

            var tracks = timeline.GetOutputTracks().ToArray();
            var cameraTracks = tracks.OfType<CinemachineTrack>().ToArray();
            var shotCount = cameraTracks
                .SelectMany(track => track.GetClips())
                .Count(clip => clip.asset is CinemachineShot);
            if (shotCount == 0)
            {
                return null;
            }

            var audioTracks = tracks.OfType<AudioTrack>().ToArray();
            return new TimelineInventory
            {
                assetPath = AssetDatabase.GetAssetPath(timeline),
                timelineName = timeline.name,
                duration = timeline.duration,
                cameraTrackCount = cameraTracks.Length,
                shotCount = shotCount,
                animationTrackCount = tracks.OfType<AnimationTrack>().Count(),
                audioTrackCount = audioTracks.Length,
                audioClipCount = audioTracks
                    .SelectMany(track => track.GetClips())
                    .Count(clip =>
                        clip.asset is AudioPlayableAsset audioAsset &&
                        audioAsset.clip != null)
            };
        }

        private static List<TimelineInventory> DiscoverLoadedSceneTimelines(
            Scene scene)
        {
            return Resources
                .FindObjectsOfTypeAll<UnityEngine.Playables.PlayableDirector>()
                .Where(director =>
                    director.gameObject.scene == scene &&
                    director.playableAsset is TimelineAsset)
                .Select(director => BuildTimelineInventory(
                    director.playableAsset as TimelineAsset))
                .Where(inventory => inventory != null)
                .GroupBy(
                    inventory => inventory.assetPath,
                    StringComparer.Ordinal)
                .Select(group => group.First())
                .OrderBy(
                    inventory => inventory.assetPath,
                    StringComparer.Ordinal)
                .ToList();
        }

        private static Dictionary<string, PrefabTimelineReferences>
            BuildPrefabTimelineMap(
                IReadOnlyDictionary<string, TimelineInventory> timelineByGuid)
        {
            var result =
                new Dictionary<string, PrefabTimelineReferences>(
                    StringComparer.Ordinal);
            foreach (var prefabGuid in AssetDatabase.FindAssets(
                "t:Prefab",
                new[] { AuthoredSceneRoot }))
            {
                var prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid);
                result[prefabGuid] = new PrefabTimelineReferences
                {
                    timelineGuids = FindPlayableDirectorTimelineGuids(prefabPath)
                        .Where(timelineByGuid.ContainsKey)
                        .Distinct(StringComparer.Ordinal)
                        .ToList(),
                    sourcePrefabGuids = FindSourcePrefabGuids(prefabPath)
                        .Distinct(StringComparer.Ordinal)
                        .ToList()
                };
            }

            return result;
        }

        private static IEnumerable<string> ResolvePrefabTimelineGuids(
            string prefabGuid,
            IReadOnlyDictionary<string, PrefabTimelineReferences> prefabTimelines,
            HashSet<string> visiting)
        {
            if (!visiting.Add(prefabGuid) ||
                !prefabTimelines.TryGetValue(
                    prefabGuid,
                    out var references))
            {
                yield break;
            }

            foreach (var timelineGuid in references.timelineGuids)
            {
                yield return timelineGuid;
            }

            foreach (var sourcePrefabGuid in references.sourcePrefabGuids)
            {
                foreach (var timelineGuid in ResolvePrefabTimelineGuids(
                    sourcePrefabGuid,
                    prefabTimelines,
                    visiting))
                {
                    yield return timelineGuid;
                }
            }

            visiting.Remove(prefabGuid);
        }

        private static DirectorDiagnostic BuildDirectorDiagnostic(
            UnityEngine.Playables.PlayableDirector director)
        {
            var timeline = (TimelineAsset)director.playableAsset;
            var tracks = timeline.GetOutputTracks().ToArray();
            return new DirectorDiagnostic
            {
                directorPath = GetHierarchyPath(director.transform),
                timelinePath = AssetDatabase.GetAssetPath(timeline),
                timelineName = timeline.name,
                duration = director.duration,
                animationTracks = tracks
                    .OfType<AnimationTrack>()
                    .Select(track => BuildAnimationTrackDiagnostic(director, track))
                    .ToList(),
                cameraTrackCount = tracks.OfType<CinemachineTrack>().Count(),
                shotCount = tracks
                    .OfType<CinemachineTrack>()
                    .SelectMany(track => track.GetClips())
                    .Count(clip => clip.asset is CinemachineShot),
                resolvedCameraCount = tracks
                    .OfType<CinemachineTrack>()
                    .SelectMany(track => track.GetClips())
                    .Select(clip => clip.asset as CinemachineShot)
                    .Count(shot =>
                        shot != null &&
                        shot.VirtualCamera.Resolve(director) != null),
                audioClipPaths = tracks
                    .OfType<AudioTrack>()
                    .SelectMany(track => track.GetClips())
                    .Select(clip => clip.asset as AudioPlayableAsset)
                    .Where(asset => asset?.clip != null)
                    .Select(asset => AssetDatabase.GetAssetPath(asset.clip))
                    .ToList()
            };
        }

        private static AnimationTrackDiagnostic BuildAnimationTrackDiagnostic(
            UnityEngine.Playables.PlayableDirector director,
            AnimationTrack track)
        {
            var binding = director.GetGenericBinding(track);
            var animator = binding as Animator;
            var clips = track.GetClips()
                .Select(clip => clip.asset as AnimationPlayableAsset)
                .Where(asset => asset?.clip != null)
                .Select(asset => asset.clip)
                .ToList();
            if (track.infiniteClip != null)
            {
                clips.Add(track.infiniteClip);
            }

            var transformPaths = clips
                .Distinct()
                .SelectMany(AnimationUtility.GetCurveBindings)
                .Where(bindingInfo => bindingInfo.type == typeof(Transform))
                .Select(bindingInfo => bindingInfo.path)
                .Distinct(StringComparer.Ordinal)
                .ToArray();
            return new AnimationTrackDiagnostic
            {
                trackName = track.name,
                muted = track.muted,
                bindingType = binding?.GetType().FullName ?? string.Empty,
                bindingName = binding is UnityEngine.Object unityObject
                    ? unityObject.name
                    : string.Empty,
                animatorPath = animator != null
                    ? GetHierarchyPath(animator.transform)
                    : string.Empty,
                animatorIsHuman = animator != null && animator.isHuman,
                animatorActive = animator != null &&
                    animator.gameObject.activeInHierarchy,
                avatarName = animator?.avatar != null
                    ? animator.avatar.name
                    : string.Empty,
                avatarIsValid = animator?.avatar != null &&
                    animator.avatar.isValid,
                transformCount = animator != null
                    ? animator.GetComponentsInChildren<Transform>(true).Length
                    : 0,
                animationClipCount = clips.Distinct().Count(),
                transformCurvePathCount = transformPaths.Length,
                firstTransformPaths = transformPaths.Take(12).ToList()
            };
        }

        private static IEnumerable<string> FindPlayableDirectorTimelineGuids(
            string scenePath)
        {
            var fullPath = GetFullAssetPath(scenePath);
            var isPlayableDirector = false;
            foreach (var line in File.ReadLines(fullPath))
            {
                if (line.StartsWith("--- !u!", StringComparison.Ordinal))
                {
                    isPlayableDirector = line.StartsWith(
                        "--- !u!320 ",
                        StringComparison.Ordinal);
                    continue;
                }

                if (!isPlayableDirector ||
                    line.IndexOf(
                        "m_PlayableAsset:",
                        StringComparison.Ordinal) < 0)
                {
                    continue;
                }

                const string guidMarker = "guid: ";
                var guidStart = line.IndexOf(
                    guidMarker,
                    StringComparison.Ordinal);
                if (guidStart < 0)
                {
                    continue;
                }

                guidStart += guidMarker.Length;
                if (line.Length >= guidStart + 32)
                {
                    yield return line.Substring(guidStart, 32);
                }
            }
        }

        private static IEnumerable<string> FindSourcePrefabGuids(
            string assetPath)
        {
            var fullPath = GetFullAssetPath(assetPath);
            foreach (var line in File.ReadLines(fullPath))
            {
                if (line.IndexOf(
                        "m_SourcePrefab:",
                        StringComparison.Ordinal) < 0)
                {
                    continue;
                }

                const string guidMarker = "guid: ";
                var guidStart = line.IndexOf(
                    guidMarker,
                    StringComparison.Ordinal);
                if (guidStart < 0)
                {
                    continue;
                }

                guidStart += guidMarker.Length;
                if (line.Length >= guidStart + 32)
                {
                    yield return line.Substring(guidStart, 32);
                }
            }
        }

        private static IEnumerable<string> FindReferencedGuids(
            string assetPath)
        {
            var fullPath = GetFullAssetPath(assetPath);
            const string guidMarker = "guid: ";
            foreach (var line in File.ReadLines(fullPath))
            {
                var searchStart = 0;
                while (searchStart < line.Length)
                {
                    var guidStart = line.IndexOf(
                        guidMarker,
                        searchStart,
                        StringComparison.Ordinal);
                    if (guidStart < 0)
                    {
                        break;
                    }

                    guidStart += guidMarker.Length;
                    if (line.Length >= guidStart + 32)
                    {
                        yield return line.Substring(guidStart, 32);
                    }

                    searchStart = guidStart + 32;
                }
            }
        }

        private static string GetFullAssetPath(string assetPath)
        {
            var projectRoot = Directory.GetParent(Application.dataPath)?.FullName
                ?? throw new InvalidOperationException(
                    "Unable to resolve the Unity project root.");
            return Path.GetFullPath(Path.Combine(projectRoot, assetPath));
        }

        private static void EnsureEditMode()
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
            {
                throw new InvalidOperationException(
                    "Camera dataset operations must run in Edit Mode.");
            }
        }

        private static void EnsureNoDirtyScenes()
        {
            if (PrefabStageUtility.GetCurrentPrefabStage() != null)
            {
                throw new InvalidOperationException(
                    "Close Prefab Mode before batch export.");
            }

            var dirtyScenePaths = Enumerable.Range(0, SceneManager.sceneCount)
                .Select(SceneManager.GetSceneAt)
                .Where(scene => scene.isDirty)
                .Select(scene => string.IsNullOrWhiteSpace(scene.path)
                    ? $"<untitled:{scene.name}>"
                    : scene.path)
                .ToArray();
            if (dirtyScenePaths.Length > 0)
            {
                throw new InvalidOperationException(
                    "Save or discard dirty scenes before batch export: " +
                    string.Join(", ", dirtyScenePaths));
            }
        }

        private static void RestoreSceneSetup(SceneSetup[] setup)
        {
            if (setup != null && setup.Length > 0)
            {
                EditorSceneManager.RestoreSceneManagerSetup(setup);
                return;
            }

            EditorSceneManager.NewScene(
                NewSceneSetup.DefaultGameObjects,
                NewSceneMode.Single);
        }

        private static string StableHash(string value)
        {
            using var sha256 = SHA256.Create();
            var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(value));
            return string.Concat(bytes
                .Take(4)
                .Select(item => item.ToString("x2")));
        }

        private static string SanitizeFileName(string value, int maximumLength)
        {
            var invalidCharacters = Path.GetInvalidFileNameChars();
            var sanitized = new string(value
                .Select(character => invalidCharacters.Contains(character)
                    ? '_'
                    : character)
                .ToArray())
                .Trim();
            if (string.IsNullOrWhiteSpace(sanitized))
            {
                sanitized = "Scene";
            }

            return sanitized.Length <= maximumLength
                ? sanitized
                : sanitized.Substring(0, maximumLength);
        }

        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 void WriteJsonAtomic<T>(string path, T value)
        {
            var directory = Path.GetDirectoryName(path);
            if (!string.IsNullOrWhiteSpace(directory))
            {
                Directory.CreateDirectory(directory);
            }

            var payload = JsonUtility.ToJson(value, true);
            const int maximumAttempts = 8;
            for (var attempt = 1; attempt <= maximumAttempts; attempt++)
            {
                var temporaryPath = $"{path}.tmp";
                try
                {
                    File.WriteAllText(
                        temporaryPath,
                        payload,
                        Encoding.UTF8);
                    if (File.Exists(path))
                    {
                        File.Replace(temporaryPath, path, null);
                    }
                    else
                    {
                        File.Move(temporaryPath, path);
                    }

                    return;
                }
                catch (IOException) when (attempt < maximumAttempts)
                {
                    System.Threading.Thread.Sleep(attempt * 125);
                }
                catch (UnauthorizedAccessException)
                    when (attempt < maximumAttempts)
                {
                    System.Threading.Thread.Sleep(attempt * 125);
                }
            }
        }

        [Serializable]
        private sealed class InventoryReport
        {
            public string schemaVersion;
            public string createdUtc;
            public string unityVersion;
            public string assetRoot;
            public int candidateSceneCount;
            public int uniqueTimelineCount;
            public List<SceneCandidate> candidates;
        }

        [Serializable]
        private sealed class ActiveSceneDiagnosticReport
        {
            public string schemaVersion;
            public string createdUtc;
            public string scenePath;
            public List<DirectorDiagnostic> directors;
        }

        [Serializable]
        private sealed class DirectorDiagnostic
        {
            public string directorPath;
            public string timelinePath;
            public string timelineName;
            public double duration;
            public List<AnimationTrackDiagnostic> animationTracks;
            public int cameraTrackCount;
            public int shotCount;
            public int resolvedCameraCount;
            public List<string> audioClipPaths;
        }

        [Serializable]
        private sealed class AnimationTrackDiagnostic
        {
            public string trackName;
            public bool muted;
            public string bindingType;
            public string bindingName;
            public string animatorPath;
            public bool animatorIsHuman;
            public bool animatorActive;
            public string avatarName;
            public bool avatarIsValid;
            public int transformCount;
            public int animationClipCount;
            public int transformCurvePathCount;
            public List<string> firstTransformPaths;
        }

        [Serializable]
        private sealed class SceneCandidate
        {
            public string scenePath;
            public List<TimelineInventory> timelines;
        }

        [Serializable]
        private sealed class TimelineInventory
        {
            public string assetPath;
            public string timelineName;
            public double duration;
            public int cameraTrackCount;
            public int shotCount;
            public int animationTrackCount;
            public int audioTrackCount;
            public int audioClipCount;
        }

        private sealed class PrefabTimelineReferences
        {
            public List<string> timelineGuids;
            public List<string> sourcePrefabGuids;
        }

        [Serializable]
        private sealed class BatchReport
        {
            public string schemaVersion;
            public string startedUtc;
            public string lastUpdatedUtc;
            public string completedUtc;
            public string state;
            public string unityVersion;
            public string assetRoot;
            public string outputRoot;
            public int candidateSceneCount;
            public string candidateInventoryFingerprint;
            public int startIndex;
            public int endExclusive;
            public bool requireAudio;
            public int processedSceneCount;
            public List<SceneExportResult> results;
            public string restoreError;
            public string fatalError;
        }

        [Serializable]
        private sealed class SceneExportResult
        {
            public int candidateIndex;
            public string scenePath;
            public List<string> timelinePaths;
            public string outputPath;
            public string status;
            public string startedUtc;
            public string completedUtc;
            public double elapsedSeconds;
            public string error;
            public string partialOutputPath;
            public int discoveredCameraTrackCount;
            public int exportedCameraTrackCount;
            public int skippedCameraTrackCount;
        }

        [Serializable]
        private sealed class DatasetManifestProbe
        {
            public string schemaVersion;
            public string scenePath;
            public string sourceFingerprint;
            public int sampleRate;
            public int discoveredCameraTrackCount;
            public int exportedCameraTrackCount;
            public List<SkippedCameraTrackProbe> skippedCameraTracks;
            public List<SongProbe> songs;
        }

        [Serializable]
        private sealed class SkippedCameraTrackProbe
        {
            public string timelineAssetPath;
            public string trackName;
            public bool muted;
            public string reason;
        }

        [Serializable]
        private sealed class SongProbe
        {
            public string folderName;
            public int frameCount;
            public int sharedBoneCount;
            public int shotCount;
            public int missingCameraFrames;
            public int missingJointSamples;
            public int missingRootSamples;
            public string cameraTrackName;
            public string motionTimelineAssetPath;
            public string audioFile;
            public string jointsFile;
            public string cameraFile;
            public string rootFile;
            public string shotIndexFile;
            public string timeFile;
            public string shotsFile;
            public string previewFile;
        }
    }
}
