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 galaxy_repository.v1.GalaxyRepositoryGrpc;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.BrowseChildrenReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.BrowseChildrenRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DeployEvent;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.DiscoverHierarchyRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GalaxyObject;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.GetLastDeployTimeRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionReply;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.TestConnectionRequest;
import galaxy_repository.v1.GalaxyRepositoryOuterClass.WatchDeployEventsRequest;
import com.google.protobuf.Timestamp;
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.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLException;

/**
 * Thin wrapper around the generated {@link GalaxyRepositoryGrpc} stubs that
 * exposes the three metadata-only RPCs of the Galaxy Repository service in
 * idiomatic Java types. Mirrors the constructor and option-handling style of
 * {@link MxGatewayClient}.
 */
public final class GalaxyRepositoryClient implements AutoCloseable {
    private static final int DISCOVER_HIERARCHY_PAGE_SIZE = 5000;
    private static final int BROWSE_CHILDREN_PAGE_SIZE = 500;

    private final ManagedChannel ownedChannel;
    private final MxGatewayClientOptions options;
    private final GalaxyRepositoryGrpc.GalaxyRepositoryBlockingStub blockingStub;
    private final GalaxyRepositoryGrpc.GalaxyRepositoryFutureStub futureStub;
    private final GalaxyRepositoryGrpc.GalaxyRepositoryStub asyncStub;

    private GalaxyRepositoryClient(ManagedChannel channel, MxGatewayClientOptions options) {
        this.ownedChannel = channel;
        this.options = options;
        Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
        blockingStub = GalaxyRepositoryGrpc.newBlockingStub(intercepted);
        futureStub = GalaxyRepositoryGrpc.newFutureStub(intercepted);
        asyncStub = GalaxyRepositoryGrpc.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 GalaxyRepositoryClient(Channel channel, MxGatewayClientOptions options) {
        this.ownedChannel = null;
        this.options = Objects.requireNonNull(options, "options");
        Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
        blockingStub = GalaxyRepositoryGrpc.newBlockingStub(intercepted);
        futureStub = GalaxyRepositoryGrpc.newFutureStub(intercepted);
        asyncStub = GalaxyRepositoryGrpc.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 GalaxyRepositoryClient connect(MxGatewayClientOptions options) {
        return new GalaxyRepositoryClient(createChannel(options), options);
    }

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

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

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

    /**
     * Invokes the {@code TestConnection} RPC and returns the {@code ok} flag.
     *
     * @return {@code true} when the gateway reached the Galaxy Repository database
     * @throws MxGatewayException on transport or protocol failure
     */
    public boolean testConnection() {
        try {
            TestConnectionReply reply = rawBlockingStub().testConnection(TestConnectionRequest.getDefaultInstance());
            return reply.getOk();
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("galaxy test connection", error);
        }
    }

    /**
     * Invokes {@code TestConnection} asynchronously.
     *
     * @return a future completed with the {@code ok} flag, or completed
     *         exceptionally with {@link MxGatewayException} on failure
     */
    public CompletableFuture<Boolean> testConnectionAsync() {
        return toCompletable(rawFutureStub().testConnection(TestConnectionRequest.getDefaultInstance()))
                .thenApply(TestConnectionReply::getOk);
    }

    /**
     * Invokes the {@code GetLastDeployTime} RPC.
     *
     * @return the time of the last deploy, or {@link Optional#empty()} when the
     *         server reports {@code present=false}
     * @throws MxGatewayException on transport or protocol failure
     */
    public Optional<Instant> getLastDeployTime() {
        try {
            GetLastDeployTimeReply reply =
                    rawBlockingStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance());
            return mapDeployTime(reply);
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("galaxy get last deploy time", error);
        }
    }

    /**
     * Invokes {@code GetLastDeployTime} asynchronously.
     *
     * @return a future completed with the time of the last deploy, or
     *         {@link Optional#empty()} when the server reports {@code present=false};
     *         completed exceptionally with {@link MxGatewayException} on failure
     */
    public CompletableFuture<Optional<Instant>> getLastDeployTimeAsync() {
        return toCompletable(rawFutureStub().getLastDeployTime(GetLastDeployTimeRequest.getDefaultInstance()))
                .thenApply(GalaxyRepositoryClient::mapDeployTime);
    }

    /**
     * Invokes the {@code DiscoverHierarchy} RPC and returns the generated
     * {@link GalaxyObject} messages directly. Callers can read every field of
     * the proto message without an extra DTO layer.
     *
     * @return the Galaxy object hierarchy
     * @throws MxGatewayException on transport or protocol failure
     */
    public List<GalaxyObject> discoverHierarchy() {
        try {
            java.util.ArrayList<GalaxyObject> objects = new java.util.ArrayList<>();
            java.util.HashSet<String> seenPageTokens = new java.util.HashSet<>();
            String pageToken = "";
            do {
                DiscoverHierarchyReply reply = rawBlockingStub().discoverHierarchy(DiscoverHierarchyRequest.newBuilder()
                        .setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
                        .setPageToken(pageToken)
                        .build());
                objects.addAll(reply.getObjectsList());
                pageToken = reply.getNextPageToken();
                if (!pageToken.isBlank() && !seenPageTokens.add(pageToken)) {
                    throw new MxGatewayException(
                            "galaxy discover hierarchy returned repeated page token: " + pageToken);
                }
            } while (!pageToken.isBlank());
            return objects;
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("galaxy discover hierarchy", error);
        }
    }

    /**
     * Invokes {@code DiscoverHierarchy} asynchronously.
     *
     * @return a future completed with the Galaxy object hierarchy, or completed
     *         exceptionally with {@link MxGatewayException} on failure
     */
    public CompletableFuture<List<GalaxyObject>> discoverHierarchyAsync() {
        return discoverHierarchyPageAsync("", new java.util.ArrayList<>(), new java.util.HashSet<>());
    }

    /**
     * Lazy-browse entry point: fetches the root layer of the Galaxy hierarchy.
     * Each returned {@link LazyBrowseNode} can be expanded on demand via
     * {@link LazyBrowseNode#expand()} to load its direct children.
     *
     * @return the root nodes (no parent selector) with default options
     * @throws MxGatewayException on transport or protocol failure
     */
    public List<LazyBrowseNode> browse() {
        return browse(null);
    }

    /**
     * Lazy-browse entry point with caller-supplied filters / shape.
     *
     * @param options filter and shape options; {@code null} means {@link BrowseChildrenOptions#empty()}
     * @return the root nodes matching the options
     * @throws MxGatewayException on transport or protocol failure
     */
    public List<LazyBrowseNode> browse(BrowseChildrenOptions options) {
        BrowseChildrenOptions effective = options == null ? BrowseChildrenOptions.empty() : options;
        return browseChildrenInner(null, effective);
    }

    /**
     * Issues a single {@code BrowseChildren} RPC and returns the raw reply.
     * Callers wanting full control over pagination can drive the loop themselves.
     *
     * @param request the request to send
     * @return the reply
     * @throws MxGatewayException on transport or protocol failure
     */
    public BrowseChildrenReply browseChildrenRaw(BrowseChildrenRequest request) {
        try {
            return rawBlockingStub().browseChildren(request);
        } catch (RuntimeException error) {
            if (error instanceof MxGatewayException) {
                throw error;
            }
            throw MxGatewayErrors.fromGrpc("galaxy browse children", error);
        }
    }

    /**
     * Drives the BrowseChildren paging loop for a single parent (or roots when
     * {@code parentGobjectId} is {@code null}). Detects repeated page tokens to
     * avoid infinite loops on a buggy server.
     */
    List<LazyBrowseNode> browseChildrenInner(Integer parentGobjectId, BrowseChildrenOptions options) {
        java.util.ArrayList<LazyBrowseNode> nodes = new java.util.ArrayList<>();
        java.util.HashSet<String> seenPageTokens = new java.util.HashSet<>();
        String pageToken = "";
        while (true) {
            BrowseChildrenRequest.Builder builder = BrowseChildrenRequest.newBuilder()
                    .setPageSize(BROWSE_CHILDREN_PAGE_SIZE)
                    .setPageToken(pageToken)
                    .setAlarmBearingOnly(options.isAlarmBearingOnly())
                    .setHistorizedOnly(options.isHistorizedOnly());
            if (parentGobjectId != null) {
                builder.setParentGobjectId(parentGobjectId.intValue());
            }
            if (!options.getCategoryIds().isEmpty()) {
                builder.addAllCategoryIds(options.getCategoryIds());
            }
            if (!options.getTemplateChainContains().isEmpty()) {
                builder.addAllTemplateChainContains(options.getTemplateChainContains());
            }
            if (!options.getTagNameGlob().isEmpty()) {
                builder.setTagNameGlob(options.getTagNameGlob());
            }
            if (options.getIncludeAttributes() != null) {
                builder.setIncludeAttributes(options.getIncludeAttributes());
            }

            BrowseChildrenReply reply = browseChildrenRaw(builder.build());

            for (int i = 0; i < reply.getChildrenCount(); i++) {
                boolean hint = i < reply.getChildHasChildrenCount() && reply.getChildHasChildren(i);
                nodes.add(new LazyBrowseNode(this, reply.getChildren(i), hint, options));
            }

            pageToken = reply.getNextPageToken();
            if (pageToken == null || pageToken.isEmpty()) {
                return nodes;
            }
            if (!seenPageTokens.add(pageToken)) {
                throw new MxGatewayException(
                        "galaxy browse children returned repeated page token: " + pageToken);
            }
        }
    }

    /**
     * Subscribes to {@code WatchDeployEvents} via the async stub and consumes
     * results through a blocking iterator. Closing the returned stream cancels
     * the underlying gRPC call.
     *
     * @param lastSeenDeployTime optional. When non-{@code null}, the bootstrap
     *     event is suppressed if the cached deploy time matches.
     * @return an iterator-style stream of deploy events
     */
    public DeployEventStream watchDeployEvents(Instant lastSeenDeployTime) {
        DeployEventStream stream = new DeployEventStream(16);
        withStreamDeadline(rawAsyncStub()).watchDeployEvents(buildWatchRequest(lastSeenDeployTime), stream.observer());
        return stream;
    }

    /**
     * Iterator-style alias for {@link #watchDeployEvents(Instant)} matching the
     * task-spec signature.
     *
     * @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
     * @return an iterator over deploy events
     */
    public Iterator<DeployEvent> watchDeployEventsIterator(Instant lastSeenDeployTime) {
        return watchDeployEvents(lastSeenDeployTime);
    }

    /**
     * Subscribes to {@code WatchDeployEvents} via the async stub, dispatching
     * each event to {@code observer}. The returned subscription is cancellable
     * and {@link AutoCloseable}.
     *
     * @param lastSeenDeployTime optional cached deploy time for bootstrap suppression
     * @param observer caller-supplied observer that receives events and completion
     * @return a cancellable subscription handle
     * @throws NullPointerException if {@code observer} is {@code null}
     */
    public DeployEventSubscription watchDeployEventsAsync(
            Instant lastSeenDeployTime, StreamObserver<DeployEvent> observer) {
        Objects.requireNonNull(observer, "observer");
        DeployEventSubscription subscription = new DeployEventSubscription();
        withStreamDeadline(rawAsyncStub())
                .watchDeployEvents(buildWatchRequest(lastSeenDeployTime), subscription.wrap(observer));
        return subscription;
    }

    private static WatchDeployEventsRequest buildWatchRequest(Instant lastSeenDeployTime) {
        WatchDeployEventsRequest.Builder builder = WatchDeployEventsRequest.newBuilder();
        if (lastSeenDeployTime != null) {
            builder.setLastSeenDeployTime(Timestamp.newBuilder()
                    .setSeconds(lastSeenDeployTime.getEpochSecond())
                    .setNanos(lastSeenDeployTime.getNano())
                    .build());
        }
        return builder.build();
    }

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

    @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 Optional<Instant> mapDeployTime(GetLastDeployTimeReply reply) {
        if (!reply.getPresent()) {
            return Optional.empty();
        }
        Timestamp ts = reply.getTimeOfLastDeploy();
        return Optional.of(Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos()));
    }

    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 galaxy repository TLS", error);
            }
        } else {
            builder.useTransportSecurity();
        }
        if (!options.serverNameOverride().isBlank()) {
            builder.overrideAuthority(options.serverNameOverride());
        }
        return builder.build();
    }

    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 CompletableFuture<List<GalaxyObject>> discoverHierarchyPageAsync(
            String pageToken, java.util.ArrayList<GalaxyObject> objects, java.util.HashSet<String> seenPageTokens) {
        DiscoverHierarchyRequest request = DiscoverHierarchyRequest.newBuilder()
                .setPageSize(DISCOVER_HIERARCHY_PAGE_SIZE)
                .setPageToken(pageToken)
                .build();
        return toCompletable(rawFutureStub().discoverHierarchy(request)).thenCompose(reply -> {
            objects.addAll(reply.getObjectsList());
            if (reply.getNextPageToken().isBlank()) {
                return CompletableFuture.completedFuture(objects);
            }
            if (!seenPageTokens.add(reply.getNextPageToken())) {
                CompletableFuture<List<GalaxyObject>> failed = new CompletableFuture<>();
                failed.completeExceptionally(new MxGatewayException(
                        "galaxy discover hierarchy returned repeated page token: " + reply.getNextPageToken()));
                return failed;
            }
            return discoverHierarchyPageAsync(reply.getNextPageToken(), objects, seenPageTokens);
        });
    }

    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("galaxy async call", runtimeException));
                            return;
                        }
                        target.completeExceptionally(error);
                    }
                },
                MoreExecutors.directExecutor());
        target.whenComplete((ignoredResult, ignoredError) -> {
            if (target.isCancelled()) {
                source.cancel(true);
            }
        });
        return target;
    }
}
