// Copyright 2025 Microsoft Corporation
//
// 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 coreapi

import (
	"fmt"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"

	azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"

	"github.com/Azure/ARO-HCP/internal/api/metadataapi"
)

// HCPOpenShiftClusterNodePool represents a node pool resource for ARO HCP
// OpenShift clusters.
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type HCPOpenShiftClusterNodePool struct {
	// PartitionKey holds the lowercased subscriptionID.
	CosmosMetadata `json:"cosmosMetadata"`

	TrackedResource
	// Written by: Frontend PUT/PATCH NodePool, OperationNodePoolCreate, OperationNodePoolUpdate, OperationNodePoolDelete
	Properties HCPOpenShiftClusterNodePoolProperties `json:"properties,omitempty"`
	// Written by: Frontend PUT/PATCH/DELETE NodePool, OperationNodePool* controllers, NodePoolClusterServiceCreate, NodePoolDeletion* controllers
	ServiceProviderProperties HCPOpenShiftClusterNodePoolServiceProviderProperties `json:"serviceProviderProperties,omitempty"`
	Identity                  *ManagedServiceIdentity                              `json:"identity,omitempty"`
	// Written by: NodePoolDegradedAggregator, NodePoolRequirementsValidAggregator
	Status HCPOpenShiftClusterNodePoolStatus `json:"status"`
}

// HCPOpenShiftClusterNodePoolStatus contains the observed state of the node pool.
type HCPOpenShiftClusterNodePoolStatus struct {
	// Conditions are the top-level HCPOpenShiftClusterNodePool status conditions.
	// Each Condition Type represents a condition and it should be unique among all conditions.
	// Written by: NodePoolDegradedAggregator
	// +optional
	// +patchMergeKey=type
	// +patchStrategy=merge
	// +listType=map
	// +listMapKey=type
	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`

	// UserFacingConditions is a list of conditions that tracks user-facing node pool
	// conditions. Each Condition Type should be unique among all conditions.
	// The conditions here are exposed to the ARM API. This means that UserFacingConditions
	// must not contain any internal details. This also means the Type and Reason
	// values become part of the public API.
	// Addition of new conditions here should be done only when strictly necessary, sparingly and only done
	// when there is a clear benefit to doing so. We expect the number of conditions at this
	// level to be kept to a minimum.
	// Written by: NodePoolRequirementsValidAggregator
	// +optional
	// +patchMergeKey=type
	// +patchStrategy=merge
	// +listType=map
	// +listMapKey=type
	UserFacingConditions []metav1.Condition `json:"userFacingConditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}

var _ CosmosPersistable = &HCPOpenShiftClusterNodePool{}

// HCPOpenShiftClusterNodePoolProperties represents the property bag of a
// HCPOpenShiftClusterNodePool resource.
type HCPOpenShiftClusterNodePoolProperties struct {
	// Written by: Frontend PUT/PATCH/DELETE NodePool, OperationNodePoolCreate, OperationNodePoolUpdate, OperationNodePoolDelete
	ProvisioningState ProvisioningState `json:"provisioningState,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	Version NodePoolVersionProfile `json:"version,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	Platform NodePoolPlatformProfile `json:"platform,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	Replicas int32 `json:"replicas,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	AutoRepair bool `json:"autoRepair,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	AutoScaling *NodePoolAutoScaling `json:"autoScaling,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	Labels map[string]string `json:"labels,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	Taints []Taint `json:"taints,omitempty"`
	// Written by: Frontend PUT/PATCH NodePool
	NodeDrainTimeoutMinutes *int32 `json:"nodeDrainTimeoutMinutes,omitempty"`
}

type HCPOpenShiftClusterNodePoolServiceProviderProperties struct {
	// Written by: Frontend PUT NodePool (Create), NodePoolClusterServiceCreate, NodePoolDeletionClusterServiceIDClearer
	ClusterServiceID *metadataapi.InternalID `json:"clusterServiceID,omitempty"`
	// Written by: Frontend PUT/PATCH/DELETE NodePool, OperationNodePoolCreate, OperationNodePoolUpdate, OperationNodePoolDelete
	ActiveOperationID string `json:"activeOperationId,omitempty"`
	// DeletionTimestamp is the timestamp at which the NodePool deletion was requested.
	// The timestamp is in UTC.
	// A nil value indicates that the NodePool deletion has not been requested.
	// Written by: Frontend DELETE NodePool
	DeletionTimestamp *metav1.Time `json:"deletionTimestamp,omitempty"`
	// ClusterServiceDeletionTimestamp is written when a dispatch of a Cluster
	// Service Delete NodePool request against Cluster Service for this node
	// pool has been handled.
	// Written by: NodePoolClusterServiceDeleteDispatch
	ClusterServiceDeletionTimestamp *metav1.Time `json:"clusterServiceDeletionTimestamp,omitempty"`

	// Written by: Frontend DELETE NodePool
	UsesNewNodePoolDeletionApproach bool `json:"usesNewNodePoolDeletionApproach"`

	// CreateOperationCompletionDeadline is the time by which the node pool creation operation must complete.
	// If it is not complete by this time, the operation will be marked as failed with the best message we can give at the time.
	// The default value is 60 minutes after the creation request is received.
	// When the subscription.HasRegisteredFeature(api.FeatureExperimentalReleaseFeatures), this value can be set
	// using the ExperimentalNodePoolTagPrefix + "max-creation-duration" tag, specified as a time.Duration.
	// The operation node pool create controller uses this value to decide about marking the install as failed.
	// The e2e tests set this value to one minute less than the default timeout.
	CreateOperationCompletionDeadline *metav1.Time `json:"createOperationCompletionDeadline,omitempty"`
}

// NodePoolVersionProfile represents the worker node pool version.
// Visbility for the entire struct is "read create update".
type NodePoolVersionProfile struct {
	ID           string `json:"id,omitempty"`
	ChannelGroup string `json:"channelGroup,omitempty"`
}

// NodePoolPlatformProfile represents a worker node pool configuration.
// Visibility for the entire struct is "read create".
type NodePoolPlatformProfile struct {
	// SubnetID is the resource ID of the subnet to use for this Node Pool's virtual machines.
	// * If SubnetID is nil on CREATE, mutation defaults it to the parent cluster's subnet.
	// * SubnetID must belong to the same VNet as the parent cluster's subnet.
	// * The same subnet may be used across Node Pools of the same cluster (and may match
	//   the parent cluster's subnet), but a subnet cannot be reused across different
	//   ARO-HCP Clusters.
	SubnetID               *azcorearm.ResourceID `json:"subnetId,omitempty"`
	VMSize                 string                `json:"vmSize,omitempty"`
	EnableEncryptionAtHost bool                  `json:"enableEncryptionAtHost"`
	OSDisk                 OSDiskProfile         `json:"osDisk"`
	// AvailabilityZone can be empty.
	// * If AvailabilityZone is not specified during creation, then the Node Pool is not part of any Availability Zone and the Node Pool's
	//   virtual machines are included in an Availability Set.
	// * If AvailabilityZone is specified, then the virtual machines are deployed as standalone instances in the specified Availability Zone.
	// * It must not be specified for Node Pools whose parent cluster is in an Azure location that does not support Availability Zones
	// * It is a logical Availability Zone
	// * It is optional during creation and immutable
	AvailabilityZone string `json:"availabilityZone,omitempty"`
}

// OSDiskProfile represents a OS Disk configuration.
// Visibility for the entire struct is "read create".
type OSDiskProfile struct {
	SizeGiB                *int32                             `json:"sizeGiB,omitempty"`
	DiskStorageAccountType metadataapi.DiskStorageAccountType `json:"diskStorageAccountType,omitempty"`
	EncryptionSetID        *azcorearm.ResourceID              `json:"encryptionSetId,omitempty"`
	DiskType               metadataapi.OsDiskType             `json:"diskType,omitempty"`
}

// NodePoolAutoScaling represents a node pool autoscaling configuration.
// Visibility for the entire struct is "read create update".
// max=200 for both Min and Max when the node pool's Platform.AvailabilityZone is unset.
type NodePoolAutoScaling struct {
	Min int32 `json:"min,omitempty"`
	Max int32 `json:"max,omitempty"`
}

// Taint represents a Kubernetes taint for a node.
// Visibility for the entire struct is "read create update".
type Taint struct {
	Effect metadataapi.Effect `json:"effect,omitempty"`
	Key    string             `json:"key,omitempty"`
	Value  string             `json:"value,omitempty"`
}

func NewDefaultHCPOpenShiftClusterNodePool(resourceID *azcorearm.ResourceID, azureLocation string) *HCPOpenShiftClusterNodePool {
	return &HCPOpenShiftClusterNodePool{
		TrackedResource: NewTrackedResource(resourceID, azureLocation),
		Properties: HCPOpenShiftClusterNodePoolProperties{
			Version: NodePoolVersionProfile{
				ChannelGroup: DefaultNodePoolVersionChannelGroup,
			},
			Platform: NodePoolPlatformProfile{
				OSDisk: OSDiskProfile{
					SizeGiB:                ptr.To(DefaultNodePoolOSDiskSizeGiB),
					DiskStorageAccountType: metadataapi.DiskStorageAccountTypePremium_LRS,
					DiskType:               metadataapi.OsDiskTypeManaged,
				},
			},
			AutoRepair: true,
		},
	}
}

// EnsureDefaults fills in default values for fields that may be absent in
// Cosmos documents created before the field was introduced, or on the create
// and preflight paths where the internal type is constructed from external input.
// Only fields where the zero value is never valid user input are safe to default
// here (string enums). See the DDR at docs/api-version-defaults-and-storage.md.
//
// This method should be treated as append-only. Avoid removing defaulting
// rules until all Cosmos documents have been verified to contain the field.
func (np *HCPOpenShiftClusterNodePool) EnsureDefaults() {
	if len(np.Properties.Platform.OSDisk.DiskStorageAccountType) == 0 {
		np.Properties.Platform.OSDisk.DiskStorageAccountType = metadataapi.DiskStorageAccountTypePremium_LRS
	}
	if len(np.Properties.Platform.OSDisk.DiskType) == 0 {
		np.Properties.Platform.OSDisk.DiskType = metadataapi.OsDiskTypeManaged
	}
}

func (nodePool *HCPOpenShiftClusterNodePool) validateSubnetID(cluster *HCPOpenShiftCluster) []CloudErrorBody {
	var errorDetails []CloudErrorBody

	if nodePool.Properties.Platform.SubnetID == nil {
		return nil
	}

	// Cluster and node pool subnet IDs have already passed syntax validation so
	// parsing should not fail. If parsing does somehow fail then skip the validation.

	clusterVNet := cluster.CustomerProperties.Platform.SubnetID.Parent.String()
	nodePoolVNet := nodePool.Properties.Platform.SubnetID.Parent.String()

	if !strings.EqualFold(nodePoolVNet, clusterVNet) {
		errorDetails = append(errorDetails, CloudErrorBody{
			Code:    CloudErrorCodeInvalidRequestContent,
			Message: fmt.Sprintf("Subnet '%s' must belong to the same VNet as the parent cluster VNet '%s'", nodePool.Properties.Platform.SubnetID, clusterVNet),
			Target:  "properties.platform.subnetId",
		})
	}

	return errorDetails
}

func (nodePool *HCPOpenShiftClusterNodePool) Validate(cluster *HCPOpenShiftCluster) []CloudErrorBody {
	var errorDetails []CloudErrorBody

	if cluster != nil {
		errorDetails = append(errorDetails, nodePool.validateSubnetID(cluster)...)
	}

	return errorDetails
}
