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

using System.Diagnostics;
using System.IO.Pipes;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.NET.Sdk.Razor.Tool;

namespace Microsoft.AspNetCore.Razor.Tasks
{
    public abstract class DotNetToolTask : ToolTask
    {
        // From https://github.com/dotnet/corefx/blob/29cd6a0b0ac2993cee23ebaf36ca3d4bce6dd75f/src/System.IO.Pipes/ref/System.IO.Pipes.cs#L93.
        // Using the enum value directly as this option is not available in netstandard.
        private const PipeOptions PipeOptionCurrentUserOnly = (PipeOptions)536870912;
        private string _dotnetPath;

        private CancellationTokenSource _razorServerCts;

        public bool Debug { get; set; }

        public bool DebugTool { get; set; }

        [Required]
        public string ToolAssembly { get; set; }

        public bool UseServer { get; set; }

        // Specifies whether we should fallback to in-process execution if server execution fails.
        public bool ForceServer { get; set; }

        // Specifies whether server execution is allowed when PipeOptions.CurrentUserOnly is not available.
        // For testing purposes only.
        public bool SuppressCurrentUserOnlyPipeOptions { get; set; }

        public string PipeName { get; set; }

        protected override string ToolName => "dotnet";

        // If we're debugging then make all of the stdout gets logged in MSBuild
        protected override MessageImportance StandardOutputLoggingImportance => DebugTool ? MessageImportance.High : base.StandardOutputLoggingImportance;

        protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High;

        internal abstract string Command { get; }

        protected override string GenerateFullPathToTool() => DotNetPath;

        private string DotNetPath
        {
            get
            {
                if (!string.IsNullOrEmpty(_dotnetPath))
                {
                    return _dotnetPath;
                }

                var dotnetHostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH");
                _dotnetPath = string.IsNullOrEmpty(dotnetHostPath) ? ToolExe : dotnetHostPath;

                return _dotnetPath;
            }
        }

        protected override string GenerateCommandLineCommands()
        {
            return $"exec \"{ToolAssembly}\"" + (DebugTool ? " --debug" : "");
        }

        protected override string GetResponseFileSwitch(string responseFilePath)
        {
            return "@\"" + responseFilePath + "\"";
        }

        protected abstract override string GenerateResponseFileCommands();

        public override bool Execute()
        {
            if (Debug)
            {
#if NET5_0_OR_GREATER
                var processId = Environment.ProcessId;
#else
                var processId = Process.GetCurrentProcess().Id;
#endif

                Log.LogMessage(MessageImportance.High, "Waiting for debugger in pid: {0}", processId);
                while (!Debugger.IsAttached)
                {
                    Thread.Sleep(TimeSpan.FromSeconds(3));
                }
            }

            return base.Execute();
        }

        protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands)
        {
            if (UseServer &&
                TryExecuteOnServer(pathToTool, responseFileCommands, commandLineCommands, out var result))
            {
                return result;
            }

            return base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands);
        }

        protected override void LogToolCommand(string message)
        {
            if (Debug)
            {
                Log.LogMessage(MessageImportance.High, message);
            }
            else
            {
                base.LogToolCommand(message);
            }
        }

        public override void Cancel()
        {
            base.Cancel();

            _razorServerCts?.Cancel();
        }

        protected virtual bool TryExecuteOnServer(
            string pathToTool,
            string responseFileCommands,
            string commandLineCommands,
            out int result)
        {
#if !NETFRAMEWORK
            if (!SuppressCurrentUserOnlyPipeOptions && !Enum.IsDefined(typeof(PipeOptions), PipeOptionCurrentUserOnly))
            {
                // For security reasons, we don't want to spin up a server that doesn't
                // restrict requests only to the current user.
                result = -1;

                return ForceServer;
            }
#endif

            Log.LogMessage(StandardOutputLoggingImportance, "Server execution started.");
            using (_razorServerCts = new CancellationTokenSource())
            {
                Log.LogMessage(StandardOutputLoggingImportance, $"CommandLine = '{commandLineCommands}'");
                Log.LogMessage(StandardOutputLoggingImportance, $"ServerResponseFile = '{responseFileCommands}'");

                // The server contains the tools for discovering tag helpers and generating Razor code.
                var clientDir = Path.GetFullPath(Path.GetDirectoryName(ToolAssembly));
                var workingDir = CurrentDirectoryToUse();
                var tempDir = ServerConnection.GetTempPath(workingDir);
                var serverPaths = new ServerPaths(
                    clientDir,
                    workingDir: workingDir,
                    tempDir: tempDir);

                var arguments = GetArguments(responseFileCommands);

                var responseTask = ServerConnection.RunOnServer(PipeName, arguments, serverPaths, _razorServerCts.Token, debug: DebugTool);
                responseTask.Wait(_razorServerCts.Token);

                var response = responseTask.Result;
                if (response.Type == ServerResponse.ResponseType.Completed &&
                    response is CompletedServerResponse completedResponse)
                {
                    result = completedResponse.ReturnCode;

                    if (result == 0)
                    {
                        // Server execution succeeded.
                        Log.LogMessage(StandardOutputLoggingImportance, $"Server execution completed with return code {result}.");

                        // There might still be warnings in the error output.
                        if (LogStandardErrorAsError)
                        {
                            LogErrors(completedResponse.ErrorOutput);
                        }
                        else
                        {
                            LogMessages(completedResponse.ErrorOutput, StandardErrorLoggingImportance);
                        }

                        return true;
                    }
                    else if (result == 2)
                    {
                        // Server execution completed with a legit error. No need to fallback to cli execution.
                        Log.LogMessage(StandardOutputLoggingImportance, $"Server execution completed with return code {result}. For more info, check the server log file in the location specified by the RAZORBUILDSERVER_LOG environment variable.");

                        if (LogStandardErrorAsError)
                        {
                            LogErrors(completedResponse.ErrorOutput);
                        }
                        else
                        {
                            LogMessages(completedResponse.ErrorOutput, StandardErrorLoggingImportance);
                        }

                        return true;
                    }
                    else
                    {
                        // Server execution completed with an error but we still want to fallback to cli execution.
                        Log.LogMessage(StandardOutputLoggingImportance, $"Server execution completed with return code {result}. For more info, check the server log file in the location specified by the RAZORBUILDSERVER_LOG environment variable.");
                    }
                }
                else
                {
                    // Server execution failed. Fallback to cli execution.
                    Log.LogMessage(
                        StandardOutputLoggingImportance,
                        $"Server execution failed with response {response.Type}. For more info, check the server log file in the location specified by the RAZORBUILDSERVER_LOG environment variable.");

                    result = -1;
                }

                if (ForceServer)
                {
                    // We don't want to fallback to in-process execution.
                    return true;
                }

                Log.LogMessage(StandardOutputLoggingImportance, "Fallback to in-process execution.");
            }

            return false;
        }

        private void LogMessages(string output, MessageImportance messageImportance)
        {
            var lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var line in lines)
            {
                if (!string.IsNullOrWhiteSpace(line))
                {
                    var trimmedMessage = line.Trim();
                    Log.LogMessageFromText(trimmedMessage, messageImportance);
                }
            }
        }

        private void LogErrors(string output)
        {
            var lines = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            foreach (var line in lines)
            {
                if (!string.IsNullOrWhiteSpace(line))
                {
                    var trimmedMessage = line.Trim();
                    Log.LogError(trimmedMessage);
                }
            }
        }

        /// <summary>
        /// Get the current directory that the compiler should run in.
        /// </summary>
        private string CurrentDirectoryToUse()
        {
            // ToolTask has a method for this. But it may return null. Use the process directory
            // if ToolTask didn't override. MSBuild uses the process directory.
            var workingDirectory = GetWorkingDirectory();
            if (string.IsNullOrEmpty(workingDirectory))
            {
                workingDirectory = Directory.GetCurrentDirectory();
            }
            return workingDirectory;
        }

        private IList<string> GetArguments(string responseFileCommands)
        {
            var list = responseFileCommands.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
            return list;
        }

        protected override bool HandleTaskExecutionErrors()
        {
            if (!HasLoggedErrors)
            {
                var toolCommand = Path.GetFileNameWithoutExtension(ToolAssembly) + " " + Command;
                // Show a slightly better error than the standard ToolTask message that says "dotnet" failed.
                Log.LogError($"{toolCommand} exited with code {ExitCode}.");
                return false;
            }

            return base.HandleTaskExecutionErrors();
        }
    }
}
