package handlers

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/glueops/autoglue/internal/api/httpmiddleware"
	"github.com/glueops/autoglue/internal/common"
	"github.com/glueops/autoglue/internal/handlers/dto"
	"github.com/glueops/autoglue/internal/models"
	"github.com/glueops/autoglue/internal/utils"
	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
	"gorm.io/gorm"
)

// ListClusters godoc
//
//	@ID				ListClusters
//	@Summary		List clusters (org scoped)
//	@Description	Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			q			query		string	false	"Name contains (case-insensitive)"
//	@Success		200			{array}		dto.ClusterResponse
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		500			{string}	string	"failed to list clusters"
//	@Router			/clusters [get]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func ListClusters(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		q := db.Where("organization_id = ?", orgID)
		if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
			q = q.Where(`name ILIKE ?`, "%"+needle+"%")
		}

		var rows []models.Cluster
		if err := q.
			Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			Find(&rows).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		out := make([]dto.ClusterResponse, 0, len(rows))
		for _, row := range rows {
			cr := clusterToDTO(row)

			if row.EncryptedKubeconfig != "" && row.KubeIV != "" && row.KubeTag != "" {
				kubeconfig, err := utils.DecryptForOrg(orgID, row.EncryptedKubeconfig, row.KubeIV, row.KubeTag, db)
				if err != nil {
					utils.WriteError(w, http.StatusInternalServerError, "kubeconfig_decrypt_failed", "failed to decrypt kubeconfig")
					return
				}
				cr.Kubeconfig = &kubeconfig
			}
			out = append(out, cr)
		}
		utils.WriteJSON(w, http.StatusOK, out)
	}
}

// GetCluster godoc
//
//	@ID				GetCluster
//	@Summary		Get a single cluster by ID (org scoped)
//	@Description	Returns a cluster with all related resources (domain, record set, load balancers, bastion, node pools).
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID} [get]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func GetCluster(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var cluster models.Cluster
		if err := db.
			Where("id = ? AND organization_id = ?", clusterID, orgID).
			Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster).Error; err != nil {

			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		resp := clusterToDTO(cluster)

		if cluster.EncryptedKubeconfig != "" && cluster.KubeIV != "" && cluster.KubeTag != "" {
			kubeconfig, err := utils.DecryptForOrg(orgID, cluster.EncryptedKubeconfig, cluster.KubeIV, cluster.KubeTag, db)
			if err != nil {
				utils.WriteError(w, http.StatusInternalServerError, "kubeconfig_decrypt_failed", "failed to decrypt kubeconfig")
				return
			}
			resp.Kubeconfig = &kubeconfig
		}

		utils.WriteJSON(w, http.StatusOK, resp)
	}
}

// CreateCluster godoc
//
//	@ID				CreateCluster
//	@Summary		Create cluster (org scoped)
//	@Description	Creates a cluster. Status is managed by the system and starts as `pre_pending` for validation.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string						false	"Organization UUID"
//	@Param			body		body		dto.CreateClusterRequest	true	"payload"
//	@Success		201			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"invalid json"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		500			{string}	string	"create failed"
//	@Router			/clusters [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func CreateCluster(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		var in dto.CreateClusterRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		certificateKey, err := GenerateSecureHex(32)
		if err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "internal_error", "failed to generate certificate key")
			return
		}

		randomToken, err := GenerateFormattedToken()
		if err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "internal_error", "failed to generate random token")
			return
		}

		c := models.Cluster{
			OrganizationID: orgID,
			Name:           in.Name,
			Provider:       in.ClusterProvider,
			Region:         in.Region,
			Status:         models.ClusterStatusPrePending,
			LastError:      "",
			CertificateKey: certificateKey,
			RandomToken:    randomToken,
			DockerImage:    in.DockerImage,
			DockerTag:      in.DockerTag,
		}

		if err := db.Create(&c).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusCreated, clusterToDTO(c))
	}
}

// UpdateCluster godoc
//
//	@ID				UpdateCluster
//	@Summary		Update basic cluster details (org scoped)
//	@Description	Updates the cluster name, provider, and/or region. Status is managed by the system.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string						false	"Organization UUID"
//	@Param			clusterID	path		string						true	"Cluster ID"
//	@Param			body		body		dto.UpdateClusterRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID} [patch]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func UpdateCluster(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.UpdateClusterRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		// Apply only provided fields
		if in.Name != nil {
			cluster.Name = *in.Name
		}
		if in.ClusterProvider != nil {
			cluster.Provider = *in.ClusterProvider
		}
		if in.Region != nil {
			cluster.Region = *in.Region
		}

		if in.DockerImage != nil {
			cluster.DockerImage = *in.DockerImage
		}

		if in.DockerTag != nil {
			cluster.DockerTag = *in.DockerTag
		}

		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		// Any change to the cluster config may require re-validation.
		_ = markClusterNeedsValidation(db, cluster.ID)

		// Preload for a rich response
		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// DeleteCluster godoc
//
//	@ID				DeleteCluster
//	@Summary		Delete a cluster (org scoped)
//	@Description	Deletes the cluster. Related resources are cleaned up via DB constraints (e.g. CASCADE).
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		204			{string}	string	"deleted"
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID} [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func DeleteCluster(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		tx := db.Where("id = ? AND organization_id = ?", clusterID, orgID).Delete(&models.Cluster{})
		if tx.Error != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}
		if tx.RowsAffected == 0 {
			utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
			return
		}

		w.WriteHeader(http.StatusNoContent)
	}
}

// AttachCaptainDomain godoc
//
//	@ID				AttachCaptainDomain
//	@Summary		Attach a captain domain to a cluster
//	@Description	Sets captain_domain_id on the cluster. Validation of shape happens asynchronously.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string							false	"Organization UUID"
//	@Param			clusterID	path		string							true	"Cluster ID"
//	@Param			body		body		dto.AttachCaptainDomainRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster or domain not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/captain-domain [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func AttachCaptainDomain(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.AttachCaptainDomainRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		// Ensure domain exists and belongs to the org
		var domain models.Domain
		if err := db.Where("id = ? AND organization_id = ?", in.DomainID, orgID).First(&domain).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "domain_not_found", "domain not found for organization")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.CaptainDomainID = &domain.ID
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		if err := markClusterNeedsValidation(db, cluster.ID); err != nil {
			// Don't fail the request, just log if you have logging.
		}

		// Preload domain for response
		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}
		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// DetachCaptainDomain godoc
//
//	@ID				DetachCaptainDomain
//	@Summary		Detach the captain domain from a cluster
//	@Description	Clears captain_domain_id on the cluster. This will likely cause the cluster to become incomplete.
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/captain-domain [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func DetachCaptainDomain(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.CaptainDomainID = nil
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// AttachControlPlaneRecordSet godoc
//
//	@ID				AttachControlPlaneRecordSet
//	@Summary		Attach a control plane record set to a cluster
//	@Description	Sets control_plane_record_set_id on the cluster.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string						false	"Organization UUID"
//	@Param			clusterID	path		string						true	"Cluster ID"
//	@Param			body		body		dto.AttachRecordSetRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster or record set not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/control-plane-record-set [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func AttachControlPlaneRecordSet(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.AttachRecordSetRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		// record sets are indirectly org-scoped via their domain
		var rs models.RecordSet
		if err := db.
			Joins("JOIN domains d ON d.id = record_sets.domain_id").
			Where("record_sets.id = ? AND d.organization_id = ?", in.RecordSetID, orgID).
			First(&rs).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "recordset_not_found", "record set not found for organization")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.ControlPlaneRecordSetID = &rs.ID
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// DetachControlPlaneRecordSet godoc
//
//	@ID				DetachControlPlaneRecordSet
//	@Summary		Detach the control plane record set from a cluster
//	@Description	Clears control_plane_record_set_id on the cluster.
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/control-plane-record-set [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func DetachControlPlaneRecordSet(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.ControlPlaneRecordSetID = nil
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// AttachAppsLoadBalancer godoc
//
//	@ID				AttachAppsLoadBalancer
//	@Summary		Attach an apps load balancer to a cluster
//	@Description	Sets apps_load_balancer_id on the cluster.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string							false	"Organization UUID"
//	@Param			clusterID	path		string							true	"Cluster ID"
//	@Param			body		body		dto.AttachLoadBalancerRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster or load balancer not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/apps-load-balancer [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func AttachAppsLoadBalancer(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.AttachLoadBalancerRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		var lb models.LoadBalancer
		if err := db.Where("id = ? AND organization_id = ?", in.LoadBalancerID, orgID).First(&lb).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "lb_not_found", "load balancer not found for organization")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.AppsLoadBalancerID = &lb.ID
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// DetachAppsLoadBalancer godoc
//
//	@ID				DetachAppsLoadBalancer
//	@Summary		Detach the apps load balancer from a cluster
//	@Description	Clears apps_load_balancer_id on the cluster.
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/apps-load-balancer [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func DetachAppsLoadBalancer(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.AppsLoadBalancerID = nil
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// AttachGlueOpsLoadBalancer godoc
//
//	@ID				AttachGlueOpsLoadBalancer
//	@Summary		Attach a GlueOps/control-plane load balancer to a cluster
//	@Description	Sets glueops_load_balancer_id on the cluster.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string							false	"Organization UUID"
//	@Param			clusterID	path		string							true	"Cluster ID"
//	@Param			body		body		dto.AttachLoadBalancerRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster or load balancer not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/glueops-load-balancer [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func AttachGlueOpsLoadBalancer(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.AttachLoadBalancerRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		var lb models.LoadBalancer
		if err := db.Where("id = ? AND organization_id = ?", in.LoadBalancerID, orgID).First(&lb).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "lb_not_found", "load balancer not found for organization")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.GlueOpsLoadBalancerID = &lb.ID
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// DetachGlueOpsLoadBalancer godoc
//
//	@ID				DetachGlueOpsLoadBalancer
//	@Summary		Detach the GlueOps/control-plane load balancer from a cluster
//	@Description	Clears glueops_load_balancer_id on the cluster.
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/glueops-load-balancer [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func DetachGlueOpsLoadBalancer(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.GlueOpsLoadBalancerID = nil
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// AttachBastionServer godoc
//
//	@ID				AttachBastionServer
//	@Summary		Attach a bastion server to a cluster
//	@Description	Sets bastion_server_id on the cluster.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string						false	"Organization UUID"
//	@Param			clusterID	path		string						true	"Cluster ID"
//	@Param			body		body		dto.AttachBastionRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster or server not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/bastion [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func AttachBastionServer(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.AttachBastionRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		var server models.Server
		if err := db.Where("id = ? AND organization_id = ?", in.ServerID, orgID).First(&server).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found for organization")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.BastionServerID = &server.ID
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// DetachBastionServer godoc
//
//	@ID				DetachBastionServer
//	@Summary		Detach the bastion server from a cluster
//	@Description	Clears bastion_server_id on the cluster.
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/bastion [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func DetachBastionServer(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.BastionServerID = nil
		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// SetClusterKubeconfig godoc
//
//	@ID				SetClusterKubeconfig
//	@Summary		Set (or replace) the kubeconfig for a cluster
//	@Description	Stores the kubeconfig encrypted per organization. The kubeconfig is never returned in responses.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string						false	"Organization UUID"
//	@Param			clusterID	path		string						true	"Cluster ID"
//	@Param			body		body		dto.SetKubeconfigRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/kubeconfig [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func SetClusterKubeconfig(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.SetKubeconfigRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		ct, iv, tag, err := utils.EncryptForOrg(orgID, []byte(in.Kubeconfig), db)
		if err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "encryption_error", "failed to encrypt kubeconfig")
			return
		}

		cluster.EncryptedKubeconfig = ct
		cluster.KubeIV = iv
		cluster.KubeTag = tag

		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// ClearClusterKubeconfig godoc
//
//	@ID				ClearClusterKubeconfig
//	@Summary		Clear the kubeconfig for a cluster
//	@Description	Removes the encrypted kubeconfig, IV, and tag from the cluster record.
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/kubeconfig [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func ClearClusterKubeconfig(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var cluster models.Cluster
		if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		cluster.EncryptedKubeconfig = ""
		cluster.KubeIV = ""
		cluster.KubeTag = ""

		if err := db.Save(&cluster).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// AttachNodePool godoc
//
//	@ID				AttachNodePool
//	@Summary		Attach a node pool to a cluster
//	@Description	Adds an entry in the cluster_node_pools join table.
//	@Tags			Clusters
//	@Accept			json
//	@Produce		json
//	@Param			X-Org-ID	header		string						false	"Organization UUID"
//	@Param			clusterID	path		string						true	"Cluster ID"
//	@Param			body		body		dto.AttachNodePoolRequest	true	"payload"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster or node pool not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/node-pools [post]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func AttachNodePool(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		var in dto.AttachNodePoolRequest
		if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
			return
		}

		// Load cluster (org scoped)
		var cluster models.Cluster
		if err := db.
			Where("id = ? AND organization_id = ?", clusterID, orgID).
			First(&cluster).Error; err != nil {

			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		// Load node pool (org scoped)
		var np models.NodePool
		if err := db.
			Where("id = ? AND organization_id = ?", in.NodePoolID, orgID).
			First(&np).Error; err != nil {

			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "nodepool_not_found", "node pool not found for organization")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		// Create association in join table
		if err := db.Model(&cluster).Association("NodePools").Append(&np); err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to attach node pool")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		// Reload for rich response
		if err := db.
			Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {

			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// DetachNodePool godoc
//
//	@ID				DetachNodePool
//	@Summary		Detach a node pool from a cluster
//	@Description	Removes an entry from the cluster_node_pools join table.
//	@Tags			Clusters
//	@Produce		json
//	@Param			X-Org-ID	header		string	false	"Organization UUID"
//	@Param			clusterID	path		string	true	"Cluster ID"
//	@Param			nodePoolID	path		string	true	"Node Pool ID"
//	@Success		200			{object}	dto.ClusterResponse
//	@Failure		400			{string}	string	"bad request"
//	@Failure		401			{string}	string	"Unauthorized"
//	@Failure		403			{string}	string	"organization required"
//	@Failure		404			{string}	string	"cluster or node pool not found"
//	@Failure		500			{string}	string	"db error"
//	@Router			/clusters/{clusterID}/node-pools/{nodePoolID} [delete]
//	@Security		BearerAuth
//	@Security		OrgKeyAuth
//	@Security		OrgSecretAuth
func DetachNodePool(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
		if !ok {
			utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
			return
		}

		clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
			return
		}

		nodePoolID, err := uuid.Parse(chi.URLParam(r, "nodePoolID"))
		if err != nil {
			utils.WriteError(w, http.StatusBadRequest, "bad_nodepool_id", "invalid node pool id")
			return
		}

		var cluster models.Cluster
		if err := db.
			Where("id = ? AND organization_id = ?", clusterID, orgID).
			First(&cluster).Error; err != nil {

			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		var np models.NodePool
		if err := db.
			Where("id = ? AND organization_id = ?", nodePoolID, orgID).
			First(&np).Error; err != nil {

			if errors.Is(err, gorm.ErrRecordNotFound) {
				utils.WriteError(w, http.StatusNotFound, "nodepool_not_found", "node pool not found for organization")
				return
			}
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		if err := db.Model(&cluster).Association("NodePools").Delete(&np); err != nil {
			utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to detach node pool")
			return
		}

		_ = markClusterNeedsValidation(db, cluster.ID)

		if err := db.
			Preload("CaptainDomain").
			Preload("ControlPlaneRecordSet").
			Preload("AppsLoadBalancer").
			Preload("GlueOpsLoadBalancer").
			Preload("BastionServer").
			Preload("NodePools").
			Preload("NodePools.Labels").
			Preload("NodePools.Annotations").
			Preload("NodePools.Taints").
			Preload("NodePools.Servers").
			First(&cluster, "id = ?", cluster.ID).Error; err != nil {

			utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
			return
		}

		utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
	}
}

// -- Helpers

func clusterToDTO(c models.Cluster) dto.ClusterResponse {
	var bastion *dto.ServerResponse
	if c.BastionServer != nil {
		b := serverToDTO(*c.BastionServer)
		bastion = &b
	}

	var captainDomain *dto.DomainResponse
	if c.CaptainDomainID != nil && c.CaptainDomain.ID != uuid.Nil {
		dr := domainToDTO(c.CaptainDomain)
		captainDomain = &dr
	}

	var controlPlane *dto.RecordSetResponse
	if c.ControlPlaneRecordSet != nil {
		rr := recordSetToDTO(*c.ControlPlaneRecordSet)
		controlPlane = &rr
	}

	var cfqdn *string
	if captainDomain != nil && controlPlane != nil {
		fq := fmt.Sprintf("%s.%s", controlPlane.Name, captainDomain.DomainName)
		cfqdn = &fq
	}

	var appsLB *dto.LoadBalancerResponse
	if c.AppsLoadBalancer != nil {
		lr := loadBalancerToDTO(*c.AppsLoadBalancer)
		appsLB = &lr
	}

	var glueOpsLB *dto.LoadBalancerResponse
	if c.GlueOpsLoadBalancer != nil {
		lr := loadBalancerToDTO(*c.GlueOpsLoadBalancer)
		glueOpsLB = &lr
	}

	nps := make([]dto.NodePoolResponse, 0, len(c.NodePools))
	for _, np := range c.NodePools {
		nps = append(nps, nodePoolToDTO(np))
	}

	return dto.ClusterResponse{
		ID:                    c.ID,
		Name:                  c.Name,
		CaptainDomain:         captainDomain,
		ControlPlaneRecordSet: controlPlane,
		ControlPlaneFQDN:      cfqdn,
		AppsLoadBalancer:      appsLB,
		GlueOpsLoadBalancer:   glueOpsLB,
		BastionServer:         bastion,
		Provider:              c.Provider,
		Region:                c.Region,
		Status:                c.Status,
		LastError:             c.LastError,
		RandomToken:           c.RandomToken,
		CertificateKey:        c.CertificateKey,
		NodePools:             nps,
		DockerImage:           c.DockerImage,
		DockerTag:             c.DockerTag,
		CreatedAt:             c.CreatedAt,
		UpdatedAt:             c.UpdatedAt,
	}
}

func nodePoolToDTO(np models.NodePool) dto.NodePoolResponse {
	labels := make([]dto.LabelResponse, 0, len(np.Labels))
	for _, l := range np.Labels {
		labels = append(labels, dto.LabelResponse{
			Key:   l.Key,
			Value: l.Value,
		})
	}

	annotations := make([]dto.AnnotationResponse, 0, len(np.Annotations))
	for _, a := range np.Annotations {
		annotations = append(annotations, dto.AnnotationResponse{
			Key:   a.Key,
			Value: a.Value,
		})
	}

	taints := make([]dto.TaintResponse, 0, len(np.Taints))
	for _, t := range np.Taints {
		taints = append(taints, dto.TaintResponse{
			Key:    t.Key,
			Value:  t.Value,
			Effect: t.Effect,
		})
	}

	servers := make([]dto.ServerResponse, 0, len(np.Servers))
	for _, s := range np.Servers {
		servers = append(servers, serverToDTO(s))
	}

	return dto.NodePoolResponse{
		AuditFields: common.AuditFields{
			ID:             np.ID,
			OrganizationID: np.OrganizationID,
			CreatedAt:      np.CreatedAt,
			UpdatedAt:      np.UpdatedAt,
		},
		Name:        np.Name,
		Role:        dto.NodeRole(np.Role),
		Labels:      labels,
		Annotations: annotations,
		Taints:      taints,
		Servers:     servers,
	}
}

func serverToDTO(s models.Server) dto.ServerResponse {
	return dto.ServerResponse{
		ID:               s.ID,
		Hostname:         s.Hostname,
		PrivateIPAddress: s.PrivateIPAddress,
		PublicIPAddress:  s.PublicIPAddress,
		Role:             s.Role,
		Status:           s.Status,
		SSHUser:          s.SSHUser,
		SshKeyID:         s.SshKeyID,
		CreatedAt:        s.CreatedAt.UTC().Format(time.RFC3339),
		UpdatedAt:        s.UpdatedAt.UTC().Format(time.RFC3339),
	}
}

func domainToDTO(d models.Domain) dto.DomainResponse {
	return dto.DomainResponse{
		ID:             d.ID.String(),
		OrganizationID: d.OrganizationID.String(),
		DomainName:     d.DomainName,
		ZoneID:         d.ZoneID,
		Status:         d.Status,
		LastError:      d.LastError,
		CredentialID:   d.CredentialID.String(),
		CreatedAt:      d.CreatedAt.UTC().Format(time.RFC3339),
		UpdatedAt:      d.UpdatedAt.UTC().Format(time.RFC3339),
	}
}

func recordSetToDTO(rs models.RecordSet) dto.RecordSetResponse {
	return dto.RecordSetResponse{
		ID:          rs.ID.String(),
		DomainID:    rs.DomainID.String(),
		Name:        rs.Name,
		Type:        rs.Type,
		TTL:         rs.TTL,
		Values:      []byte(rs.Values),
		Fingerprint: rs.Fingerprint,
		Status:      rs.Status,
		Owner:       rs.Owner,
		LastError:   rs.LastError,
		CreatedAt:   rs.CreatedAt.UTC().Format(time.RFC3339),
		UpdatedAt:   rs.UpdatedAt.UTC().Format(time.RFC3339),
	}
}

func loadBalancerToDTO(lb models.LoadBalancer) dto.LoadBalancerResponse {
	return dto.LoadBalancerResponse{
		ID:               lb.ID,
		OrganizationID:   lb.OrganizationID,
		Name:             lb.Name,
		Kind:             lb.Kind,
		PublicIPAddress:  lb.PublicIPAddress,
		PrivateIPAddress: lb.PrivateIPAddress,
		CreatedAt:        lb.CreatedAt,
		UpdatedAt:        lb.UpdatedAt,
	}
}

func GenerateSecureHex(n int) (string, error) {
	bytes := make([]byte, n)
	if _, err := rand.Read(bytes); err != nil {
		return "", fmt.Errorf("failed to generate random bytes: %w", err)
	}
	return hex.EncodeToString(bytes), nil
}

func GenerateFormattedToken() (string, error) {
	part1, err := GenerateSecureHex(3)
	if err != nil {
		return "", fmt.Errorf("failed to generate token part 1: %w", err)
	}
	part2, err := GenerateSecureHex(8)
	if err != nil {
		return "", fmt.Errorf("failed to generate token part 2: %w", err)
	}
	return fmt.Sprintf("%s.%s", part1, part2), nil
}

func markClusterNeedsValidation(db *gorm.DB, clusterID uuid.UUID) error {
	return db.Model(&models.Cluster{}).Where("id = ?", clusterID).Updates(map[string]any{
		"status":     models.ClusterStatusPrePending,
		"last_error": "",
	}).Error
}
