// 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 ocm

//go:generate $MOCKGEN -typed -source=client.go -destination=mock_client.go -package ocm ClusterServiceClientSpec

import (
	"context"
	"fmt"
	"strings"

	sdk "github.com/openshift-online/ocm-sdk-go"
	arohcpv1alpha1 "github.com/openshift-online/ocm-sdk-go/arohcp/v1alpha1"
	cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"

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

// CS cluster property keys and values used in the Cluster Service API.
const (
	CSPropertyProvisionShardID = "provision_shard_id"
	CSPropertyNoopProvision    = "provisioner_noop_provision"
	CSPropertyNoopDeprovision  = "provisioner_noop_deprovision"
	CSPropertySingleReplica    = "hosted_cluster_single_replica"
	CSPropertySizeOverride     = "hosted_cluster_size_override"
	CSPropertyCPOImageOverride = "control_plane_operator_image"

	CSPropertyEnabled                    = "true"
	CSPropertyE2EMinimalControlPlaneSize = "e2e_minimal"
)

type ClusterServiceClientSpec interface {
	// GetCluster sends a GET request to fetch a cluster from Cluster Service.
	GetCluster(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.Cluster, error)

	// GetClusterStatus sends a GET request to fetch a cluster's status from Cluster Service.
	GetClusterStatus(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.ClusterStatus, error)

	// GetClusterProvisionShard sends a GET request to fetch a cluster's provision shard from Cluster Service.
	GetClusterProvisionShard(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.ProvisionShard, error)

	// GetClusterInflightChecks sends a GET request to fetch a cluster's inflight checks from Cluster Service.
	GetClusterInflightChecks(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.InflightCheckList, error)

	// GetClusterHypershiftDetails sends a GET request to fetch a cluster's hypershift details from Cluster Service.
	GetClusterHypershiftDetails(ctx context.Context, internalID InternalID) (*cmv1.HypershiftConfig, error)

	// PostCluster sends a POST request to create a cluster in Cluster Service.
	PostCluster(ctx context.Context, clusterBuilder *arohcpv1alpha1.ClusterBuilder) (*arohcpv1alpha1.Cluster, error)

	// UpdateCluster sends a PATCH request to update a cluster in Cluster Service.
	UpdateCluster(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.ClusterBuilder) (*arohcpv1alpha1.Cluster, error)

	// DeleteCluster sends a DELETE request to delete a cluster from Cluster Service.
	DeleteCluster(ctx context.Context, internalID InternalID) error

	// ListClusters prepares a GET request with the given search expression. Call Items() on
	// the returned iterator in a for/range loop to execute the request and paginate over results,
	// then call GetError() to check for an iteration error.
	ListClusters(searchExpression string) ClusterListIterator

	// GetNodePool sends a GET request to fetch a node pool from Cluster Service.
	GetNodePool(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.NodePool, error)

	// GetNodePoolStatus sends a GET request to fetch a node pool's status from Cluster Service.
	GetNodePoolStatus(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.NodePoolStatus, error)

	// PostNodePool sends a POST request to create a node pool in Cluster Service.
	PostNodePool(ctx context.Context, clusterInternalID InternalID, builder *arohcpv1alpha1.NodePoolBuilder) (*arohcpv1alpha1.NodePool, error)

	// UpdateNodePool sends a PATCH request to update a node pool in Cluster Service.
	UpdateNodePool(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.NodePoolBuilder) (*arohcpv1alpha1.NodePool, error)

	// DeleteNodePool sends a DELETE request to delete a node pool from Cluster Service.
	DeleteNodePool(ctx context.Context, internalID InternalID) error

	// ListNodePools prepares a GET request with the given search expression. Call Items() on
	// the returned iterator in a for/range loop to execute the request and paginate over results,
	// then call GetError() to check for an iteration error.
	ListNodePools(clusterInternalID InternalID, searchExpression string) NodePoolListIterator

	// GetExternalAuth sends a GET request to fetch an external auth config from Cluster Service.
	GetExternalAuth(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.ExternalAuth, error)

	// PostExternalAuth sends a POST request to create an external auth config in Cluster Service.
	PostExternalAuth(ctx context.Context, clusterInternalID InternalID, builder *arohcpv1alpha1.ExternalAuthBuilder) (*arohcpv1alpha1.ExternalAuth, error)

	// UpdateExternalAuth sends a PATCH request to update an external auth config in Cluster Service.
	UpdateExternalAuth(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.ExternalAuthBuilder) (*arohcpv1alpha1.ExternalAuth, error)

	// DeleteExternalAuth sends a DELETE request to delete an external auth config from Cluster Service.
	DeleteExternalAuth(ctx context.Context, internalID InternalID) error

	// ListExternalAuths prepares a GET request with the given search expression. Call Items() on
	// the returned iterator in a for/range loop to execute the request and paginate over results,
	// then call GetError() to check for an iteration error.
	ListExternalAuths(clusterInternalID InternalID, searchExpression string) ExternalAuthListIterator

	// GetBreakGlassCredential sends a GET request to fetch a break-glass cluster credential from Cluster Service.
	GetBreakGlassCredential(ctx context.Context, internalID InternalID) (*cmv1.BreakGlassCredential, error)

	// PostBreakGlassCredential sends a POST request to create a break-glass cluster credential in Cluster Service.
	PostBreakGlassCredential(ctx context.Context, clusterInternalID InternalID) (*cmv1.BreakGlassCredential, error)

	// DeleteBreakGlassCredentials sends a DELETE request to revoke all break-glass credentials for a cluster in Cluster Service.
	DeleteBreakGlassCredentials(ctx context.Context, clusterInternalID InternalID) error

	// ListBreakGlassCredentials prepares a GET request with the given search expression. Call
	// Items() on the returned iterator in a for/range loop to execute the request and paginate
	// over results, then call GetError() to check for an iteration error.
	ListBreakGlassCredentials(clusterInternalID InternalID, searchExpression string) BreakGlassCredentialListIterator

	// GetVersion sends a GET request to fetch cluster version
	GetVersion(ctx context.Context, versionName string) (*arohcpv1alpha1.Version, error)

	// ListVersions prepares a GET request.
	// Call Items() on the returned iterator with options in a for/range loop to execute the request and paginate over results,
	// then call GetError() to check for an iteration error.
	ListVersions() *VersionsListIterator

	// ListControlPlaneUpgradePolicies prepares a GET request to list control plane upgrade policies for a cluster.
	// Call Items() on the returned iterator in a for/range loop to execute the request and paginate over results,
	// then call GetError() to check for an iteration error.
	ListControlPlaneUpgradePolicies(clusterInternalID InternalID, orderBy string) ControlPlaneUpgradePolicyListIterator

	// PostControlPlaneUpgradePolicy sends a POST request to create a control plane upgrade policy in Cluster Service.
	PostControlPlaneUpgradePolicy(ctx context.Context, clusterInternalID InternalID, builder *arohcpv1alpha1.ControlPlaneUpgradePolicyBuilder) (*arohcpv1alpha1.ControlPlaneUpgradePolicy, error)

	// ListProvisionShards prepares a GET request to list provision shards.
	// Call Items() on the returned iterator in a for/range loop to execute the request and paginate over results,
	// then call GetError() to check for an iteration error.
	ListProvisionShards() ProvisionShardListIterator

	// GetProvisionShard sends a GET request to fetch a provision shard from Cluster Service.
	// The internalID is the internal ID of the provision shard.
	GetProvisionShard(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.ProvisionShard, error)

	// PostProvisionShard sends a POST request to create a provision shard in Cluster Service.
	PostProvisionShard(ctx context.Context, builder *arohcpv1alpha1.ProvisionShardBuilder) (*arohcpv1alpha1.ProvisionShard, error)

	// UpdateProvisionShard sends a PATCH request to update a provision shard in Cluster Service.
	UpdateProvisionShard(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.ProvisionShardBuilder) (*arohcpv1alpha1.ProvisionShard, error)

	// DeleteProvisionShard sends a DELETE request to delete a provision shard from Cluster Service.
	DeleteProvisionShard(ctx context.Context, internalID InternalID) error
	// ListNodePoolUpgradePolicies prepares a GET request to list node pool upgrade policies for a node pool.
	// Call Items() on the returned iterator in a for/range loop to execute the request and paginate over results,
	// then call GetError() to check for an iteration error.
	ListNodePoolUpgradePolicies(nodePoolInternalID InternalID, orderBy string) NodePoolUpgradePolicyListIterator

	// PostNodePoolUpgradePolicy sends a POST request to create a node pool upgrade policy in Cluster Service.
	PostNodePoolUpgradePolicy(ctx context.Context, nodePoolInternalID InternalID, builder *arohcpv1alpha1.NodePoolUpgradePolicyBuilder) (*arohcpv1alpha1.NodePoolUpgradePolicy, error)
}

type clusterServiceClient struct {
	// Conn is an ocm-sdk-go connection to Cluster Service
	conn *sdk.Connection
}

func NewClusterServiceClient(conn *sdk.Connection) ClusterServiceClientSpec {
	return &clusterServiceClient{
		conn: conn,
	}
}

// resolveClusterLinks replaces link objects with full objects that are
// necessary to fully construct an HCPOpenShiftCluster model.
func resolveClusterLinks(ctx context.Context, conn *sdk.Connection, cluster *arohcpv1alpha1.Cluster) (*arohcpv1alpha1.Cluster, error) {
	builder := arohcpv1alpha1.NewCluster().Copy(cluster)

	autoscaler, ok := cluster.GetAutoscaler()
	if ok && autoscaler.Link() {
		autoscalerClient := arohcpv1alpha1.NewAutoscalerClient(conn, autoscaler.HREF())

		autoscalerGetResponse, err := autoscalerClient.Get().SendContext(ctx)
		if err != nil {
			return nil, utils.TrackError(err)
		}
		autoscaler, ok = autoscalerGetResponse.GetBody()
		if !ok {
			return nil, fmt.Errorf("empty autoscaler response body")
		}

		builder.Autoscaler(arohcpv1alpha1.NewClusterAutoscaler().Copy(autoscaler))
	}

	return builder.Build()
}

// resolveNodePoolLinks replaces link objects with full objects that are
// necessary to fully construct an HCPOpenShiftClusterNodePool model.
func resolveNodePoolLinks(ctx context.Context, conn *sdk.Connection, nodePool *arohcpv1alpha1.NodePool) (*arohcpv1alpha1.NodePool, error) {
	builder := arohcpv1alpha1.NewNodePool().Copy(nodePool)

	version, ok := nodePool.GetVersion()
	if ok && version.Link() {
		versionClient := arohcpv1alpha1.NewVersionClient(conn, version.HREF())

		versionGetResponse, err := versionClient.Get().SendContext(ctx)
		if err != nil {
			return nil, utils.TrackError(err)
		}
		version, ok = versionGetResponse.GetBody()
		if !ok {
			return nil, fmt.Errorf("empty version response body")
		}

		builder.Version(arohcpv1alpha1.NewVersion().Copy(version))
	}

	return builder.Build()
}

func (csc *clusterServiceClient) GetCluster(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.Cluster, error) {
	client, ok := getAroHCPClusterClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", internalID)
	}
	clusterGetResponse, err := client.Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	cluster, ok := clusterGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return resolveClusterLinks(ctx, csc.conn, cluster)
}

func (csc *clusterServiceClient) GetClusterStatus(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.ClusterStatus, error) {
	client, ok := getAroHCPClusterClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", internalID)
	}
	clusterStatusGetResponse, err := client.Status().Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	status, ok := clusterStatusGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return status, nil
}

func (csc *clusterServiceClient) GetClusterProvisionShard(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.ProvisionShard, error) {
	client, ok := getAroHCPClusterClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", internalID)
	}
	provisionShardGetResponse, err := client.ProvisionShard().Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	provisionShard, ok := provisionShardGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return provisionShard, nil
}

func (csc *clusterServiceClient) ListProvisionShards() ProvisionShardListIterator {
	provisionShardsListRequest := csc.conn.AroHCP().V1alpha1().ProvisionShards().List()
	return &provisionShardListIterator{conn: csc.conn, request: provisionShardsListRequest}
}

func (csc *clusterServiceClient) GetProvisionShard(ctx context.Context, provisionShardInternalID InternalID) (*arohcpv1alpha1.ProvisionShard, error) {
	client, ok := getAroHCPProvisionShardClient(provisionShardInternalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a provision shard: %s", provisionShardInternalID)
	}
	provisionShardGetResponse, err := client.Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	provisionShard, ok := provisionShardGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return provisionShard, nil
}

func (csc *clusterServiceClient) PostProvisionShard(ctx context.Context, builder *arohcpv1alpha1.ProvisionShardBuilder) (*arohcpv1alpha1.ProvisionShard, error) {
	provisionShard, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	provisionShardsAddResponse, err := csc.conn.AroHCP().V1alpha1().ProvisionShards().Add().Body(provisionShard).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	provisionShard, ok := provisionShardsAddResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return provisionShard, nil
}

func (csc *clusterServiceClient) UpdateProvisionShard(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.ProvisionShardBuilder) (*arohcpv1alpha1.ProvisionShard, error) {
	provisionShard, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	client, ok := getAroHCPProvisionShardClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a provision shard: %s", internalID)
	}
	clusterUpdateResponse, err := client.Update().Body(provisionShard).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	provisionShard, ok = clusterUpdateResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return provisionShard, nil
}

func (csc *clusterServiceClient) DeleteProvisionShard(ctx context.Context, internalID InternalID) error {
	client, ok := getAroHCPProvisionShardClient(internalID, csc.conn)
	if !ok {
		return fmt.Errorf("OCM path is not a provision shard: %s", internalID)
	}
	_, err := client.Delete().SendContext(ctx)
	return utils.TrackError(err)
}

func (csc *clusterServiceClient) GetClusterInflightChecks(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.InflightCheckList, error) {
	client, ok := getAroHCPClusterClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", internalID)
	}
	clusterInflightChecksResponse, err := client.InflightChecks().List().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	inflightChecks, ok := clusterInflightChecksResponse.GetItems()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return inflightChecks, nil
}

// At this point, there is no way to get the hypershift details of a cluster via
// the AroHCP().V1alpha1().Clusters(), not does an equivalent endpoint for Hypershift() exist.
// To still be able to consume the hypershift details, we make a call to the non-ARO-HCP CS endpoint.
func (csc *clusterServiceClient) GetClusterHypershiftDetails(ctx context.Context, internalID InternalID) (*cmv1.HypershiftConfig, error) {
	client, ok := getClusterClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", internalID)
	}
	hypershiftGetResponse, err := client.Hypershift().Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	hypershiftConfig, ok := hypershiftGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return hypershiftConfig, nil
}

func (csc *clusterServiceClient) PostCluster(ctx context.Context, clusterBuilder *arohcpv1alpha1.ClusterBuilder) (*arohcpv1alpha1.Cluster, error) {
	cluster, err := clusterBuilder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	clustersAddResponse, err := csc.conn.AroHCP().V1alpha1().Clusters().Add().Body(cluster).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	cluster, ok := clustersAddResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return resolveClusterLinks(ctx, csc.conn, cluster)
}

func (csc *clusterServiceClient) UpdateCluster(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.ClusterBuilder) (*arohcpv1alpha1.Cluster, error) {
	cluster, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	client, ok := getAroHCPClusterClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", internalID)
	}
	clusterUpdateResponse, err := client.Update().Body(cluster).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	cluster, ok = clusterUpdateResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return resolveClusterLinks(ctx, csc.conn, cluster)
}

func (csc *clusterServiceClient) DeleteCluster(ctx context.Context, internalID InternalID) error {
	client, ok := getAroHCPClusterClient(internalID, csc.conn)
	if !ok {
		return fmt.Errorf("OCM path is not a cluster: %s", internalID)
	}
	_, err := client.Delete().SendContext(ctx)
	return utils.TrackError(err)
}

func (csc *clusterServiceClient) ListClusters(searchExpression string) ClusterListIterator {
	clustersListRequest := csc.conn.AroHCP().V1alpha1().Clusters().List()
	if searchExpression != "" {
		clustersListRequest.Search(searchExpression)
	}
	return &clusterListIterator{conn: csc.conn, request: clustersListRequest}
}

func (csc *clusterServiceClient) GetNodePool(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.NodePool, error) {
	client, ok := GetNodePoolClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a node pool: %s", internalID)
	}
	nodePoolGetResponse, err := client.Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	nodePool, ok := nodePoolGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}

	return resolveNodePoolLinks(ctx, csc.conn, nodePool)
}

func (csc *clusterServiceClient) GetNodePoolStatus(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.NodePoolStatus, error) {
	client, ok := GetNodePoolClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a node pool: %s", internalID)
	}
	nodePoolStatusGetResponse, err := client.Status().Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	status, ok := nodePoolStatusGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return status, nil
}

func (csc *clusterServiceClient) PostNodePool(ctx context.Context, clusterInternalID InternalID, builder *arohcpv1alpha1.NodePoolBuilder) (*arohcpv1alpha1.NodePool, error) {
	client, ok := getAroHCPClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)
	}
	nodePool, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	nodePoolsAddResponse, err := client.NodePools().Add().Body(nodePool).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	nodePool, ok = nodePoolsAddResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return resolveNodePoolLinks(ctx, csc.conn, nodePool)
}

func (csc *clusterServiceClient) UpdateNodePool(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.NodePoolBuilder) (*arohcpv1alpha1.NodePool, error) {
	client, ok := GetNodePoolClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a node pool: %s", internalID)
	}
	nodePool, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	nodePoolUpdateResponse, err := client.Update().Body(nodePool).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	nodePool, ok = nodePoolUpdateResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return resolveNodePoolLinks(ctx, csc.conn, nodePool)
}

func (csc *clusterServiceClient) DeleteNodePool(ctx context.Context, internalID InternalID) error {
	client, ok := GetNodePoolClient(internalID, csc.conn)
	if !ok {
		return fmt.Errorf("OCM path is not a node pool: %s", internalID)
	}
	_, err := client.Delete().SendContext(ctx)
	return utils.TrackError(err)
}

func (csc *clusterServiceClient) ListNodePools(clusterInternalID InternalID, searchExpression string) NodePoolListIterator {
	client, ok := getAroHCPClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return &nodePoolListIterator{err: fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)}
	}
	nodePoolsListRequest := client.NodePools().List()
	if searchExpression != "" {
		nodePoolsListRequest.Search(searchExpression)
	}
	return &nodePoolListIterator{conn: csc.conn, request: nodePoolsListRequest}
}

func (csc *clusterServiceClient) GetExternalAuth(ctx context.Context, internalID InternalID) (*arohcpv1alpha1.ExternalAuth, error) {
	client, ok := GetExternalAuthClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not an external auth: %s", internalID)
	}
	externalAuthGetResponse, err := client.Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	externalAuth, ok := externalAuthGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return externalAuth, nil
}

func (csc *clusterServiceClient) PostExternalAuth(ctx context.Context, clusterInternalID InternalID, builder *arohcpv1alpha1.ExternalAuthBuilder) (*arohcpv1alpha1.ExternalAuth, error) {
	client, ok := getAroHCPClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)
	}
	externalAuth, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	externalAuthsAddResponse, err := client.ExternalAuthConfig().ExternalAuths().Add().Body(externalAuth).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	externalAuth, ok = externalAuthsAddResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return externalAuth, nil

}

func (csc *clusterServiceClient) UpdateExternalAuth(ctx context.Context, internalID InternalID, builder *arohcpv1alpha1.ExternalAuthBuilder) (*arohcpv1alpha1.ExternalAuth, error) {
	client, ok := GetExternalAuthClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not an external auth: %s", internalID)
	}
	externalAuth, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	externalAuthUpdateResponse, err := client.Update().Body(externalAuth).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	externalAuth, ok = externalAuthUpdateResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return externalAuth, nil
}

func (csc *clusterServiceClient) DeleteExternalAuth(ctx context.Context, internalID InternalID) error {
	client, ok := GetExternalAuthClient(internalID, csc.conn)
	if !ok {
		return fmt.Errorf("OCM path is not a external auth: %s", internalID)
	}
	_, err := client.Delete().SendContext(ctx)
	return utils.TrackError(err)
}

func (csc *clusterServiceClient) ListExternalAuths(clusterInternalID InternalID, searchExpression string) ExternalAuthListIterator {
	client, ok := getAroHCPClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return &externalAuthListIterator{err: fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)}
	}
	externalAuthsListRequest := client.ExternalAuthConfig().ExternalAuths().List()
	// FIXME ExternalAuthsListRequest is missing a Search method.
	//       Presently it doesn't matter since we only support one
	//       ExternalAuth instance per cluster. Search will become
	//       more important if we ever support multiple.
	//if searchExpression != "" {
	//	externalAuthsListRequest.Search(searchExpression)
	//}
	return &externalAuthListIterator{request: externalAuthsListRequest}
}

func (csc *clusterServiceClient) GetBreakGlassCredential(ctx context.Context, internalID InternalID) (*cmv1.BreakGlassCredential, error) {
	client, ok := GetBreakGlassCredentialClient(internalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a break-glass credential: %s", internalID)
	}
	breakGlassCredentialGetResponse, err := client.Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	breakGlassCredential, ok := breakGlassCredentialGetResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return breakGlassCredential, nil
}

func (csc *clusterServiceClient) PostBreakGlassCredential(ctx context.Context, clusterInternalID InternalID) (*cmv1.BreakGlassCredential, error) {
	client, ok := getClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)
	}
	breakGlassCredential, err := cmv1.NewBreakGlassCredential().Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	breakGlassCredentialsAddResponse, err := client.BreakGlassCredentials().Add().Body(breakGlassCredential).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	breakGlassCredential, ok = breakGlassCredentialsAddResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return breakGlassCredential, nil
}

func (csc *clusterServiceClient) DeleteBreakGlassCredentials(ctx context.Context, clusterInternalID InternalID) error {
	client, ok := getClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)
	}
	_, err := client.BreakGlassCredentials().Delete().SendContext(ctx)
	return utils.TrackError(err)
}

func (csc *clusterServiceClient) ListBreakGlassCredentials(clusterInternalID InternalID, searchExpression string) BreakGlassCredentialListIterator {
	client, ok := getClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return &breakGlassCredentialListIterator{err: fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)}
	}
	breakGlassCredentialsListRequest := client.BreakGlassCredentials().List()
	if searchExpression != "" {
		breakGlassCredentialsListRequest.Search(searchExpression)
	}
	return &breakGlassCredentialListIterator{request: breakGlassCredentialsListRequest}
}

func (csc *clusterServiceClient) GetVersion(ctx context.Context, versionName string) (*arohcpv1alpha1.Version, error) {

	if !strings.HasPrefix(versionName, metadataapi.OpenShiftVersionPrefix) {
		versionName = metadataapi.OpenShiftVersionPrefix + versionName
	}
	client := csc.conn.AroHCP().V1alpha1().Versions().Version(versionName)

	resp, err := client.Get().SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	version, ok := resp.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return version, nil
}

func (csc *clusterServiceClient) ListVersions() *VersionsListIterator {
	versionsListRequest := csc.conn.AroHCP().V1alpha1().Versions().List()
	return &VersionsListIterator{request: versionsListRequest}
}

func (csc *clusterServiceClient) ListControlPlaneUpgradePolicies(clusterInternalID InternalID, orderBy string) ControlPlaneUpgradePolicyListIterator {
	client, ok := getAroHCPClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return &controlPlaneUpgradePolicyListIterator{err: fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)}
	}
	policiesListRequest := client.ControlPlaneUpgradePolicies().List()
	if orderBy != "" {
		policiesListRequest.Parameter("order", orderBy)
	}
	return &controlPlaneUpgradePolicyListIterator{request: policiesListRequest}
}

func (csc *clusterServiceClient) PostControlPlaneUpgradePolicy(ctx context.Context, clusterInternalID InternalID, builder *arohcpv1alpha1.ControlPlaneUpgradePolicyBuilder) (*arohcpv1alpha1.ControlPlaneUpgradePolicy, error) {
	client, ok := getAroHCPClusterClient(clusterInternalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a cluster: %s", clusterInternalID)
	}
	policy, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	policiesAddResponse, err := client.ControlPlaneUpgradePolicies().Add().Body(policy).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	policy, ok = policiesAddResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return policy, nil
}

func (csc *clusterServiceClient) ListNodePoolUpgradePolicies(nodePoolInternalID InternalID, orderBy string) NodePoolUpgradePolicyListIterator {
	client, ok := GetNodePoolClient(nodePoolInternalID, csc.conn)
	if !ok {
		return &nodePoolUpgradePolicyListIterator{err: fmt.Errorf("OCM path is not a node pool: %s", nodePoolInternalID)}
	}
	policiesListRequest := client.UpgradePolicies().List()
	if orderBy != "" {
		policiesListRequest.Parameter("order", orderBy)
	}
	return &nodePoolUpgradePolicyListIterator{request: policiesListRequest}
}

func (csc *clusterServiceClient) PostNodePoolUpgradePolicy(ctx context.Context, nodePoolInternalID InternalID, builder *arohcpv1alpha1.NodePoolUpgradePolicyBuilder) (*arohcpv1alpha1.NodePoolUpgradePolicy, error) {
	client, ok := GetNodePoolClient(nodePoolInternalID, csc.conn)
	if !ok {
		return nil, fmt.Errorf("OCM path is not a node pool: %s", nodePoolInternalID)
	}
	policy, err := builder.Build()
	if err != nil {
		return nil, utils.TrackError(err)
	}
	policiesAddResponse, err := client.UpgradePolicies().Add().Body(policy).SendContext(ctx)
	if err != nil {
		return nil, utils.TrackError(err)
	}
	policy, ok = policiesAddResponse.GetBody()
	if !ok {
		return nil, fmt.Errorf("empty response body")
	}
	return policy, nil
}

// NewOpenShiftVersionXY parses the given version, stripping off any
// OpenShift prefix ("openshift-"), and suffix ("-<channel_group>") and returns a new Version X.Y.
func NewOpenShiftVersionXY(v string) string {
	v = ConvertOpenShiftVersionNoPrefix(v)
	parts := strings.Split(v, ".")
	if len(parts) >= 2 {
		v = parts[0] + "." + parts[1]
	}
	return v
}

// NewOpenShiftVersionXYZ parses the given version and converts it to CS readable version
// CS readable version "openshift-v<X.Y.Z>" or "openshift-v<X.Y.Z>-channel_group"
func NewOpenShiftVersionXYZ(v, cg string) string {
	var csVersion string

	if len(v) > 0 {
		// Separate version from prerelease (e.g., "4.19.0" from "0.nightly-2025-01-01")
		versionPart := v
		prereleasePart := ""
		if hyphenIndex := strings.Index(v, "-"); hyphenIndex != -1 {
			versionPart = v[:hyphenIndex]
			prereleasePart = v[hyphenIndex:] // includes the hyphen
		}

		parts := strings.Split(versionPart, ".")
		if len(parts) == 1 {
			parts = append(parts, "0")
		}

		// If no patch version provided (X.Y format), append default patch version
		// Otherwise preserve the provided patch version (X.Y.Z format)
		if len(parts) == 2 {
			// Patch version is managed by Red Hat. This will be computed automatically to the latest
			// as part of https://github.com/Azure/ARO-HCP/pull/4477
			if patch, ok := map[string]string{"4.19": "34", "4.20": "25", "4.21": "20", "4.22": "1"}[v]; ok {
				parts = append(parts, patch)
			} else {
				parts = append(parts, "0")
			}
		}

		csVersion = metadataapi.OpenShiftVersionPrefix + strings.Join(parts, ".") + prereleasePart

		// Only append channel group if it's not empty and not "stable"
		// Versions will look as:
		// stable: opensfhit-vX.Y.Z
		// candidate: openshift-vX.Y.Z-candidate
		// or candidate: openshift-vX.Y.Z[-prerelease]-candidate
		// which can be <-rc.V> or <-ec.V>
		// nightly: openshift-vX.Y.Z[-prerelease]-candidate
		// [-prerelease] is in the format -0.nightly-<arch>-<timestamp>
		// i.e. `-0.nightly-multi-2025-11-07-08293`
		if len(cg) > 0 && cg != "stable" {
			csVersion = csVersion + "-" + cg
		}
	}

	return csVersion
}

// ConvertOpenShiftVersionNoPrefix strips off openshift-v prefix
func ConvertOpenShiftVersionNoPrefix(v string) string {
	return strings.Replace(v, metadataapi.OpenShiftVersionPrefix, "", 1)
}

// ConvertOpenShiftVersionAddPrefix adds openshift-v prefix
func ConvertOpenShiftVersionAddPrefix(v string) string {
	if len(v) > 0 && !strings.HasPrefix(v, metadataapi.OpenShiftVersionPrefix) {
		return metadataapi.OpenShiftVersionPrefix + v
	}
	return v
}
