// Assets/_Project/Scripts/Editor/Levels/LevelAuthoringPlayModeHook.cs
using UnityEditor;
using UnityEngine;
using TD.Levels;

namespace TD.Levels.Editor
{
    /// <summary>
    /// Static editor-only hook that intercepts Play mode entry and validates the
    /// scene's LevelAuthoring against its baked LevelData. Catches the common
    /// failure mode of "edited the scene, forgot to bake, played stale data".
    ///
    /// Decision tree (executes in PlayModeStateChange.ExitingEditMode, before
    /// Play actually begins, so we can abort cleanly):
    ///   1. No LevelAuthoring in scene  -> proceed silently
    ///   2. Multiple LevelAuthoring     -> error dialog, abort
    ///   3. targetAsset null            -> 2-button "Continue Anyway / Cancel"
    ///   4. AuthoringHash empty         -> 2-button "Bake &amp; Play / Cancel"
    ///   5. Hashes match                -> proceed silently
    ///   6. Hashes differ               -> 3-button "Bake &amp; Play / Play Anyway / Cancel"
    ///
    /// Aborts via EditorApplication.isPlaying = false during ExitingEditMode.
    /// During this window EditorApplication.isPlayingOrWillChangePlaymode is
    /// true but isPlaying is false; setting isPlaying = false at this point
    /// cancels the Play attempt cleanly.
    /// </summary>
    [InitializeOnLoad]
    internal static class LevelAuthoringPlayModeHook
    {
        // Static constructor runs on every editor reload (script recompile or
        // domain reload), wiring the event handler. It's idempotent because
        // we always remove first (no-op if not subscribed) then add.
        static LevelAuthoringPlayModeHook()
        {
            EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
            EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
        }

        private static void OnPlayModeStateChanged(PlayModeStateChange state)
        {
            // We only intercept the moment we're about to enter Play mode.
            // Other phases (EnteredPlayMode, ExitingPlayMode, EnteredEditMode)
            // are not our concern.
            if (state != PlayModeStateChange.ExitingEditMode)
                return;

            // Find LevelAuthoring instances in the scene. We only consider
            // active ones; an inactive LevelAuthoring is treated as
            // "not present" because its volumes won't be discovered by the
            // bake's scoped scan either.
            //
            // FindObjectsByType is the Unity 6+ replacement for the deprecated
            // FindObjectsOfType. We use the no-sort-mode overload: in Unity 6.4+
            // the FindObjectsSortMode parameter has been deprecated (Unity is
            // moving from InstanceID toward EntityId, and stable sort order
            // can't be guaranteed in the new world). Passing it now produces
            // a CS0618 warning. We don't need stable order here -- a single
            // expected result, or zero, or "more than one" handled as an error.
            var authorings = Object.FindObjectsByType<LevelAuthoring>(
                FindObjectsInactive.Exclude);

            // Case 1: No LevelAuthoring in scene -> proceed silently.
            // This is the normal case for menu scenes, bootstrap scenes, etc.
            if (authorings.Length == 0)
                return;

            // Case 2: Multiple LevelAuthoring -> error and abort.
            // The bake assumes one per scene (it walks _LevelAuthoring's
            // subtree), so playing with two would be ambiguous.
            if (authorings.Length > 1)
            {
                EditorUtility.DisplayDialog(
                    "Multiple LevelAuthoring components",
                    $"Found {authorings.Length} LevelAuthoring components in this scene. " +
                    "Only one is allowed per scene. Remove the extras before entering Play mode.",
                    "OK");
                AbortPlay();
                return;
            }

            var authoring = authorings[0];

            // Case 3: targetAsset is null. We can't dirty-check without one,
            // because there's no LevelData to compare the current scene's
            // hash against. Ask the user if they want to play anyway.
            if (authoring.targetAsset == null)
            {
                bool proceed = EditorUtility.DisplayDialog(
                    "No LevelData target assigned",
                    $"The LevelAuthoring on '{authoring.gameObject.name}' has no targetAsset assigned, " +
                    "so its scene state cannot be checked against baked data.\n\n" +
                    "Continue into Play mode anyway?",
                    "Continue Anyway",
                    "Cancel");

                if (!proceed)
                    AbortPlay();
                return;
            }

            // Case 4: Asset has never been baked (hash empty/null).
            // Offer Bake & Play, since there's no useful "play with stale data"
            // option when there's no baked data at all.
            string bakedHash = authoring.targetAsset.AuthoringHash;
            if (string.IsNullOrEmpty(bakedHash))
            {
                bool bakeAndPlay = EditorUtility.DisplayDialog(
                    "Level has never been baked",
                    $"'{authoring.targetAsset.name}' has no baked data yet. " +
                    "Bake now and then enter Play mode?",
                    "Bake && Play",
                    "Cancel");

                if (!bakeAndPlay)
                {
                    AbortPlay();
                    return;
                }

                // Lean A: if Bake & Play is requested and the bake fails,
                // abort Play. The bake errors will already be in the console.
                bool bakeOk = LevelBakePipeline.Bake(authoring);
                if (!bakeOk)
                {
                    Debug.LogError("[PlayModeHook] Bake failed; aborting Play. See above for errors.");
                    AbortPlay();
                }
                return;
            }

            // Compute the hash of the current scene state and compare to baked.
            // ComputeAuthoringHash(LevelAuthoring) is a pure function over the
            // same canonical input string the bake uses (it delegates to the
            // same private hash routine), so equal strings means equivalent
            // bakes.
            string currentHash = LevelBakePipeline.ComputeAuthoringHash(authoring);

            // Case 5: Hashes match -> baked data is in sync with the scene.
            // Proceed silently; this is the happy path.
            if (currentHash == bakedHash)
                return;

            // Case 6: Hashes differ -> 3-button dialog with the explicit
            // "Play Anyway" escape hatch. DisplayDialogComplex returns:
            //   0 = primary button (Bake & Play)
            //   1 = "alt" button    (Cancel)
            //   2 = secondary       (Play Anyway)
            // The button-to-return-code mapping is fixed by Unity's API, so
            // we have to map our intent onto it carefully.
            int choice = EditorUtility.DisplayDialogComplex(
                "Scene differs from baked data",
                $"The scene has been edited since '{authoring.targetAsset.name}' was last baked. " +
                "Playing now will use stale baked data.\n\n" +
                "Bake the level before playing, play with stale data anyway, or cancel?",
                "Bake && Play", // ok / button 0
                "Cancel",       // alt / button 1
                "Play Anyway"); // secondary / button 2

            switch (choice)
            {
                case 0: // Bake & Play
                    bool ok = LevelBakePipeline.Bake(authoring);
                    if (!ok)
                    {
                        Debug.LogError("[PlayModeHook] Bake failed; aborting Play. See above for errors.");
                        AbortPlay();
                    }
                    return;

                case 2: // Play Anyway
                    // Lean C: log a console warning so debugging weirdness
                    // later has a breadcrumb. Don't persist anything; the
                    // user explicitly chose this.
                    Debug.LogWarning(
                        $"[PlayModeHook] Entering Play mode with stale baked data for " +
                        $"'{authoring.targetAsset.name}'. Last bake: {authoring.targetAsset.LastBakeTimestamp}.");
                    return;

                case 1: // Cancel
                default:
                    AbortPlay();
                    return;
            }
        }

        /// <summary>
        /// Cancels the Play mode transition. Must be called during
        /// ExitingEditMode for the cancel to take effect cleanly.
        /// </summary>
        private static void AbortPlay()
        {
            EditorApplication.isPlaying = false;
        }
    }
}