// NOTE: prefer to use cluster.go
package gcp

import (
	"context"
	"fmt"
	"strings"

	containerapiv1 "cloud.google.com/go/container/apiv1"
	"cloud.google.com/go/container/apiv1/containerpb"
	"google.golang.org/api/compute/v1"
	"google.golang.org/api/container/v1"
)

const (
	gkeURLTemplate      = "projects/%s/locations/%s/clusters/%s"
	nodePoolURLTemplate = "projects/%s/locations/%s/clusters/%s/nodePools/%s"
)

// UpgradeCluster upgrades a gke cluster to a specified version.
// Since the gke version has a suffix, e.g. `1.27.3-gke.1700`, we need to find the exact version from the valid versions first.
func (c *ClientSet) UpgradeCluster(ctx context.Context, project, region, name, version string) (selectedVersion string, err error) {
	severCfg, err := c.container.Projects.Locations.GetServerConfig(fmt.Sprintf("projects/%s/locations/%s", project, region)).Context(ctx).Do()
	if err != nil {
		return "", fmt.Errorf("failed to get server config: %v", err)
	}
	for _, v := range severCfg.ValidMasterVersions {
		if strings.HasPrefix(v, version) {
			selectedVersion = v
			break
		}
	}
	if selectedVersion == "" {
		return "", fmt.Errorf("no matching version found in %v", severCfg.ValidMasterVersions)
	}

	op, err := c.container.Projects.Locations.Clusters.Update(
		fmt.Sprintf(gkeURLTemplate, project, region, name),
		&container.UpdateClusterRequest{
			Update: &container.ClusterUpdate{
				DesiredMasterVersion: selectedVersion,
			},
		},
	).Context(ctx).Do()
	if err != nil {
		return "", fmt.Errorf("failed to update cluster to version %s: %v", selectedVersion, err)
	}
	if op.Error != nil {
		return "", fmt.Errorf("operation error updating cluster to version %s: %v", selectedVersion, op.Error)
	}
	return selectedVersion, nil
}

func (c *ClientSet) IsContainerOperationDone(ctx context.Context, name string) (bool, error) {
	op, err := c.containerOprationSvc.Get(name).Context(ctx).Do()
	if err != nil {
		if isNotFound(err) {
			return true, nil
		}
		return false, err
	}
	// Possible values:
	//   "STATUS_UNSPECIFIED" - Not set.
	//   "PENDING" - The operation has been created.
	//   "RUNNING" - The operation is currently running.
	//   "DONE" - The operation is done, either cancelled or completed.
	//   "ABORTING" - The operation is aborting.
	switch op.Status {
	case "DONE":
		return true, nil
	case "PENDING", "RUNNING":
		return false, nil
	default:
		return false, fmt.Errorf("operation in %s status, err: %s", op.Status, op.Error.Message)
	}
}

func (c *ClientSet) SetClusterMaintenanceExclusion(ctx context.Context, project, region, name, maintenanceExclusionName string, timeWindow *container.TimeWindow) (string, error) {
	clusterName := fmt.Sprintf(gkeURLTemplate, project, region, name)
	cluster, err := c.container.Projects.Locations.Clusters.Get(clusterName).Context(ctx).Do()
	if err != nil {
		return "", err
	}
	if cluster.MaintenancePolicy != nil &&
		cluster.MaintenancePolicy.Window != nil &&
		cluster.MaintenancePolicy.Window.MaintenanceExclusions != nil &&
		cluster.MaintenancePolicy.Window.MaintenanceExclusions[maintenanceExclusionName].StartTime == timeWindow.StartTime &&
		cluster.MaintenancePolicy.Window.MaintenanceExclusions[maintenanceExclusionName].EndTime == timeWindow.EndTime {
		return "", nil
	}

	request := &container.SetMaintenancePolicyRequest{
		ClusterId: name,
		Name:      clusterName,
		ProjectId: project,
		MaintenancePolicy: &container.MaintenancePolicy{
			Window: &container.MaintenanceWindow{
				MaintenanceExclusions: map[string]container.TimeWindow{
					maintenanceExclusionName: *timeWindow,
				},
			},
		},
	}
	if cluster.MaintenancePolicy != nil {
		request.MaintenancePolicy.ResourceVersion = cluster.MaintenancePolicy.ResourceVersion
	}
	op, err := c.container.Projects.Locations.Clusters.SetMaintenancePolicy(clusterName, request).Context(ctx).Do()
	if err != nil {
		return "", err
	}
	return op.Name, nil
}

func (c *ClientSet) UpgradeNodePool(ctx context.Context, project, region, gkeName, nodePoolName, version string) error {
	client, err := containerapiv1.NewClusterManagerClient(ctx)
	if err != nil {
		return fmt.Errorf("failed to create cluster manager client: %v", err)
	}
	defer client.Close()

	req := &containerpb.UpdateNodePoolRequest{
		NodeVersion: version,          // required
		ImageType:   "cos_containerd", // required
		Name:        fmt.Sprintf(nodePoolURLTemplate, project, region, gkeName, nodePoolName),
	}
	op, err := client.UpdateNodePool(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to update node pool %s to version %s: %v", nodePoolName, version, err)
	}
	if op.Error != nil {
		return fmt.Errorf("operation error of updating node pool %s: %v", nodePoolName, op.Error)
	}
	fmt.Printf("type: %s\n status: %s\n detail: %s\n", op.OperationType.String(), op.Status, op.Detail)
	return nil
}

// DisableManagedPrometheus disables the managed prometheus for a gke cluster.
// It enables the managed prometheus by default if the version is v1.27+, and it will add a DaemonSet.
func (c *ClientSet) DisableManagedPrometheus(ctx context.Context, project, region, name string) (string, error) {
	clusterName := fmt.Sprintf(gkeURLTemplate, project, region, name)
	cluster, err := c.container.Projects.Locations.Clusters.Get(clusterName).Context(ctx).Do()
	if err != nil {
		return "", fmt.Errorf("failed to get gke cluster %s: %v", clusterName, err)
	}

	if cluster.MonitoringConfig == nil || cluster.MonitoringConfig.ManagedPrometheusConfig == nil || !cluster.MonitoringConfig.ManagedPrometheusConfig.Enabled {
		return "", nil
	}

	op, err := c.container.Projects.Locations.Clusters.Update(
		clusterName,
		&container.UpdateClusterRequest{
			Update: &container.ClusterUpdate{
				DesiredMonitoringConfig: &container.MonitoringConfig{
					// if the monitoring component is not disable, the managed prometheus could not be disabled
					// and get an error "INVALID_ARGUMENT: SYSTEM_COMPONENTS monitoring must be enabled if any monitoring is enabled".
					// set EnableComponents to empty to disable all monitoring components.
					ComponentConfig: &container.MonitoringComponentConfig{
						EnableComponents: []string{},
					},
					ManagedPrometheusConfig: &container.ManagedPrometheusConfig{
						Enabled: false,
					},
				},
			},
		},
	).Context(ctx).Do()
	if err != nil {
		return "", fmt.Errorf("failed to update cluster: %v", err)
	}
	return op.Name, nil
}

func (c *ClientSet) EnabelSubSetting(ctx context.Context, project, region, name string) error {
	clusterName := fmt.Sprintf(gkeURLTemplate, project, region, name)
	cluster, err := c.container.Projects.Locations.Clusters.Get(clusterName).Context(ctx).Do()
	if err != nil {
		return fmt.Errorf("failed to get gke cluster %s: %v", clusterName, err)
	}
	if cluster.NetworkConfig != nil && cluster.NetworkConfig.EnableL4ilbSubsetting {
		return nil
	}
	_, err = c.container.Projects.Locations.Clusters.Update(clusterName, &container.UpdateClusterRequest{
		Update: &container.ClusterUpdate{
			DesiredL4ilbSubsettingConfig: &container.ILBSubsettingConfig{
				Enabled: true,
			},
		},
	}).Context(ctx).Do()
	if err != nil {
		return fmt.Errorf("failed to update cluster %s: %v", clusterName, err)
	}
	fmt.Printf("update cluster %s l4ilb subsetting config success\n", clusterName)
	return nil
}

// ModifyBackendServiceLogging control ILB logging of backend service
func (c *ClientSet) ModifyBackendServiceLogging(ctx context.Context, inputs *ModifyBackendServiceLoggingInputs) (string, error) {
	project := inputs.GCPProject
	region := inputs.GCPRegion
	bsName := inputs.BackendService

	// query existing backend service
	bs, err := c.compute.RegionBackendServices.Get(project, region, bsName).Context(ctx).Do()
	if err != nil {
		return "", fmt.Errorf("failed to get backend service: %w", err)
	}

	// update logConfig
	bs.LogConfig = &compute.BackendServiceLogConfig{
		Enable:          inputs.Enabled,
		ForceSendFields: []string{"Enable", "SampleRate", "OptionalMode"},
	}

	if inputs.Enabled {
		// Update existing logconfig
		bs.LogConfig.SampleRate = inputs.SampleRate
		// One of [INCLUDE_ALL_OPTIONAL, EXCLUDE_ALL_OPTIONAL, CUSTOM].
		// All optional fields see: https://cloud.google.com/load-balancing/docs/internal/internal-logging-monitoring?hl=zh-cn%20#logs-format
		bs.LogConfig.OptionalMode = inputs.MetadataMode
		if bs.LogConfig.OptionalMode == "CUSTOM" {
			bs.LogConfig.OptionalFields = inputs.MetadataFields
			bs.LogConfig.ForceSendFields = append(bs.LogConfig.ForceSendFields, "OptionalFields")
		}

	} else {
		// Disable LogConfig
		bs.LogConfig.OptionalMode = ""
		bs.LogConfig.OptionalFields = []string{}
		bs.LogConfig.ForceSendFields = append(bs.LogConfig.ForceSendFields, "OptionalFields")
	}

	op, err := c.compute.RegionBackendServices.Patch(project, region, bsName, bs).Context(ctx).Do()
	if err != nil {
		return "", fmt.Errorf("failed to update backend service: %w", err)
	}

	return op.Name, nil
}

func (c *ClientSet) ModifyVPCFlowLog(ctx context.Context, inputs *ModifyVPCFlowLogInputs) (string, error) {
	project := inputs.GCPProject
	region := inputs.GCPRegion
	subnetName := inputs.GCPSubnetName

	computeSvc, err := compute.NewService(ctx)
	if err != nil {
		return "", err
	}
	subnet, err := computeSvc.Subnetworks.Get(project, region, subnetName).Do()
	if err != nil {
		return "", err
	}

	if inputs.Enabled {
		// AS 1 patch request cannot update multiple fields, patch this switch-field first
		// UPDATE existing LogConfig
		subnet.LogConfig = &compute.SubnetworkLogConfig{
			Enable:              inputs.Enabled,
			FlowSampling:        inputs.SampleRate, // [0.0, 1.0]
			AggregationInterval: inputs.AggregationInterval,
			Metadata:            inputs.MetadataMode,   // Options: EXCLUDE_ALL_METADATA, CUSTOM_METADATA, INCLUDE_ALL_METADATA
			MetadataFields:      inputs.MetadataFields, // Such as: "connection", "src_instance", "dest_location" etc.
			ForceSendFields:     []string{"FlowSampling", "MetadataFields"},
		}

		if subnet.LogConfig.AggregationInterval == "" {
			subnet.LogConfig.AggregationInterval = "INTERVAL_15_MIN"
		}

		if subnet.LogConfig.Metadata == "" {
			subnet.LogConfig.Metadata = "EXCLUDE_ALL_METADATA"
		}

	} else {
		// DISABLE existing LogConfig
		subnet.LogConfig = nil
		subnet.EnableFlowLogs = false
		// The only way to disable Logconfig is to set these 2 fields at one time.
		subnet.ForceSendFields = []string{"EnableFlowLogs"}
		subnet.NullFields = []string{"LogConfig"}
	}

	op, err := computeSvc.Subnetworks.Patch(project, region, subnetName, subnet).Do()
	if err != nil {
		return "", err
	}

	return op.Name, nil
}
