using System;
using System.Collections.Generic;
using Intrepid.Diagnostics;
using Intrepid.Utilities;
using Sirenix.OdinInspector;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace Intrepid.Gameplay.Journey
{
    [Serializable]
    [CreateAssetMenu(fileName = "Journey Service", menuName = "GON/Services/Journey Service")]
    public class JourneyService : ScriptableObject
    {
        [GroupInjections, SerializeField] 
        private JGraphSO journeyGraph;

        [GroupInjections, SerializeField, ReadOnly, ListDrawerSettings(ShowFoldout = false)]
        private List<StageTag> stagesTags = new();

        private Dictionary<StageTag, JStage> StageMap { get; } = new();

        private Dictionary<string, StageTag> StageTagMap { get; } = new();

        private Dictionary<string, JEndpoint> EndpointMap { get; } = new();

        private Dictionary<string, StageTag> EndpointStageTagMap { get; } = new();

        private Dictionary<string, Dictionary<string, List<JConnection>>> StageConnections { get; } = new();

#if UNITY_EDITOR
        [PropertyOrder(-10)]
        [GroupInjections, Button(ButtonSizes.Large), GUIColor(0.35f, 0.8f, .35f)]
        protected void FindAllStageTags()
        {
            stagesTags ??= new();
            stagesTags.Clear();

            var guids = AssetDatabase.FindAssets("t:StageTag");

            foreach (var guid in guids)
            {
                var path = AssetDatabase.GUIDToAssetPath(guid);
                var stageTag = AssetDatabase.LoadAssetAtPath<StageTag>(path);

                if (stageTag.IsNull())
                {
                    continue;
                }

                stagesTags.Add(stageTag);
            }

            EditorUtility.SetDirty(this);
        }
#endif

        [GroupDebug, Button(ButtonSizes.Medium)]
        private void ValidateStages()
        {
            if (journeyGraph.IsNull())
            {
                DLogger.LogError($"{nameof(journeyGraph)} should not be null or empty");
                return;
            }

            if (stagesTags.IsNull() || stagesTags.Count == 0)
            {
                DLogger.LogError($"{nameof(stagesTags)} should not be null or empty");
                return;
            }

            var stageTagMap = BuildStageTagMap();

            foreach (var jStage in journeyGraph.stages)
            {
                if (!stageTagMap.TryGetValue(jStage.id, out var tag))
                {
                    DLogger.LogDebug($"Stage {jStage.id} is not found in the stage tag list");
                    continue;
                }

                tag.TestIfExists();
            }
        }

        public Dictionary<string, StageTag>.ValueCollection Initialize()
        {
            if (journeyGraph.IsNull())
            {
                throw new NullReferenceException($"{nameof(journeyGraph)} should not be null or empty");
            }

            ClearRuntimeCache();

            BuildStageTagCache();
            BuildEndpointCache();
            BuildStageConnectionCache();
            BuildStageMapCache();

            return StageTagMap.Values;
        }

        private void ClearRuntimeCache()
        {
            StageMap.Clear();
            StageTagMap.Clear();
            EndpointMap.Clear();
            EndpointStageTagMap.Clear();
            StageConnections.Clear();
        }

        private Dictionary<string, StageTag> BuildStageTagMap()
        {
            var map = new Dictionary<string, StageTag>();

            if (stagesTags.IsNull())
            {
                return map;
            }

            foreach (var stageTag in stagesTags)
            {
                if (stageTag.IsNull())
                {
                    continue;
                }

                if (string.IsNullOrWhiteSpace(stageTag.StageId))
                {
                    DLogger.LogWarn($"StageTag {stageTag.name} has null or empty StageId");
                    continue;
                }

                if (!map.TryAdd(stageTag.StageId, stageTag))
                {
                    DLogger.LogWarn($"Duplicated StageTag for StageId {stageTag.StageId}");
                }
            }

            return map;
        }

        private void BuildStageTagCache()
        {
            var map = BuildStageTagMap();

            foreach (var pair in map)
            {
                StageTagMap.Add(pair.Key, pair.Value);
            }
        }

        private void BuildEndpointCache()
        {
            foreach (var endpoint in journeyGraph.endpoints)
            {
                if (endpoint.IsNull())
                {
                    continue;
                }

                if (string.IsNullOrWhiteSpace(endpoint.id))
                {
                    DLogger.LogWarn("Endpoint with null or empty id found");
                    continue;
                }

                if (!EndpointMap.TryAdd(endpoint.id, endpoint))
                {
                    DLogger.LogWarn($"Duplicated endpoint id {endpoint.id}");
                    continue;
                }

                EnsureStageConnectionBucket(endpoint.stageId, endpoint.id);

                if (StageTagMap.TryGetValue(endpoint.stageId, out var stageTag))
                {
                    EndpointStageTagMap.TryAdd(endpoint.id, stageTag);
                }
                else
                {
                    DLogger.LogWarn($"Endpoint {endpoint.id} references stage {endpoint.stageId}, but no StageTag was found");
                }
            }
        }

        private void BuildStageConnectionCache()
        {
            foreach (var connection in journeyGraph.connections)
            {
                if (connection.IsNull())
                {
                    continue;
                }

                if (!EndpointMap.TryGetValue(connection.endpointAId, out var endpointA))
                {
                    DLogger.LogWarn($"Connection {connection.id} has invalid endpoint A: {connection.endpointAId}");
                    continue;
                }

                if (!EndpointMap.TryGetValue(connection.endpointBId, out var endpointB))
                {
                    DLogger.LogWarn($"Connection {connection.id} has invalid endpoint B: {connection.endpointBId}");
                    continue;
                }

                StageConnections[endpointA.stageId][endpointA.id].Add(connection);
                StageConnections[endpointB.stageId][endpointB.id].Add(connection);
            }
        }

        private void BuildStageMapCache()
        {
            foreach (var jStage in journeyGraph.stages)
            {
                if (jStage.IsNull())
                {
                    continue;
                }

                if (StageTagMap.TryGetValue(jStage.id, out var tag))
                {
                    StageMap.TryAdd(tag, jStage);
                }
                else
                {
                    DLogger.LogWarn($"Stage {jStage.id} is not found in the stage tag list");
                }
            }
        }

        private void EnsureStageConnectionBucket(string stageId, string endpointId)
        {
            if (!StageConnections.TryGetValue(stageId, out var stageConnections))
            {
                stageConnections = new Dictionary<string, List<JConnection>>();
                StageConnections[stageId] = stageConnections;
            }

            stageConnections.TryAdd(endpointId, new List<JConnection>());
        }

        public bool TryGetJConnections(
            string stageId,
            string endpointId,
            out IReadOnlyList<JConnection> jConnections)
        {
            if (StageConnections.TryGetValue(stageId, out var stageConnections) &&
                stageConnections.TryGetValue(endpointId, out var connections))
            {
                jConnections = connections;
                return true;
            }

            jConnections = Array.Empty<JConnection>();
            return false;
        }

        public bool TryGetStageTagFromEndpoint(string endpointId, out StageTag stageTag)
        {
            return EndpointStageTagMap.TryGetValue(endpointId, out stageTag);
        }

        public bool TryGetEndpoint(string endpointId, out JEndpoint endpoint)
        {
            return EndpointMap.TryGetValue(endpointId, out endpoint);
        }

        public bool TryGetJStage(StageTag stageTag, out JStage jStage)
        {
            return StageMap.TryGetValue(stageTag, out jStage);
        }
    }
}