package com.zb.mom.ww.mxgateway.client;

import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.ClientCallStreamObserver;
import io.grpc.stub.ClientResponseObserver;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;

/**
 * Iterator-style adaptor over the gateway {@code StreamEvents} server-streaming
 * RPC.
 *
 * <p>Events arrive on a background gRPC thread and are buffered in a bounded
 * blocking queue; the iterator drains them on the calling thread. Closing the
 * stream cancels the underlying gRPC call. If the queue overflows the call is
 * cancelled and a follow-up call to {@link #next()} throws
 * {@link MxGatewayException}.
 *
 * <p><strong>Single consumer.</strong> This stream is not safe to drain from
 * more than one thread. Interleave {@link #hasNext()}/{@link #next()} (or
 * {@link #nextItem()}) from a single consumer only; concurrent drains race on
 * the internal cursor.
 *
 * <p><strong>Reconnect-replay gap.</strong> When the stream was resumed via
 * {@code StreamEventsRequest.after_worker_sequence} and the requested cursor
 * predates the oldest event the gateway still retains, the gateway emits a
 * single gap sentinel {@link MxEvent} at the head of the stream with its
 * {@code replay_gap} field set (family unspecified, body oneof unset). It is a
 * distinct, non-terminal signal, forwarded verbatim — never synthesized and
 * never swallowed. {@link #next()} returns it as a normal {@link MxEvent}
 * (callers can test {@code event.hasReplayGap()}); {@link #nextItem()} wraps it
 * in an {@link MxEventStreamItem} whose {@link MxEventStreamItem#isReplayGap()}
 * is {@code true}. On a gap the consumer must discard cached per-item state and
 * re-snapshot, then resume without a further gap by requesting
 * {@code oldest_available_sequence - 1} as the next {@code after_worker_sequence}.
 */
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
    private static final Object END = new Object();

    private final BlockingQueue<Object> queue;
    private volatile ClientCallStreamObserver<StreamEventsRequest> requestStream;
    private volatile boolean closed;
    private Object next;

    MxEventStream(int capacity) {
        queue = new ArrayBlockingQueue<>(capacity);
    }

    ClientResponseObserver<StreamEventsRequest, MxEvent> observer() {
        return new ClientResponseObserver<>() {
            @Override
            public void beforeStart(ClientCallStreamObserver<StreamEventsRequest> requestStream) {
                MxEventStream.this.requestStream = requestStream;
            }

            @Override
            public void onNext(MxEvent value) {
                offer(value);
            }

            @Override
            public void onError(Throwable error) {
                if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed) {
                    offer(END);
                    return;
                }
                offer(error);
            }

            @Override
            public void onCompleted() {
                offer(END);
            }
        };
    }

    @Override
    public boolean hasNext() {
        if (next == END) {
            return false;
        }
        if (next == null) {
            next = take();
        }
        if (next instanceof RuntimeException runtimeException) {
            next = END;
            throw runtimeException;
        }
        if (next instanceof Throwable throwable) {
            next = END;
            throw new MxGatewayException("gateway stream events failed: " + throwable.getMessage(), throwable);
        }
        return next != END;
    }

    @Override
    public MxEvent next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        Object value = next;
        next = null;
        return (MxEvent) value;
    }

    /**
     * Drains the next stream element as a typed {@link MxEventStreamItem} so a
     * consumer can branch on the reconnect-replay gap sentinel via
     * {@link MxEventStreamItem#isReplayGap()} without inspecting the raw event.
     *
     * <p>Equivalent to wrapping {@link #next()}; the gap sentinel is surfaced as
     * a distinct typed item rather than being swallowed, and normal events are
     * returned unchanged on {@link MxEventStreamItem#event()}. Do not mix
     * {@link #next()} and {@code nextItem()} on the same element — each call
     * advances the single shared cursor.
     *
     * @return the next stream item
     * @throws NoSuchElementException if the stream is exhausted
     */
    public MxEventStreamItem nextItem() {
        return new MxEventStreamItem(next());
    }

    @Override
    public void close() {
        closed = true;
        ClientCallStreamObserver<StreamEventsRequest> stream = requestStream;
        if (stream != null) {
            stream.cancel("client cancelled event stream", null);
        }
        offer(END);
    }

    private Object take() {
        while (true) {
            try {
                return queue.take();
            } catch (InterruptedException error) {
                Thread.currentThread().interrupt();
                return new StatusRuntimeException(Status.CANCELLED.withDescription("interrupted while reading events"));
            }
        }
    }

    private void offer(Object value) {
        Objects.requireNonNull(value, "value");
        if (value == END) {
            if (!queue.offer(value)) {
                queue.clear();
                queue.offer(value);
            }
            return;
        }
        if (!queue.offer(value)) {
            ClientCallStreamObserver<StreamEventsRequest> stream = requestStream;
            if (stream != null) {
                stream.cancel("client event stream queue overflowed", null);
            }
            queue.clear();
            queue.offer(new MxGatewayException("gateway stream events queue overflowed"));
            queue.offer(END);
        }
    }
}
