using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Cysharp.Threading.Tasks;
using Darkmatter.Core.Contracts.Services.Audio;
using Darkmatter.Core.Data.Dynamic.Services.Audio;
using Darkmatter.Core.Enums.Services.Audio;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.Networking;
using UnityEngine.Serialization;

namespace Darkmatter.Services.Audio
{
    public class AudioService : MonoBehaviour, IAudioService
    {

        [SerializeField] private int pooledSourceCount = 32;
        [SerializeField] private Transform sourceRoot;
        [SerializeField] private AudioListener listener;

        [Tooltip(
            "Max simultaneously playing sources on AISfx channel. <=0 disables cap. Excess plays steal the farthest (or oldest) AISfx source.")]
        [SerializeField, Min(0)]
        private int maxAISfxInstances = 16;

        [Header("Audio Mixer")] [SerializeField]
        private AudioMixer audioMixer;

        [SerializeField] private AudioMixerSnapshot[] snapshots;

        [SerializeField, Min(0f), FormerlySerializedAs("cameraFxTransitionSeconds")]
        private float snapshotTransitionDuration = 0.35f;

        [SerializeField, FormerlySerializedAs("cameraFxEasing")]
        private AnimationCurve snapshotEasing = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);

        [Header("Audio Mixer Groups")] [SerializeField]
        private AudioMixerGroup sfxMixerGroup;

        [SerializeField] private AudioMixerGroup vehicleSfxMixerGroup;
        [SerializeField] private AudioMixerGroup engineMixerGroup;
        [SerializeField] private AudioMixerGroup aiSfx;
        [SerializeField] private AudioMixerGroup passengerChatterMixerGroup;
        [SerializeField] private AudioMixerGroup musicMixerGroup;
        [SerializeField] private AudioMixerGroup ambienceMixerGroup;
        [SerializeField] private AudioMixerGroup weatherMixerGroup;
        [SerializeField] private AudioMixerGroup bgmMixerGroup;
        [SerializeField] private AudioMixerGroup uiMixerGroup;
        [SerializeField] private AudioMixerGroup radioMixerGroup;
        [SerializeField] private AudioMixerGroup vehicleBrakeGroup;

        private class SourceContext
        {
            public AudioSource Source;
            public ushort Generation;
            public AudioChannel Channel;
            public bool PausedByService;
            public float BaseVolume = 1f;
            public float StartTime;
        }

        private readonly List<SourceContext> _sources = new();
        private readonly HashSet<AudioChannel> _pausedChannels = new();
        private readonly Dictionary<AudioChannel, float> _channelVolumes = new();

        private AudioListener _cachedListener;

        private CancellationTokenSource _lifetimeCts;
        private CancellationTokenSource _snapshotBlendCts;
        private Dictionary<AudioChannel, AudioMixerGroup> _mixerGroups;
        private float[] _currentWeights;

        public UniTask InitializeAsync(CancellationToken cancellationToken)
        {
            if (sourceRoot == null) sourceRoot = transform;
            _lifetimeCts = new CancellationTokenSource();

            InitializeChannelVolumes();
            ResolveAudioMixer();
            InitializeMixerGroups();
            InitializeSnapshots();
            if (HasSnapshots()) ApplyWeights(_currentWeights);

            for (int i = 0; i < pooledSourceCount; i++)
            {
                if (cancellationToken.IsCancellationRequested)
                    break;
                ExpandPool();
            }

            return UniTask.CompletedTask;
        }

        public async UniTask<AudioClip> LoadClipFromPath(string path, CancellationToken cancellationToken)
        {
            if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
            {
                Debug.LogWarning($"[AudioService] File not found: {path}");
                return null;
            }

            string url = "file://" + path;
            var audioType = GetAudioType(path);

            using var request = UnityWebRequestMultimedia.GetAudioClip(url, audioType);
            if (request.downloadHandler is DownloadHandlerAudioClip handler)
            {
                handler.streamAudio = true;
            }

            var operation = request.SendWebRequest();

            while (!operation.isDone && !cancellationToken.IsCancellationRequested)
            {
                await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
            }

            if (cancellationToken.IsCancellationRequested)
            {
                throw new OperationCanceledException(cancellationToken);
            }

            if (request.result is UnityWebRequest.Result.ConnectionError or UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError($"[AudioService] Error loading: {request.error} ({path})");
                return null;
            }

            AudioClip clip = DownloadHandlerAudioClip.GetContent(request);
            if (clip == null)
            {
                Debug.LogError($"[AudioService] Failed to decode audio clip: {path}");
                return null;
            }

            clip.name = Path.GetFileNameWithoutExtension(path);

            return clip;
        }

        private static AudioType GetAudioType(string path)
        {
            return Path.GetExtension(path).ToLowerInvariant() switch
            {
                ".wav" => AudioType.WAV,
                ".ogg" => AudioType.OGGVORBIS,
                _ => AudioType.MPEG
            };
        }

        private void InitializeChannelVolumes()
        {
            foreach (AudioChannel channel in Enum.GetValues(typeof(AudioChannel)))
            {
                if (!_channelVolumes.ContainsKey(channel))
                {
                    _channelVolumes[channel] = 1f;
                }
            }
        }

        private void InitializeMixerGroups()
        {
            sfxMixerGroup = ResolveMixerGroup(sfxMixerGroup, "SFX", null);
            vehicleSfxMixerGroup = ResolveMixerGroup(vehicleSfxMixerGroup, "Vehicle SFX", sfxMixerGroup);
            engineMixerGroup = ResolveMixerGroup(engineMixerGroup, "Engine", sfxMixerGroup);
            passengerChatterMixerGroup =
                ResolveMixerGroup(passengerChatterMixerGroup, "Passenger Chatter", sfxMixerGroup);
            aiSfx = ResolveMixerGroup(aiSfx, "AI SFX", sfxMixerGroup);
            musicMixerGroup = ResolveMixerGroup(musicMixerGroup, "Music", null);
            ambienceMixerGroup = ResolveMixerGroup(ambienceMixerGroup, "Ambience", null);
            weatherMixerGroup = ResolveMixerGroup(weatherMixerGroup, "Weather", ambienceMixerGroup);
            bgmMixerGroup = ResolveMixerGroup(bgmMixerGroup, "BGM", null);
            uiMixerGroup = ResolveMixerGroup(uiMixerGroup, "UI", null);
            radioMixerGroup = ResolveMixerGroup(radioMixerGroup, "Radio", null);
            vehicleBrakeGroup = ResolveMixerGroup(vehicleBrakeGroup, "Vehicle Brake", sfxMixerGroup);

            _mixerGroups = new Dictionary<AudioChannel, AudioMixerGroup>
            {
                { AudioChannel.Sfx, sfxMixerGroup },
                { AudioChannel.VehicleSfx, vehicleSfxMixerGroup },
                { AudioChannel.Engine, engineMixerGroup },
                { AudioChannel.AISfx, aiSfx },
                { AudioChannel.PassengerChatter, passengerChatterMixerGroup },
                { AudioChannel.Music, musicMixerGroup },
                { AudioChannel.Ambience, ambienceMixerGroup },
                { AudioChannel.Weather, weatherMixerGroup },
                { AudioChannel.BGM, bgmMixerGroup },
                { AudioChannel.UI, uiMixerGroup },
                { AudioChannel.Radio, radioMixerGroup },
                { AudioChannel.VehicleBrake, vehicleBrakeGroup }
            };
        }

        private AudioMixer ResolveAudioMixer()
        {
            if (audioMixer != null)
            {
                return audioMixer;
            }

            audioMixer = ResolveAudioMixerFromGroup(sfxMixerGroup)
                         ?? ResolveAudioMixerFromGroup(vehicleSfxMixerGroup)
                         ?? ResolveAudioMixerFromGroup(engineMixerGroup)
                         ?? ResolveAudioMixerFromGroup(passengerChatterMixerGroup)
                         ?? ResolveAudioMixerFromGroup(musicMixerGroup)
                         ?? ResolveAudioMixerFromGroup(ambienceMixerGroup)
                         ?? ResolveAudioMixerFromGroup(weatherMixerGroup)
                         ?? ResolveAudioMixerFromGroup(bgmMixerGroup)
                         ?? ResolveAudioMixerFromGroup(uiMixerGroup);

            return audioMixer;
        }

        private static AudioMixer ResolveAudioMixerFromGroup(AudioMixerGroup group)
        {
            return group != null ? group.audioMixer : null;
        }

        private AudioMixerGroup ResolveMixerGroup(AudioMixerGroup configuredGroup, string groupName,
            AudioMixerGroup fallback)
        {
            if (configuredGroup != null)
            {
                return configuredGroup;
            }

            AudioMixer mixer = ResolveAudioMixer();
            if (mixer == null)
            {
                return fallback;
            }

            AudioMixerGroup[] matches = mixer.FindMatchingGroups(groupName);
            for (int i = 0; i < matches.Length; i++)
            {
                if (matches[i] != null && matches[i].name == groupName)
                {
                    return matches[i];
                }
            }

            return matches.Length > 0 ? matches[0] : fallback;
        }

        private void InitializeSnapshots()
        {
            if (snapshots != null && snapshots.Length > 0 && _currentWeights == null)
            {
                _currentWeights = new float[snapshots.Length];
                _currentWeights[0] = 1f;
            }
        }

        private void ExpandPool()
        {
            var go = new GameObject($"AudioSource_{_sources.Count}");
            go.transform.SetParent(sourceRoot);
            var source = go.AddComponent<AudioSource>();
            source.playOnAwake = false;
            source.dopplerLevel = 0f;

            var context = new SourceContext
            {
                Source = source,
                Generation = 0
            };
            _sources.Add(context);
        }

        public AudioHandle Play(in AudioRequest request)
        {
            if (request.StopChannelBeforePlay)
            {
                StopChannel(request.Channel);
            }

            var index = -1;
            if (HasInstanceCap(request.Channel, out int cap))
            {
                if (TryStealCappedChannelSource(request, cap, out int stolenIndex, out bool denyFarther))
                {
                    index = stolenIndex;
                }
                else if (denyFarther)
                {
                    return AudioHandle.Invalid;
                }
            }

            if (index == -1)
            {
                for (int i = 0; i < _sources.Count; i++)
                {
                    if (!_sources[i].Source.isPlaying && !_sources[i].PausedByService)
                    {
                        index = i;
                        break;
                    }
                }

                if (index == -1)
                {
                    ExpandPool();
                    index = _sources.Count - 1;
                }
            }

            var context = _sources[index];

            unchecked
            {
                context.Generation++;
            }

            if (context.Generation == 0) context.Generation = 1;

            context.Channel = request.Channel;
            context.BaseVolume = request.Volume01;
            context.PausedByService = false;
            context.StartTime = Time.unscaledTime;
            var source = context.Source;

            source.clip = request.Clip;
            source.outputAudioMixerGroup =
                _mixerGroups != null && _mixerGroups.TryGetValue(request.Channel, out var mixerGroup)
                    ? mixerGroup
                    : null;
            source.volume = CalculateSourceVolume(context);
            source.pitch = request.Pitch;
            source.spatialBlend = request.Spatial3D ? 1f : 0f;
            source.minDistance = request.MinDistance;
            source.maxDistance = Mathf.Max(request.MinDistance + 0.01f, request.MaxDistance);
            source.loop = request.PlayMode == AudioPlayMode.Loop;

            source.transform.localPosition = Vector3.zero;

            source.Play();
            if (IsChannelPaused(request.Channel))
            {
                source.Pause();
                context.PausedByService = true;
            }

            if (request.FollowTarget != null && _lifetimeCts != null)
            {
                FollowAudioTargetAsync(context, request.FollowTarget, context.Generation, _lifetimeCts.Token).Forget();
            }

            return new AudioHandle(index + 1, context.Generation);
        }

        public void Stop(AudioHandle handle, float fadeOutSeconds = 0)
        {
            if (TryGetSourceContext(handle, out var context))
            {
                context.PausedByService = false;
                if (fadeOutSeconds > 0 && _lifetimeCts != null)
                {
                    FadeAndStopAsync(context, fadeOutSeconds, handle.Generation, _lifetimeCts.Token).Forget();
                }
                else
                {
                    context.PausedByService = false;
                    context.Source.Stop();
                }
            }
        }

        public void SetPitch(AudioHandle handle, float pitch)
        {
            if (TryGetSource(handle, out var source))
            {
                source.pitch = pitch;
            }
        }

        public void SetVolume(AudioHandle handle, float volume01)
        {
            if (TryGetSourceContext(handle, out var context))
            {
                context.BaseVolume = volume01;
                context.Source.volume = CalculateSourceVolume(context);
            }
        }

        public bool IsPlaying(AudioHandle handle)
        {
            return TryGetSourceContext(handle, out var context) &&
                   (context.Source.isPlaying || context.PausedByService);
        }

        public void SetChannelPaused(AudioChannel channel, bool paused)
        {
            if (paused)
            {
                if (!_pausedChannels.Add(channel))
                {
                    return;
                }

                foreach (var ctx in _sources)
                {
                    if (!PauseChannelAffects(channel, ctx.Channel) || !ctx.Source.isPlaying)
                    {
                        continue;
                    }

                    ctx.Source.Pause();
                    ctx.PausedByService = true;
                }

                return;
            }

            if (!_pausedChannels.Remove(channel))
            {
                return;
            }

            foreach (var ctx in _sources)
            {
                if (!ctx.PausedByService || IsChannelPaused(ctx.Channel))
                {
                    continue;
                }

                ctx.PausedByService = false;
                ctx.Source.UnPause();
            }
        }

        public void SetChannelVolume(AudioChannel channel, float volume01)
        {
            volume01 = Mathf.Clamp01(volume01);
            _channelVolumes[channel] = volume01;

            if (_mixerGroups != null && _mixerGroups.TryGetValue(channel, out var mixerGroup) && mixerGroup != null)
            {
                float dB = volume01 > 0 ? Mathf.Log10(volume01) * 20f : -80f;
                mixerGroup.audioMixer.SetFloat($"{channel}Volume", dB);
            }

            foreach (var ctx in _sources)
            {
                if (ctx.Source != null && VolumeChannelAffects(channel, ctx.Channel))
                {
                    ctx.Source.volume = CalculateSourceVolume(ctx);
                }
            }
        }

        public void SetSnapshotWeights(params float[] weights)
        {
            if (!HasSnapshots()) return;

            int count = snapshots.Length;
            var target = new float[count];
            for (int i = 0; i < count; i++)
                target[i] = i < weights.Length ? weights[i] : 0f;

            _snapshotBlendCts?.Cancel();
            _snapshotBlendCts?.Dispose();
            _snapshotBlendCts = new CancellationTokenSource();

            if (snapshotTransitionDuration <= 0f)
            {
                ApplyWeights(target);
                return;
            }

            BlendSnapshotsAsync(target, _snapshotBlendCts.Token).Forget();
        }

        public void TransitionSnapshotWeights(float duration, params float[] weights)
        {
            if (!HasSnapshots()) return;

            _snapshotBlendCts?.Cancel();
            _snapshotBlendCts?.Dispose();
            _snapshotBlendCts = null;

            int count = snapshots.Length;
            var target = new float[count];
            for (int i = 0; i < count; i++)
                target[i] = i < weights.Length ? weights[i] : 0f;

            Array.Copy(target, _currentWeights, _currentWeights.Length);
            audioMixer.TransitionToSnapshots(snapshots, target, Mathf.Max(0f, duration));
        }

        private bool HasSnapshots() => audioMixer != null && snapshots is { Length: > 0 };

        private async UniTaskVoid BlendSnapshotsAsync(float[] targetWeights, CancellationToken cancellationToken)
        {
            float[] startWeights = (float[])_currentWeights.Clone();
            float elapsed = 0f;

            try
            {
                while (elapsed < snapshotTransitionDuration)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    elapsed += Time.deltaTime;
                    float t = Mathf.Clamp01(elapsed / snapshotTransitionDuration);
                    float easedT = snapshotEasing != null ? snapshotEasing.Evaluate(t) : t;

                    for (int i = 0; i < _currentWeights.Length; i++)
                        _currentWeights[i] = Mathf.Lerp(startWeights[i], targetWeights[i], easedT);

                    audioMixer.TransitionToSnapshots(snapshots, _currentWeights, 0f);
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                ApplyWeights(targetWeights);
            }
            catch (OperationCanceledException)
            {
            }
        }

        private void ApplyWeights(float[] weights)
        {
            if (!HasSnapshots()) return;
            Array.Copy(weights, _currentWeights, _currentWeights.Length);
            audioMixer.TransitionToSnapshots(snapshots, _currentWeights, 0f);
        }

        private float CalculateSourceVolume(SourceContext context)
        {
            return context.BaseVolume * GetEffectiveChannelVolume(context.Channel);
        }

        private float GetEffectiveChannelVolume(AudioChannel channel)
        {
            float volume = GetStoredChannelVolume(channel);

            if (IsSfxChildChannel(channel))
            {
                volume *= GetStoredChannelVolume(AudioChannel.Sfx);
            }
            else if (channel == AudioChannel.Weather)
            {
                volume *= GetStoredChannelVolume(AudioChannel.Ambience);
            }

            return volume;
        }

        private float GetStoredChannelVolume(AudioChannel channel)
        {
            return _channelVolumes.TryGetValue(channel, out float volume) ? volume : 1f;
        }

        private static bool VolumeChannelAffects(AudioChannel changedChannel, AudioChannel sourceChannel)
        {
            return changedChannel == sourceChannel ||
                   changedChannel == AudioChannel.Sfx && IsSfxChildChannel(sourceChannel) ||
                   changedChannel == AudioChannel.Ambience && sourceChannel == AudioChannel.Weather;
        }

        private bool IsChannelPaused(AudioChannel channel)
        {
            return _pausedChannels.Contains(channel) ||
                   _pausedChannels.Contains(AudioChannel.Sfx) && IsSfxChildChannel(channel) ||
                   _pausedChannels.Contains(AudioChannel.Ambience) && channel == AudioChannel.Weather;
        }

        private static bool PauseChannelAffects(AudioChannel changedChannel, AudioChannel sourceChannel)
        {
            return changedChannel == sourceChannel ||
                   changedChannel == AudioChannel.Sfx && IsSfxChildChannel(sourceChannel) ||
                   changedChannel == AudioChannel.Ambience && sourceChannel == AudioChannel.Weather;
        }

        private static bool IsSfxChildChannel(AudioChannel channel)
        {
            return channel is AudioChannel.VehicleSfx or AudioChannel.Engine or AudioChannel.PassengerChatter;
        }

        private AudioListener GetActiveListener()
        {
            if (_cachedListener != null && _cachedListener.isActiveAndEnabled) return _cachedListener;
            if (listener != null && listener.isActiveAndEnabled)
            {
                _cachedListener = listener;
                return _cachedListener;
            }
            return null;
        }

        private bool HasInstanceCap(AudioChannel channel, out int cap)
        {
            if (channel == AudioChannel.AISfx && maxAISfxInstances > 0)
            {
                cap = maxAISfxInstances;
                return true;
            }

            cap = 0;
            return false;
        }

        private bool TryStealCappedChannelSource(in AudioRequest request, int cap, out int index, out bool denyFarther)
        {
            index = -1;
            denyFarther = false;
            int active = 0;
            int victim = -1;
            float victimScore = float.NegativeInfinity;
            float victimDistSq = 0f;
            float now = Time.unscaledTime;

            var listener = GetActiveListener();
            Vector3 listenerPos = listener != null ? listener.transform.position : Vector3.zero;
            bool hasListener = listener != null;

            for (int i = 0; i < _sources.Count; i++)
            {
                var ctx = _sources[i];
                if (ctx.Channel != request.Channel) continue;
                if (!ctx.Source.isPlaying && !ctx.PausedByService) continue;

                active++;

                float distSq = hasListener && ctx.Source.spatialBlend > 0f
                    ? (ctx.Source.transform.position - listenerPos).sqrMagnitude
                    : 0f;
                float age = now - ctx.StartTime;
                float score = distSq * 1000f + age;

                if (score > victimScore)
                {
                    victimScore = score;
                    victimDistSq = distSq;
                    victim = i;
                }
            }

            if (active < cap || victim < 0) return false;

            if (request.Spatial3D && hasListener && request.FollowTarget != null)
            {
                float requesterDistSq = (request.FollowTarget.position - listenerPos).sqrMagnitude;
                if (requesterDistSq >= victimDistSq)
                {
                    denyFarther = true;
                    return false;
                }
            }

            var v = _sources[victim];
            v.PausedByService = false;
            v.Source.Stop();
            index = victim;
            return true;
        }

        private void StopChannel(AudioChannel channel)
        {
            foreach (var ctx in _sources)
            {
                if ((ctx.Source.isPlaying || ctx.PausedByService) && ctx.Channel == channel)
                {
                    ctx.PausedByService = false;
                    ctx.Source.Stop();
                }
            }
        }

        private bool TryGetSourceContext(AudioHandle handle, out SourceContext context)
        {
            context = null;
            if (!handle.IsValid) return false;

            int index = handle.Id - 1;
            if (index < 0 || index >= _sources.Count) return false;

            context = _sources[index];
            if (context.Source == null) return false;
            return context.Generation == handle.Generation;
        }

        private bool TryGetSource(AudioHandle handle, out AudioSource source)
        {
            if (TryGetSourceContext(handle, out var context))
            {
                source = context.Source;
                return true;
            }

            source = null;
            return false;
        }

        private async UniTaskVoid FollowAudioTargetAsync(SourceContext context, Transform target, ushort generation,
            CancellationToken cancellationToken)
        {
            try
            {
                var source = context.Source;
                while (source != null &&
                       (source.isPlaying || context.PausedByService) &&
                       target != null &&
                       context.Generation == generation)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    source.transform.position = target.position;
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                if (source != null && context.Generation == generation)
                {
                    source.transform.localPosition = Vector3.zero;
                }
            }
            catch (OperationCanceledException)
            {
            }
        }

        private async UniTaskVoid FadeAndStopAsync(SourceContext context, float duration, ushort generation,
            CancellationToken cancellationToken)
        {
            var source = context.Source;
            float startVolume = source.volume;
            float time = 0;

            try
            {
                while (time < duration && source != null && source.isPlaying && context.Generation == generation)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    time += Time.deltaTime;
                    source.volume = Mathf.Lerp(startVolume, 0f, time / duration);
                    await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
                }

                if (source != null && context.Generation == generation)
                {
                    source.Stop();
                    source.volume = CalculateSourceVolume(context);
                }
            }
            catch (OperationCanceledException)
            {
                if (source != null && context.Generation == generation)
                {
                    source.Stop();
                    source.volume = CalculateSourceVolume(context);
                }
            }
        }

        private void OnDestroy()
        {
            _snapshotBlendCts?.Cancel();
            _snapshotBlendCts?.Dispose();
            _snapshotBlendCts = null;

            _lifetimeCts?.Cancel();
            _lifetimeCts?.Dispose();
            _lifetimeCts = null;
        }
    }
}