using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Text;
using System.Threading.Tasks;

using C4IT.FASD.Base;
using C4IT.MultiLanguage;
using C4IT.Security;

using C4IT.Logging;
using static C4IT.Logging.cLogManager;

namespace FasdCockpitBase
{
    public class cExternalToolExecutor
    {
        public interface iNamedParameter
        {
            string Title { get; set; }
            string GetValue();
            (string Title, string Value) GetTitleValuePair();
        }

        [DllImport("shell32.dll", EntryPoint = "ShellExecute")]
        public static extern long ShellExecute(int hwnd, string cmd, string file, string param1, string param2, int swmode);

        private static cCredential RemoteToolAlternateCredentials;

        public class cProcessResult
        {
            public cProcessResult(int returnCode) { ReturnCode = returnCode; }
            public int ReturnCode = -1;
            public string StandardOutput = null;
            public string StandardError = null;
        }

        static protected cCredential GetRemoteToolCredentials(string Title)
        {
            if (RemoteToolAlternateCredentials == null)
            {
                var Caption = cMultiLanguageSupport.GetItem("RemoteTool.Credentials.Caption");
                Caption = string.Format(Caption, Title);
                var Message = cMultiLanguageSupport.GetItem("RemoteTool.Credentials.Message");

                RemoteToolAlternateCredentials = cInputCredentials.GetCredentials(Caption, Message);
            }

            return RemoteToolAlternateCredentials;
        }

        static protected void AddFileSecurity(string fileName, string account, FileSystemRights rights, AccessControlType controlType)
        {
            try
            {
                FileSecurity fSecurity = File.GetAccessControl(fileName);
                fSecurity.AddAccessRule(new FileSystemAccessRule(account, rights, controlType));
                File.SetAccessControl(fileName, fSecurity);
            }
            catch (Exception E)
            {
                LogException(E);
            }
        }

        static protected string ReplaceEnvironmentVariables(string input)
        {
            var output = input;

            try
            {
                var specialFolders = Enum.GetValues(typeof(Environment.SpecialFolder)).Cast<Environment.SpecialFolder>();

                foreach (var specialFolder in specialFolders)
                {
                    var specialFolderName = "%" + specialFolder.ToString() + "%";
                    var specialFolderPath = Environment.GetFolderPath(specialFolder);
                    output = output.Replace(specialFolderName, specialFolderPath);
                }

                output = Environment.ExpandEnvironmentVariables(output);
            }
            catch (Exception E)
            {
                LogException(E);
            }

            return output;
        }

        static protected async Task<cProcessResult> RunProcessAsync(string Cmd, string Params, cCredential Credentials, bool Hidden)
        {
            try
            {
                cProcessResult processResult = new cProcessResult(0);

                var process = createProcessInstance(Cmd, Params, Credentials, true, Hidden);

                TaskCompletionSource<int> _resultAwaiter = null;
                _resultAwaiter = new TaskCompletionSource<int>();
                EventHandler resultEventHandler = null;
                resultEventHandler = (sender, args) =>
                {
                    process.Exited -= resultEventHandler;
                    _resultAwaiter.SetResult(process.ExitCode);
                };
                process.Exited += resultEventHandler;
                process.EnableRaisingEvents = true;

                try
                {
                    process.Start();
                }
                catch (Exception E)
                {
                    LogException(E, LogLevels.Warning);
                    return new cProcessResult(E.HResult);
                }

                var t1 = process.StandardOutput.ReadToEndAsync();
                var t2 = process.StandardError.ReadToEndAsync();
                var t3 = _resultAwaiter.Task;

                await Task.WhenAll(t1, t2, t3);

                processResult.StandardOutput = (await t1);
                processResult.StandardError = (await t2);
                processResult.ReturnCode = (await t3);

                return processResult;
            }
            catch (Exception E)
            {
                LogException(E);
                return new cProcessResult(E.HResult);
            }
        }

        static protected cProcessResult StartProcessAsync(string Cmd, string Params, cCredential Credentials, bool Hidden)
        {
            try
            {
                using (var process = createProcessInstance(Cmd, Params, Credentials, false, Hidden))
                {
                    var _s = process.Start();
                }
                return new cProcessResult(0);
            }
            catch (Exception E)
            {
                LogException(E, LogLevels.Warning);
                return new cProcessResult(E.HResult);
            }
        }

        static protected Process createProcessInstance(string Cmd, string Params, cCredential Credentials, bool Wait, bool Hidden)
        {
            try
            {
                var processInformation = new ProcessStartInfo(Cmd, Params);
                processInformation.UseShellExecute = false;
                if (Credentials != null)
                {
                    processInformation.LoadUserProfile = true;
                    processInformation.UserName = Credentials.User;
                    processInformation.Password = Credentials.Password;
                    processInformation.Domain = Credentials.Domain;
                }

                if (Hidden)
                {
                    processInformation.CreateNoWindow = true;
                    processInformation.WindowStyle = ProcessWindowStyle.Hidden;
                }

                if (Wait)
                {
                    processInformation.RedirectStandardOutput = true;
                    processInformation.RedirectStandardError = true;
                }

                var process = new Process() { StartInfo = processInformation };

                return process;
            }
            catch (Exception E)
            {
                LogException(E);
            }

            return null;
        }

        static public string ReplaceParameters(string input, Dictionary<string, iNamedParameter> NamedParameters, bool ReplaceEnvironment, Dictionary<cAdjustableParameter, object> ParameterDictionary = null)
        {
            string output = input;
            try
            {
                if (input != null)
                {
                    if (ParameterDictionary != null)
                    {
                        foreach (var parameter in ParameterDictionary)
                        {
                            var _str = parameter.Key.GetValueString(parameter.Value);
                            output = output.Replace("%" + parameter.Key.ParameterName + "%", _str);
                        }
                    }

                    if (NamedParameters != null)
                        foreach (var namedParameter in NamedParameters)
                        {
                            try
                            {
                                var parameterName = namedParameter.Key;

                                var namedParameterKeyValuePair = namedParameter.Value.GetTitleValuePair();
                                var parameterLabel = namedParameterKeyValuePair.Title;
                                var parameterValue = namedParameterKeyValuePair.Value;

                                output = output.Replace("%" + parameterName + ".Label%", parameterLabel);
                                output = output.Replace("%" + parameterName + ".Value%", parameterValue);
                            }
                            catch { }
                        }

                    if (ReplaceEnvironment)
                        output = ReplaceEnvironmentVariables(output);
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }
            return output;
        }

        public static async Task<cProcessResult> StartLocalActionAsync(cFasdQuickActionLocal localAction, Dictionary<string, iNamedParameter> NamedParameters, string ProgramTitle, Dictionary<cAdjustableParameter, object> ParameterDictionary = null)
        {
            cProcessResult _resultProcess = new cProcessResult(0);

            cCredential Credentials = null;
            if (localAction.StartWithAlternateCredentials)
                Credentials = GetRemoteToolCredentials(ProgramTitle);

            const string constPowershellPath = @"%System%\WindowsPowerShell\v1.0\powershell.exe";

            string parameters = null;
            string cmd = null;
            string scriptName = null;
            bool dontUseShell = true;

            bool isHidden = false;
            bool isScript = false;
            bool doWait = (localAction.ColumnOutputFormattings?.Count > 0);

            if (localAction is cFasdQuickActionLocalScript quickActionScript)
            {
                isScript = true;
                isHidden = true;
                doWait = true;

                cmd = constPowershellPath;
                scriptName = Path.GetTempFileName();
                if (File.Exists(scriptName))
                    File.Delete(scriptName);
                scriptName += ".ps1";

                if (quickActionScript.IsBase64)
                    File.WriteAllBytes(scriptName, quickActionScript.ScriptBinary);
                else
                    File.WriteAllText(scriptName, quickActionScript.Script, Encoding.Unicode);

                if (localAction.StartWithAlternateCredentials && (Credentials != null))
                    AddFileSecurity(scriptName, Credentials.GetAccount(true), FileSystemRights.Read, AccessControlType.Allow);

                parameters = $"-NoLogo -ExecutionPolicy bypass -file \"{scriptName}\"";

                if (localAction.Parameters != null)
                    parameters += " " + localAction.Parameters;
            }
            else if (localAction is cFasdQuickActionLocalCmd quickActionCmd)
            {
                cmd = quickActionCmd.Cmd;
                parameters = quickActionCmd.Parameters;
                dontUseShell = quickActionCmd.DontUseShell;
            }
            else
                return _resultProcess;

            cmd = ReplaceEnvironmentVariables(cmd);

            if (parameters != null)
                parameters = ReplaceParameters(parameters, NamedParameters, true, ParameterDictionary);

            try
            {
                if (!dontUseShell && !localAction.StartWithAlternateCredentials)
                {
                    var PRet = (int)ShellExecute(0, "open", cmd, parameters, null, 5);

                    /*
                    * The Microsoft docs states as following regarding the return value of a ShellExecute:
                    * https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutea
                    * 
                    * If the function succeeds, it returns a value greater than 32. 
                    * If the function fails, it returns an error value that indicates the cause of the failure. 
                    * The return value is cast as an HINSTANCE for backward compatibility with 16-bit Windows applications. It is not a true HINSTANCE, however. 
                    * It can be cast only to an INT_PTR and compared to either 32 or the following error codes below.
                    */
                    const int highestFailureReturnCode = 32;

                    // Our internal cPorcessResult is built that way a ReturnCode of 0 is considered as successfull.
                    // This has to be mapped to the HINSTANCE result code above.
                    if (PRet > highestFailureReturnCode)
                        PRet = 0;

                    _resultProcess = new cProcessResult(PRet);
                }
                else
                {
                    await Task.Run(async () =>
                    {
                        if (localAction.RequireAdministrator && localAction.StartWithAlternateCredentials)
                        {
                            cmd = ReplaceEnvironmentVariables("%System%\\WindowsPowerShell\\v1.0\\powershell.exe");
                            var argumentList = parameters.Replace("\"", "\"\"");
                            var Cmd = $"Start-Process \"{cmd}\" -WindowStyle Hidden -Wait -Verb \"runas\" -ArgumentList \"{argumentList}\"";
                            var parameterByteArray = Encoding.Unicode.GetBytes(Cmd);
                            parameters = Convert.ToBase64String(parameterByteArray);
                            parameters = "-NonInteractive -NoLogo -ExecutionPolicy bypass -EncodedCommand " + parameters;
                            isHidden = true;
                        }

                        if (doWait)
                            _resultProcess = await RunProcessAsync(cmd, parameters, Credentials, isHidden);
                        else
                            _resultProcess = StartProcessAsync(cmd, parameters, Credentials, isHidden);
                    });
                }
            }
            catch (Exception E)
            {
                LogException(E);
            }

            if (isScript)
            {
                try
                {
                    File.Delete(scriptName);
                }
                catch { }
                ;
            }

            return _resultProcess;
        }

    }
}
