/*
 * 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 android.net.Uri;
import android.util.Pair;

import com.google.android.downloader.DownloadConstraints;
import com.google.android.downloader.DownloadConstraints.NetworkType;
import com.google.android.downloader.DownloadDestination;
import com.google.android.downloader.DownloadRequest;
import com.google.android.downloader.DownloadResult;
import com.google.android.downloader.Downloader;
import com.google.android.downloader.OAuthTokenProvider;
import com.google.android.libraries.mobiledatadownload.DownloadException;
import com.google.android.libraries.mobiledatadownload.DownloadException.DownloadResultCode;
import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeRequest;
import com.google.android.libraries.mobiledatadownload.downloader.CheckContentChangeResponse;
import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException;
import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadDestinationOpener;
import com.google.android.libraries.mobiledatadownload.file.integration.downloader.DownloadMetadataStore;
import com.google.android.libraries.mobiledatadownload.internal.logging.LogUtil;
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;

import java.io.IOException;
import java.net.URI;
import java.util.concurrent.Executor;

import javax.annotation.Nullable;

/**
 * An implementation of the {@link
 * com.google.android.libraries.mobiledatadownload.downloader.FileDownloader} using <internal>
 */
public final class Offroad2FileDownloader implements FileDownloader {
    private static final String TAG = "Offroad2FileDownloader";

    private final Downloader downloader;
    private final SynchronousFileStorage fileStorage;
    private final Executor downloadExecutor;
    private final DownloadMetadataStore downloadMetadataStore;
    private final ExceptionHandler exceptionHandler;
    //  private final Optional<Supplier<CookieJar>> cookieJarSupplierOptional;
    private final Optional<Integer> defaultTrafficTag;
    @Nullable
    private final OAuthTokenProvider authTokenProvider;

    public Offroad2FileDownloader(
            Downloader downloader,
            SynchronousFileStorage fileStorage,
            Executor downloadExecutor,
            @Nullable OAuthTokenProvider authTokenProvider,
            DownloadMetadataStore downloadMetadataStore,
            ExceptionHandler exceptionHandler,
//      Optional<Supplier<CookieJar>> cookieJarSupplierOptional,
            Optional<Integer> defaultTrafficTag) {
        this.downloader = downloader;
        this.fileStorage = fileStorage;
        this.downloadExecutor = downloadExecutor;
        this.authTokenProvider = authTokenProvider;
        this.downloadMetadataStore = downloadMetadataStore;
        this.exceptionHandler = exceptionHandler;
//    this.cookieJarSupplierOptional = cookieJarSupplierOptional;
        this.defaultTrafficTag = defaultTrafficTag;
    }

    @Override
    public ListenableFuture<Void> startDownloading(
            com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest
                    fileDownloaderRequest) {
        String fileName = Strings.nullToEmpty(fileDownloaderRequest.fileUri().getLastPathSegment());

        DownloadDestination downloadDestination;
        try {
            downloadDestination = buildDownloadDestination(fileDownloaderRequest.fileUri());
        } catch (DownloadException e) {
            return Futures.immediateFailedFuture(e);
        }

        DownloadRequest offroad2DownloadRequest =
                buildDownloadRequest(fileDownloaderRequest, downloadDestination);

        FluentFuture<DownloadResult> resultFuture = downloader.execute(offroad2DownloadRequest);

        LogUtil.d(
                "%s: Data download scheduled for file: %s", TAG,
                fileDownloaderRequest.urlToDownload());

        return PropagatedFluentFuture.from(resultFuture)
                .catchingAsync(
                        Exception.class,
                        cause -> {
                            LogUtil.d(
                                    cause,
                                    "%s: Failed to download file %s due to: %s",
                                    TAG,
                                    fileName,
                                    Strings.nullToEmpty(cause.getMessage()));

                            DownloadException exception =
                                    exceptionHandler.mapToDownloadException("failure in download!",
                                            cause);

                            return Futures.immediateFailedFuture(exception);
                        },
                        downloadExecutor)
                .transformAsync(
                        (DownloadResult result) -> {
                            LogUtil.d(
                                    "%s: Downloaded file %s, bytes written: %d",
                                    TAG, fileName, result.bytesWritten());
                            return PropagatedFutures.catchingAsync(
                                    downloadMetadataStore.delete(fileDownloaderRequest.fileUri()),
                                    Exception.class,
                                    e -> {
                                        // Failing to clean up metadata shouldn't cause a failure
                                        // in the future, log and
                                        // return void.
                                        LogUtil.d(e, "%s: Failed to cleanup metadata", TAG);
                                        return Futures.immediateVoidFuture();
                                    },
                                    downloadExecutor);
                        },
                        downloadExecutor);
    }

    @Override
    public ListenableFuture<CheckContentChangeResponse> isContentChanged(
            CheckContentChangeRequest checkContentChangeRequest) {
        return Futures.immediateFailedFuture(
                new UnsupportedOperationException(
                        "Checking for content changes is currently unsupported for Downloader2"));
    }

    private DownloadDestination buildDownloadDestination(Uri destinationUri)
            throws DownloadException {
        try {
            // Create DownloadDestination using mobstore
            // NOTE: the use of DirectExecutor here should be fine since all async operations
            // of DownloadDestination happen within Downloader2 IOExecutor. Consider replacing
            // this with
            // lightweight executor.
            return fileStorage.open(
                    destinationUri,
                    DownloadDestinationOpener.create(downloadMetadataStore));
        } catch (IOException e) {
            if (e instanceof MalformedUriException
                    || e.getCause() instanceof IllegalArgumentException) {
                LogUtil.e("%s: The file uri is invalid, uri = %s", TAG, destinationUri);
                throw DownloadException.builder()
                        .setDownloadResultCode(DownloadResultCode.MALFORMED_FILE_URI_ERROR)
                        .setCause(e)
                        .build();
            } else {
                LogUtil.e(e, "%s: Unable to create DownloadDestination for file %s", TAG,
                        destinationUri);
                // TODO: the result code is the most equivalent to downloader1 -- consider
                // creating a separate result code that's more appropriate for downloader2.
                throw DownloadException.builder()
                        .setDownloadResultCode(
                                DownloadResultCode.UNABLE_TO_CREATE_MOBSTORE_RESPONSE_WRITER_ERROR)
                        .setCause(e)
                        .build();
            }
        }
    }

    private DownloadRequest buildDownloadRequest(
            com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest
                    fileDownloaderRequest,
            DownloadDestination downloadDestination) {
        DownloadRequest.Builder requestBuilder =
                downloader.newRequestBuilder(
                        URI.create(fileDownloaderRequest.urlToDownload()), downloadDestination);

//    if (cookieJarSupplierOptional.isPresent()) {
//     requestBuilder.setCookieJar(cookieJarSupplierOptional.get().get());
//    }

        requestBuilder.setOAuthTokenProvider(authTokenProvider);

        if (com.google.android.libraries.mobiledatadownload.downloader.DownloadConstraints
                .NETWORK_CONNECTED
                == fileDownloaderRequest.downloadConstraints()) {
            requestBuilder.setDownloadConstraints(DownloadConstraints.NETWORK_CONNECTED);
        } else {
            // Use all network types except cellular and require unmetered network.
            requestBuilder.setDownloadConstraints(
                    DownloadConstraints.builder()
                            .addRequiredNetworkType(NetworkType.WIFI)
                            .addRequiredNetworkType(NetworkType.ETHERNET)
                            .addRequiredNetworkType(NetworkType.BLUETOOTH)
                            .setRequireUnmeteredNetwork(true)
                            .build());
        }

        // TODO(b/237653774): Enable traffic tagging.
    /*if (fileDownloaderRequest.trafficTag() > 0) {
      // Prefer traffic tag from request.
      requestBuilder.setTrafficStatsTag(fileDownloaderRequest.trafficTag());
    } else if (defaultTrafficTag.isPresent() && defaultTrafficTag.get() > 0) {
      // Use default traffic tag as a fallback if present.
      requestBuilder.setTrafficStatsTag(defaultTrafficTag.get());
    }*/

        for (Pair<String, String> header : fileDownloaderRequest.extraHttpHeaders()) {
            requestBuilder.addHeader(header.first, header.second);
        }

        return requestBuilder.build();
    }
}
