// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.

//go:build windows
// +build windows

package diagnosticsutil

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/aws/amazon-ssm-agent/agent/appconfig"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/log/logger"
	"github.com/aws/amazon-ssm-agent/agent/proxyconfig"
	"github.com/aws/amazon-ssm-agent/core/workerprovider/longrunningprovider/model"
	"golang.org/x/sys/windows/registry"
)

const (
	// ExpectedServiceRunningUser is the user we expect the agent to be running as
	ExpectedServiceRunningUser = "NT AUTHORITY\\SYSTEM"

	// newlineCharacter is the system specific newline character
	newlineCharacter = "\r\n"

	defaultCommandTimeOut = 30 * time.Second
)

var powershellArgs = []string{"-InputFormat", "None", "-Noninteractive", "-NoProfile", "-ExecutionPolicy", "unrestricted"}

func executePowershellCommandWithTimeout(timeout time.Duration, command string) (string, error) {
	args := append(powershellArgs, "-Command", command)
	return ExecuteCommandWithTimeout(timeout, appconfig.PowerShellPluginCommandName, args...)
}

func executePowershellScriptWithTimeout(timeout time.Duration, scriptPath string, scriptArgs ...string) (string, error) {
	args := append(powershellArgs, "-File", scriptPath)
	args = append(args, scriptArgs...)
	return ExecuteCommandWithTimeout(timeout, appconfig.PowerShellPluginCommandName, args...)
}

func isHttpOrHttpsProxyConfigured(proxyEnv map[string]string) bool {
	_, ishttpSet := proxyEnv[proxyconfig.PROXY_VAR_HTTP]
	_, ishttpsSet := proxyEnv[proxyconfig.PROXY_VAR_HTTPS]

	return ishttpSet || ishttpsSet
}

func isBypassProxyConfigured(proxyEnv map[string]string) bool {
	_, isBypassSet := proxyEnv[proxyconfig.PROXY_VAR_BYPASS]

	return isBypassSet
}

func setWindowsBypass(log log.T, strToParse string, proxyEnv map[string]string) {
	if len(strToParse) == 0 {
		return
	}

	proxyBypassList := proxyconfig.ParseProxyBypass(log, strToParse)
	if len(proxyBypassList) > 0 {
		proxyEnv[proxyconfig.PROXY_VAR_BYPASS] = strings.Join(proxyBypassList, ",")
	}
}

func setWindowsProxy(log log.T, strToParse string, proxyEnv map[string]string) {
	if len(strToParse) == 0 {
		return
	}

	proxySettings := proxyconfig.ParseProxySettings(log, strToParse)

	// http proxy
	if proxySettings.HttpProxy != nil {
		proxyEnv["http_proxy"] = proxySettings.HttpProxy.String()
	}

	// https proxy
	if proxySettings.HttpsProxy != nil {
		proxyEnv["https_proxy"] = proxySettings.HttpsProxy.String()
	}
}

func assumeSystemUserEnvironmentProxy(proxyEnv map[string]string) {
	SysRegKey, err := registry.OpenKey(registry.LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment", registry.QUERY_VALUE)
	if err != nil {
		return
	}

	defer SysRegKey.Close()
	for _, key := range proxyconfig.ProxyEnvVariables {
		// Service registry has priority over system env variables, don't overwrite if value already set
		if _, ok := proxyEnv[key]; ok {
			continue
		}

		systemVal, _, err := SysRegKey.GetStringValue(key)
		if err != nil {
			continue
		}

		proxyEnv[key] = systemVal
	}
}

func assumeLocalSystemUserEnvironmentProxy(proxyEnv map[string]string) {
	localSysRegKey, err := registry.OpenKey(registry.USERS, ".DEFAULT\\Environment", registry.QUERY_VALUE)
	if err != nil {
		return
	}

	defer localSysRegKey.Close()
	for _, key := range proxyconfig.ProxyEnvVariables {
		// System user env variables have priority over local user env variables, don't overwrite if value already set
		if _, ok := proxyEnv[key]; ok {
			continue
		}

		localSystemVal, _, err := localSysRegKey.GetStringValue(key)
		if err != nil {
			continue
		}

		proxyEnv[key] = localSystemVal
	}
}

func assumeAgentServiceEnvironmentProxy(proxyEnv map[string]string) {
	// golang windows registry module does not support REG_MULTI_SZ registry value types and produces an error 'unexpected key value type'
	command := `(Get-Item -Path 'HKLM:\System\CurrentControlSet\Services\AmazonSSMAgent').GetValue('Environment')`

	output, err := executePowershellCommandWithTimeout(2*time.Second, command)
	if err != nil {
		return
	}

	if len(output) == 0 {
		return
	}

	// Create proxy key map
	supportedProxyVars := map[string]bool{}
	for _, proxyKey := range proxyconfig.ProxyEnvVariables {
		supportedProxyVars[proxyKey] = true
	}

	for _, envKeyVal := range strings.Split(output, newlineCharacter) {
		envSplit := strings.Split(strings.TrimSpace(envKeyVal), "=")
		if len(envSplit) < 2 {
			// if split at = does not create two indices, skip
			continue
		}

		if supportedProxyVars[envSplit[0]] {
			// Override environment variables because amazonssmagent registry has highest precedent
			proxyEnv[envSplit[0]] = envSplit[1]
		}
	}
}

func assumeInternetExplorerProxySettings(proxyEnv map[string]string) {
	silentLog := logger.NewSilentLogger()

	commandFormatStr := `(Get-Item -Path 'Registry::HKEY_USERS\.DEFAULT\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings').GetValue('%s')`

	// Check if IE proxy is enabled
	isEnabled, err := executePowershellCommandWithTimeout(2*time.Second, fmt.Sprintf(commandFormatStr, "ProxyEnable"))
	if err != nil || isEnabled != "1" {
		return
	}

	if !isHttpOrHttpsProxyConfigured(proxyEnv) {
		// Get proxy server settings
		proxyServerStr, err := executePowershellCommandWithTimeout(2*time.Second, fmt.Sprintf(commandFormatStr, "ProxyServer"))
		if err == nil {
			setWindowsProxy(silentLog, proxyServerStr, proxyEnv)
		}
	}

	// Check if bypass is already set, if it is return
	if isBypassProxyConfigured(proxyEnv) {
		return
	}

	// Get proxy server settings
	proxyBypassStr, err := executePowershellCommandWithTimeout(2*time.Second, fmt.Sprintf(commandFormatStr, "ProxyOverride"))
	if err == nil {
		setWindowsBypass(silentLog, proxyBypassStr, proxyEnv)
	}
}

func assumeWinHTTPProxySettings(proxyEnv map[string]string) {
	silentLog := logger.NewSilentLogger()
	if isHttpOrHttpsProxyConfigured(proxyEnv) && isBypassProxyConfigured(proxyEnv) {
		// both proxy and bypass are set, ignore winhttp
		return
	}

	// script to query winhttp proxy settings using powershell
	scriptStr := `$QueryString=$args[0]
$MethodDefinition = @'
using System.Runtime.InteropServices;
public enum AccessType
{
    DefaultProxy = 0,
    NamedProxy = 3,
    NoProxy = 1
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WINHTTP_PROXY_INFO
{
    public AccessType AccessType;
    public string Proxy;
    public string Bypass;
}
public class WinHttp
{
    [DllImport("winhttp.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    public static extern bool WinHttpGetDefaultProxyConfiguration(ref WINHTTP_PROXY_INFO config);
}
'@
    $Assembly = Add-Type -TypeDefinition $MethodDefinition -PassThru
    $object = New-Object WINHTTP_PROXY_INFO
    $object.AccessType = [AccessType]::DefaultProxy
    $ret = [WinHttp]::WinHttpGetDefaultProxyConfiguration([ref]$object)

	If ($QueryString -eq 'Proxy') {
		Write-Host $object.Proxy
	} Else {
		Write-Host $object.Bypass
	}`
	scriptPath := filepath.Join(appconfig.SSMDataPath, "ssm_cli_query_winhttp_proxy.ps1")
	f, err := os.Create(scriptPath)
	if err != nil {
		return
	}
	defer os.Remove(scriptPath)

	_, err = fmt.Fprintln(f, scriptStr)
	f.Close()
	if err != nil {
		return
	}

	if !isHttpOrHttpsProxyConfigured(proxyEnv) {
		proxyServerStr, err := executePowershellScriptWithTimeout(10*time.Second, scriptPath, "Proxy")
		if err == nil {
			setWindowsProxy(silentLog, proxyServerStr, proxyEnv)
		}
	}

	// Check if bypass is already set, if it is return
	if isBypassProxyConfigured(proxyEnv) {
		return
	}

	proxyBypassStr, err := executePowershellScriptWithTimeout(10*time.Second, scriptPath, "Bypass")
	if err == nil {
		setWindowsBypass(silentLog, proxyBypassStr, proxyEnv)
	}
}

// AssumeAgentEnvironmentProxy reads system and localsystem environment, and amazonssmagent registry to assume agent proxy
func AssumeAgentEnvironmentProxy() {
	proxyEnv := map[string]string{}

	// AmazonSSMAgent service registry
	assumeAgentServiceEnvironmentProxy(proxyEnv)

	// System environment vars
	assumeSystemUserEnvironmentProxy(proxyEnv)

	// Local system user vars
	assumeLocalSystemUserEnvironmentProxy(proxyEnv)

	// Internet Explorer proxy settings
	assumeInternetExplorerProxySettings(proxyEnv)

	// WinHTTP proxy settings
	assumeWinHTTPProxySettings(proxyEnv)

	// Set proxy for windows
	for _, proxyVar := range proxyconfig.ProxyEnvVariables {
		if val, ok := proxyEnv[proxyVar]; ok {
			os.Setenv(proxyVar, val)
		} else {
			os.Setenv(proxyVar, "")
		}
	}
}

func GetUserRunningAgentProcess() (string, error) {
	command := `(Get-Process -Name amazon-ssm-agent -IncludeUserName | Select-Object -Property UserName | ConvertTo-Csv -NoTypeInformation | Select-Object -Skip 1) -replace '"'`

	cmdOutput, err := executePowershellCommandWithTimeout(2*time.Second, command)
	if err != nil {
		return "", fmt.Errorf("failed to query for user running amazon-ssm-agent: %s", err)
	}

	return cmdOutput, nil
}

func getAgentFilePath() (string, error) {
	return appconfig.DefaultSSMAgentBinaryPath, nil
}

func getAgentProcessPath() (string, error) {
	return model.SSMAgentBinaryName, nil
}
