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}.
 */
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;
    }

    @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);
        }
    }
}
