using System.Collections.Generic;
using FishNet;
using FishNet.Object;
using FishNet.Object.Synchronizing;
using UnityEngine;
using IStalk.Data;
using IStalk.Runner;

namespace IStalk.Eye
{
    public class LoSManager : NetworkBehaviour
    {
        public static LoSManager Instance { get; private set; }

        [SerializeField] private LayerMask _wallLayer;
        [SerializeField] private LayerMask _runnerLayer;

        private readonly SyncList<int> _visibleRunnerIds = new();
        private float _tickInterval;
        private float _tickTimer;
        private List<RunnerController> _runners = new();

        private Dictionary<int, float> _invisibleRunners = new();
        private List<DecoyBlip> _decoyBlips = new();
        private List<BlindZone> _blindZones = new();

        private GlobalRules_SO Rules => GlobalRules_SO.Instance;

        public IReadOnlyList<int> VisibleRunnerIds => (IReadOnlyList<int>)_visibleRunnerIds;
        public IReadOnlyList<DecoyBlip> ActiveDecoys => _decoyBlips;
        public event System.Action<List<int>> OnVisibilityChanged;

        public override void OnStartNetwork()
        {
            base.OnStartNetwork();
            Instance = this;
        }

        public override void OnStartServer()
        {
            base.OnStartServer();
            _tickInterval = 1f / Rules.losTicksPerSecond;
        }

        public void RegisterRunner(RunnerController runner)
        {
            if (!_runners.Contains(runner))
                _runners.Add(runner);
        }

        public void UnregisterRunner(RunnerController runner)
        {
            _runners.Remove(runner);
        }

        private void Update()
        {
            if (!IsServerStarted) return;

            TickTimers(Time.deltaTime);

            _tickTimer += Time.deltaTime;
            if (_tickTimer < _tickInterval) return;
            _tickTimer -= _tickInterval;

            CleanupNullRunners();
            PerformLoSCheck();
        }

        private void TickTimers(float dt)
        {
            var expiredInvis = new List<int>();
            var keys = new List<int>(_invisibleRunners.Keys);
            foreach (int key in keys)
            {
                _invisibleRunners[key] -= dt;
                if (_invisibleRunners[key] <= 0)
                    expiredInvis.Add(key);
            }
            foreach (int id in expiredInvis) _invisibleRunners.Remove(id);

            for (int i = _decoyBlips.Count - 1; i >= 0; i--)
            {
                _decoyBlips[i].TimeLeft -= dt;
                if (_decoyBlips[i].TimeLeft <= 0) _decoyBlips.RemoveAt(i);
            }

            for (int i = _blindZones.Count - 1; i >= 0; i--)
            {
                _blindZones[i].TimeLeft -= dt;
                if (_blindZones[i].TimeLeft <= 0) _blindZones.RemoveAt(i);
            }
        }

        private void CleanupNullRunners()
        {
            for (int i = _runners.Count - 1; i >= 0; i--)
            {
                if (_runners[i] == null)
                    _runners.RemoveAt(i);
            }
        }

        private void PerformLoSCheck()
        {
            Vector3 eyePos = transform.position;
            eyePos.y = 50f;
            var newVisible = new List<int>();

            foreach (var runner in _runners)
            {
                if (_invisibleRunners.ContainsKey(runner.ObjectId)) continue;

                Vector3 runnerPos = runner.transform.position + Vector3.up;

                bool blinded = false;
                foreach (var zone in _blindZones)
                {
                    if (Vector3.Distance(runnerPos, zone.Center) < zone.Radius)
                    {
                        blinded = true;
                        break;
                    }
                }
                if (blinded) continue;

                Vector3 dir = runnerPos - eyePos;
                float dist = dir.magnitude;

                if (dist > Rules.losMaxDistance) continue;

                if (!Physics.Raycast(eyePos, dir.normalized, dist, _wallLayer))
                {
                    newVisible.Add(runner.ObjectId);
                }
            }

            foreach (var decoy in _decoyBlips)
                newVisible.Add(decoy.FakeId);

            bool changed = newVisible.Count != _visibleRunnerIds.Count;
            if (!changed)
            {
                for (int i = 0; i < newVisible.Count; i++)
                {
                    if (!_visibleRunnerIds.Contains(newVisible[i]))
                    {
                        changed = true;
                        break;
                    }
                }
            }

            if (changed)
            {
                _visibleRunnerIds.Clear();
                foreach (var id in newVisible)
                    _visibleRunnerIds.Add(id);
                OnVisibilityChanged?.Invoke(newVisible);
            }
        }

        public bool IsRunnerVisible(int objectId) => _visibleRunnerIds.Contains(objectId);

        public void SetRunnerInvisible(int objectId, float duration)
        {
            _invisibleRunners[objectId] = duration;
        }

        public void AddDecoyBlip(Vector3 position, float duration)
        {
            int fakeId = -(1000 + _decoyBlips.Count);
            _decoyBlips.Add(new DecoyBlip { Position = position, TimeLeft = duration, FakeId = fakeId });
        }

        public void BlindEyeArea(Vector3 center, float radius, float duration)
        {
            _blindZones.Add(new BlindZone { Center = center, Radius = radius, TimeLeft = duration });
        }

        public void RevealAllRunners(float duration)
        {
            foreach (var runner in _runners)
            {
                if (!_visibleRunnerIds.Contains(runner.ObjectId))
                    _visibleRunnerIds.Add(runner.ObjectId);
            }
        }
    }

    public class DecoyBlip
    {
        public Vector3 Position;
        public float TimeLeft;
        public int FakeId;
    }

    public class BlindZone
    {
        public Vector3 Center;
        public float Radius;
        public float TimeLeft;
    }
}
