using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Enums;
using Models.Interfaces;
using ScriptableObjects;
using Services.Interfaces;
using Structs;
using UnityEngine;
using Utils;

namespace Services
{
    public class BombService : IBombService {
        private readonly GameVariables gameVariables;
        private readonly IGameBoard gameBoard;
        
        private Vector2Int lastSwapFrom;
        private Vector2Int lastSwapTo;
        
        private BombSpawnRequest? pendingBombSpawn;
        public BombSpawnRequest? PendingBombSpawn => this.pendingBombSpawn;

        public BombService(GameVariables gameVariables, IGameBoard gameBoard) {
            this.gameVariables = gameVariables;
            this.gameBoard = gameBoard;
        }

        public void SetLastSwap(Vector2Int from, Vector2Int to) {
            this.lastSwapFrom = from;
            this.lastSwapTo = to;
            
            ClearPendingBombs();
        }

        public UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, List<Vector2Int> bombCandidates) {
            List<Vector2Int> initialBombs = new List<Vector2Int>();
            foreach (Vector2Int p in bombCandidates) {
                if (!GemUtils.IsInBounds(p, this.gameBoard)) continue;

                if (protectedPositions != null && protectedPositions.Contains(p))
                    continue;

                Gem gem = this.gameBoard.GetGemAt(p);
                if (gem is { Type: GemType.Bomb })
                    initialBombs.Add(p);
            }
            
            return UniTask.FromResult(initialBombs.Distinct().ToList());
        }

        public List<Vector2Int> ApplyPendingBombSpawns(Action<Vector2Int, GemType, bool> spawnGem) {
            List<Vector2Int> positions = new List<Vector2Int>();
            BombSpawnRequest? bombSpawnRequest = PendingBombSpawn;
            
            if (bombSpawnRequest != null) {
                BombSpawnRequest bombRequest = PendingBombSpawn.GetValueOrDefault();
                positions.Add(bombRequest.Position);
                spawnGem(bombRequest.Position, bombRequest.Color, true);
            }

            ClearPendingBombs();
            return positions;
        }
        
        public void DetectBombSpawnFromLastSwap(List<Gem> currentMatches) {
            Vector2Int from = this.lastSwapFrom;
            Vector2Int to = this.lastSwapTo;

            TryCreateBombSpawnAt(from, currentMatches);
            TryCreateBombSpawnAt(to, currentMatches);
        }
        
        private void TryCreateBombSpawnAt(Vector2Int pivot, List<Gem> currentMatches) {
            Gem pivotGem = this.gameBoard.GetGemAt(pivot);
            if (pivotGem == null)
                return;

            // If it's already a bomb, don't create another.
            if (pivotGem.Type == GemType.Bomb)
                return;

            if (currentMatches.All(g => g.Position != pivot))
                return;

            // Only create a bomb if pivot is part of a straight 4+ line of the SAME color.
            int longestLine = GetLongestMatchedLineThroughPivot(pivot, pivotGem.MatchColor);
            if (longestLine < 4)
                return;

            // Prevent duplicates for the same cell.
            if (this.pendingBombSpawn.GetValueOrDefault().Position == pivot)
                return;

            this.pendingBombSpawn = new BombSpawnRequest(pivot, pivotGem.MatchColor);
        }

        public async UniTask DetonateChainAsync(
            IReadOnlyList<Vector2Int> initialBombs,
            Func<Vector2Int, UniTask> destroyAtAsync,
            IGameBoard gameBoard)
        {
            if (initialBombs == null || initialBombs.Count == 0)
                return;

            int waveDelayMs = Mathf.RoundToInt(this.gameVariables.bombDelay * 1000f);

            HashSet<Vector2Int> processedBombs = new HashSet<Vector2Int>();

            Queue<Vector2Int> waveQueue = new Queue<Vector2Int>(
                initialBombs.Where(p =>
                {
                    if (!GemUtils.IsInBounds(p, gameBoard)) return false;
                    Gem g = gameBoard.GetGemAt(p);
                    return g is { Type: GemType.Bomb };
                })
            );

            while (waveQueue.Count > 0)
            {
                // current wave (per bomb)
                List<Vector2Int> waveBombs = new List<Vector2Int>();
                while (waveQueue.Count > 0)
                {
                    Vector2Int b = waveQueue.Dequeue();
                    if (processedBombs.Contains(b))
                        continue;

                    if (!GemUtils.IsInBounds(b, gameBoard))
                        continue;

                    Gem g = gameBoard.GetGemAt(b);
                    if (g is not { Type: GemType.Bomb })
                        continue;

                    processedBombs.Add(b);
                    waveBombs.Add(b);
                }

                if (waveBombs.Count == 0)
                    continue;

                // delay once per wave
                if (waveDelayMs > 0)
                    await UniTask.Delay(waveDelayMs);

                HashSet<Vector2Int> nextWaveBombs = new HashSet<Vector2Int>();
                HashSet<Vector2Int> toDestroyNow = new HashSet<Vector2Int>();

                for (int i = 0; i < waveBombs.Count; i++)
                {
                    Vector2Int bombPos = waveBombs[i];

                    // destroy self when it detonates
                    toDestroyNow.Add(bombPos);

                    foreach (Vector2Int p in DiamondAreaInclusive(bombPos, this.gameVariables.bombRadius))
                    {
                        if (!GemUtils.IsInBounds(p, gameBoard))
                            continue;

                        if (p == bombPos)
                            continue;

                        Gem cellGem = gameBoard.GetGemAt(p);
                        if (cellGem == null)
                            continue;

                        if (cellGem.Type == GemType.Bomb)
                        {
                            // bombs in range are NOT destroyed now. triggered to explode in a later wave.
                            if (!processedBombs.Contains(p))
                                nextWaveBombs.Add(p);

                            continue;
                        }

                        // Non-bomb gem gets destroyed by this bomb
                        toDestroyNow.Add(p);
                    }
                }

                // Destroy everything for this wave (non-bombs in range + the detonating bombs themselves)
                foreach (Vector2Int p in toDestroyNow)
                    await destroyAtAsync(p);

                // Schedule the next wave (triggered bombs)
                foreach (Vector2Int b in nextWaveBombs)
                    waveQueue.Enqueue(b);
            }
        }

        private static IEnumerable<Vector2Int> DiamondAreaInclusive(Vector2Int center, int radius)
        {
            // Manhattan-distance filled diamond:
            for (int distanceX = -radius; distanceX <= radius; distanceX++)
            {
                int remaining = radius - Mathf.Abs(distanceX);
                for (int distanceY = -remaining; distanceY <= remaining; distanceY++)
                {
                    yield return new Vector2Int(center.x + distanceX, center.y + distanceY);
                }
            }
        }
        
        private int GetLongestMatchedLineThroughPivot(Vector2Int pivot, GemType color) {
            int horizontal = 1 + CountSameColorInDirection(pivot, Vector2Int.left, color)
                               + CountSameColorInDirection(pivot, Vector2Int.right, color);

            int vertical = 1 + CountSameColorInDirection(pivot, Vector2Int.up, color)
                             + CountSameColorInDirection(pivot, Vector2Int.down, color);

            return Mathf.Max(horizontal, vertical);
        }

        private int CountSameColorInDirection(Vector2Int start, Vector2Int direction, GemType color) {
            int count = 0;
            Vector2Int oivot = start + direction;

            while (oivot.x >= 0 && oivot.x < this.gameBoard.Width && oivot.y >= 0 && oivot.y < this.gameBoard.Height) {
                Gem g = this.gameBoard.GetGemAt(oivot);
                if (g == null || g.Type == GemType.Bomb || g.MatchColor != color)
                    break;

                count++;
                oivot += direction;
            }

            return count;
        }
        
        public void ClearPendingBombs() {
            this.pendingBombSpawn = null;
        }
    }
}