using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

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

/// <summary>
/// Maps between worker IPC types and gRPC contract types.
/// </summary>
public sealed class MxAccessGrpcMapper
{
    private readonly TimeProvider _timeProvider;

    /// <summary>
    /// Initializes the mapper with an optional time provider.
    /// </summary>
    /// <param name="timeProvider">Time provider for timestamps; defaults to system time if null.</param>
    public MxAccessGrpcMapper(TimeProvider? timeProvider = null)
    {
        _timeProvider = timeProvider ?? TimeProvider.System;
    }

    /// <summary>
    /// Maps a gRPC MX command request to a worker command.
    /// </summary>
    /// <param name="request">Request payload.</param>
    /// <returns>The mapped <see cref="WorkerCommand"/> ready for worker dispatch.</returns>
    public WorkerCommand MapCommand(MxCommandRequest request)
    {
        ArgumentNullException.ThrowIfNull(request);
        ArgumentNullException.ThrowIfNull(request.Command);

        return MapCommand(request.Command);
    }

    /// <summary>
    /// Maps a gRPC MX command to a worker command. Callers that already hold the command — including
    /// the constraint pipeline, whose rewritten command is not the one on the request — use this
    /// overload rather than cloning a whole request to carry a single field (GWC-29); nothing outside
    /// the command is read here.
    /// </summary>
    /// <param name="command">Command payload.</param>
    /// <returns>The mapped <see cref="WorkerCommand"/> ready for worker dispatch.</returns>
    public WorkerCommand MapCommand(MxCommand command)
    {
        ArgumentNullException.ThrowIfNull(command);

        // The clone is required and must stay: the caller may hand us the gRPC-owned request command,
        // and the caller keeps reading it after dispatch (TrackCommandReply). Cloning here is what makes
        // WorkerClient.CreateCommandEnvelope's no-aliasing invariant true.
        return new WorkerCommand
        {
            Command = command.Clone(),
            EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()),
        };
    }

    /// <summary>
    /// Maps a worker command reply to a gRPC MX command reply.
    /// </summary>
    /// <param name="reply">Worker command reply.</param>
    /// <returns>The mapped <see cref="MxCommandReply"/>, or a protocol-violation reply if the worker reply carried no public payload.</returns>
    public MxCommandReply MapCommandReply(WorkerCommandReply reply)
    {
        ArgumentNullException.ThrowIfNull(reply);

        if (reply.Reply is null)
        {
            return new MxCommandReply
            {
                ProtocolStatus = ProtocolViolation("Worker command reply did not contain a public reply payload."),
            };
        }

        // GWC-07 / IPC-05: ownership transfer, not a deep clone — the same rule MapEvent follows,
        // applied to the other (and larger, on bulk reads) hot-path message. The enclosing
        // WorkerCommandReply is parsed fresh from a single pipe frame in WorkerClient's read loop
        // and is single-consumer by construction: CompleteCommand's TryRemove hands it to exactly
        // one PendingCommand awaiter, that awaiter is the gRPC Invoke handler, and the handler's
        // one call is this mapping. Nothing else aliases or reads reply.Reply afterwards — the
        // enclosing WorkerCommandReply is discarded here. We therefore move the inner
        // MxCommandReply into the gRPC response instead of copying it; the handler owning it
        // outright is also what makes BulkConstraintPlan.MergeDeniedInto's in-place splice safe.
        // If a second consumer of the same WorkerCommandReply is ever added, restore a .Clone().
        return reply.Reply;
    }

    /// <summary>
    /// Maps a worker event to a gRPC MX event.
    /// </summary>
    /// <param name="workerEvent">Worker event to map.</param>
    /// <returns>The mapped <see cref="MxEvent"/>, or an unspecified-family event if the worker event carried no public payload.</returns>
    public MxEvent MapEvent(WorkerEvent workerEvent)
    {
        ArgumentNullException.ThrowIfNull(workerEvent);

        // GWC-07 / IPC-05: ownership transfer, not a deep clone. The enclosing WorkerEvent is
        // parsed fresh from a single pipe frame in WorkerClient's read loop and is discarded
        // immediately after this mapping — the SessionEventDistributor pump is its single
        // consumer (GWC-01 claims the worker event channel as single-reader), so nothing else
        // aliases or mutates workerEvent.Event. We therefore move the inner MxEvent into the
        // outbound graph instead of cloning it. Downstream the pump fans this one MxEvent to
        // every subscriber and retains it in the replay ring, but that sharing is READ-ONLY
        // (subscribers only yield/filter it), so a single shared instance is safe. If a second
        // consumer of WorkerEvent is ever added, restore a .Clone() here to re-isolate.
        return workerEvent.Event ?? new MxEvent
        {
            Family = MxEventFamily.Unspecified,
            RawStatus = "Worker event did not contain a public event payload.",
        };
    }

    /// <summary>
    /// Creates an OK protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Ok"/>.</returns>
    public static ProtocolStatus Ok(string message = "OK")
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.Ok,
            Message = message,
        };
    }

    /// <summary>
    /// Creates an InvalidRequest protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.InvalidRequest"/>.</returns>
    public static ProtocolStatus InvalidRequest(string message)
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.InvalidRequest,
            Message = message,
        };
    }

    /// <summary>
    /// Creates a SessionNotFound protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.SessionNotFound"/>.</returns>
    public static ProtocolStatus SessionNotFound(string message)
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.SessionNotFound,
            Message = message,
        };
    }

    /// <summary>
    /// Creates a SessionNotReady protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.SessionNotReady"/>.</returns>
    public static ProtocolStatus SessionNotReady(string message)
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.SessionNotReady,
            Message = message,
        };
    }

    /// <summary>
    /// Creates a WorkerUnavailable protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.WorkerUnavailable"/>.</returns>
    public static ProtocolStatus WorkerUnavailable(string message)
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.WorkerUnavailable,
            Message = message,
        };
    }

    /// <summary>
    /// Creates a Timeout protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Timeout"/>.</returns>
    public static ProtocolStatus Timeout(string message)
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.Timeout,
            Message = message,
        };
    }

    /// <summary>
    /// Creates a Canceled protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.Canceled"/>.</returns>
    public static ProtocolStatus Canceled(string message)
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.Canceled,
            Message = message,
        };
    }

    /// <summary>
    /// Creates a ProtocolViolation protocol status.
    /// </summary>
    /// <param name="message">Status message.</param>
    /// <returns>A <see cref="ProtocolStatus"/> with code <see cref="ProtocolStatusCode.ProtocolViolation"/>.</returns>
    public static ProtocolStatus ProtocolViolation(string message)
    {
        return new ProtocolStatus
        {
            Code = ProtocolStatusCode.ProtocolViolation,
            Message = message,
        };
    }
}
