//  Copyright 2019 Google Inc. 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.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License 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.

// Package utils contains helper utils for osconfig_tests.
package utils

import (
	"fmt"
	"math/rand"
	"path"
	"strings"
	"time"

	daisyCompute "github.com/GoogleCloudPlatform/compute-image-tools/daisy/compute"
	"github.com/GoogleCloudPlatform/osconfig/e2e_tests/compute"
	"github.com/GoogleCloudPlatform/osconfig/e2e_tests/config"
	api "google.golang.org/api/compute/v1"
	"google.golang.org/grpc/status"
)

var (
	testingPkgsProjectName = "gce-pkg-osconfig-testing"

	yumInstallAgent = `
sed -i 's/repo_gpgcheck=1/repo_gpgcheck=0/g' /etc/yum.repos.d/google-cloud.repo
sleep 10
systemctl stop google-osconfig-agent
while ! yum install -y google-osconfig-agent; do
if [[ n -gt 3 ]]; then
  exit 1
fi
n=$[$n+1]
sleep 5
done
systemctl start google-osconfig-agent` + CurlPost

	zypperInstallAgent = `
sleep 10
systemctl stop google-osconfig-agent
zypper -n remove google-osconfig-agent
while ! zypper -n -i --no-gpg-checks install google-osconfig-agent; do
if [[ n -gt 2 ]]; then
  # Zypper repos are flaky, we retry 3 times then just continue, the agent may be installed fine.
  zypper --no-refresh -n -i --no-gpg-checks install google-osconfig-agent
  break
fi
n=$[$n+1]
sleep 5
done
systemctl start google-osconfig-agent` + CurlPost

	// CosSetup sets up serial logging on COS.
	CosSetup = `
sleep 10
sed -i 's/^#ForwardToConsole=no/ForwardToConsole=yes/' /etc/systemd/journald.conf
sed -i 's/^#MaxLevelConsole=info/MaxLevelConsole=debug/' /etc/systemd/journald.conf
MaxLevelConsole=debug
systemctl force-reload systemd-journald
systemctl restart google-osconfig-agent` + CurlPost

	// CurlPost indicates agent is installed.
	CurlPost = `
uri=http://metadata.google.internal/computeMetadata/v1/instance/guest-attributes/guestInventory/LastUpdated
curl -X DELETE $uri -H "Metadata-Flavor: Google"

uri=http://metadata.google.internal/computeMetadata/v1/instance/guest-attributes/osconfig_tests/install_done
curl -X PUT --data "1" $uri -H "Metadata-Flavor: Google"
`

	windowsPost = `
$uri = 'http://metadata.google.internal/computeMetadata/v1/instance/guest-attributes/guestInventory/LastUpdated'
Invoke-RestMethod -Method DELETE -Uri $uri -Headers @{"Metadata-Flavor" = "Google"}
Start-Sleep 10
$uri = 'http://metadata.google.internal/computeMetadata/v1/instance/guest-attributes/osconfig_tests/install_done'
Invoke-RestMethod -Method PUT -Uri $uri -Headers @{"Metadata-Flavor" = "Google"} -Body 1
`
)

// getRegionFromZone extracts the region from a zone name.
// Example: If zone is "us-central1-a", this would return "us-central1"
func getRegionFromZone(zone string) string {
	parts := strings.Split(zone, "-")
	return strings.Join(parts[:len(parts)-1], "-")
}

// pickTestRegionForArtifactRegistry selects a random zone from the configured zones to pull osconfig-agent package from AR & selected-region
func pickTestRegionForArtifactRegistry() string {
	zones := config.Zones()

	if len(zones) == 0 {
		// default region for tests
		return "us-central1"
	}

	zoneKeys := make([]string, 0, len(zones))
	for k := range zones {
		zoneKeys = append(zoneKeys, k)
	}
	randomIndex := rand.Intn(len(zoneKeys))
	randomZone := zoneKeys[randomIndex]

	return getRegionFromZone(randomZone)
}

// getRepoLineForApt returns the repo line that should be added to apt sources.list file
func getRepoLineForApt(osName string) string {
	repo := config.AgentRepo()
	if repo == "testing" {
		testRegion := pickTestRegionForArtifactRegistry()
		return fmt.Sprintf("deb ar+https://%s-apt.pkg.dev/projects/%s google-osconfig-agent-%s-testing main",
			testRegion, testingPkgsProjectName, osName)
	}
	return fmt.Sprintf("deb http://packages.cloud.google.com/apt google-osconfig-agent-%s-%s main", osName, repo)
}

// InstallOSConfigDeb installs the osconfig agent on deb based systems.
func InstallOSConfigDeb(image string) string {
	if config.AgentRepo() == "" {
		return CurlPost
	}
	osName := GetDebOsName(image)
	return fmt.Sprintf(`
sleep 10
systemctl stop google-osconfig-agent

# disable the backports repo for debian-11
sed -i 's/^.*debian bullseye-backports main.*$//g' /etc/apt/sources.list

# install gnupg2 if not exist
apt-get update
apt-get install -y gnupg2

# install apt-transport-artifact-registry
apt-get install -y apt-transport-artifact-registry

echo '%s' >> /etc/apt/sources.list

curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
apt-get update
apt-get install -y google-osconfig-agent
systemctl start google-osconfig-agent`+CurlPost, getRepoLineForApt(osName))
}

// InstallOSConfigGooGet installs the osconfig agent on Windows systems.
func InstallOSConfigGooGet() string {
	if config.AgentRepo() == "" {
		return windowsPost
	}
	removeAgentCmd := `c:\programdata\googet\googet.exe -noconfirm remove google-osconfig-agent`
	if config.AgentRepo() == "stable" {
		return fmt.Sprintf(`
%s
c:\programdata\googet\googet.exe -noconfirm install google-osconfig-agent`, removeAgentCmd) + windowsPost
	} else if config.AgentRepo() == "testing" {
		testRegion := pickTestRegionForArtifactRegistry()
		agentRepo := config.AgentRepo()
		return fmt.Sprintf(`
%s
c:\programdata\googet\googet.exe addrepo google-osconfig-agent-googet-testing https://%s-googet.pkg.dev/projects/%s/repos/google-osconfig-agent-googet-%s

# set useoauth to true in repo new file
$filePath = 'C:\ProgramData\GooGet\repos\google-osconfig-agent-googet-testing.repo'
(Get-Content $filePath) -replace 'useoauth: false', 'useoauth: true' | Set-Content $filePath

c:\programdata\googet\googet.exe -noconfirm install google-osconfig-agent`+windowsPost, removeAgentCmd, testRegion, testingPkgsProjectName, agentRepo)
	}
	return fmt.Sprintf(`
%s
c:\programdata\googet\googet.exe -noconfirm install -sources https://packages.cloud.google.com/yuck/repos/google-osconfig-agent-%s google-osconfig-agent
`+windowsPost, removeAgentCmd, config.AgentRepo())
}

// getYumRepoBaseURL returns the repo baseUrl that should be added to repo file google-osconfig-agent.repo
func getYumRepoBaseURL(osType string) string {
	agentRepo := config.AgentRepo()
	if agentRepo == "testing" {
		testRegion := pickTestRegionForArtifactRegistry()
		return fmt.Sprintf("https://%s-yum.pkg.dev/projects/%s/google-osconfig-agent-%s-testing", testRegion, testingPkgsProjectName, osType)
	}
	return fmt.Sprintf("https://packages.cloud.google.com/yum/repos/google-osconfig-agent-%s-%s", osType, agentRepo)
}

func getYumRepoSetup(osType string) string {
	gpgcheck := 1

	// According to doc, pkg name differ according to ELv version
	// doc: https://cloud.google.com/artifact-registry/docs/os-packages/rpm/configure#prepare-yum
	format := "dnf"
	if osType == "el7" {
		format = "yum"
	}

	repoConfig := fmt.Sprintf(`
# install yum-plugin-artifact-registry
yum makecache
yum install -y %s-plugin-artifact-registry

cat > /etc/yum.repos.d/google-osconfig-agent.repo <<EOM
[google-osconfig-agent]
name=Google OSConfig Agent Repository
baseurl=%s
enabled=1
gpgcheck=%d
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg
			https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOM`, format, getYumRepoBaseURL(osType), gpgcheck)

	return repoConfig
}

// InstallOSConfigEL9 installs the osconfig agent on el9 based systems. (RHEL)
func InstallOSConfigEL9() string {
	if config.AgentRepo() == "" {
		return CurlPost
	}
	if config.AgentRepo() == "stable" {
		return yumInstallAgent
	}
	if config.AgentRepo() == "staging" {
		return getYumRepoSetup("el9") + yumInstallAgent
	}
	return getYumRepoSetup("el9") + yumInstallAgent
}

// InstallOSConfigEL8 installs the osconfig agent on el8 based systems. (RHEL)
func InstallOSConfigEL8() string {
	if config.AgentRepo() == "" {
		return CurlPost
	}
	if config.AgentRepo() == "stable" {
		return yumInstallAgent
	}
	if config.AgentRepo() == "staging" {
		return getYumRepoSetup("el8") + yumInstallAgent
	}
	return getYumRepoSetup("el8") + yumInstallAgent
}

// InstallOSConfigEL7 installs the osconfig agent on el7 based systems.
func InstallOSConfigEL7() string {
	if config.AgentRepo() == "" {
		return CurlPost
	}
	if config.AgentRepo() == "stable" {
		return yumInstallAgent
	}
	if config.AgentRepo() == "staging" {
		return getYumRepoSetup("el7") + yumInstallAgent
	}
	return getYumRepoSetup("el7") + yumInstallAgent
}

// containsAnyOf checks if a string contains any substring from a given list.
func containsAnyOf(str string, substrings []string) bool {
	for _, substring := range substrings {
		if strings.Contains(str, substring) {
			return true
		}
	}
	return false
}

// InstallOSConfigEL installs the osconfig agent on el based systems.
func InstallOSConfigEL(image string) string {
	imageName := path.Base(image)
	switch {
	case image == "9" || containsAnyOf(imageName, []string{"rhel-9", "rhel-sap-9", "centos-stream-9", "rocky-linux-9"}):
		return InstallOSConfigEL9()
	case image == "8" || containsAnyOf(imageName, []string{"rhel-8", "rhel-sap-8", "centos-stream-8", "rocky-linux-8"}):
		return InstallOSConfigEL8()
	case image == "7" || containsAnyOf(imageName, []string{"rhel-7", "rhel-sap-7", "centos-7"}):
		return InstallOSConfigEL7()
	}
	return ""
}

func getZypperRepoSetup(osType string) string {

	gpgcheck := 1

	// TODO: Allow SUSE tests to pull packages from test project.
	repoConfig := fmt.Sprintf(`
cat > /etc/zypp/repos.d/google-osconfig-agent.repo <<EOM
[google-osconfig-agent]
name=Google OSConfig Agent Repository
baseurl=%s
enabled=1
gpgcheck=%d
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg
			https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOM`, getYumRepoBaseURL(osType), gpgcheck)

	return repoConfig
}

// InstallOSConfigSUSE installs the osconfig agent on suse systems.
func InstallOSConfigSUSE() string {
	if config.AgentRepo() == "" {
		return ""
	}
	if config.AgentRepo() == "staging" || config.AgentRepo() == "stable" {
		return getZypperRepoSetup("el8") + zypperInstallAgent
	}
	return getZypperRepoSetup("el8") + zypperInstallAgent
}

// GetDebOsName returns the equivalent os_name for deb version (e.g. debian-11 --> bullseye)
func GetDebOsName(image string) string {
	imageName := path.Base(image)
	switch {
	case image == "10" || containsAnyOf(imageName, []string{"debian-10", "buster"}):
		return "buster"
	case image == "11" || containsAnyOf(imageName, []string{"debian-11", "bullseye"}):
		return "bullseye"
	case image == "12" || containsAnyOf(imageName, []string{"debian-12", "bookworm"}):
		return "bookworm"
	}
	return ""
}

// DowngradeBullseyeAptImages is a single image that are used for testing downgrade case with apt-get
var DowngradeBullseyeAptImages = map[string]string{
	"debian-cloud/debian-11": "projects/debian-cloud/global/images/debian-11-bullseye-v20231010",
}

// HeadBusterAptImages empty for now as debian-10 wil reach EOL and some of its repos are not reachable anymore.
var HeadBusterAptImages = map[string]string{
	// Debian images.
}

// HeadBullseyeAptImages is a map of names to image paths for public debian-11 images
var HeadBullseyeAptImages = map[string]string{
	// Debian images.
	"debian-cloud/debian-11": "projects/debian-cloud/global/images/family/debian-11",
}

// HeadBookwormAptImages is a map of names to image paths for public debian-12 images
var HeadBookwormAptImages = map[string]string{
	// Debian images.
	"debian-cloud/debian-12": "projects/debian-cloud/global/images/family/debian-12",
}

// HeadSUSEImages is a map of names to image paths for public SUSE images.
var HeadSUSEImages = func() map[string]string {
	imgsMap := make(map[string]string)

	// TODO: enable SUSE tests to use testing pkgs after Artifact Registry supports zypper installation from private repos
	if config.AgentRepo() != "testing" {
		imgsMap = map[string]string{
			"suse-cloud/sles-12-latest-sp": "projects/suse-cloud/global/images/family/sles-12",
			"suse-cloud/sles-15-latest-sp": "projects/suse-cloud/global/images/family/sles-15",
			"suse-cloud/sles-15-sp5":       "projects/suse-cloud/global/images/family/sles-15-sp5",
			"suse-cloud/sles-15-sp6":       "projects/suse-cloud/global/images/family/sles-15-sp6",

			"suse-sap-cloud/sles-12-sp5-sap": "projects/suse-sap-cloud/global/images/family/sles-12-sp5-sap",
			"suse-sap-cloud/sles-15-sp5-sap": "projects/suse-sap-cloud/global/images/family/sles-15-sp5-sap",
			"suse-sap-cloud/sles-15-sp6-sap": "projects/suse-sap-cloud/global/images/family/sles-15-sp6-sap",
			"suse-sap-cloud/sles-15-sp7-sap": "projects/suse-sap-cloud/global/images/family/sles-15-sp7-sap",

			"suse-sap-cloud/sles-15-sp5-hardened-sap": "projects/suse-sap-cloud/global/images/family/sles-sap-15-sp5-hardened",

			"opensuse-cloud/opensuse-leap-15": "projects/opensuse-cloud/global/images/family/opensuse-leap",
		}
	}
	return imgsMap
}()

// OldSUSEImages is a map of names to image paths for public SUSE images.
var OldSUSEImages = func() map[string]string {
	imgsMap := make(map[string]string)

	// TODO: enable SUSE tests to use testing pkgs after Artifact Registry supports zypper installation from private repos
	if config.AgentRepo() != "testing" {
		imgsMap = map[string]string{}
	}
	return imgsMap
}()

// HeadEL8Images is a map of names to image paths for public EL8 image families. (RHEL, CentOS, Rocky)
var HeadEL8Images = map[string]string{
	"rhel-cloud/rhel-8": "projects/rhel-cloud/global/images/family/rhel-8",

	"rhel-sap-cloud/rhel-8-6-sap":  "projects/rhel-sap-cloud/global/images/family/rhel-8-6-sap-ha",
	"rhel-sap-cloud/rhel-8-8-sap":  "projects/rhel-sap-cloud/global/images/family/rhel-8-8-sap-ha",
	"rhel-sap-cloud/rhel-8-10-sap": "projects/rhel-sap-cloud/global/images/family/rhel-8-10-sap-ha",

	"rocky-linux-cloud/rocky-linux-8":               "projects/rocky-linux-cloud/global/images/family/rocky-linux-8",
	"rocky-linux-cloud/rocky-linux-8-optimized-gcp": "projects/rocky-linux-cloud/global/images/family/rocky-linux-8-optimized-gcp",
}

// OldEL8Images is a map of names to image paths for old EL8 images. (RHEL, CentOS, Rocky)
var OldEL8Images = map[string]string{
	// Currently empty
}

// HeadEL9Images is a map of names to image paths for public EL9 image families. (RHEL, CentOS, Rocky)
var HeadEL9Images = map[string]string{
	"centos-cloud/centos-stream-9": "projects/centos-cloud/global/images/family/centos-stream-9",

	"rhel-cloud/rhel-9": "projects/rhel-cloud/global/images/family/rhel-9",

	"rhel-sap-cloud/rhel-9-0-sap": "projects/rhel-sap-cloud/global/images/family/rhel-9-0-sap-ha",
	"rhel-sap-cloud/rhel-9-2-sap": "projects/rhel-sap-cloud/global/images/family/rhel-9-2-sap-ha",

	"rocky-linux-cloud/rocky-linux-9":               "projects/rocky-linux-cloud/global/images/family/rocky-linux-9",
	"rocky-linux-cloud/rocky-linux-9-optimized-gcp": "projects/rocky-linux-cloud/global/images/family/rocky-linux-9-optimized-gcp",
}

// OldEL9Images is a map of names to image paths for old EL9 images. (RHEL, CentOS, Rocky)
var OldEL9Images = map[string]string{
	// Currently empty
}

// HeadELImages is a map of names to image paths for public EL image families. (RHEL, CentOS, Rocky)
var HeadELImages = func() (newMap map[string]string) {
	newMap = make(map[string]string)
	for k, v := range HeadEL8Images {
		newMap[k] = v
	}
	for k, v := range HeadEL9Images {
		newMap[k] = v
	}
	return
}()

// HeadAptImages is a map of names to image paths for public EL image families. (RHEL, CentOS, Rocky)
var HeadAptImages = func() (newMap map[string]string) {
	newMap = make(map[string]string)
	for k, v := range HeadBusterAptImages {
		newMap[k] = v
	}
	for k, v := range HeadBullseyeAptImages {
		newMap[k] = v
	}
	for k, v := range HeadBookwormAptImages {
		newMap[k] = v
	}
	return
}()

// HeadWindowsImages is a map of names to image paths for public Windows image families.
var HeadWindowsImages = map[string]string{
	"windows-cloud/windows-2016":      "projects/windows-cloud/global/images/family/windows-2016",
	"windows-cloud/windows-2016-core": "projects/windows-cloud/global/images/family/windows-2016-core",
	"windows-cloud/windows-2019":      "projects/windows-cloud/global/images/family/windows-2019",
	"windows-cloud/windows-2019-core": "projects/windows-cloud/global/images/family/windows-2019-core",

	// Testing of win-2022-dc disabled because of https://techcommunity.microsoft.com/t5/windows-server-for-it-pro/faulty-patches-on-server-2022/m-p/4028125

	/*
		"windows-cloud/windows-2022":         "projects/windows-cloud/global/images/family/windows-2022",
		"windows-cloud/windows-2022-core":    "projects/windows-cloud/global/images/family/windows-2022-core",
	*/
}

// OldWindowsImages is a map of names to image paths for old Windows images.
var OldWindowsImages = map[string]string{
	// Currently empty
}

// HeadCOSImages is a map of names to image paths for public COS image families.
var HeadCOSImages = map[string]string{
	"cos-cloud/cos-stable": "projects/cos-cloud/global/images/family/cos-stable",
	"cos-cloud/cos-beta":   "projects/cos-cloud/global/images/family/cos-beta",
	"cos-cloud/cos-dev":    "projects/cos-cloud/global/images/family/cos-dev",
}

// RandString generates a random string of n length.
func RandString(n int) string {
	gen := rand.New(rand.NewSource(time.Now().UnixNano()))
	letters := "bdghjlmnpqrstvwxyz0123456789"
	b := make([]byte, n)
	for i := range b {
		b[i] = letters[gen.Int63()%int64(len(letters))]
	}
	return string(b)
}

// GetStatusFromError return a string that contains all information
// about the error that is created from a status
func GetStatusFromError(err error) string {
	if s, ok := status.FromError(err); ok {
		return fmt.Sprintf("code: %q, message: %q, details: %q", s.Code(), s.Message(), s.Details())
	}
	return fmt.Sprintf("%v", err)
}

// This pool is just used for CreateComputeInstance so that we limit our calls to the API during the heavy create process.
var pool = make(chan struct{}, 10)

// CreateComputeInstance is an utility function to create gce instance
func CreateComputeInstance(metadataitems []*api.MetadataItems, client daisyCompute.Client, machineType, image, name, projectID, zone, serviceAccountEmail string, serviceAccountScopes []string) (*compute.Instance, error) {
	pool <- struct{}{}
	defer func() {
		<-pool
	}()
	var items []*api.MetadataItems

	// enable debug logging and guest-attributes for all test instances
	items = append(items, compute.BuildInstanceMetadataItem("enable-os-config-debug", "true"))
	items = append(items, compute.BuildInstanceMetadataItem("enable-guest-attributes", "true"))
	if config.AgentSvcEndpoint() != "" {
		items = append(items, compute.BuildInstanceMetadataItem("os-config-endpoint", config.AgentSvcEndpoint()))
	}

	for _, item := range metadataitems {
		items = append(items, item)
	}

	i := &api.Instance{
		Name:        name,
		MachineType: fmt.Sprintf("projects/%s/zones/%s/machineTypes/%s", projectID, zone, machineType),
		NetworkInterfaces: []*api.NetworkInterface{
			{
				Subnetwork: fmt.Sprintf("projects/%s/regions/%s/subnetworks/default", projectID, zone[:len(zone)-2]),
				AccessConfigs: []*api.AccessConfig{
					{
						Type: "ONE_TO_ONE_NAT",
					},
				},
			},
		},
		Metadata: &api.Metadata{
			Items: items,
		},
		Disks: []*api.AttachedDisk{
			{
				AutoDelete: true,
				Boot:       true,
				InitializeParams: &api.AttachedDiskInitializeParams{
					SourceImage: image,
					DiskType:    fmt.Sprintf("projects/%s/zones/%s/diskTypes/pd-balanced", projectID, zone),
				},
			},
		},
		ServiceAccounts: []*api.ServiceAccount{
			{
				Email:  serviceAccountEmail,
				Scopes: serviceAccountScopes,
			},
		},
		Labels: map[string]string{"name": name},
	}

	inst, err := compute.CreateInstance(client, projectID, zone, i)
	if err != nil {
		return nil, err
	}

	return inst, nil
}
