using UnityEngine;
using UnityEngine.AI;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// Spawn zone classification for cash bags.
/// Contested zones are between team vaults (center of map) — forces team encounters.
/// </summary>
public enum SpawnZone
{
    Safe,       // Near a vault — low risk, low reward
    Neutral,    // Between center and vaults — medium risk
    Contested   // Center of map between vaults — high risk, high reward
}

/// <summary>
/// CashBagSpawner — Wave-based spawning of cash bags with object pooling.
/// Uses zone-weighted spawning to force team encounters in contested areas.
/// </summary>
public class CashBagSpawner : MonoBehaviour
{
    [Header("Cash Bag Settings")]
    [SerializeField] private GameObject cashBagPrefab;
    [SerializeField] private float cashPerBag = 100f;
    
    [Header("Spawn Limits")]
    [SerializeField] private int maxBagCount = 15;
    [SerializeField] private float initialSpawnPercent = 0.60f;     // 60% on start
    
    [Header("Wave Settings")]
    [SerializeField] private float waveInterval = 30f;
    [SerializeField] private int bagsPerWave = 3;
    
    [Header("Spawn Area")]
    [SerializeField] private Transform[] spawnPoints;
    [SerializeField] private bool useRandomSpawning = false;
    [SerializeField] private Vector3 spawnAreaCenter = Vector3.zero;
    [SerializeField] private Vector3 spawnAreaSize = new Vector3(20f, 0f, 20f);
    [SerializeField] private float minSpawnDistance = 5f;           // Min distance between bags
    [SerializeField] private float playerAvoidDistance = 5f;        // Distance from players
    [SerializeField] private float vaultAvoidDistance = 6f;         // Distance from vaults
    
    [Header("Zone-Weighted Spawning")]
    [Tooltip("Chance to spawn in contested center zone (forces team encounters)")]
    [SerializeField, Range(0f, 1f)] private float contestedZoneWeight = 0.60f;
    [Tooltip("Chance to spawn in neutral middle zone")]
    [SerializeField, Range(0f, 1f)] private float neutralZoneWeight  = 0.30f;
    [Tooltip("Chance to spawn in safe zone near vaults")]
    [SerializeField, Range(0f, 1f)] private float safeZoneWeight     = 0.10f;
    [Tooltip("Value multiplier for contested zone bags")]
    [SerializeField] private float contestedValueMultiplier = 2.0f;
    [Tooltip("Value multiplier for neutral zone bags")]
    [SerializeField] private float neutralValueMultiplier   = 1.0f;
    [Tooltip("Value multiplier for safe zone bags")]
    [SerializeField] private float safeValueMultiplier      = 0.5f;
    
    [Header("Debug")]
    [SerializeField] private bool showSpawnGizmos = true;
    
    // ─── Runtime ─────────────────────────────────────────────
    private ObjectPool _pool;
    private readonly List<CashBag> _activeBags = new List<CashBag>();
    private readonly HashSet<int> _occupiedSpawnPoints = new HashSet<int>(); // tracks which spawn points have an active bag
    private readonly Dictionary<int, float> _spawnPointCooldowns = new Dictionary<int, float>(); // prevents same-point reuse
    private const float SPAWN_POINT_COOLDOWN = 8f; // seconds before a freed point can be reused
    private int _totalSpawned;
    private bool _isSpawning;
    private Coroutine _waveCoroutine;
    
    // ─── Zone Classification ─────────────────────────────────
    private readonly List<int> _contestedPoints = new List<int>();
    private readonly List<int> _neutralPoints   = new List<int>();
    private readonly List<int> _safePoints      = new List<int>();
    private bool _zonesClassified;
    private Vector3 _mapCenter; // midpoint between vaults — the contested zone center

    public int ActiveBagCount => _activeBags.Count;
    public int TotalSpawned => _totalSpawned;
    
    // ─── Lifecycle ───────────────────────────────────────────

    private void Start()
    {
        if (SelectionOptions.Instance == null || !SelectionOptions.Instance.isCashSystemSelected)
        {
            enabled = false;
            return;
        }
        
        // Find spawn points by tag if none assigned
        if ((spawnPoints == null || spawnPoints.Length == 0) && !useRandomSpawning)
        {
            GameObject[] spawnPointObjects = GameObject.FindGameObjectsWithTag("CashBagSpawn");
            if (spawnPointObjects.Length > 0)
            {
                spawnPoints = new Transform[spawnPointObjects.Length];
                for (int i = 0; i < spawnPointObjects.Length; i++)
                    spawnPoints[i] = spawnPointObjects[i].transform;
            }
            else
            {
                useRandomSpawning = true;
            }
        }
        
        // Initialize object pool (pre-warm up to maxBagCount and parent under this transform)
        if (cashBagPrefab != null)
        {
            _pool = new ObjectPool(cashBagPrefab, initialSize: maxBagCount, maxSize: maxBagCount, parent: transform);
        }
        
        // Classify spawn points into zones for weighted spawning
        ClassifySpawnPoints();
        
        StartSpawning();
    }

    private void OnEnable()
    {
        GameEvents.OnBagDespawned += OnBagDespawned;
        // NOTE: We intentionally do NOT subscribe to OnBagCollected here.
        // A bag being picked up is NOT the same as a bag leaving the field —
        // the carrier is still running around with it. We only remove from
        // _activeBags when the bag is truly gone (despawned/deposited).
    }

    private void OnDisable()
    {
        GameEvents.OnBagDespawned -= OnBagDespawned;
    }

    // ─── Spawning ────────────────────────────────────────────
    
    public void StartSpawning()
    {
        if (_isSpawning) return;
        _isSpawning = true;
        
        int initialCount = Mathf.RoundToInt(maxBagCount * initialSpawnPercent);
        
        for (int i = 0; i < initialCount; i++)
            SpawnBag();
        
        GameEvents.FireActiveBagCountChanged(_activeBags.Count);
        GameEvents.FirePopupMessage("CASH DROPPED!", 1.5f);
        
        // Start wave timer
        _waveCoroutine = StartCoroutine(WaveSpawnRoutine());
    }
    
    private IEnumerator WaveSpawnRoutine()
    {
        while (_isSpawning)
        {
            yield return new WaitForSeconds(waveInterval);
            if (!_isSpawning) break;
            
            // Clean up null references
            _activeBags.RemoveAll(b => b == null || !b.gameObject.activeInHierarchy);
            
            int canSpawn = maxBagCount - _activeBags.Count;
            int toSpawn = Mathf.Min(bagsPerWave, canSpawn);
            
            if (toSpawn <= 0) continue;
            
            for (int i = 0; i < toSpawn; i++)
            {
                SpawnBag();
                yield return new WaitForSeconds(0.15f); // Stagger
            }
            
            GameEvents.FireActiveBagCountChanged(_activeBags.Count);
            GameEvents.FirePopupMessage($"+{toSpawn} CASH!", 1f);
        }
    }
    
    private void SpawnBag()
    {
        if (cashBagPrefab == null) return;
        
        // Determine spawn zone for value multiplier
        SpawnZone zone = _zonesClassified ? PickWeightedZone() : SpawnZone.Neutral;
        
        Vector3 spawnPos = _zonesClassified ? GetZoneSpawnPosition(zone) : GetValidSpawnPosition();
        if (spawnPos == Vector3.zero && !useRandomSpawning) return;
        
        // Snap to NavMesh
        if (NavMesh.SamplePosition(spawnPos, out NavMeshHit navHit, 5f, NavMesh.AllAreas))
            spawnPos = navHit.position + Vector3.up * 0.7f;
        
        // Use pool if available, fallback to Instantiate
        GameObject bagObj;
        if (_pool != null)
        {
            bagObj = _pool.Get(spawnPos, Quaternion.identity);
            if (bagObj == null)
            {
                bagObj = Instantiate(cashBagPrefab, spawnPos, Quaternion.identity);
            }
        }
        else
        {
            bagObj = Instantiate(cashBagPrefab, spawnPos, Quaternion.identity);
        }
        
        CashBag bag = bagObj.GetComponent<CashBag>();
        if (bag == null) bag = bagObj.AddComponent<CashBag>();
        
        // Apply zone-based value multiplier — contested bags are worth MORE
        float zoneMultiplier = GetZoneValueMultiplier(zone);
        bag.SetCashValue(cashPerBag * zoneMultiplier);
        _activeBags.Add(bag);
        _totalSpawned++;
        
        // Ensure the bag gets a minimap marker
        if (bag.GetComponent<MinimapObjectMarker>() == null)
            bag.gameObject.AddComponent<MinimapObjectMarker>();
        
        GameEvents.FireBagSpawned(bag);
    }

    // ─── Zone Classification ─────────────────────────────────

    /// <summary>
    /// Classifies each spawn point as Contested, Neutral, or Safe based on
    /// its distance from the map center (midpoint between team vaults).
    /// This ensures bags spawn more often in areas where teams will clash.
    /// </summary>
    private void ClassifySpawnPoints()
    {
        if (spawnPoints == null || spawnPoints.Length == 0 || useRandomSpawning)
        {
            _zonesClassified = false;
            return;
        }

        _contestedPoints.Clear();
        _neutralPoints.Clear();
        _safePoints.Clear();

        // Find vault positions to determine map layout
        var vaults = EntityRegistry<TeamVault>.GetAll();
        if (vaults == null || vaults.Count < 2)
        {
            // Can't classify without vaults — treat all as neutral
            for (int i = 0; i < spawnPoints.Length; i++)
                if (spawnPoints[i] != null)
                    _neutralPoints.Add(i);
            _zonesClassified = true;
            _mapCenter = spawnAreaCenter;
            DevLog.Log("[CashBagSpawner] <2 vaults found, all spawn points classified as Neutral.");
            return;
        }

        // Calculate map center (midpoint between vaults — XZ plane)
        Vector3 v0 = vaults[0].transform.position;
        Vector3 v1 = vaults[1].transform.position;
        _mapCenter = (v0 + v1) * 0.5f;
        _mapCenter.y = 0f; // Normalize to ground

        // Find the maximum distance from center to any spawn point (for normalization)
        float maxDist = 0f;
        for (int i = 0; i < spawnPoints.Length; i++)
        {
            if (spawnPoints[i] == null) continue;
            Vector3 flatPos = spawnPoints[i].position;
            flatPos.y = 0f;
            float d = Vector3.Distance(flatPos, _mapCenter);
            if (d > maxDist) maxDist = d;
        }
        
        if (maxDist < 1f) maxDist = 1f; // Prevent division by zero

        // Classify: inner 40% = contested, 40-70% = neutral, outer 70%+ = safe
        for (int i = 0; i < spawnPoints.Length; i++)
        {
            if (spawnPoints[i] == null) continue;
            
            Vector3 flatPos = spawnPoints[i].position;
            flatPos.y = 0f;
            float normalizedDist = Vector3.Distance(flatPos, _mapCenter) / maxDist;

            if (normalizedDist < 0.40f)
                _contestedPoints.Add(i);
            else if (normalizedDist < 0.70f)
                _neutralPoints.Add(i);
            else
                _safePoints.Add(i);
        }

        _zonesClassified = true;
        DevLog.Log($"[CashBagSpawner] Zone classification: " +
                   $"Contested={_contestedPoints.Count}, " +
                   $"Neutral={_neutralPoints.Count}, " +
                   $"Safe={_safePoints.Count}");
    }

    /// <summary>
    /// Selects a spawn zone based on weighted random distribution.
    /// 60% contested, 30% neutral, 10% safe by default.
    /// Falls back to any available zone if the chosen zone is empty.
    /// </summary>
    private SpawnZone PickWeightedZone()
    {
        float r = Random.value;
        float cumulative = 0f;

        cumulative += contestedZoneWeight;
        if (r < cumulative && _contestedPoints.Count > 0)
            return SpawnZone.Contested;

        cumulative += neutralZoneWeight;
        if (r < cumulative && _neutralPoints.Count > 0)
            return SpawnZone.Neutral;

        if (_safePoints.Count > 0)
            return SpawnZone.Safe;

        // Fallback: pick whichever zone has points
        if (_contestedPoints.Count > 0) return SpawnZone.Contested;
        if (_neutralPoints.Count > 0) return SpawnZone.Neutral;
        if (_safePoints.Count > 0) return SpawnZone.Safe;

        return SpawnZone.Neutral; // absolute fallback
    }

    /// <summary>Returns the value multiplier for a given spawn zone.</summary>
    private float GetZoneValueMultiplier(SpawnZone zone)
    {
        switch (zone)
        {
            case SpawnZone.Contested: return contestedValueMultiplier;
            case SpawnZone.Neutral:   return neutralValueMultiplier;
            case SpawnZone.Safe:      return safeValueMultiplier;
            default:                  return 1f;
        }
    }

    /// <summary>Returns the spawn point list for a given zone.</summary>
    private List<int> GetZonePoints(SpawnZone zone)
    {
        switch (zone)
        {
            case SpawnZone.Contested: return _contestedPoints;
            case SpawnZone.Neutral:   return _neutralPoints;
            case SpawnZone.Safe:      return _safePoints;
            default:                  return _neutralPoints;
        }
    }

    // ─── Position Validation ─────────────────────────────────

    private Vector3 GetValidSpawnPosition()
    {
        return useRandomSpawning ? GetRandomSpawnPosition() : GetSpawnPointPosition();
    }

    /// <summary>
    /// Zone-aware spawn position selection. Picks from the specified zone's spawn points,
    /// falling back to other zones if all points in the chosen zone are occupied.
    /// </summary>
    private Vector3 GetZoneSpawnPosition(SpawnZone preferredZone)
    {
        if (!_zonesClassified || useRandomSpawning)
            return GetValidSpawnPosition();

        // Try preferred zone first
        Vector3 pos = GetSpawnPointFromZone(GetZonePoints(preferredZone));
        if (pos != Vector3.zero) return pos;

        // Fallback: try other zones (contested > neutral > safe priority)
        if (preferredZone != SpawnZone.Contested)
        {
            pos = GetSpawnPointFromZone(_contestedPoints);
            if (pos != Vector3.zero) return pos;
        }
        if (preferredZone != SpawnZone.Neutral)
        {
            pos = GetSpawnPointFromZone(_neutralPoints);
            if (pos != Vector3.zero) return pos;
        }
        if (preferredZone != SpawnZone.Safe)
        {
            pos = GetSpawnPointFromZone(_safePoints);
            if (pos != Vector3.zero) return pos;
        }

        // Absolute fallback: original unfiltered method
        return GetSpawnPointPosition();
    }

    /// <summary>
    /// Picks a valid spawn position from a specific set of spawn point indices.
    /// </summary>
    private Vector3 GetSpawnPointFromZone(List<int> zoneIndices)
    {
        if (zoneIndices == null || zoneIndices.Count == 0)
            return Vector3.zero;

        // Clean up expired cooldowns
        List<int> expired = null;
        foreach (var kvp in _spawnPointCooldowns)
        {
            if (Time.time >= kvp.Value)
            {
                if (expired == null) expired = new List<int>(4);
                expired.Add(kvp.Key);
            }
        }
        if (expired != null)
            foreach (int idx in expired)
                _spawnPointCooldowns.Remove(idx);

        // Shuffle to randomize within zone
        List<int> shuffled = new List<int>(zoneIndices);
        for (int i = shuffled.Count - 1; i > 0; i--)
        {
            int j = Random.Range(0, i + 1);
            (shuffled[i], shuffled[j]) = (shuffled[j], shuffled[i]);
        }

        // Pick first unoccupied, valid, non-cooldown point
        foreach (int idx in shuffled)
        {
            if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
            if (_occupiedSpawnPoints.Contains(idx)) continue;
            if (_spawnPointCooldowns.ContainsKey(idx)) continue;
            if (IsPositionValid(spawnPoints[idx].position))
            {
                _occupiedSpawnPoints.Add(idx);
                return spawnPoints[idx].position;
            }
        }

        // Fallback: allow cooldown points in this zone
        foreach (int idx in shuffled)
        {
            if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
            if (_occupiedSpawnPoints.Contains(idx)) continue;
            if (IsPositionValid(spawnPoints[idx].position))
            {
                _occupiedSpawnPoints.Add(idx);
                return spawnPoints[idx].position;
            }
        }

        return Vector3.zero;
    }
    
    private Vector3 GetSpawnPointPosition()
    {
        if (spawnPoints == null || spawnPoints.Length == 0)
            return Vector3.zero;
        
        // Clean up expired cooldowns
        List<int> expired = null;
        foreach (var kvp in _spawnPointCooldowns)
        {
            if (Time.time >= kvp.Value)
            {
                if (expired == null) expired = new List<int>(4);
                expired.Add(kvp.Key);
            }
        }
        if (expired != null)
            foreach (int idx in expired)
                _spawnPointCooldowns.Remove(idx);

        // Build a list of indices, then shuffle to randomize
        List<int> indices = new List<int>(spawnPoints.Length);
        for (int i = 0; i < spawnPoints.Length; i++)
            indices.Add(i);
        
        for (int i = indices.Count - 1; i > 0; i--)
        {
            int j = Random.Range(0, i + 1);
            (indices[i], indices[j]) = (indices[j], indices[i]);
        }
        
        // Pick the first unoccupied, valid, non-cooldown spawn point
        foreach (int idx in indices)
        {
            if (spawnPoints[idx] == null) continue;
            if (_occupiedSpawnPoints.Contains(idx)) continue; // skip occupied
            if (_spawnPointCooldowns.ContainsKey(idx)) continue; // skip recently used
            if (IsPositionValid(spawnPoints[idx].position))
            {
                _occupiedSpawnPoints.Add(idx);
                return spawnPoints[idx].position;
            }
        }

        // Fallback: allow cooldown points if nothing else available
        foreach (int idx in indices)
        {
            if (spawnPoints[idx] == null) continue;
            if (_occupiedSpawnPoints.Contains(idx)) continue;
            if (IsPositionValid(spawnPoints[idx].position))
            {
                _occupiedSpawnPoints.Add(idx);
                return spawnPoints[idx].position;
            }
        }
        
        return Vector3.zero;
    }
    
    private Vector3 GetRandomSpawnPosition()
    {
        for (int i = 0; i < 30; i++)
        {
            Vector3 randomPos = spawnAreaCenter + new Vector3(
                Random.Range(-spawnAreaSize.x / 2, spawnAreaSize.x / 2),
                0.5f,
                Random.Range(-spawnAreaSize.z / 2, spawnAreaSize.z / 2)
            );
            
            if (IsPositionValid(randomPos))
                return randomPos;
        }
        
        return spawnAreaCenter + new Vector3(Random.Range(-5f, 5f), 0.5f, Random.Range(-5f, 5f));
    }
    
    private bool IsPositionValid(Vector3 pos)
    {
        // Check distance from active bags (using EntityRegistry — zero alloc)
        var bags = EntityRegistry<CashBag>.GetAll();
        for (int i = 0; i < bags.Count; i++)
        {
            if (bags[i] != null && Vector3.Distance(pos, bags[i].transform.position) < minSpawnDistance)
                return false;
        }
        
        // Check distance from all characters (using TeamMember static registry)
        foreach (TeamMember member in TeamMember.AllMembers)
        {
            if (member != null && Vector3.Distance(pos, member.transform.position) < playerAvoidDistance)
                return false;
        }
        
        // Check distance from vaults
        var vaults = EntityRegistry<TeamVault>.GetAll();
        for (int i = 0; i < vaults.Count; i++)
        {
            if (vaults[i] != null && Vector3.Distance(pos, vaults[i].transform.position) < vaultAvoidDistance)
                return false;
        }

        return true;
    }

    // ─── Event Handlers ──────────────────────────────────────

    private void OnBagDespawned(CashBag bag)
    {
        _activeBags.Remove(bag);
        ReleaseSpawnPoint(bag);
        GameEvents.FireActiveBagCountChanged(_activeBags.Count);

        // Immediately spawn a replacement at a DIFFERENT position
        // so the field always has bags and they rotate across spawn points
        if (_isSpawning && _activeBags.Count < maxBagCount)
        {
            StartCoroutine(DelayedRespawn(0.5f));
        }
    }

    /// <summary>Respawn a bag after a short delay at a different position.</summary>
    private IEnumerator DelayedRespawn(float delay)
    {
        yield return new WaitForSeconds(delay);
        if (!_isSpawning) yield break;

        // Clean up null references
        _activeBags.RemoveAll(b => b == null || !b.gameObject.activeInHierarchy);

        if (_activeBags.Count < maxBagCount)
        {
            SpawnBag();
            GameEvents.FireActiveBagCountChanged(_activeBags.Count);
        }
    }
    
    /// <summary>Called when a bag is collected (legacy API for compatibility).</summary>
    public void OnBagCollected(CashBag bag)
    {
        // Legacy API: only remove if bag is truly gone (inactive).
        // Don't remove carried bags — they're still in play.
        if (bag != null && !bag.gameObject.activeInHierarchy)
        {
            if (_activeBags.Contains(bag))
                _activeBags.Remove(bag);
            ReleaseSpawnPoint(bag);
        }
    }
    
    /// <summary>
    /// Free the spawn point closest to a removed bag so it can be reused.
    /// Applies a cooldown so the same point isn't immediately reused.
    /// </summary>
    private void ReleaseSpawnPoint(CashBag bag)
    {
        if (bag == null || spawnPoints == null) return;
        
        float bestDist = float.MaxValue;
        int bestIdx = -1;
        
        foreach (int idx in _occupiedSpawnPoints)
        {
            if (idx < 0 || idx >= spawnPoints.Length || spawnPoints[idx] == null) continue;
            float dist = Vector3.Distance(bag.SpawnPosition, spawnPoints[idx].position);
            if (dist < bestDist)
            {
                bestDist = dist;
                bestIdx = idx;
            }
        }
        
        // Only release if the bag was reasonably close to the spawn point
        if (bestIdx >= 0 && bestDist < minSpawnDistance * 2f)
        {
            _occupiedSpawnPoints.Remove(bestIdx);
            // Add cooldown so next spawn goes to a DIFFERENT position
            _spawnPointCooldowns[bestIdx] = Time.time + SPAWN_POINT_COOLDOWN;
        }
    }
    
    public void StopSpawning()
    {
        _isSpawning = false;
        if (_waveCoroutine != null)
            StopCoroutine(_waveCoroutine);
    }
    
    private void OnDestroy()
    {
        StopSpawning();
    }
    
    private void OnDrawGizmos()
    {
        if (!showSpawnGizmos) return;
        
        Gizmos.color = new Color(0, 1, 0, 0.3f);
        Gizmos.DrawCube(spawnAreaCenter, spawnAreaSize);
        Gizmos.color = Color.green;
        Gizmos.DrawWireCube(spawnAreaCenter, spawnAreaSize);
        
        if (spawnPoints != null)
        {
            for (int i = 0; i < spawnPoints.Length; i++)
            {
                if (spawnPoints[i] == null) continue;
                
                // Color by zone classification
                if (_zonesClassified)
                {
                    if (_contestedPoints.Contains(i))
                        Gizmos.color = Color.red;          // Contested = RED (danger zone)
                    else if (_neutralPoints.Contains(i))
                        Gizmos.color = Color.yellow;       // Neutral = YELLOW
                    else if (_safePoints.Contains(i))
                        Gizmos.color = Color.green;        // Safe = GREEN
                    else
                        Gizmos.color = Color.white;
                }
                else
                {
                    Gizmos.color = Color.yellow;
                }
                
                Gizmos.DrawSphere(spawnPoints[i].position, 0.5f);
            }
            
            // Draw map center (contested zone center)
            if (_zonesClassified)
            {
                Gizmos.color = new Color(1f, 0f, 0f, 0.5f);
                Gizmos.DrawWireSphere(_mapCenter, 3f);
            }
        }
    }
}
