/*
 * Copyright 2022 Google LLC
 *
 * 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.android.libraries.mobiledatadownload.downloader.offroad;

import com.google.android.downloader.RequestException;
import com.google.android.libraries.mobiledatadownload.DownloadException;
import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;

/**
 * Handles mapping exceptions from Downloader2 into the equivalent MDD {@link DownloadException}.
 *
 * <p>Common exceptions are parsed and handled by default, but the underlying network stack may
 * include special Exceptions and/or error codes that need to be parsed. If this is the case, a
 * {@link NetworkStackExceptionHandler} can be provided to perform this parsing.
 */
public final class ExceptionHandler {
  /**
   * The maximum amount of attempts we recurse before stopping in {@link
   * #mapExceptionToDownloadResultCode}.
   */
  private static final int EXCEPTION_TO_CODE_RECURSION_LIMIT = 5;

  /** The handler of underlying network stack failures. */
  private final NetworkStackExceptionHandler internalExceptionHandler;

  private ExceptionHandler(NetworkStackExceptionHandler internalExceptionHandler) {
    this.internalExceptionHandler = internalExceptionHandler;
  }

  /** Convenience method to return a new ExceptionHandler with default handling. */
  public static ExceptionHandler withDefaultHandling() {
    return new ExceptionHandler(new NetworkStackExceptionHandler() {});
  }

  /** Return a new instance with specific handling for a network stack. */
  public static ExceptionHandler withNetworkStackHandling(
      NetworkStackExceptionHandler internalExceptionHandler) {
    return new ExceptionHandler(internalExceptionHandler);
  }

  /**
   * Map given failure to a {@link DownloadException}.
   *
   * <p>For most cases, this method does not need to be overridden.
   *
   * <p><em>NOTE:</em> If the given throwable is already a {@link DownloadException}, it is returned
   * immediately. In this case, the given message will <b>not</b> be used (the message from the
   * given throwable will be used instead).
   *
   * @param message top-level message that should be used for the returned {@link DownloadException}
   * @param throwable generic throwable that should be mapped to {@link DownloadException}
   * @return {@link DownloadException} that wraps around given throwable with appropriate {@link
   *     DownloadResultCode}
   */
  public DownloadException mapToDownloadException(String message, Throwable throwable) {
    if (throwable instanceof DownloadException) {
      // Exception is already an MDD DownloadException -- return it.
      return (DownloadException) throwable;
    }

    DownloadResultCode code = mapExceptionToDownloadResultCode(throwable, /* iteration= */ 0);

    return DownloadException.builder()
        .setMessage(message)
        .setDownloadResultCode(code)
        .setCause(throwable)
        .build();
  }

  /**
   * Map exception to {@link DownloadResultCode}.
   *
   * @param throwable the exception to map to a {@link DownloadResultCode}
   */
  private DownloadResultCode mapExceptionToDownloadResultCode(Throwable throwable, int iteration) {
    // Check recursion limit and return unknown error if it is hit.
    if (iteration >= EXCEPTION_TO_CODE_RECURSION_LIMIT) {
      return DownloadResultCode.UNKNOWN_ERROR;
    }

    DownloadResultCode networkStackMapperResult =
        internalExceptionHandler.mapFromNetworkStackException(throwable);
    if (!networkStackMapperResult.equals(DownloadResultCode.UNKNOWN_ERROR)) {
      // network stack mapper returned known result code -- return it instead of performing common
      // mapping.
      return networkStackMapperResult;
    }

    if (throwable instanceof DownloadException) {
      // exception in the chain is already an MDD DownloadException -- use its code
      return ((DownloadException) throwable).getDownloadResultCode();
    }

    if (throwable instanceof RequestException) {
      // Check error details for http status code error.
      if (((RequestException) throwable).getErrorDetails().getHttpStatusCode() != -1) {
        // error code has an associated http status code, mark it as HTTP_ERROR
        return DownloadResultCode.ANDROID_DOWNLOADER_HTTP_ERROR;
      }
    }

    if (throwable.getCause() != null) {
      // Exception has an underlying cause -- attempt mapping on that cause.
      return mapExceptionToDownloadResultCode(throwable.getCause(), iteration + 1);
    }

    if (throwable instanceof com.google.android.downloader.DownloadException) {
      // If DownloadException is not wrapping anything, we can't determine the error further -- mark
      // it as a general Downloader2 error.
      return DownloadResultCode.ANDROID_DOWNLOADER2_ERROR;
    }

    // We couldn't parse any common errors, return an unknown error
    return DownloadResultCode.UNKNOWN_ERROR;
  }

  /**
   * Interface to handle parsing exceptions from an underlying network stack.
   *
   * <p>If an underlying network stack is used which can throw special exceptions or has an error
   * code map, consider implementing this to provide better handling of exceptions.
   */
  public static interface NetworkStackExceptionHandler {
    /**
     * Map Custom Exception to {@link DownloadResultCode}.
     *
     * <p>Underlying network stacks may have specific exceptions that can be used to determine the
     * best DownloadResultCode. This method should be overridden to check for such exceptions.
     *
     * <p>If a known {@link DownloadResultCode} is returned (i.e not UNKNOWN_ERROR), it will be
     * used.
     *
     * <p>By default, an UNKNOWN_ERROR is returned.
     */
    default DownloadResultCode mapFromNetworkStackException(Throwable throwable) {
      return DownloadResultCode.UNKNOWN_ERROR;
    }
  }
}
