#nullable enable
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text;

namespace Barotrauma.Networking
{
    [NetworkSerialize]
    internal struct PeerPacketHeaders : INetSerializableStruct
    {
        public DeliveryMethod DeliveryMethod;
        public PacketHeader PacketHeader;
        public ConnectionInitialization? Initialization;

        public readonly void Deconstruct(
            out DeliveryMethod deliveryMethod,
            out PacketHeader packetHeader,
            out ConnectionInitialization? initialization)
        {
            deliveryMethod = DeliveryMethod;
            packetHeader = PacketHeader;
            initialization = Initialization;
        }
    }

    [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
    internal struct ClientAuthTicketAndVersionPacket : INetSerializableStruct
    {
        public string Name;
        public Option<int> OwnerKey;
        public Option<AccountId> AccountId;
        public Option<AuthenticationTicket> AuthTicket;
        public string GameVersion;
        public Identifier Language;
    }

    [NetworkSerialize]
    internal readonly record struct P2POwnerToServerHeader
        (string? EndpointStr, AccountInfo AccountInfo) : INetSerializableStruct
    {
        public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr ?? "");
    }

    [NetworkSerialize]
    internal readonly record struct P2PServerToOwnerHeader
        (string? EndpointStr) : INetSerializableStruct
    {
        public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr ?? "");
    }

    [NetworkSerialize]
    internal struct P2PInitializationRelayPacket : INetSerializableStruct
    {
        public ulong LobbyID;
        public PeerPacketMessage Message;
    }

    [NetworkSerialize]
    internal readonly record struct P2PInitializationOwnerPacket(
        string Name,
        AccountId AccountId)
    : INetSerializableStruct;


    [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
    internal struct ServerPeerContentPackageOrderPacket : INetSerializableStruct
    {
        public string ServerName;
        public ImmutableArray<ServerContentPackage> ContentPackages;
        public bool AllowModDownloads;
    }

    [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
    internal struct PeerPacketMessage : INetSerializableStruct
    {
        public byte[] Buffer;
        public readonly int Length => Buffer.Length;

        public readonly IReadMessage GetReadMessageUncompressed() => new ReadWriteMessage(Buffer, 0, Length * 8, copyBuf: false);
        public readonly IReadMessage GetReadMessage(bool isCompressed, NetworkConnection conn) => new ReadOnlyMessage(Buffer, isCompressed, 0, Length, conn);
    }

    [NetworkSerialize(ArrayMaxSize = byte.MaxValue)]
    internal struct ClientPeerPasswordPacket : INetSerializableStruct
    {
        public byte[] Password;
    }

    [NetworkSerialize]
    internal struct ServerPeerPasswordPacket : INetSerializableStruct
    {
        public Option<int> Salt;
        public Option<int> RetriesLeft;
    }

    [NetworkSerialize]
    internal readonly record struct DoSProtectionPacket(string EndpointStr, bool ShouldBan) : INetSerializableStruct
    {
        public Option<P2PEndpoint> Endpoint => P2PEndpoint.Parse(EndpointStr);
    }

    [NetworkSerialize]
    internal readonly struct PeerDisconnectPacket : INetSerializableStruct
    {
        public readonly DisconnectReason DisconnectReason;

        public readonly string AdditionalInformation;

        private PeerDisconnectPacket(
            DisconnectReason disconnectReason,
            string additionalInformation = "")
        {
            DisconnectReason = disconnectReason;
            AdditionalInformation = additionalInformation;
        }

        public LocalizedString ChatMessage(string? name)
        {
            if (string.IsNullOrEmpty(name))
            {
                name = TextManager.Get("ServerMessage.UnknownClient").Value;
            }

            LocalizedString message = DisconnectReason switch
            {
                DisconnectReason.Disconnected => TextManager.GetWithVariable("ServerMessage.ClientLeftServer",
                        "[client]", name),
                DisconnectReason.Banned => TextManager.GetWithVariable("servermessage.bannedfromserver", "[client]", name),
                DisconnectReason.Kicked => TextManager.GetWithVariable("servermessage.kickedfromserver", "[client]", name),
                _ => TextManager.GetWithVariables("ChatMsg.DisconnectedWithReason",
                        ("[client]", name),
                        ("[reason]", TextManager.Get($"ChatMsg.DisconnectReason.{DisconnectReason}")))
            };
            if (!string.IsNullOrEmpty(AdditionalInformation) &&
                DisconnectReason is DisconnectReason.Banned or DisconnectReason.Kicked)
            {
                message += " "+ TextManager.Get("banreason") + " " + TextManager.GetServerMessage(AdditionalInformation);
            }
            return message;
        }


        private LocalizedString MsgWithReason
            => TextManager.Get($"DisconnectReason.{DisconnectReason}")
               + "\n\n"
               + TextManager.Get("banreason") + " " + TextManager.GetServerMessage(AdditionalInformation);

        private LocalizedString ServerMessage
            => TextManager.Get($"ServerMessage.{DisconnectReason}");
        
        public LocalizedString PopupMessage
            => DisconnectReason switch
            {
                DisconnectReason.Banned => MsgWithReason,
                DisconnectReason.Kicked => MsgWithReason,
                DisconnectReason.InvalidVersion => TextManager.GetWithVariables("DisconnectMessage.InvalidVersion",
                    ("[version]", AdditionalInformation),
                    ("[clientversion]", GameMain.Version.ToString())),
                DisconnectReason.ExcessiveDesyncOldEvent => ServerMessage,
                DisconnectReason.ExcessiveDesyncRemovedEvent => ServerMessage,
                DisconnectReason.SyncTimeout => ServerMessage,
                DisconnectReason.AuthenticationFailed => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback(TextManager.Get("ChatMsg.DisconnectReason.AuthenticationRequired")),
                _ => TextManager.Get($"DisconnectReason.{DisconnectReason}").Fallback($"{TextManager.Get("ConnectionLost")} ({DisconnectReason})")
            };

        public LocalizedString ReconnectMessage
            => PopupMessage + "\n\n" + TextManager.Get("ConnectionLostReconnecting");

        public PlayerConnectionChangeType ConnectionChangeType
            => DisconnectReason switch
            {
                DisconnectReason.Banned => PlayerConnectionChangeType.Banned,
                DisconnectReason.Kicked => PlayerConnectionChangeType.Kicked,
                _ => PlayerConnectionChangeType.Disconnected
            };

        public bool ShouldAttemptReconnect
            => DisconnectReason
                is DisconnectReason.ExcessiveDesyncOldEvent
                or DisconnectReason.ExcessiveDesyncRemovedEvent
                or DisconnectReason.Timeout
                or DisconnectReason.SyncTimeout
                or DisconnectReason.SteamP2PTimeOut;

        public bool IsEventSyncError
            => DisconnectReason
                is DisconnectReason.ExcessiveDesyncOldEvent
                or DisconnectReason.ExcessiveDesyncRemovedEvent
                or DisconnectReason.SyncTimeout;

        public bool ShouldCreateAnalyticsEvent
            => DisconnectReason is not (
                   DisconnectReason.Disconnected
                   or DisconnectReason.ServerShutdown
                   or DisconnectReason.ServerFull
                   or DisconnectReason.Banned
                   or DisconnectReason.Kicked
                   or DisconnectReason.TooManyFailedLogins
                   or DisconnectReason.InvalidVersion);

        /// <summary>
        /// This exists because Lidgren doesn't readily support
        /// sending anything other than a string through a disconnect
        /// packet, so this thing needs a sufficiently nasty string
        /// representation that can be decoded with some certainty
        /// that it won't get mangled by user input.
        /// </summary>
        public string ToLidgrenStringRepresentation()
        {
            static string strToBase64(string str)
                => Convert.ToBase64String(Encoding.UTF8.GetBytes(str));

            return DisconnectReason
                   + NetworkMagicStrings.LidgrenDisconnectSeparator
                   + strToBase64(AdditionalInformation);
        }

        public static Option<PeerDisconnectPacket> FromLidgrenStringRepresentation(string str)
        {
            // Lidgren has some hardcoded disconnect strings that it uses
            // when it detects that a connection has failed. We can handle
            // timeouts, so let's look for strings related to that and return
            // an appropriate PeerDisconnectPacket.
            switch (str)
            {
                case Lidgren.Network.NetConnection.NoResponseMessage:
                case "Connection timed out":
                case "Reconnecting":
                    return Option.Some(WithReason(DisconnectReason.Timeout));
            }
            
            static string base64ToStr(string base64)
                => Encoding.UTF8.GetString(Convert.FromBase64String(base64));

            string[] split = str.Split(NetworkMagicStrings.LidgrenDisconnectSeparator);
            if (split.Length != 2) { return Option.None; }
            if (!Enum.TryParse(split[0], out DisconnectReason disconnectReason)) { return Option.None; }
            return Option.Some(new PeerDisconnectPacket(disconnectReason, base64ToStr(split[1])));
        }
        
        public static PeerDisconnectPacket Custom(string customMessage)
            => new PeerDisconnectPacket(
                DisconnectReason.Unknown,
                customMessage);

        public static PeerDisconnectPacket WithReason(DisconnectReason disconnectReason)
            => new PeerDisconnectPacket(disconnectReason);
        
        public static PeerDisconnectPacket Kicked(string? msg)
            => new PeerDisconnectPacket(DisconnectReason.Kicked, msg ?? "");

        public static PeerDisconnectPacket Banned(string? msg)
            => new PeerDisconnectPacket(DisconnectReason.Banned, msg ?? "");
        
        public static PeerDisconnectPacket InvalidVersion()
            => new PeerDisconnectPacket(
                DisconnectReason.InvalidVersion,
                GameMain.Version.ToString());

        public static PeerDisconnectPacket SteamP2PError(Steamworks.P2PSessionError error)
            => new PeerDisconnectPacket(
                DisconnectReason.SteamP2PError,
                error.ToString());
        
        public static PeerDisconnectPacket SteamAuthError(Steamworks.BeginAuthResult error)
            => new PeerDisconnectPacket(
                DisconnectReason.AuthenticationFailed,
                $"{nameof(Steamworks.BeginAuthResult)}.{error}");
        
        public static PeerDisconnectPacket SteamAuthError(Steamworks.AuthResponse error)
            => new PeerDisconnectPacket(
                DisconnectReason.AuthenticationFailed,
                $"{nameof(Steamworks.AuthResponse)}.{error}");
    }

    // ReSharper disable MemberCanBePrivate.Global, FieldCanBeMadeReadOnly.Global, UnassignedField.Global
    public sealed class ServerContentPackage : INetSerializableStruct
    {
        [NetworkSerialize]
        public string Name = "";

        [NetworkSerialize(ArrayMaxSize = ushort.MaxValue)]
        public byte[] HashBytes = Array.Empty<byte>();

        [NetworkSerialize]
        public string UgcId = "";

        [NetworkSerialize]
        public uint InstallTimeDiffInSeconds;

        [NetworkSerialize]
        public bool IsMandatory;

        [NetworkSerialize]
        public bool IsVanilla;

        private Md5Hash? cachedHash;
        private DateTime? cachedDateTime;

        public Md5Hash Hash
        {
            get => cachedHash ??= Md5Hash.BytesAsHash(HashBytes);
            set
            {
                cachedHash = value;
                HashBytes = value.ByteRepresentation;
            }
        }

        public DateTime InstallTime => cachedDateTime ??= DateTime.UtcNow + TimeSpan.FromSeconds(InstallTimeDiffInSeconds);
        public RegularPackage? RegularPackage => 
            ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name.Equals(Name) && p.Hash.Equals(Hash)) ??
            ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Hash.Equals(Hash));

        public CorePackage? CorePackage =>
            ContentPackageManager.CorePackages.FirstOrDefault(p => p.Name.Equals(Name) && p.Hash.Equals(Hash)) ??
            ContentPackageManager.CorePackages.FirstOrDefault(p => p.Hash.Equals(Hash));
        public ContentPackage? ContentPackage => (ContentPackage?)RegularPackage ?? CorePackage;

        public ServerContentPackage() { }
        
        public ServerContentPackage(ContentPackage contentPackage, SerializableDateTime referenceTime)
        {
            Name = contentPackage.Name;
            Hash = contentPackage.Hash;
            UgcId = contentPackage.UgcId.TryUnwrap(out var ugcId)
                ? ugcId.StringRepresentation
                : "";
            IsMandatory = !contentPackage.Files.All(f => f is SubmarineFile);
            IsVanilla = contentPackage == ContentPackageManager.VanillaCorePackage;
            InstallTimeDiffInSeconds =
                contentPackage.InstallTime.TryUnwrap(out var installTime)
                    ? (uint)(installTime - referenceTime).TotalSeconds
                    : 0;
        }

        public string GetPackageStr() => $"\"{Name}\" (hash {Hash.ShortRepresentation})";
    }
}