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

import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.Duration;
import io.grpc.Channel;
import io.grpc.ClientInterceptors;
import io.grpc.ManagedChannel;
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
import io.grpc.stub.StreamObserver;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLException;
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmReply;
import mxaccess_gateway.v1.MxaccessGateway.AcknowledgeAlarmRequest;
import mxaccess_gateway.v1.MxaccessGateway.ActiveAlarmSnapshot;
import mxaccess_gateway.v1.MxaccessGateway.AlarmFeedMessage;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
import mxaccess_gateway.v1.MxaccessGateway.QueryActiveAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.StreamAlarmsRequest;
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;

/**
 * Idiomatic Java wrapper around the generated {@code MxAccessGateway} gRPC
 * stubs.
 *
 * <p>Owns or borrows a {@link ManagedChannel}, attaches a
 * {@link MxGatewayAuthInterceptor} carrying the configured API key, and
 * exposes blocking, future, and async stub variants. Translates protocol
 * status failures into typed {@link MxGatewayException} subclasses.
 */
public final class MxGatewayClient implements AutoCloseable {
    private final ManagedChannel ownedChannel;
    private final MxGatewayClientOptions options;
    private final MxAccessGatewayGrpc.MxAccessGatewayBlockingStub blockingStub;
    private final MxAccessGatewayGrpc.MxAccessGatewayFutureStub futureStub;
    private final MxAccessGatewayGrpc.MxAccessGatewayStub asyncStub;

    private MxGatewayClient(ManagedChannel channel, MxGatewayClientOptions options) {
        this.ownedChannel = channel;
        this.options = options;
        Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
        blockingStub = MxAccessGatewayGrpc.newBlockingStub(intercepted);
        futureStub = MxAccessGatewayGrpc.newFutureStub(intercepted);
        asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
    }

    /**
     * Constructs a client over a caller-managed {@link Channel}. The caller
     * owns channel lifecycle; {@link #close()} is a no-op for this constructor.
     *
     * @param channel the gRPC channel to use for outbound calls
     * @param options the client options carrying the API key and timeouts
     * @throws NullPointerException if {@code options} is {@code null}
     */
    public MxGatewayClient(Channel channel, MxGatewayClientOptions options) {
        this.ownedChannel = null;
        this.options = Objects.requireNonNull(options, "options");
        Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
        blockingStub = MxAccessGatewayGrpc.newBlockingStub(intercepted);
        futureStub = MxAccessGatewayGrpc.newFutureStub(intercepted);
        asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
    }

    /**
     * Builds a new client and owns its channel; {@link #close()} shuts the
     * channel down.
     *
     * @param options the client options carrying the endpoint and credentials
     * @return a connected client
     */
    public static MxGatewayClient connect(MxGatewayClientOptions options) {
        return new MxGatewayClient(createChannel(options), options);
    }

    /**
     * Returns the underlying blocking stub with the per-call deadline applied.
     *
     * @return the blocking stub
     */
    public MxAccessGatewayGrpc.MxAccessGatewayBlockingStub rawBlockingStub() {
        return withDeadline(blockingStub);
    }

    /**
     * Returns the underlying future stub with the per-call deadline applied.
     *
     * @return the future stub
     */
    public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
        return withDeadline(futureStub);
    }

    /**
     * Returns the underlying async stub. Stream deadlines are applied per call.
     *
     * @return the async stub
     */
    public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
        return asyncStub;
    }

    /**
     * Opens a gateway session and returns a typed handle for further commands.
     *
     * @param request the {@code OpenSessionRequest} to send
     * @return a session bound to the resulting {@code OpenSessionReply}
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxGatewaySession openSession(OpenSessionRequest request) {
        OpenSessionReply reply = openSessionRaw(request);
        return new MxGatewaySession(this, reply);
    }

    /**
     * Opens a gateway session using the configured call timeout for the
     * worker command timeout and a caller-supplied client session name.
     *
     * @param clientSessionName the human-readable session name reported by the gateway
     * @return a session bound to the resulting {@code OpenSessionReply}
     * @throws MxGatewayException on transport or protocol failure
     */
    public MxGatewaySession openSession(String clientSessionName) {
        return openSession(OpenSessionRequest.newBuilder()
                .setClientSessionName(clientSessionName)
                .setCommandTimeout(Duration.newBuilder()
                        .setSeconds(options.callTimeout().toSeconds())
                        .setNanos(options.callTimeout().toNanosPart())
                        .build())
                .build());
    }

    /**
     * Invokes {@code OpenSession} and returns the raw reply.
     *
     * @param request the {@code OpenSessionRequest} to send
     * @return the raw {@code OpenSessionReply}
     * @throws MxGatewayException on transport or protocol failure
     */
    public OpenSessionReply openSessionRaw(OpenSessionRequest request) {
        try {
            OpenSessionReply reply = rawBlockingStub().openSession(request);
            MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
            return reply;
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("open session", error);
        }
    }

    /**
     * Invokes {@code OpenSession} asynchronously.
     *
     * @param request the {@code OpenSessionRequest} to send
     * @return a future completed with the raw reply, or completed exceptionally
     *         with {@link MxGatewayException} on failure
     */
    public CompletableFuture<OpenSessionReply> openSessionAsync(OpenSessionRequest request) {
        CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
        return future.thenApply(reply -> {
            MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
            return reply;
        });
    }

    /**
     * Invokes the {@code Invoke} unary RPC and validates both the protocol
     * status and any MXAccess-side failure carried in the reply.
     *
     * @param request the {@code MxCommandRequest} to send
     * @return the raw command reply
     * @throws MxGatewayException on transport or protocol failure
     * @throws MxAccessException when the worker reports an MXAccess COM-side failure
     */
    public MxCommandReply invoke(MxCommandRequest request) {
        try {
            MxCommandReply reply = rawBlockingStub().invoke(request);
            MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
            MxGatewayErrors.ensureMxAccessSuccess("invoke", reply);
            return reply;
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("invoke", error);
        }
    }

    /**
     * Invokes the {@code Invoke} RPC asynchronously.
     *
     * @param request the {@code MxCommandRequest} to send
     * @return a future completed with the raw reply, or completed exceptionally
     *         with {@link MxGatewayException} (including {@link MxAccessException})
     *         on failure
     */
    public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request) {
        CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
        return future.thenApply(reply -> {
            MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
            MxGatewayErrors.ensureMxAccessSuccess("invoke", reply);
            return reply;
        });
    }

    /**
     * Invokes the {@code CloseSession} unary RPC.
     *
     * @param request the {@code CloseSessionRequest} to send
     * @return the raw reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public CloseSessionReply closeSessionRaw(CloseSessionRequest request) {
        try {
            CloseSessionReply reply = rawBlockingStub().closeSession(request);
            MxGatewayErrors.ensureProtocolSuccess("close session", reply.getProtocolStatus(), null);
            return reply;
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("close session", error);
        }
    }

    /**
     * Subscribes to the {@code StreamEvents} server-streaming RPC and exposes
     * results as a blocking iterator. Closing the returned stream cancels the
     * underlying gRPC call.
     *
     * @param request the {@code StreamEventsRequest} carrying the session id and resume cursor
     * @return an iterator-style stream of events
     */
    public MxEventStream streamEvents(StreamEventsRequest request) {
        MxEventStream stream = new MxEventStream(16);
        withStreamDeadline(rawAsyncStub()).streamEvents(request, stream.observer());
        return stream;
    }

    /**
     * Subscribes to {@code StreamEvents} and dispatches each event to the
     * supplied observer. The returned subscription is cancellable.
     *
     * @param request the {@code StreamEventsRequest} to send
     * @param observer caller-supplied observer that receives events and completion
     * @return a cancellable subscription handle
     */
    public MxGatewayEventSubscription streamEventsAsync(
            StreamEventsRequest request, StreamObserver<MxEvent> observer) {
        MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
        withStreamDeadline(rawAsyncStub()).streamEvents(request, subscription.wrap(observer));
        return subscription;
    }

    /**
     * Acknowledges an active MXAccess alarm condition through the gateway.
     *
     * <p>The gateway authenticates the request against the API key's
     * {@code invoke:alarm-ack} scope and forwards the acknowledge to the
     * worker's MXAccess session; the resulting native MxStatus is returned
     * in the reply. Acks are idempotent at the MxAccess layer.
     *
     * @param request the {@code AcknowledgeAlarmRequest}
     * @return the raw acknowledge reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public AcknowledgeAlarmReply acknowledgeAlarm(AcknowledgeAlarmRequest request) {
        try {
            AcknowledgeAlarmReply reply = rawBlockingStub().acknowledgeAlarm(request);
            MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null);
            return reply;
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("acknowledge alarm", error);
        }
    }

    /**
     * Acknowledges an active MXAccess alarm condition asynchronously.
     *
     * @param request the {@code AcknowledgeAlarmRequest}
     * @return a future completed with the raw reply, or completed exceptionally
     *         with {@link MxGatewayException} on failure
     */
    public CompletableFuture<AcknowledgeAlarmReply> acknowledgeAlarmAsync(AcknowledgeAlarmRequest request) {
        CompletableFuture<AcknowledgeAlarmReply> future = toCompletable(rawFutureStub().acknowledgeAlarm(request));
        return future.thenApply(reply -> {
            MxGatewayErrors.ensureProtocolSuccess("acknowledge alarm", reply.getProtocolStatus(), null);
            return reply;
        });
    }

    /**
     * Streams a snapshot of all alarms currently Active or ActiveAcked — the
     * gateway's ConditionRefresh equivalent. Used after reconnect to seed
     * local Part 9 state.
     *
     * @param request the {@code QueryActiveAlarmsRequest}, optionally scoped by
     *                alarm-reference prefix
     * @param observer caller-supplied observer that receives snapshots and completion
     * @return a cancellable subscription handle
     */
    public MxGatewayActiveAlarmsSubscription queryActiveAlarms(
            QueryActiveAlarmsRequest request, StreamObserver<ActiveAlarmSnapshot> observer) {
        MxGatewayActiveAlarmsSubscription subscription = new MxGatewayActiveAlarmsSubscription();
        withStreamDeadline(rawAsyncStub()).queryActiveAlarms(request, subscription.wrap(observer));
        return subscription;
    }

    /**
     * Attaches to the gateway's central alarm feed. The stream opens with one
     * {@code AlarmFeedMessage} per currently-active alarm (the ConditionRefresh
     * snapshot), then a single {@code snapshot_complete}, then a
     * {@code transition} for every subsequent raise / acknowledge / clear.
     *
     * <p>Served by the gateway's always-on alarm monitor — no worker session is
     * opened — so any number of clients may attach.
     *
     * @param request the {@code StreamAlarmsRequest}, optionally scoped by
     *                alarm-reference prefix
     * @param observer caller-supplied observer that receives feed messages and completion
     * @return a cancellable subscription handle
     */
    public MxGatewayAlarmFeedSubscription streamAlarms(
            StreamAlarmsRequest request, StreamObserver<AlarmFeedMessage> observer) {
        MxGatewayAlarmFeedSubscription subscription = new MxGatewayAlarmFeedSubscription();
        withStreamDeadline(rawAsyncStub()).streamAlarms(request, subscription.wrap(observer));
        return subscription;
    }

    @Override
    public void close() {
        if (ownedChannel != null) {
            ownedChannel.shutdown();
        }
    }

    /**
     * Shuts the owned channel down and waits up to the configured connect
     * timeout for termination, forcibly shutting it down on timeout. No-op
     * for clients that do not own their channel.
     *
     * @throws InterruptedException if the calling thread is interrupted while waiting
     */
    public void closeAndAwaitTermination() throws InterruptedException {
        if (ownedChannel != null) {
            ownedChannel.shutdown();
            if (!ownedChannel.awaitTermination(options.connectTimeout().toMillis(), TimeUnit.MILLISECONDS)) {
                ownedChannel.shutdownNow();
            }
        }
    }

    private static ManagedChannel createChannel(MxGatewayClientOptions options) {
        NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
                .maxInboundMessageSize(options.maxGrpcMessageBytes());
        if (!options.connectTimeout().isNegative()) {
            builder.withOption(
                    io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
                    Math.toIntExact(options.connectTimeout().toMillis()));
        }
        if (options.plaintext()) {
            builder.usePlaintext();
        } else if (options.caCertificatePath() != null) {
            try {
                builder.sslContext(GrpcSslContexts.forClient()
                        .trustManager(options.caCertificatePath().toFile())
                        .build());
            } catch (SSLException error) {
                throw new MxGatewayException("failed to configure gateway TLS", error);
            }
        } else if (!options.requireCertificateValidation()) {
            try {
                builder.sslContext(GrpcSslContexts.forClient()
                        .trustManager(io.grpc.netty.shaded.io.netty.handler.ssl.util
                                .InsecureTrustManagerFactory.INSTANCE)
                        .build());
            } catch (SSLException error) {
                throw new MxGatewayException("failed to configure lenient gateway TLS", error);
            }
        } else {
            builder.useTransportSecurity();
        }
        if (!options.serverNameOverride().isBlank()) {
            builder.overrideAuthority(options.serverNameOverride());
        }
        return builder.build();
    }

    /**
     * Package-visible test seam — creates a raw {@link ManagedChannel} from the
     * given options without attaching auth interceptors. Used by TLS fixture
     * tests to verify channel construction behaviour without a full
     * {@link MxGatewayClient} wrapper.
     *
     * @param options the client options
     * @return a new {@link ManagedChannel}
     */
    static ManagedChannel createChannelForTests(MxGatewayClientOptions options) {
        return createChannel(options);
    }

    private <T extends io.grpc.stub.AbstractStub<T>> T withDeadline(T stub) {
        if (options.callTimeout().isNegative()) {
            return stub;
        }
        return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
    }

    private <T extends io.grpc.stub.AbstractStub<T>> T withStreamDeadline(T stub) {
        if (options.streamTimeout() == null || options.streamTimeout().isNegative()) {
            return stub;
        }
        return stub.withDeadlineAfter(options.streamTimeout().toNanos(), TimeUnit.NANOSECONDS);
    }

    private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
        CompletableFuture<T> target = new CompletableFuture<>();
        Futures.addCallback(
                source,
                new FutureCallback<>() {
                    @Override
                    public void onSuccess(T result) {
                        target.complete(result);
                    }

                    @Override
                    public void onFailure(Throwable error) {
                        if (error instanceof RuntimeException runtimeException) {
                            target.completeExceptionally(MxGatewayErrors.fromGrpc("async call", runtimeException));
                            return;
                        }
                        target.completeExceptionally(error);
                    }
                },
                MoreExecutors.directExecutor());
        target.whenComplete((ignoredResult, ignoredError) -> {
            if (target.isCancelled()) {
                source.cancel(true);
            }
        });
        return target;
    }

    static ProtocolStatusCode okStatusCode() {
        return ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK;
    }
}
