using System.Collections.Generic;
using UnityEngine;
using Colyseus;
using Colyseus.Schema;

public class ArcadeNetworkManager : MonoBehaviour
{
    public static ArcadeNetworkManager Instance { get; private set; }

    [Header("Connection")]
    [SerializeField] private string serverUrl = "https://al-arcade-server.caprover.al-arcade.com";
    [SerializeField] private string roomName = "lobby";

    [Header("Prefabs")]
    [SerializeField] private GameObject remotePlayerPrefab;
    [SerializeField] private GameObject localPlayerPrefab;

    [Header("Spawn")]
    [SerializeField] private Transform spawnPoint;

    private Client _client;
    private Room<LobbyState> _room;
    private Dictionary<string, RemotePlayerController> _remotePlayers = new();
    private GameObject _localPlayer;
    private PlayerNameTag _localNameTag;
    private string _myDisplayName;

    public Room<LobbyState> Room => _room;
    public string SessionId => _room?.SessionId;
    public bool IsConnected => _room != null;
    public string MyDisplayName => _myDisplayName;

    public event System.Action<string, NetworkWorldObject> OnObjectStateChanged;

    private void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    private async void Start()
    {
        _client = new Client(serverUrl);

        try
        {
            _room = await _client.JoinOrCreate<LobbyState>(roomName);
            Debug.Log($"[Network] Connected as {_room.SessionId}");
            BindCallbacks();
            SpawnLocalPlayer();
        }
        catch (System.Exception e)
        {
            Debug.LogError($"[Network] Connection failed: {e.Message}");
            SpawnLocalPlayerOffline();
        }
    }

    private void BindCallbacks()
    {
        var callbacks = Callbacks.Get(_room);

        callbacks.OnAdd(state => state.players, (sessionId, player) =>
        {
            if (sessionId == _room.SessionId)
            {
                _myDisplayName = player.displayName;
                UpdateLocalNameTag();
                return;
            }

            SpawnRemotePlayer(sessionId, player);

            callbacks.OnChange(player, () =>
            {
                UpdateRemotePlayer(sessionId, player);
            });
        });

        callbacks.OnRemove(state => state.players, (sessionId, player) =>
        {
            RemoveRemotePlayer(sessionId);
        });

        callbacks.OnAdd(state => state.objects, (objectId, worldObject) =>
        {
            callbacks.OnChange(worldObject, () =>
            {
                OnObjectStateChanged?.Invoke(objectId, worldObject);
            });
        });

        _room.OnLeave += (code) =>
        {
            Debug.Log($"[Network] Disconnected ({code})");
            ClearRemotePlayers();
        };
    }

    private void SpawnLocalPlayer()
    {
        if (localPlayerPrefab == null) return;

        Vector3 pos = spawnPoint != null ? spawnPoint.position : Vector3.zero;
        _localPlayer = Instantiate(localPlayerPrefab, pos, Quaternion.identity);
        _localNameTag = _localPlayer.GetComponentInChildren<PlayerNameTag>();
    }

    private void SpawnLocalPlayerOffline()
    {
        if (localPlayerPrefab == null) return;
        Vector3 pos = spawnPoint != null ? spawnPoint.position : Vector3.zero;
        _localPlayer = Instantiate(localPlayerPrefab, pos, Quaternion.identity);
        _localNameTag = _localPlayer.GetComponentInChildren<PlayerNameTag>();
    }

    private void SpawnRemotePlayer(string sessionId, NetworkPlayer playerData)
    {
        if (_remotePlayers.ContainsKey(sessionId)) return;
        if (remotePlayerPrefab == null) return;

        var pos = new Vector3(playerData.x, playerData.y, playerData.z);
        var go = Instantiate(remotePlayerPrefab, pos, Quaternion.Euler(0, playerData.rot, 0));
        go.name = $"Remote_{playerData.displayName}";

        var controller = go.GetComponent<RemotePlayerController>();
        if (controller != null)
        {
            controller.Initialize(sessionId, playerData.displayName, playerData.skinIndex);
            controller.SetTarget(pos, playerData.rot, playerData.anim);
        }

        _remotePlayers[sessionId] = controller;
    }

    private void UpdateRemotePlayer(string sessionId, NetworkPlayer playerData)
    {
        if (!_remotePlayers.TryGetValue(sessionId, out var controller)) return;
        if (controller == null) return;

        var pos = new Vector3(playerData.x, playerData.y, playerData.z);
        controller.SetTarget(pos, playerData.rot, playerData.anim);
        controller.UpdateDisplayName(playerData.displayName);
    }

    private void RemoveRemotePlayer(string sessionId)
    {
        if (!_remotePlayers.TryGetValue(sessionId, out var controller)) return;

        if (controller != null)
            Destroy(controller.gameObject);

        _remotePlayers.Remove(sessionId);
    }

    private void ClearRemotePlayers()
    {
        foreach (var kvp in _remotePlayers)
        {
            if (kvp.Value != null)
                Destroy(kvp.Value.gameObject);
        }
        _remotePlayers.Clear();
    }

    private void UpdateLocalNameTag()
    {
        if (_localNameTag != null)
            _localNameTag.SetName(_myDisplayName);
    }

    public void SendMove(Vector3 position, float rotation, string anim)
    {
        if (_room == null) return;

        _ = _room.Send("move", new Dictionary<string, object>
        {
            { "x", position.x },
            { "y", position.y },
            { "z", position.z },
            { "rot", rotation },
            { "anim", anim }
        });
    }

    public void SendRename(string newName)
    {
        if (_room == null) return;

        _ = _room.Send("rename", new Dictionary<string, object>
        {
            { "name", newName }
        });

        _myDisplayName = newName;
        UpdateLocalNameTag();
    }

    public void SendInteract(string objectId, string action = "")
    {
        if (_room == null) return;

        _ = _room.Send("interact", new Dictionary<string, object>
        {
            { "objectId", objectId },
            { "action", action }
        });
    }

    public void SendRelease(string objectId)
    {
        if (_room == null) return;

        _ = _room.Send("release", new Dictionary<string, object>
        {
            { "objectId", objectId }
        });
    }

    private async void OnDestroy()
    {
        if (_room != null)
            await _room.Leave();
    }
}
