using System.Security.Claims;
using AyCode.Core;
using AyCode.Core.Extensions;
using AyCode.Core.Helpers;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Models.Server.DynamicMethods;
using AyCode.Services.SignalRs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;

namespace AyCode.Services.Server.SignalRs;

public abstract class AcWebSignalRHubBase<TSignalRTags, TLogger>(IConfiguration configuration, TLogger logger)
    : Hub<IAcSignalRHubItemServer>, IAcSignalRHubServer where TSignalRTags : AcSignalRTags where TLogger : AcLoggerBase
{
    /// <summary>
    /// Registry for dynamic method lookups with lazy initialization.
    /// </summary>
    protected readonly AcDynamicMethodRegistry<SignalRAttribute> DynamicMethodRegistry = new();
    
    protected TLogger Logger = logger;
    protected IConfiguration Configuration = configuration;

    // Static init: route protocol & deserializer diagnostics through AcLoggerBase
    private static readonly object _diagnosticInitLock = new();
    private static bool _diagnosticInitialized;

    protected void InitDiagnosticLoggerIfNeeded()
    {
        if (_diagnosticInitialized) return;
        lock (_diagnosticInitLock)
        {
            if (_diagnosticInitialized) return;
            AcBinaryHubProtocol.DiagnosticLogger ??= msg => Logger.Debug(msg);
            AcBinaryDeserializer.DiagnosticLogger ??= msg => Logger.Debug(msg);
            _diagnosticInitialized = true;
        }
    }

    protected AcSerializerOptions SerializerOptions = new AcBinarySerializerOptions();

    /// <summary>
    /// Enable diagnostic logging for binary serialization debugging.
    /// Set to true to log hex dumps of serialized response data.
    /// </summary>
    public static bool EnableBinaryDiagnostics { get; set; }

    #region Connection Lifecycle

    public override async Task OnConnectedAsync()
    {
        InitDiagnosticLoggerIfNeeded();

        Logger.Debug($"Server OnConnectedAsync; ConnectionId: {GetConnectionId()}; UserIdentifier: {GetUserIdentifier()}");

        LogContextUserNameAndId();
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var connectionId = GetConnectionId();
        var userIdentifier = GetUserIdentifier();
        
        if (exception == null)
            Logger.Debug($"Server OnDisconnectedAsync; ConnectionId: {connectionId}; UserIdentifier: {userIdentifier}");
        else
            Logger.Error($"Server OnDisconnectedAsync; ConnectionId: {connectionId}; UserIdentifier: {userIdentifier}", exception);

        LogContextUserNameAndId();
        await base.OnDisconnectedAsync(exception);
    }

    #endregion

    #region Message Processing

    public virtual Task OnReceiveMessage(int messageTag, int? requestId, SignalParams signalParams, object data)
    {
        return ProcessOnReceiveMessage(messageTag, signalParams, requestId, null);
    }

    public virtual IAsyncEnumerable<byte[]> OnReceiveStreamMessage(int messageTag, byte[]? messageBytes)
    {
        var parameterBytes = messageBytes is { Length: > 0 }
            ? SignalRSerializationHelper.DeserializeFromBinary<byte[][]>(messageBytes)
            : null;
        return ProcessOnStreamMessage(messageTag, parameterBytes, Context.ConnectionAborted);
    }

    protected virtual async IAsyncEnumerable<byte[]> ProcessOnStreamMessage(int messageTag, byte[][]? parameterBytes, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var tagName = ConstHelper.NameByValue<TSignalRTags>(messageTag);

        Logger.Debug($"Server OnReceiveStreamMessage; ConnectionId: {GetConnectionId()}; {tagName}");

        try
        {
            if (AcDomain.IsDeveloperVersion) LogContextUserNameAndId();

            // Build SignalParams from raw byte[][] for stream path
            var signalParams = new SignalParams { Status = SignalResponseStatus.Success };
            if (parameterBytes is { Length: > 0 })
                signalParams.Parameters = parameterBytes.ToBinary();

            if (TryFindAndInvokeMethod(messageTag, signalParams, tagName, out var responseData))
            {
                if (responseData == null)
                    yield break;

                var resultType = responseData.GetType();
                var elementType = GetAsyncEnumerableElementType(resultType);

                if (elementType != null)
                {
                    var typedEnumerable = GetTypedStream(elementType, responseData, messageTag, cancellationToken);
                    await foreach (var chunk in typedEnumerable.WithCancellation(cancellationToken))
                    {
                        yield return chunk;
                    }
                }
                else
                {
                    Logger.Warning($"Method '{tagName}' does not return IAsyncEnumerable. Returning normal message as single chunk.");
                    var responseMessage = CreateStreamResponseMessage(messageTag, SignalResponseStatus.Success, responseData);
                    yield return SignalRSerializationHelper.SerializeToBinary(responseMessage);
                }
            }
            else
            {
                Logger.Warning($"Not found dynamic method for the tag! {tagName}");
            }
        }
        finally
        {
            Logger.Debug($"Server closed OnReceiveStreamMessage; ConnectionId: {GetConnectionId()}; {tagName}");
        }
    }

    private static readonly System.Collections.Concurrent.ConcurrentDictionary<Type, System.Reflection.MethodInfo> _streamMethods = new();

    private IAsyncEnumerable<byte[]> GetTypedStream(Type elementType, object responseData, int messageTag, CancellationToken ct)
    {
        var methodInfo = _streamMethods.GetOrAdd(elementType, type =>
            typeof(AcWebSignalRHubBase<TSignalRTags, TLogger>)
                .GetMethod(nameof(EnumerateGenericAsync), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
                .MakeGenericMethod(type));

        return (IAsyncEnumerable<byte[]>)methodInfo.Invoke(this, [responseData, messageTag, ct])!;
    }

    private async IAsyncEnumerable<byte[]> EnumerateGenericAsync<T>(object result, int messageTag, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
    {
        var enumerable = (IAsyncEnumerable<T>)result;
        await foreach (var item in enumerable.WithCancellation(cancellationToken))
        {
            if (item is byte[] bytes)
            {
                yield return bytes;
            }
            else if (item is ISignalRMessage sigMsg)
            {
                yield return SignalRSerializationHelper.SerializeToBinary(sigMsg);
            }
            else
            {
                var msg = CreateStreamResponseMessage(messageTag, SignalResponseStatus.Success, item);
                yield return SignalRSerializationHelper.SerializeToBinary(msg);
            }
        }
    }

    private static Type? GetAsyncEnumerableElementType(Type type)
    {
        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))
            return type.GetGenericArguments()[0];

        foreach (var intf in type.GetInterfaces())
        {
            if (intf.IsGenericType && intf.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))
                return intf.GetGenericArguments()[0];
        }

        return null;
    }

    protected virtual async Task ProcessOnReceiveMessage(int messageTag, SignalParams signalParams, int? requestId, Func<string, Task>? notFoundCallback)
    {
        var tagName = ConstHelper.NameByValue<TSignalRTags>(messageTag);

        Logger.Debug($"Server OnReceiveMessage; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}");

        try
        {
            if (AcDomain.IsDeveloperVersion) LogContextUserNameAndId();

            if (TryFindAndInvokeMethod(messageTag, signalParams, tagName, out var responseData))
            {
                if (Logger.LogLevel <= LogLevel.Debug)
                    Logger.Debug($"responseData ready ({SerializerOptions.SerializerType})");

                await ResponseToCaller(messageTag, SignalResponseStatus.Success, responseData, requestId, signalParams);
                return;
            }

            Logger.Warning($"Not found dynamic method for the tag! {tagName}");
            notFoundCallback?.Invoke(tagName);
        }
        catch (Exception ex)
        {
            Logger.Error($"Server OnReceiveMessage; {ex.Message}; {tagName}", ex);
        }

        await ResponseToCaller(messageTag, SignalResponseStatus.Error, null, requestId);
    }

    /// <summary>
    /// Creates a SignalResponseDataMessage for stream path (serialized as wire format blob).
    /// Main send path uses SendMessageToClient directly — no wrapper needed.
    /// </summary>
    protected SignalResponseDataMessage CreateStreamResponseMessage(int messageTag, SignalResponseStatus status, object? responseData)
        => new(messageTag, status, responseData, SerializerOptions);

    /// <summary>
    /// Finds and invokes the method registered for the given message tag.
    /// </summary>
    private bool TryFindAndInvokeMethod(int messageTag, SignalParams signalParams, string tagName, out object? responseData)
    {
        responseData = null;

        var result = DynamicMethodRegistry.GetMethodByMessageTag(messageTag);
        if (!result.HasValue)
            return false;

        var (instance, methodInfoModel) = result.Value;
        var methodName = $"{instance.GetType().Name}.{methodInfoModel.MethodInfo.Name}";
        var paramValues = DeserializeParameters(signalParams, methodInfoModel, tagName, methodName);

        Logger.Debug(paramValues == null 
            ? $"Found dynamic method for the tag! method: {methodName}(); {tagName}" 
            : $"Found dynamic method for the tag! method: {methodName}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}");

        responseData = methodInfoModel.MethodInfo.InvokeMethod(instance, paramValues);

        if (methodInfoModel.Attribute.SendToOtherClientType != SendToClientType.None)
            SendMessageToOthers(methodInfoModel.Attribute.SendToOtherClientTag, responseData).Forget();

        return true;
    }

    /// <summary>
    /// Deserializes parameters from SignalParams using GetParameterValues.
    /// Validates that required parameters are present.
    /// </summary>
    private static object[]? DeserializeParameters(SignalParams signalParams, AcMethodInfoModel<SignalRAttribute> methodInfoModel, string tagName, string methodName)
    {
        var paramInfos = methodInfoModel.ParamInfos;
        if (paramInfos is not { Length: > 0 })
            return null;

        var paramValues = signalParams.GetParameterValues(paramInfos);

        if (paramValues is null)
        {
            if (paramInfos.All(p => p.HasDefaultValue))
                return paramInfos.Select(p => p.DefaultValue).ToArray();

            throw new ArgumentException($"Message has no parameters but method '{methodName}' requires parameters; {tagName}");
        }

        // Validate: null in a non-optional parameter slot means it was missing
        for (var i = 0; i < paramInfos.Length; i++)
        {
            if (paramValues[i] is null && !paramInfos[i].HasDefaultValue && paramInfos[i].ParameterType.IsValueType)
                throw new ArgumentException($"Method '{methodName}' requires parameter '{paramInfos[i].Name}' (index {i}) but it was not sent; {tagName}");
        }

        return paramValues;
    }

    #endregion

    #region Response Methods

    protected virtual Task ResponseToCallerWithContent(int messageTag, object? content)
        => SendMessageToClient(Clients.Caller, messageTag, SignalResponseStatus.Success, content);

    protected virtual Task ResponseToCaller(int messageTag, SignalResponseStatus status, object? responseData, int? requestId, SignalParams? clientSignalParams = null)
        => SendMessageToClient(Clients.Caller, messageTag, status, responseData, requestId, clientSignalParams);

    protected virtual Task SendMessageToUserIdWithContent(string userId, int messageTag, object? content)
        => SendMessageToClient(Clients.User(userId), messageTag, SignalResponseStatus.Success, content);

    protected virtual Task SendMessageToConnectionIdWithContent(string connectionId, int messageTag, object? content)
        => SendMessageToClient(Clients.Client(connectionId), messageTag, SignalResponseStatus.Success, content);

    protected virtual Task SendMessageToOthers(int messageTag, object? content)
        => SendMessageToClient(Clients.Others, messageTag, SignalResponseStatus.Success, content);

    protected virtual Task SendMessageToAll(int messageTag, object? content)
        => SendMessageToClient(Clients.All, messageTag, SignalResponseStatus.Success, content);

    /// <summary>
    /// Sends message to client. Protocol serializes responseData directly to pipe (zero-copy write).
    /// clientSignalParams: the original client request's SignalParams (contains IsRawBytesData flag).
    /// </summary>
    protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag,
        SignalResponseStatus status, object? responseData, int? requestId = null, SignalParams? clientSignalParams = null)
    {
        var isRawBytes = clientSignalParams?.IsRawBytesData == true;

        // IsRawBytesData: client wants raw byte[] — pre-serialize here, protocol passes through as-is.
        // Single serialize (here) → single deserialize (consumer). No double ser/deser.
        if (isRawBytes && responseData != null && responseData is not byte[])
        {
            // Pass explicit runtime type — the generic ToBinary<T>() overload would infer T = object
            // here (since `responseData` is statically `object?`), emitting an object-typed wire payload
            // instead of the concrete type. See ACCORE-BIN bug fix 2026-05-26.
            responseData = SerializerOptions.SerializerType == AcSerializerType.Binary
                ? responseData.ToBinary(responseData.GetType())
                : AyCode.Core.Compression.GzipHelper.Compress(responseData.ToJson());
        }

        //responseData = AcBinarySerializer.Serialize(responseData);
        // TODO: BWO serialize bug workaround — pre-serialize responseData to byte[] here so protocol uses
        // byte[] fast-path, bypassing AcBinarySerializer.Serialize(value, IBufferWriter<byte>, options).
        // Set isRawBytes = true so client deserializes via ArrayBinaryInput. Remove when BWO bug is fixed.
        var signalParams = new SignalParams
        {
            Status = status,
            DataSerializerType = SerializerOptions.SerializerType,
            IsRawBytesData = isRawBytes
        };

        var tagName = ConstHelper.NameByValue<TSignalRTags>(messageTag);

        Logger.Debug($"Server sending message to client; requestId: {requestId}; Aborted: {IsConnectionAborted()}; ConnectionId: {GetConnectionId()}; {tagName}");

        await sendTo.OnReceiveMessage(messageTag, requestId, signalParams, responseData!);

        Logger.Debug($"Server sent message to client; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}");
    }

    #endregion

    #region Context Accessor Methods

    protected virtual string GetConnectionId() => Context.ConnectionId;
    protected virtual bool IsConnectionAborted() => Context.ConnectionAborted.IsCancellationRequested;
    protected virtual string? GetUserIdentifier() => Context.UserIdentifier;
    protected virtual ClaimsPrincipal? GetUser() => Context.User;

    #endregion

    #region Logging

    protected virtual void LogContextUserNameAndId()
    {
        var user = GetUser();
        if (user == null) return;

        var userName = user.Identity?.Name;
        Guid.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out var userId);

        if (AcDomain.IsDeveloperVersion) 
            Logger.WarningConditional($"SignalR.Context; userName: {userName}; userId: {userId}");
        else 
            Logger.Debug($"SignalR.Context; userName: {userName}; userId: {userId}");
    }

    #endregion
}