using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GitHub.DistributedTask.ObjectTemplating;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Handlers;
using Pipelines = GitHub.DistributedTask.Pipelines;

namespace GitHub.Runner.Worker
{
    public enum ActionRunStage
    {
        Pre,
        Main,
        Post,
    }

    [ServiceLocator(Default = typeof(ActionRunner))]
    public interface IActionRunner : IStep, IRunnerService
    {
        ActionRunStage Stage { get; set; }
        Pipelines.ActionStep Action { get; set; }
    }

    public sealed class ActionRunner : RunnerService, IActionRunner
    {
        private bool _didFullyEvaluateDisplayName = false;

        private string _displayName;

        public ActionRunStage Stage { get; set; }

        public string Condition { get; set; }

        public TemplateToken ContinueOnError => Action?.ContinueOnError;

        public string DisplayName
        {
            get
            {
                // TODO: remove the Action.DisplayName check post m158 deploy, it is done for back compat for older servers
                if (!string.IsNullOrEmpty(Action?.DisplayName))
                {
                    return Action?.DisplayName;
                }
                return string.IsNullOrEmpty(_displayName) ? "run" : _displayName;
            }
            set
            {
                _displayName = value;
            }
        }

        public IExecutionContext ExecutionContext { get; set; }

        public Pipelines.ActionStep Action { get; set; }

        public TemplateToken Timeout => Action?.TimeoutInMinutes;

        public async Task RunAsync()
        {
            // Validate args.
            Trace.Entering();
            ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
            ArgUtil.NotNull(Action, nameof(Action));
            var taskManager = HostContext.GetService<IActionManager>();
            var handlerFactory = HostContext.GetService<IHandlerFactory>();

            // Load the task definition and choose the handler.
            Definition definition = taskManager.LoadAction(ExecutionContext, Action);
            ArgUtil.NotNull(definition, nameof(definition));

            ActionExecutionData handlerData = definition.Data?.Execution;
            ArgUtil.NotNull(handlerData, nameof(handlerData));

            List<JobExtensionRunner> localActionContainerSetupSteps = null;
            // Handle Composite Local Actions
            // Need to download and expand the tree of referenced actions
            if (handlerData.ExecutionType == ActionExecutionType.Composite &&
                handlerData is CompositeActionExecutionData compositeHandlerData &&
                Stage == ActionRunStage.Main &&
                Action.Reference is Pipelines.RepositoryPathReference localAction &&
                string.Equals(localAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
            {
                var actionManager = HostContext.GetService<IActionManager>();
                var prepareResult = await actionManager.PrepareActionsAsync(ExecutionContext, compositeHandlerData.Steps, ExecutionContext.Id);

                // Reload definition since post may exist now (from embedded steps that were JIT downloaded)
                definition = taskManager.LoadAction(ExecutionContext, Action);
                ArgUtil.NotNull(definition, nameof(definition));
                handlerData = definition.Data?.Execution;
                ArgUtil.NotNull(handlerData, nameof(handlerData));

                // Save container setup steps so we can reference them later
                localActionContainerSetupSteps = prepareResult.ContainerSetupSteps;
            }

            if (handlerData.HasPre &&
                Action.Reference is Pipelines.RepositoryPathReference repoAction &&
                string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
            {
                ExecutionContext.Warning($"`pre` execution is not supported for local action from '{repoAction.Path}'");
            }

            // The action has post cleanup defined.
            // we need to create timeline record for them and add them to the step list that StepRunner is using
            if (handlerData.HasPost && (Stage == ActionRunStage.Pre || Stage == ActionRunStage.Main))
            {
                string postDisplayName = $"Post {this.DisplayName}";
                if (Stage == ActionRunStage.Pre &&
                    this.DisplayName.StartsWith("Pre ", StringComparison.OrdinalIgnoreCase))
                {
                    // Trim the leading `Pre ` from the display name.
                    // Otherwise, we will get `Post Pre xxx` as DisplayName for the Post step.
                    postDisplayName = $"Post {this.DisplayName.Substring("Pre ".Length)}";
                }
                var repositoryReference = Action.Reference as RepositoryPathReference;
                var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
                var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
                    $"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";

                ExecutionContext.Debug($"Register post job cleanup for action: {repoString}");

                var actionRunner = HostContext.CreateService<IActionRunner>();
                actionRunner.Action = Action;
                actionRunner.Stage = ActionRunStage.Post;
                actionRunner.Condition = handlerData.CleanupCondition;
                actionRunner.DisplayName = postDisplayName;

                ExecutionContext.RegisterPostJobStep(actionRunner);
            }

            IStepHost stepHost = HostContext.CreateService<IDefaultStepHost>();

            ExecutionContext.WriteWebhookPayload();

            // Set GITHUB_ACTION_REPOSITORY if this Action is from a repository
            if (Action.Reference is Pipelines.RepositoryPathReference repoPathReferenceAction &&
                !string.Equals(repoPathReferenceAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
            {
                ExecutionContext.SetGitHubContext("action_repository", repoPathReferenceAction.Name);
                ExecutionContext.SetGitHubContext("action_ref", repoPathReferenceAction.Ref);
            }
            else
            {
                ExecutionContext.SetGitHubContext("action_repository", null);
                ExecutionContext.SetGitHubContext("action_ref", null);
            }

            // Setup container stephost for running inside the container.
            if (ExecutionContext.Global.Container != null)
            {
                // Make sure the required container is already created
                // Container hooks do not necessarily set 'ContainerId'
                if (!FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
                {
                    ArgUtil.NotNullOrEmpty(ExecutionContext.Global.Container.ContainerId, nameof(ExecutionContext.Global.Container.ContainerId));
                }
                var containerStepHost = HostContext.CreateService<IContainerStepHost>();
                containerStepHost.Container = ExecutionContext.Global.Container;
                stepHost = containerStepHost;
            }

            // Setup File Command Manager
            var fileCommandManager = HostContext.CreateService<IFileCommandManager>();
            fileCommandManager.InitializeFiles(ExecutionContext, null);

            // Load the inputs.
            ExecutionContext.Debug("Loading inputs");
            Dictionary<string, string> inputs;
            if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.UseContainerPathForTemplate) ?? false)
            {
                inputs = EvaluateStepInputs(stepHost);
            }
            else
            {
                var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
                inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
            }

            var userInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            foreach (KeyValuePair<string, string> input in inputs)
            {
                userInputs.Add(input.Key);
                string message = "";
                if (definition.Data?.Deprecated?.TryGetValue(input.Key, out message) == true)
                {
                    ExecutionContext.Warning(String.Format("Input '{0}' has been deprecated with message: {1}", input.Key, message));
                }
            }

            var validInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            if (handlerData.ExecutionType == ActionExecutionType.Container)
            {
                // container action always accept 'entryPoint' and 'args' as inputs
                // https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswithargs
                validInputs.Add("entryPoint");
                validInputs.Add("args");
            }
            // Merge the default inputs from the definition
            if (definition.Data?.Inputs != null)
            {
                var manifestManager = HostContext.GetService<IActionManifestManagerWrapper>();
                foreach (var input in definition.Data.Inputs)
                {
                    string key = input.Key.AssertString("action input name").Value;
                    validInputs.Add(key);
                    if (!inputs.ContainsKey(key))
                    {
                        inputs[key] = manifestManager.EvaluateDefaultInput(ExecutionContext, key, input.Value);
                    }
                }
            }

            // Validate inputs only for actions with action.yml
            if (Action.Reference.Type == Pipelines.ActionSourceType.Repository)
            {
                var unexpectedInputs = new List<string>();
                foreach (var input in userInputs)
                {
                    if (!validInputs.Contains(input))
                    {
                        unexpectedInputs.Add(input);
                    }
                }

                if (unexpectedInputs.Count > 0)
                {
                    ExecutionContext.Warning($"Unexpected input(s) '{string.Join("', '", unexpectedInputs)}', valid inputs are ['{string.Join("', '", validInputs)}']");
                }
            }

            // Load the action environment.
            ExecutionContext.Debug("Loading env");
            var environment = new Dictionary<String, String>(VarUtil.EnvironmentVariableKeyComparer);

#if OS_WINDOWS
            var envContext = ExecutionContext.ExpressionValues["env"] as DictionaryContextData;
#else
            var envContext = ExecutionContext.ExpressionValues["env"] as CaseSensitiveDictionaryContextData;
#endif
            // Apply environment from env context, env context contains job level env and action's evn block
            foreach (var env in envContext)
            {
                environment[env.Key] = env.Value.ToString();
            }

            // Apply action's intra-action state at last
            foreach (var state in ExecutionContext.IntraActionState)
            {
                environment[$"STATE_{state.Key}"] = state.Value ?? string.Empty;
            }

            // Create the handler.
            IHandler handler = handlerFactory.Create(
                            ExecutionContext,
                            Action.Reference,
                            stepHost,
                            handlerData,
                            inputs,
                            environment,
                            ExecutionContext.Global.Variables,
                            actionDirectory: definition.Directory,
                            localActionContainerSetupSteps: localActionContainerSetupSteps);

            // Print out action details and log telemetry
            handler.PrepareExecution(Stage);

            // Run the task.
            try
            {
                await handler.RunAsync(Stage);
            }
            finally
            {
                fileCommandManager.ProcessFiles(ExecutionContext, ExecutionContext.Global.Container);
            }

        }

        /// <summary>
        /// Attempts to update the DisplayName.
        /// As the "Try..." name implies, this method should never throw an exception.
        /// Returns true if the DisplayName is already present or it was successfully updated.
        /// </summary>
        public bool TryUpdateDisplayName(out bool updated)
        {
            updated = false;

            // REVIEW:  This try/catch can be removed if some future implementation of EvaluateDisplayName and UpdateTimelineRecordDisplayName
            // can make reasonable guarantees that they won't throw an exception.
            try
            {
                // This attempt is only worthwhile at the "Main" stage.
                //    When the job starts, there's an initial attempt to evaluate the DisplayName.  (see JobExtension::InitializeJob)
                //    During the "Pre" stage, we expect that no contexts will have changed since the initial evaluation.
                //    "Main" stage is handled here.
                //    During the "Post" stage, it no longer matters.
                if (this.Stage == ActionRunStage.Main && EvaluateDisplayName(this.ExecutionContext.ExpressionValues, this.ExecutionContext, out updated))
                {
                    if (updated)
                    {
                        this.ExecutionContext.UpdateTimelineRecordDisplayName(this.DisplayName);
                    }
                }
            }
            catch (Exception ex)
            {
                Trace.Warning("Caught exception while attempting to evaulate/update the step's DisplayName.  Exception Details:  {0}", ex);
            }

            // For consistency with other implementations of TryUpdateDisplayName we use !string.IsNullOrEmpty below,
            // but note that (at the time of this writing) ActionRunner::DisplayName::get always returns a non-empty string due to its fallback logic.
            // In other words, the net effect is that this particular implementation of TryUpdateDisplayName will always return true.
            return !string.IsNullOrEmpty(this.DisplayName);
        }


        /// <summary>
        /// Attempts to evaluate the DisplayName of this IActionRunner.
        /// Returns true if the DisplayName is already present or it was successfully evaluated.
        /// </summary>
        public bool EvaluateDisplayName(DictionaryContextData contextData, IExecutionContext context, out bool updated)
        {
            ArgUtil.NotNull(context, nameof(context));
            ArgUtil.NotNull(Action, nameof(Action));

            updated = false;
            // If we have already expanded the display name, don't bother attempting [re-]expansion.
            if (_didFullyEvaluateDisplayName || !string.IsNullOrEmpty(Action.DisplayName))
            {
                return true;
            }

            _displayName = GenerateDisplayName(Action, contextData, context, out bool didFullyEvaluate);

            // If we evaluated, fully mask any secrets
            if (didFullyEvaluate)
            {
                _displayName = HostContext.SecretMasker.MaskSecrets(_displayName);
                updated = true;
            }
            context.Debug($"Set step '{Action.Name}' display name to: '{_displayName}'");
            _didFullyEvaluateDisplayName = didFullyEvaluate;
            return didFullyEvaluate;
        }

        private Dictionary<String, String> EvaluateStepInputs(IStepHost stepHost)
        {
            DictionaryContextData expressionValues = ExecutionContext.GetExpressionValues(stepHost);
            var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
            var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, expressionValues, ExecutionContext.ExpressionFunctions);

            return inputs;
        }

        private string GenerateDisplayName(ActionStep action, DictionaryContextData contextData, IExecutionContext context, out bool didFullyEvaluate)
        {
            ArgUtil.NotNull(context, nameof(context));
            ArgUtil.NotNull(action, nameof(action));

            var displayName = string.Empty;
            var prefix = string.Empty;
            var tokenToParse = default(ScalarToken);
            didFullyEvaluate = false;
            // Get the token we need to parse
            // It could be passed in as the Display Name, or we have to pull it from various parts of the Action.
            if (action.DisplayNameToken != null)
            {
                tokenToParse = action.DisplayNameToken as ScalarToken;
            }
            else if (action.Reference?.Type == ActionSourceType.Repository)
            {
                prefix = PipelineTemplateConstants.RunDisplayPrefix;
                var repositoryReference = action.Reference as RepositoryPathReference;
                var pathString = string.Empty;
                if (!string.IsNullOrEmpty(repositoryReference.Path))
                {
                    // For local actions (Name is empty), don't prepend "/" to avoid "/./"
                    pathString = string.IsNullOrEmpty(repositoryReference.Name)
                        ? repositoryReference.Path
                        : $"/{repositoryReference.Path}";
                }
                var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
                    $"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
                tokenToParse = new StringToken(null, null, null, repoString);
            }
            else if (action.Reference?.Type == ActionSourceType.ContainerRegistry)
            {
                prefix = PipelineTemplateConstants.RunDisplayPrefix;
                var containerReference = action.Reference as ContainerRegistryReference;
                tokenToParse = new StringToken(null, null, null, containerReference.Image);
            }
            else if (action.Reference?.Type == ActionSourceType.Script)
            {
                prefix = PipelineTemplateConstants.RunDisplayPrefix;
                var inputs = action.Inputs.AssertMapping(null);
                foreach (var pair in inputs)
                {
                    var propertyName = pair.Key.AssertString($"{PipelineTemplateConstants.Steps}");
                    if (string.Equals(propertyName.Value, "script", StringComparison.OrdinalIgnoreCase))
                    {
                        tokenToParse = pair.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Run}");
                        break;
                    }
                }
            }
            else
            {
                context.Error($"Encountered an unknown action reference type when evaluating the display name: {action.Reference?.Type}");
                return displayName;
            }

            // If we have nothing to parse, abort
            if (tokenToParse == null)
            {
                return displayName;
            }
            // Try evaluating fully
            try
            {
                if (tokenToParse.CheckHasRequiredContext(contextData, context.ExpressionFunctions))
                {
                    var templateEvaluator = context.ToPipelineTemplateEvaluator();
                    displayName = templateEvaluator.EvaluateStepDisplayName(tokenToParse, contextData, context.ExpressionFunctions);
                    didFullyEvaluate = true;
                }
            }
            catch (TemplateValidationException e)
            {
                context.Warning($"Encountered an error when evaluating display name {tokenToParse.ToString()}. {e.Message}");
                return displayName;
            }

            // Default to a prettified token if we could not evaluate
            if (!didFullyEvaluate)
            {
                displayName = tokenToParse.ToDisplayString();
            }

            displayName = FormatStepName(prefix, displayName);
            return displayName;
        }

        private static string FormatStepName(string prefix, string stepName)
        {
            if (string.IsNullOrEmpty(stepName))
            {
                return string.Empty;
            }

            var result = stepName.TrimStart(' ', '\t', '\r', '\n');
            var firstNewLine = result.IndexOfAny(new[] { '\r', '\n' });
            if (firstNewLine >= 0)
            {
                result = result.Substring(0, firstNewLine);
            }
            return $"{prefix}{result}";
        }
    }
}
