// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Agent.Sdk;
using Agent.Sdk.Knob;
using Microsoft.TeamFoundation.DistributedTask.WebApi;
using Microsoft.VisualStudio.Services.Agent.Util;
using Newtonsoft.Json.Linq;
using Pipelines = Microsoft.TeamFoundation.DistributedTask.Pipelines;
using System.IO;

namespace Agent.Plugins.Repository
{
    public interface ISourceProvider
    {
        Task GetSourceAsync(AgentTaskPluginExecutionContext executionContext, Pipelines.RepositoryResource repository, CancellationToken cancellationToken);

        Task PostJobCleanupAsync(AgentTaskPluginExecutionContext executionContext, Pipelines.RepositoryResource repository);
    }

    public abstract class RepositoryTask : IAgentTaskPlugin
    {
        private static readonly HashSet<String> _checkoutOptions = new HashSet<String>(StringComparer.OrdinalIgnoreCase)
        {
            Pipelines.PipelineConstants.CheckoutTaskInputs.Clean,
            Pipelines.PipelineConstants.CheckoutTaskInputs.FetchDepth,
            Pipelines.PipelineConstants.CheckoutTaskInputs.Lfs,
            Pipelines.PipelineConstants.CheckoutTaskInputs.PersistCredentials,
            Pipelines.PipelineConstants.CheckoutTaskInputs.Submodules,
        };

        protected RepositoryTask()
            : this(new SourceProviderFactory())
        {
        }

        protected RepositoryTask(ISourceProviderFactory sourceProviderFactory)
        {
            SourceProviderFactory = sourceProviderFactory;
        }

        public Guid Id => Pipelines.PipelineConstants.CheckoutTask.Id;

        public ISourceProviderFactory SourceProviderFactory { get; }

        public abstract string Stage { get; }

        public abstract Task RunAsync(AgentTaskPluginExecutionContext executionContext, CancellationToken token);

        protected void MergeCheckoutOptions(AgentTaskPluginExecutionContext executionContext, Pipelines.RepositoryResource repository)
        {
            // Merge the repository checkout options
            if ((!executionContext.Variables.TryGetValue("MERGE_CHECKOUT_OPTIONS", out VariableValue mergeCheckoutOptions) || !String.Equals(mergeCheckoutOptions.Value, "false", StringComparison.OrdinalIgnoreCase)) &&
                repository.Properties.Get<JToken>(Pipelines.RepositoryPropertyNames.CheckoutOptions) is JObject checkoutOptions)
            {
                foreach (var pair in checkoutOptions)
                {
                    var inputName = pair.Key;

                    // Skip if unexpected checkout option
                    if (!_checkoutOptions.Contains(inputName))
                    {
                        executionContext.Debug($"Unexpected checkout option '{inputName}'");
                        continue;
                    }

                    // Skip if input defined
                    if (executionContext.Inputs.TryGetValue(inputName, out string inputValue) && !string.IsNullOrEmpty(inputValue))
                    {
                        continue;
                    }

                    try
                    {
                        executionContext.Inputs[inputName] = pair.Value.ToObject<String>();
                    }
                    catch (Exception ex)
                    {
                        executionContext.Debug($"Error setting the checkout option '{inputName}': {ex.Message}");
                    }
                }
            }
        }

        protected bool HasMultipleCheckouts(AgentTaskPluginExecutionContext executionContext)
        {
            return executionContext != null && RepositoryUtil.HasMultipleCheckouts(executionContext.JobSettings);
        }

        protected TeeUtil teeUtil;
        protected void initializeTeeUtil(AgentTaskPluginExecutionContext executionContext, CancellationToken cancellationToken)
        {
            teeUtil = new TeeUtil(
                executionContext.Variables.GetValueOrDefault("Agent.HomeDirectory")?.Value,
                executionContext.Variables.GetValueOrDefault("Agent.TempDirectory")?.Value,
                AgentKnobs.TeePluginDownloadRetryCount.GetValue(executionContext).AsInt(),
                executionContext.Debug,
                cancellationToken
            );
        }
    }

    public class CheckoutTask : RepositoryTask
    {
        public CheckoutTask()
        {
        }

        public CheckoutTask(ISourceProviderFactory sourceProviderFactory)
            : base(sourceProviderFactory)
        {
        }

        public override string Stage => "main";

        public override async Task RunAsync(AgentTaskPluginExecutionContext executionContext, CancellationToken token)
        {
            var sourceSkipVar = StringUtil.ConvertToBoolean(executionContext.Variables.GetValueOrDefault("agent.source.skip")?.Value) ||
                                !StringUtil.ConvertToBoolean(executionContext.Variables.GetValueOrDefault("build.syncSources")?.Value ?? bool.TrueString);
            if (sourceSkipVar)
            {
                executionContext.Output($"Skip sync source for repository.");
                return;
            }

            var repoAlias = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Repository, true);
            var repo = executionContext.Repositories.Single(x => string.Equals(x.Alias, repoAlias, StringComparison.OrdinalIgnoreCase));

            executionContext.PublishTelemetry(area: "AzurePipelinesAgent", feature: "Checkout", properties: new Dictionary<string, string>
            {
                { "RepoType", $"{repo.Type}" },
                { "HostOS", $"{PlatformUtil.HostOS}" }
            });

            MergeCheckoutOptions(executionContext, repo);

            var currentRepoPath = repo.Properties.Get<string>(Pipelines.RepositoryPropertyNames.Path);
            var workDirectory = executionContext.Variables.GetValueOrDefault("agent.workfolder")?.Value;
            var buildDirectory = executionContext.Variables.GetValueOrDefault("agent.builddirectory")?.Value;
            var tempDirectory = executionContext.Variables.GetValueOrDefault("agent.tempdirectory")?.Value;

            ArgUtil.NotNullOrEmpty(currentRepoPath, nameof(currentRepoPath));
            ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
            ArgUtil.NotNullOrEmpty(buildDirectory, nameof(buildDirectory));
            ArgUtil.NotNullOrEmpty(tempDirectory, nameof(tempDirectory));

            // Determine the path that we should clone/move the repository into
            const string sourcesDirectory = "s"; //Constants.Build.Path.SourcesDirectory
            string expectRepoPath;
            var path = executionContext.GetInput("path");
            var maxRootDirectory = buildDirectory;

            if (!string.IsNullOrEmpty(path))
            {
                // When the checkout task provides a path, always use that one
                expectRepoPath = IOUtil.ResolvePath(buildDirectory, path);

                if (AgentKnobs.AllowWorkDirectoryRepositories.GetValue(executionContext).AsBoolean())
                {
                    maxRootDirectory = workDirectory;
                }

                if (!expectRepoPath.StartsWith(maxRootDirectory.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar))
                {
                    throw new ArgumentException($"Input path '{path}' should resolve to a directory under '{maxRootDirectory}', current resolved path '{expectRepoPath}'.");
                }
            }
            else if (HasMultipleCheckouts(executionContext))
            {
                // When there are multiple checkout tasks (and this one didn't set the path), default to directory 1/s/<repoName>
                expectRepoPath = Path.Combine(buildDirectory, sourcesDirectory, RepositoryUtil.GetCloneDirectory(repo));
            }
            else
            {
                // When there's a single checkout task that doesn't have path set, default to sources directory 1/s
                expectRepoPath = Path.Combine(buildDirectory, sourcesDirectory);
            }

            // Update the repository path in the worker process
            executionContext.UpdateRepositoryPath(repoAlias, expectRepoPath);

            executionContext.Debug($"Repository requires to be placed at '{expectRepoPath}', current location is '{currentRepoPath}'");
            if (!string.Equals(currentRepoPath.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), expectRepoPath.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), IOUtil.FilePathStringComparison))
            {
                executionContext.Output($"Repository is current at '{currentRepoPath}', move to '{expectRepoPath}'.");
                var count = 1;
                var staging = Path.Combine(tempDirectory, $"_{count}");
                while (Directory.Exists(staging))
                {
                    count++;
                    staging = Path.Combine(tempDirectory, $"_{count}");
                }

                try
                {
                    executionContext.Debug($"Move existing repository '{currentRepoPath}' to '{expectRepoPath}' via staging directory '{staging}'.");
                    IOUtil.MoveDirectory(currentRepoPath, expectRepoPath, staging, CancellationToken.None);
                }
                catch (Exception ex)
                {
                    executionContext.Debug("Catch exception during repository move.");
                    executionContext.Debug(ex.ToString());
                    executionContext.Warning("Unable move and reuse existing repository to required location.");

                    try
                    {
                        await IOUtil.DeleteDirectoryWithRetry(expectRepoPath, CancellationToken.None);
                    }
                    catch (Exception ioEx)
                    {
                        executionContext.Output($"Unable to delete existing repository on required location: {ioEx.GetType()}");
                        throw;
                    }
                }

                executionContext.Output($"Repository will be located at '{expectRepoPath}'.");
                repo.Properties.Set<string>(Pipelines.RepositoryPropertyNames.Path, expectRepoPath);
            }

            if (!PlatformUtil.RunningOnWindows && string.Equals(repo.Type, Pipelines.RepositoryTypes.Tfvc, StringComparison.OrdinalIgnoreCase))
            {
                initializeTeeUtil(executionContext, token);
                await teeUtil.DownloadTeeIfAbsent();
            }

            ISourceProvider sourceProvider = SourceProviderFactory.GetSourceProvider(repo.Type);
            await sourceProvider.GetSourceAsync(executionContext, repo, token);
        }
    }

    public class CleanupTask : RepositoryTask
    {
        public override string Stage => "post";

        public override async Task RunAsync(AgentTaskPluginExecutionContext executionContext, CancellationToken token)
        {
            var repoAlias = executionContext.TaskVariables.GetValueOrDefault("repository")?.Value;
            if (!string.IsNullOrEmpty(repoAlias))
            {
                var repo = executionContext.Repositories.Single(x => string.Equals(x.Alias, repoAlias, StringComparison.OrdinalIgnoreCase));
                ArgUtil.NotNull(repo, nameof(repo));

                MergeCheckoutOptions(executionContext, repo);

                ISourceProvider sourceProvider = SourceProviderFactory.GetSourceProvider(repo.Type);
                await sourceProvider.PostJobCleanupAsync(executionContext, repo);
            }

            if (!PlatformUtil.RunningOnWindows && !AgentKnobs.DisableTeePluginRemoval.GetValue(executionContext).AsBoolean())
            {
                initializeTeeUtil(executionContext, token);
                try
                {
                    teeUtil.DeleteTee();
                }
                catch (Exception ex)
                {
                    executionContext.Output($"Unable to delete existing repository on required location. ex:{ex.GetType}");
                    throw;
                }
            }
        }
    }

    public interface ISourceProviderFactory
    {
        ISourceProvider GetSourceProvider(string repositoryType);
    }

    public class SourceProviderFactory : ISourceProviderFactory
    {
        public virtual ISourceProvider GetSourceProvider(string repositoryType)
        {
            ISourceProvider sourceProvider = null;

            if (string.Equals(repositoryType, Pipelines.RepositoryTypes.GitHub, StringComparison.OrdinalIgnoreCase) ||
                string.Equals(repositoryType, Pipelines.RepositoryTypes.GitHubEnterprise, StringComparison.OrdinalIgnoreCase))
            {
                sourceProvider = new GitHubSourceProvider();
            }
            else if (string.Equals(repositoryType, Pipelines.RepositoryTypes.Bitbucket, StringComparison.OrdinalIgnoreCase))
            {
                sourceProvider = new BitbucketGitSourceProvider();
            }
            else if (string.Equals(repositoryType, Pipelines.RepositoryTypes.ExternalGit, StringComparison.OrdinalIgnoreCase))
            {
                sourceProvider = new ExternalGitSourceProvider();
            }
            else if (string.Equals(repositoryType, Pipelines.RepositoryTypes.Git, StringComparison.OrdinalIgnoreCase))
            {
                sourceProvider = new TfsGitSourceProvider();
            }
            else if (string.Equals(repositoryType, Pipelines.RepositoryTypes.Tfvc, StringComparison.OrdinalIgnoreCase))
            {
                sourceProvider = new TfsVCSourceProvider();
            }
            else if (string.Equals(repositoryType, Pipelines.RepositoryTypes.Svn, StringComparison.OrdinalIgnoreCase))
            {
                sourceProvider = new SvnSourceProvider();
            }
            else
            {
                throw new NotSupportedException(repositoryType);
            }

            return sourceProvider;
        }
    }
}
