// Assets/_Project/Scripts/Gameplay/ChatService.cs
using Unity.Collections;
using Unity.Netcode;
using UnityEngine;
using TD.Core;

namespace TD.Gameplay
{
    /// <summary>
    /// Networked chat singleton. Carries player-typed messages between peers and
    /// exposes a local-only entry point for system messages (life lost, income
    /// changes, etc.). UI consumers subscribe to <see cref="OnMessageReceived"/>
    /// to display the feed.
    /// </summary>
    /// <remarks>
    /// <b>Authority model.</b> Player messages go client → server (via
    /// <see cref="SubmitMessage"/> + <see cref="SubmitMessageServerRpc"/>) so the
    /// server gets a chance to validate/filter, then the server broadcasts to
    /// every peer via <see cref="BroadcastMessageClientRpc"/>. The host receives
    /// its own broadcast like any other client, so a single subscription path
    /// handles every message type uniformly.
    ///
    /// <b>System messages.</b> <see cref="PostLocalSystem"/> is local-only and
    /// does NOT cross the network. Callers typically invoke it from inside an
    /// already-replicated event (e.g. <see cref="WaveManager.OnLifeLost"/>, which
    /// fires on every peer via its own ClientRpc) so each peer posts the system
    /// message itself. This avoids paying a second round-trip for events that
    /// are inherently broadcast already.
    ///
    /// <b>Scene setup.</b> Drop a <c>ChatService</c> GameObject (with a
    /// <c>NetworkObject</c>) into the gameplay scene. NGO 2.x auto-discovers
    /// the prefab — no manual registration needed.
    /// </remarks>
    [RequireComponent(typeof(NetworkObject))]
    public class ChatService : NetworkBehaviour
    {
        // ----- Singleton --------------------------------------------------

        public static ChatService Instance { get; private set; }

        // ----- Message types ----------------------------------------------

        public enum MessageKind
        {
            Player,
            System,
        }

        /// <summary>
        /// One chat feed entry. <see cref="SenderName"/> is empty for system messages.
        /// </summary>
        public readonly struct ChatEntry
        {
            public readonly MessageKind Kind;
            public readonly string      SenderName;
            public readonly string      Text;

            public ChatEntry(MessageKind kind, string senderName, string text)
            {
                Kind       = kind;
                SenderName = senderName;
                Text       = text;
            }
        }

        // ----- Local events -----------------------------------------------

        /// <summary>
        /// Fires on every peer when a player message arrives (after the
        /// server's ClientRpc) OR when a local system message is posted via
        /// <see cref="PostLocalSystem"/>. HUD subscribes to render the feed.
        /// </summary>
        public static event System.Action<ChatEntry> OnMessageReceived;

        // ----- NGO lifecycle ----------------------------------------------

        public override void OnNetworkSpawn()
        {
            if (Instance != null && Instance != this)
            {
                Debug.LogError("[ChatService] Duplicate instance detected. " +
                               "Only one ChatService should exist per scene.");
                return;
            }
            Instance = this;
        }

        public override void OnNetworkDespawn()
        {
            if (Instance == this) Instance = null;
        }

        // ----- Player messages (network round-trip) -----------------------

        /// <summary>
        /// Submits a chat message authored by the local player. Empty / whitespace
        /// strings are dropped. Text is trimmed and truncated to fit the wire
        /// payload (~120 chars).
        /// </summary>
        public void SubmitMessage(string text)
        {
            if (string.IsNullOrWhiteSpace(text)) return;
            text = text.Trim();
            if (text.Length > 120) text = text.Substring(0, 120);

            FixedString128Bytes payload = default;
            payload.Append(text);
            SubmitMessageRpc(payload);
        }

        // NGO 2.x unified RPC attribute. SendTo.Server + InvokePermission.Everyone
        // is the modern replacement for [ServerRpc(RequireOwnership = false)] —
        // any client (not just the NetworkObject's owner) may invoke it.
        [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
        private void SubmitMessageRpc(FixedString128Bytes text, RpcParams rpcParams = default)
        {
            // Validation / filtering hook — drop spam, profanity, etc. For now we
            // just broadcast unchanged. The server origin tag means whatever
            // appears in clients' chat is what the server allowed through.
            BroadcastMessageRpc(rpcParams.Receive.SenderClientId, text);
        }

        // SendTo.Everyone matches the old [ClientRpc] behavior under host mode —
        // the body runs on every peer including the host's local client, so the
        // host sees its own messages in the feed without a separate local call.
        [Rpc(SendTo.Everyone)]
        private void BroadcastMessageRpc(ulong senderClientId, FixedString128Bytes text)
        {
            string senderName = ResolveSenderName(senderClientId);
            OnMessageReceived?.Invoke(
                new ChatEntry(MessageKind.Player, senderName, text.ToString()));
        }

        // ----- System messages (local only) -------------------------------

        /// <summary>
        /// Posts a system message to the local chat feed only. Does NOT cross the
        /// network. Callers should invoke this from a code path that's already
        /// replicated on every peer (e.g. a ClientRpc handler or an event that's
        /// fired on every peer) so each peer sees the message exactly once.
        /// </summary>
        public static void PostLocalSystem(string text)
        {
            if (string.IsNullOrEmpty(text)) return;
            OnMessageReceived?.Invoke(
                new ChatEntry(MessageKind.System, "", text));
        }

        // ----- Helpers ----------------------------------------------------

        private static string ResolveSenderName(ulong clientId)
        {
            var pms = PlayerMatchState.GetForClient(clientId);
            if (pms != null && pms.Slot != PlayerSlot.None)
                return $"P{(int)pms.Slot}";
            return $"Client {clientId}";
        }
    }
}
