using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Enums;
using Models.Interfaces;
using Presenter;
using ScriptableObjects;
using Services.Interfaces;
using Structs;
using UnityEngine;
using UnityEngine.UIElements;
using Utils;
using VContainer.Unity;
using Views;
using Object = UnityEngine.Object;

namespace Services {
    public class GameBoardService : IGameBoardService, ITickable, IDisposable {
        #region Inject
        private readonly GameVariables gameVariables;
        
        private readonly IGameBoard gameBoard;
        private readonly IObjectPool<GemView> objectPool;
        
        private readonly IMatchService matchService;
        private readonly IBombService bombService;
        private readonly IScoreService scoreService;
        
        private readonly ScorePresenter scorePresenter;
        private readonly AudioPresenter audioPresenter;
        
        private readonly Transform gemsHolder;
        private readonly Transform backgroundHolder;
        #endregion
        
        #region Variables
        private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
        private GameState currentState = GameState.Setup;
        #endregion
        
        public GameBoardService(
                GameVariables gameVariables, 
                IGameBoard gameBoard, 
                IObjectPool<GemView> objectPool, 
                IMatchService matchService,
                IBombService bombService, 
                IScoreService scoreService, 
                ScorePresenter scorePresenter, 
                AudioPresenter audioPresenter,
                Transform gemsHolder,
                Transform backgroundHolder) {
            this.gameVariables = gameVariables;
            this.gameBoard = gameBoard;
            this.objectPool = objectPool;
            this.matchService = matchService;
            this.bombService = bombService;
            this.scoreService = scoreService;
            this.scorePresenter = scorePresenter;
            this.audioPresenter = audioPresenter;
            this.gemsHolder = gemsHolder;
            this.backgroundHolder = backgroundHolder;
        }
        
        public void Tick() {
            foreach (GemPresenter gemPresenter in gemPresenters) {
                gemPresenter.Tick(this.gameVariables.gemSpeed);
            }

            this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
        }
        
        //Instantiates background tiles and calls SpawnGems
        //Uses MatchService.MatchesAt to avoid matching Gems
        public void Setup() {
            List<Gem> gemsToSpawn = new List<Gem>();
            for (int x = 0; x < this.gameBoard.Width; x++)
                for (int y = 0; y < this.gameBoard.Height; y++)
                {
                    Vector2 position = new Vector2(x, y);
                    SpawnBackgroundTile(position);

                    int iterations = 0;
                    int gemToUse = -1;
                    do {
                        gemToUse = RandomUtils.RandomGemTypeAsInt();
                        iterations++;
                    } while (this.matchService.MatchesAt(position.ToVector2Int(), (GemType)gemToUse) && iterations < 100);
                    
                    gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), (GemType)gemToUse));
                }

            SpawnCascade(gemsToSpawn);
        }
        
        private Gem SetGemAt(Vector2Int position, GemType gemType, bool isBomb = false) {
            GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs);
            Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType);
            this.gameBoard.SetGemAt(position, gem);

            return gem;
        }

        private void SpawnGemGameObject(Gem gem, bool isBomb = false) {
            GemView gemView = this.objectPool.Get(isBomb ? GemType.Bomb : gem.Type, gem.Position, isBomb ? 0 : this.gameVariables.dropHeight);
            gemView.name = "Gem - " + gem.Position.x + ", " + gem.Position.y + ' ' + gem.Type;
            
            GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs);
            gemView.Bind(gem, gemValue, isBomb: isBomb);
            this.gemPresenters.Add(new GemPresenter(gem, gemView));
        }

        private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) {
            if(isBomb)
                ReleaseMatchedGems(position);
            
            SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb);
        }

        private void SpawnBackgroundTile(Vector2 position) {
            GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity);
            backgroundTile.transform.SetParent(this.backgroundHolder);
            backgroundTile.name = "BG Tile - " + position.x + ", " + position.y;
        }
        
        //Listens to InputService OnSwapRequest
        public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
            if (this.currentState != GameState.Move) 
                return false;
            
            if (!GemUtils.IsInBounds(from, this.gameBoard) || !GemUtils.IsInBounds(to, this.gameBoard)) 
                return false;

            if (!from.IsAdjacent(to)) 
                return false;
            
            this.currentState = GameState.Wait;

            ApplySwap(from, to);

            await UniTask.Delay(this.gameVariables.swapDelayMs);
            this.bombService.SetLastSwap(from, to);
            this.matchService.FindAllMatches();
            this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches);
            
            if (this.matchService.CurrentMatches.Count == 0) {
                ApplySwap(to, from);
                await UniTask.Delay(this.gameVariables.swapDelayMs);
                this.currentState = GameState.Move;
                return false;
            }
            
            List<Vector2Int> protectedPositions = this.bombService.ApplyPendingBombSpawns(SetAndSpawnGem);
            await DestroyMatchesAsync(protectedPositions);
            this.currentState = GameState.Move;
            return true;
        }

        public async UniTask<bool> TrySwitch(Vector2Int position) {
            Gem gem = this.gameBoard.GetGemAt(position);
            if(gem == null)
                return false;
            
            GemType[] normalTypes = Enum.GetValues(typeof(GemType))
                .Cast<GemType>()
                .Where(t => t != GemType.Bomb)
                .ToArray();

            if (normalTypes.Length == 0)
                return false;
            
            bool nextIsBomb;
            GemType nextTypeOrMatchColor;

            if (gem.Type != GemType.Bomb) {
                int index = Array.IndexOf(normalTypes, gem.Type);
                if (index < 0) index = 0;

                if (index < normalTypes.Length - 1) {
                    nextIsBomb = false;
                    nextTypeOrMatchColor = normalTypes[index + 1];
                } else {
                    nextIsBomb = true;
                    nextTypeOrMatchColor = normalTypes[0]; 
                }
            } else {
                int idx = Array.IndexOf(normalTypes, gem.MatchColor);
                if (idx < 0) idx = 0;

                if (idx < normalTypes.Length - 1) {
                    nextIsBomb = true;
                    nextTypeOrMatchColor = normalTypes[idx + 1];
                } else {
                    nextIsBomb = false;
                    nextTypeOrMatchColor = normalTypes[0];
                }
            }

            // Replace both model+view by releasing current and respawning at same position.
            ReleaseMatchedGems(position);
            SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb);
            return true;
        }
        
        private void ApplySwap(Vector2Int from, Vector2Int to) {
            Gem fromGem = this.gameBoard.GetGemAt(from);
            Gem toGem = this.gameBoard.GetGemAt(to);
            // swap their stored positions
            fromGem.SetPosition(to);
            toGem.SetPosition(from);

            // update grid
            this.gameBoard.SetGemAt(from, toGem);
            this.gameBoard.SetGemAt(to, fromGem);
        }
        
        private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
            List<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions);
            List<Vector2Int> initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions.Distinct().ToList());

            // If a bomb is part of the match, do NOT destroy matching pieces immediately.
            // Let the bomb's manhattan-distance explosion destroy them in sequence.
            if (initialBombs.Count > 0) {
                await this.bombService.DetonateChainAsync(
                    initialBombs,
                    DestroyAtAsync,
                    this.gameBoard);
                
                foreach (Vector2Int p in matchPositions.Distinct())
                    await DestroyAtAsync(p);

                await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
                
                await MoveGemsDown();
                return;
            }

            // For audio SFX
            bool willBreakAnyNonBombGem = matchPositions.Select(pos => this.gameBoard.GetGemAt(pos)).Where(gem => gem != null).Any(gem => gem.Type != GemType.Bomb);
            if (willBreakAnyNonBombGem)
                this.audioPresenter.OnMatch(this.gameVariables.matchSfx);

            // For score counting
            foreach (Vector2Int pos in matchPositions.Distinct().ToList()) {
                Gem gem = this.gameBoard.GetGemAt(pos);
                if (gem == null) continue;
                if (gem.Type == GemType.Bomb) continue;

                this.scoreService.ScoreCheck(gem.ScoreValue);
                ReleaseMatchedGems(pos);
            }
            await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
            await MoveGemsDown();
        }

        private UniTask DestroyAtAsync(Vector2Int pos) {
            Gem gem = this.gameBoard.GetGemAt(pos);
            if (gem == null)
                return UniTask.CompletedTask;
            
            if (gem.Type == GemType.Bomb)
                this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx);

            this.scoreService.ScoreCheck(gem.ScoreValue);
            ReleaseMatchedGems(pos);
            return UniTask.CompletedTask;
        }
        
        private void ReleaseMatchedGems(Vector2Int position) {
            List<GemView> gemsViews = this.gemsHolder.GetComponentsInChildren<GemView>().ToList();
            Gem currentGem = this.gameBoard.GetGemAt(position);
            if (currentGem != null)
            {
                GemView gemView = gemsViews.FirstOrDefault(gv => gv.Gem == currentGem);
                if (gemView is null) {
                    return;
                }
                
                this.objectPool.Release(gemView);
                RemovePresenterFor(gemView);
                this.gameBoard.SetGemAt(position, null);
            }
        }

        private async UniTask MoveGemsDown() {
            while (true)
            {
                // Build moves from a snapshot of the current grid state (no mid-wave chaining)
                List<(Vector2Int from, Vector2Int to, Gem gem)> moves = new List<(Vector2Int, Vector2Int, Gem)>();

                for (int x = 0; x < this.gameBoard.Width; x++)
                {
                    for (int y = 1; y < this.gameBoard.Height; y++)
                    {
                        Vector2Int from = new Vector2Int(x, y);
                        Vector2Int to = new Vector2Int(x, y - 1);

                        Gem gem = this.gameBoard.GetGemAt(from);
                        if (gem == null)
                            continue;

                        Gem below = this.gameBoard.GetGemAt(to);
                        if (below != null)
                            continue;

                        moves.Add((from, to, gem));
                    }
                }

                if (moves.Count == 0)
                    break;

                // Apply all moves simultaneously for this wave
                for (int i = 0; i < moves.Count; i++)
                {
                    (Vector2Int from, Vector2Int to, Gem gem) move = moves[i];
                    this.gameBoard.SetGemAt(move.to, move.gem);
                    this.gameBoard.SetGemAt(move.from, null);
                    move.gem.SetPosition(move.to);
                }

                await UniTask.Delay(this.gameVariables.cascadeDelayMs);
            }

            await FillBoard();
        }

        private async UniTask FillBoard() {
            await RefillBoard();

            this.matchService.FindAllMatches();
            if (this.matchService.CurrentMatches.Count > 0) {
                await UniTask.Delay(this.gameVariables.fillBoardDelayMs);

                // In cascades, there is no "creating slot" bomb protection.
                await DestroyMatchesAsync(new List<Vector2Int>());
            } else {
                this.currentState = GameState.Move;
            }
        }

        private async UniTask RefillBoard() {
            List<Gem> gemsToSpawn = new List<Gem>();
            for (int x = 0; x < this.gameBoard.Width; x++)
            {
                for (int y = 0; y < this.gameBoard.Height; y++)
                {
                    Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y));
                    if (currentGem == null) {
                        int gemToUse = RandomUtils.RandomGemTypeAsInt();

                        int iterations = 0;
                        while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100)
                        {
                            gemToUse = RandomUtils.RandomGemTypeAsInt();
                            iterations++;
                        }
                        
                        gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), (GemType)gemToUse));
                    }
                }
            }

            await SpawnCascade(gemsToSpawn);
        }

        private async UniTask SpawnCascade(List<Gem> gemsToSpawn)
        {
            List<IGrouping<int, Gem>> groups = gemsToSpawn
                .GroupBy(gem => gem.Position.y)
                .OrderBy(group => group.Key)
                .ToList();

            for (int i = 0; i < groups.Count; i++)
            {
                foreach (Gem gem in groups[i])
                    SpawnGemGameObject(gem);

                if (i < groups.Count - 1)
                    await UniTask.Delay(this.gameVariables.cascadeDelayMs);
            }
            
            if(this.currentState == GameState.Setup)
                this.currentState = GameState.Move;
        }
        
        private void RemovePresenterFor(GemView gemView) {
            if (gemView is null) {
                return;
            }

            GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView);
            this.gemPresenters.Remove(presenter);
        }

        public void Dispose() {
            this.objectPool.Clear();
        }
    }
}