using Grpc.Core;
using ZB.MOM.WW.MxGateway.Contracts;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Server.Grpc;

public sealed class MxAccessGrpcRequestValidator
{
    /// <summary>Validates an open session request.</summary>
    /// <param name="request">The request to validate.</param>
    public void ValidateOpenSession(OpenSessionRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);

        if (request.CommandTimeout is not null && request.CommandTimeout.ToTimeSpan() <= TimeSpan.Zero)
        {
            throw InvalidArgument("Command timeout must be greater than zero when provided.");
        }
    }

    /// <summary>Validates a close session request.</summary>
    /// <param name="request">The request to validate.</param>
    public void ValidateCloseSession(CloseSessionRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);
        RequireSessionId(request.SessionId);
    }

    /// <summary>Validates a stream events request.</summary>
    /// <param name="request">The request to validate.</param>
    public void ValidateStreamEvents(StreamEventsRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);
        RequireSessionId(request.SessionId);
    }

    /// <summary>Validates an invoke request with command payload.</summary>
    /// <param name="request">The request to validate.</param>
    public void ValidateInvoke(MxCommandRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);
        RequireSessionId(request.SessionId);

        if (request.Command is null)
        {
            throw InvalidArgument("Invoke requires a command payload.");
        }

        if (request.Command.Kind is MxCommandKind.Unspecified)
        {
            throw InvalidArgument("Invoke requires a command kind.");
        }

        ValidateCommandPayload(request.Command);
    }

    private static void RequireSessionId(string sessionId)
    {
        if (string.IsNullOrWhiteSpace(sessionId))
        {
            throw InvalidArgument("Session id is required.");
        }
    }

    private static void ValidateCommandPayload(MxCommand command)
    {
        MxCommand.PayloadOneofCase expectedPayload = ExpectedPayload(command.Kind);
        if (command.PayloadCase != expectedPayload)
        {
            throw InvalidArgument(
                $"Command kind {command.Kind} requires payload {expectedPayload} but received {command.PayloadCase}.");
        }

        // The payload case now matches the kind, so command.DrainEvents is non-null here.
        // DrainEvents is a diagnostics RPC that returns buffered events in one non-streaming
        // reply, so an unbounded max_events could pack the whole queue into a session-killing
        // frame. The worker independently clamps every reply to the same shared ceiling and
        // additionally caps it by serialized bytes; this public bound rejects an obviously-abusive
        // request loudly at the boundary. max_events = 0 is allowed and means "the worker's
        // default batch cap".
        if (command.Kind is MxCommandKind.DrainEvents
            && command.DrainEvents.MaxEvents > GatewayContractInfo.MaxDrainEventsPerCommand)
        {
            throw InvalidArgument(
                $"DrainEvents max_events ({command.DrainEvents.MaxEvents}) must not exceed "
                + $"{GatewayContractInfo.MaxDrainEventsPerCommand}; "
                + "use 0 to request the worker default batch cap.");
        }
    }

    private static MxCommand.PayloadOneofCase ExpectedPayload(MxCommandKind kind)
    {
        return kind switch
        {
            MxCommandKind.Register => MxCommand.PayloadOneofCase.Register,
            MxCommandKind.Unregister => MxCommand.PayloadOneofCase.Unregister,
            MxCommandKind.AddItem => MxCommand.PayloadOneofCase.AddItem,
            MxCommandKind.AddItem2 => MxCommand.PayloadOneofCase.AddItem2,
            MxCommandKind.RemoveItem => MxCommand.PayloadOneofCase.RemoveItem,
            MxCommandKind.Advise => MxCommand.PayloadOneofCase.Advise,
            MxCommandKind.UnAdvise => MxCommand.PayloadOneofCase.UnAdvise,
            MxCommandKind.AdviseSupervisory => MxCommand.PayloadOneofCase.AdviseSupervisory,
            MxCommandKind.AddBufferedItem => MxCommand.PayloadOneofCase.AddBufferedItem,
            MxCommandKind.SetBufferedUpdateInterval => MxCommand.PayloadOneofCase.SetBufferedUpdateInterval,
            MxCommandKind.Suspend => MxCommand.PayloadOneofCase.Suspend,
            MxCommandKind.Activate => MxCommand.PayloadOneofCase.Activate,
            MxCommandKind.Write => MxCommand.PayloadOneofCase.Write,
            MxCommandKind.Write2 => MxCommand.PayloadOneofCase.Write2,
            MxCommandKind.WriteSecured => MxCommand.PayloadOneofCase.WriteSecured,
            MxCommandKind.WriteSecured2 => MxCommand.PayloadOneofCase.WriteSecured2,
            MxCommandKind.AuthenticateUser => MxCommand.PayloadOneofCase.AuthenticateUser,
            MxCommandKind.ArchestraUserToId => MxCommand.PayloadOneofCase.ArchestraUserToId,
            MxCommandKind.AddItemBulk => MxCommand.PayloadOneofCase.AddItemBulk,
            MxCommandKind.AdviseItemBulk => MxCommand.PayloadOneofCase.AdviseItemBulk,
            MxCommandKind.RemoveItemBulk => MxCommand.PayloadOneofCase.RemoveItemBulk,
            MxCommandKind.UnAdviseItemBulk => MxCommand.PayloadOneofCase.UnAdviseItemBulk,
            MxCommandKind.SubscribeBulk => MxCommand.PayloadOneofCase.SubscribeBulk,
            MxCommandKind.UnsubscribeBulk => MxCommand.PayloadOneofCase.UnsubscribeBulk,
            MxCommandKind.WriteBulk => MxCommand.PayloadOneofCase.WriteBulk,
            MxCommandKind.Write2Bulk => MxCommand.PayloadOneofCase.Write2Bulk,
            MxCommandKind.WriteSecuredBulk => MxCommand.PayloadOneofCase.WriteSecuredBulk,
            MxCommandKind.WriteSecured2Bulk => MxCommand.PayloadOneofCase.WriteSecured2Bulk,
            MxCommandKind.ReadBulk => MxCommand.PayloadOneofCase.ReadBulk,
            MxCommandKind.Ping => MxCommand.PayloadOneofCase.Ping,
            MxCommandKind.GetSessionState => MxCommand.PayloadOneofCase.GetSessionState,
            MxCommandKind.GetWorkerInfo => MxCommand.PayloadOneofCase.GetWorkerInfo,
            MxCommandKind.DrainEvents => MxCommand.PayloadOneofCase.DrainEvents,
            MxCommandKind.ShutdownWorker => MxCommand.PayloadOneofCase.ShutdownWorker,
            _ => MxCommand.PayloadOneofCase.None,
        };
    }

    private static RpcException InvalidArgument(string detail)
    {
        return new RpcException(new Status(StatusCode.InvalidArgument, detail));
    }
}
