// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Arcade.Common;

namespace Microsoft.DotNet.Helix.Client
{
    internal class DirectoryPayload : IPayload
    {
        private static readonly IHelpers s_helpers = new Helpers();

        public DirectoryPayload(string directory, string archiveEntryPrefix)
        {
            ArchiveEntryPrefix = archiveEntryPrefix;
            DirectoryInfo = new DirectoryInfo(directory);
            if (!DirectoryInfo.Exists)
            {
                throw new DirectoryNotFoundException($"The directory '{directory}' was not found.");
            }
        }

        private const int CacheExpiryHours = 5;
        public DirectoryInfo DirectoryInfo { get; }

        public string NormalizedDirectoryPath => s_helpers.RemoveTrailingSlash(DirectoryInfo.FullName);

        public string ArchiveEntryPrefix { get; }

        public Task<string> UploadAsync(IBlobContainer payloadContainer, Action<string> log, CancellationToken cancellationToken)
            => Task.FromResult(
                s_helpers.DirectoryMutexExec(
                    () => DoUploadAsync(payloadContainer, log, cancellationToken),
                    NormalizedDirectoryPath));

        private async Task<string> DoUploadAsync(IBlobContainer payloadContainer, Action<string> log, CancellationToken cancellationToken)
        {
            await Task.Yield();
            string basePath = NormalizedDirectoryPath;

            var alreadyUploadedFile = new FileInfo(basePath + ".payload");
            if (alreadyUploadedFile.Exists && IsUpToDate(alreadyUploadedFile))
            {
                log?.Invoke($"Using previously uploaded payload for {basePath}");
                return File.ReadAllText(alreadyUploadedFile.FullName);
            }

            log?.Invoke($"Uploading payload for {basePath}");
            using (var stream = new MemoryStream())
            {
                using (var zip = new ZipArchive(stream, ZipArchiveMode.Create, true))
                {
                    foreach (FileInfo file in DirectoryInfo.EnumerateFiles("*", SearchOption.AllDirectories))
                    {
                        string relativePath =
                            file.FullName.Substring(basePath.Length + 1); // +1 prevents it from including the leading backslash
                        string zipEntryName = relativePath.Replace('\\', '/'); // Normalize slashes

                        if (!string.IsNullOrEmpty(ArchiveEntryPrefix))
                        {
                            zipEntryName = ArchiveEntryPrefix + "/" + zipEntryName;
                        }

                        zip.CreateEntryFromFile(file.FullName, zipEntryName);
                    }
                }

                stream.Position = 0;
                Uri zipUri = await payloadContainer.UploadFileAsync(stream, $"{Guid.NewGuid()}.zip", log, cancellationToken);
                File.WriteAllText(alreadyUploadedFile.FullName, zipUri.AbsoluteUri);
                return zipUri.AbsoluteUri;
            }
        }

        private bool IsUpToDate(FileInfo alreadyUploadedFile)
        {
            if (alreadyUploadedFile.LastWriteTimeUtc.AddHours(CacheExpiryHours) < DateTime.UtcNow)
            {
                return false;
            }

            var newestFileWriteTime = DirectoryInfo.EnumerateFiles("*", SearchOption.AllDirectories)
                .Select(file => file.LastWriteTimeUtc)
                .Max();
            return alreadyUploadedFile.LastWriteTimeUtc >= newestFileWriteTime;
        }
    }
}
