// Copyright 2026 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.concurrent.safeexecutor;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.internal.InternalFutureFailureAccess;
import com.google.common.util.concurrent.internal.InternalFutures;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;

/**
 * Adapter class that wraps a {@link ListenableFuture} and a {@link FutureCallback} into a {@link
 * RejectionHandlingRunnable} and an {@link Executor}.
 *
 * <p>Protects listener execution against Guava's listener error-swallowing behavior and provides
 * protected template hooks ({@link #onCallbackSuccess} and {@link #onCallbackFailure}) for custom
 * completion logic in subclasses.
 */
public class SafeCallbackListener<T> implements RejectionHandlingRunnable, Executor {
  private final ListenableFuture<T> future;
  private final FutureCallback<? super T> callback;
  private final SafeExecutor safeExecutor;

  protected SafeCallbackListener(
      ListenableFuture<T> future, FutureCallback<? super T> callback, SafeExecutor safeExecutor) {
    this.future = Preconditions.checkNotNull(future, "future");
    this.callback = Preconditions.checkNotNull(callback, "callback");
    this.safeExecutor = Preconditions.checkNotNull(safeExecutor, "safeExecutor");
  }

  protected void onCallbackSuccess(T result) {
    callback.onSuccess(result);
  }

  protected void onCallbackFailure(Throwable t) {
    callback.onFailure(t);
  }

  @Override
  public final void run() {
    // Uses InternalFutures.tryInternalFastPathGetFailure to match Guava's CallbackListener
    // implementation and extract failures in O(1) without ExecutionException allocation.
    //
    // All AbstractFuture derivatives satisfy this check but immediate futures (e.g.,
    // Futures.immediateFuture or ImmediateWriteStatus) do not implement
    // InternalFutureFailureAccess and fall through to Futures.getDone(future).
    if (future instanceof InternalFutureFailureAccess accessibleFuture) {
      Throwable failure = InternalFutures.tryInternalFastPathGetFailure(accessibleFuture);
      if (failure != null) {
        onCallbackFailure(failure);
        return;
      }
    }
    T result;
    try {
      result = Futures.getDone(future);
    } catch (ExecutionException e) {
      // No futures in use are known to reach this code path because all qualifying instances
      // should be processed by the InternalFutureFailureAccess codepath above. However,
      // ExecutionException is a checked exception that must be handled in the code and this
      // implementation is correct.
      onCallbackFailure(e.getCause());
      return;
    } catch (CancellationException e) {
      // Note that unlike ExecutionException, CancellationException is not handled by
      // InternalFutureFailureAccess.
      onCallbackFailure(e);
      return;
    }

    onCallbackSuccess(result);
  }

  @Override
  public final void handleRejection(Throwable rejectionException) {
    Throwable resolvedFailure = resolveCallbackFailure(future, rejectionException);
    onCallbackFailure(resolvedFailure);
  }

  /**
   * Implements {@link Executor} via self-dispatch to eliminate per-callback allocations.
   *
   * <p>When registered with {@code future.addListener(listener, listener)}, upon future completion
   * the target {@link ListenableFuture} invokes {@code listener.execute(command)} (where {@code
   * command} is a {@link Runnable} generated by the future, such as Guava's listener wrapper).
   *
   * <p>This method intentionally ignores the {@code unusedCommand} parameter and forwards {@code
   * this} directly to {@code safeExecutor.execute(this)}. Bypassing Guava's wrapper is sound and
   * necessary because {@code SafeCallbackListener} natively encapsulates full future result
   * extraction, exception unwrapping, and callback dispatch in {@link #run()}, while extending
   * {@link SafeExecutor} rejection handling to guarantee zero dropped listener callbacks.
   */
  @Override
  public final void execute(Runnable unusedCommand) {
    safeExecutor.execute(this);
  }

  /**
   * Resolves the failure cause to report to {@code onCallbackFailure(t)} when listener callback
   * submission is rejected.
   *
   * <p>When the underlying future completes with error, but the callback is rejected for
   * scheduling, this method tries to preserve the underlying error for more accurate error
   * reporting.
   *
   * <p><b>Essential {@code InternalFutures} usage:</b> Calling standard {@code
   * Futures.getDone(future)} on a failed future causes {@code future.get()} to instantiate a new
   * {@link ExecutionException}, invoking JVM native {@code fillInStackTrace()}. When an executor is
   * shut down or saturated, rejection notifications cascade down dependency DAGs of depth D.
   * Instantiating new exceptions and walking call stack frames at every node turns linear graph
   * draining into quadratic O(N*D) stack-walking overhead. {@code
   * InternalFutures.tryInternalFastPathGetFailure} is a public API exported by Guava that extracts
   * the stored failure reference in O(1) with zero allocations and zero stack walks, preserving
   * O(N) linear draining performance.
   */
  private static <T> Throwable resolveCallbackFailure(
      ListenableFuture<T> future, Throwable rejectionException) {
    if (!future.isDone()) {
      return rejectionException;
    }
    if (future.isCancelled()) {
      try {
        Futures.getDone(future);
      } catch (CancellationException e) {
        return e;
      } catch (ExecutionException e) {
        // This should not occur if future.isCancelled() returns true, but is this is safe.
        return e.getCause();
      }
      // Fallback for non-Guava ListenableFuture implementations where getDone might not throw
      // CancellationException despite isCancelled() returning true.
      return new CancellationException("Future was cancelled");
    }
    // Fast path: Guava AbstractFuture failure extraction in O(1) without ExecutionException or
    // stack-walking.
    if (future instanceof InternalFutureFailureAccess accessibleFuture) {
      Throwable failure = InternalFutures.tryInternalFastPathGetFailure(accessibleFuture);
      if (failure != null) {
        return failure;
      }
      return rejectionException;
    }
    // Fallback path for non-Guava ListenableFuture implementations
    try {
      Futures.getDone(future);
      return rejectionException;
    } catch (ExecutionException e) {
      return e.getCause();
    } catch (Throwable t) {
      return t;
    }
  }
}
