// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "components/update_client/background_downloader_mac_delegate.h"

#import <Foundation/Foundation.h>

#include <cstdint>

#include "base/apple/foundation_util.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "components/update_client/update_client_errors.h"
#include "url/gurl.h"

@implementation UpdateClientDownloadDelegate {
  base::FilePath _download_cache;
  UpdateClientDelegateDownloadCompleteCallback _download_complete_callback;
  UpdateClientDelegateMetricsCollectedCallback _metrics_collected_callback;
  UpdateClientDelegateDownloadProgressCallback _progress_callback;
  scoped_refptr<base::SequencedTaskRunner> _callback_runner;
}

- (instancetype)
       initWithDownloadCache:(base::FilePath)downloadCache
    downloadCompleteCallback:
        (UpdateClientDelegateDownloadCompleteCallback)downloadCompleteCallback
    metricsCollectedCallback:
        (UpdateClientDelegateMetricsCollectedCallback)metricsCollectedCallback
            progressCallback:
                (UpdateClientDelegateDownloadProgressCallback)progressCallback {
  if (self = [super init]) {
    _download_cache = downloadCache;
    _download_complete_callback = downloadCompleteCallback;
    _metrics_collected_callback = metricsCollectedCallback;
    _progress_callback = progressCallback;
    _callback_runner = base::SequencedTaskRunner::GetCurrentDefault();
  }
  return self;
}

- (void)endTask:(NSURLSessionTask*)task
    withLocation:(std::optional<base::FilePath>)location
       withError:(int)error {
  _callback_runner->PostTask(
      FROM_HERE,
      base::BindOnce(_download_complete_callback,
                     update_client::GURLWithNSURL(task.originalRequest.URL),
                     location.value_or(base::FilePath()), error,
                     task.countOfBytesReceived,
                     task.countOfBytesExpectedToReceive));
}

#pragma mark - NSURLSessionDownloadDelegate

- (void)URLSession:(NSURLSession*)session
                 downloadTask:(NSURLSessionDownloadTask*)downloadTask
    didFinishDownloadingToURL:(NSURL*)location {
  if (!base::PathExists(_download_cache) &&
      !base::CreateDirectory(_download_cache)) {
    VLOG(1) << "Failed to create download cache directory at: "
            << _download_cache;
    [self endTask:downloadTask
        withLocation:std::nullopt
           withError:static_cast<int>(update_client::CrxDownloaderError::
                                          MAC_BG_CANNOT_CREATE_DOWNLOAD_CACHE)];
    return;
  }

  const base::FilePath temp_path =
      base::apple::NSStringToFilePath(location.path);
  base::FilePath cache_path =
      _download_cache.Append(update_client::URLToFilename(
          update_client::GURLWithNSURL(downloadTask.originalRequest.URL)));
  if (!base::Move(temp_path, cache_path)) {
    DVLOG(1)
        << "Failed to move the downloaded file from the temporary location: "
        << temp_path << " to: " << cache_path;
    [self endTask:downloadTask
        withLocation:std::nullopt
           withError:static_cast<int>(update_client::CrxDownloaderError::
                                          MAC_BG_MOVE_TO_CACHE_FAIL)];
    return;
  }

  [self endTask:downloadTask
      withLocation:cache_path
         withError:static_cast<int>(update_client::CrxDownloaderError::NONE)];
}

- (void)URLSession:(NSURLSession*)session
                 downloadTask:(nonnull NSURLSessionDownloadTask*)downloadTask
                 didWriteData:(int64_t)bytesWritten
            totalBytesWritten:(int64_t)totalBytesWritten
    totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
  if (bytesWritten > 0) {
    _callback_runner->PostTask(
        FROM_HERE, base::BindOnce(_progress_callback,
                                  update_client::GURLWithNSURL(
                                      downloadTask.originalRequest.URL)));
  }
}

#pragma mark - NSURLSessionDelegate

- (void)URLSession:(NSURLSession*)session
                    task:(nonnull NSURLSessionTask*)task
    didCompleteWithError:(nullable NSError*)error {
  if (error) {
    [self endTask:task withLocation:std::nullopt withError:error.code];
  }
}

- (void)URLSession:(NSURLSession*)session
                          task:(NSURLSessionTask*)task
    didFinishCollectingMetrics:(NSURLSessionTaskMetrics*)metrics {
  _callback_runner->PostTask(
      FROM_HERE,
      base::BindOnce(_metrics_collected_callback,
                     update_client::GURLWithNSURL(task.originalRequest.URL),
                     metrics.taskInterval.duration *
                         base::TimeTicks::kMillisecondsPerSecond));
}

@end

namespace update_client {

GURL GURLWithNSURL(NSURL* url) {
  return url ? GURL(url.absoluteString.UTF8String) : GURL();
}

base::FilePath URLToFilename(const GURL& url) {
  return base::FilePath(base::HexEncode(
      base::byte_span_from_ref(base::PersistentHash(url.spec()))));
}

}  // namespace update_client
