package cluster

import (
	"errors"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/api/common"
	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
	model2 "github.com/pingcap-inc/clinic/pkg/db/model"
	cliniclog "github.com/pingcap-inc/clinic/pkg/log"
	"github.com/pingcap-inc/clinic/pkg/o11y"
)

type resourcePoolComponentSources struct {
	dedicated    func(*session.CtxUser, uint64) ([]*view.ResourcePoolComponent, error)
	resourcePool func(*session.CtxUser, uint64) ([]*view.ResourcePoolComponent, error)
	premium      func(*session.CtxUser, uint64, uint64) ([]*view.ResourcePoolComponent, error)
}

func defaultResourcePoolComponentSources() resourcePoolComponentSources {
	return resourcePoolComponentSources{
		dedicated:    datasource.GetDedicatedClusterComponents,
		resourcePool: datasource.GetResourcePoolComponents,
		premium:      datasource.GetPremiumClusterComponents,
	}
}

// ListResourcePoolComponents is the single business implementation shared by
// the legacy browser API and the Data Proxy internal facade. Compatibility
// decorations that belong only to the legacy HTTP contract must stay in its
// handler and must not be added here.
func (s *ClusterService) ListResourcePoolComponents(user *session.CtxUser, cluster *db.Cluster) ([]*view.ResourcePoolComponent, error) {
	return listResourcePoolComponents(user, cluster, defaultResourcePoolComponentSources())
}

func listResourcePoolComponents(
	user *session.CtxUser,
	cluster *db.Cluster,
	sources resourcePoolComponentSources,
) ([]*view.ResourcePoolComponent, error) {
	if cluster == nil {
		return nil, error2.ErrNotFound.New("cluster not found")
	}

	clusterID, _ := strconv.ParseUint(cluster.ClusterID, 10, 64)
	switch cluster.DeployTypeV2 {
	case o11y.DeployTypeV2Dedicated:
		result, err := sources.dedicated(user, clusterID)
		if err != nil {
			log.Error("Failed to get dedicated cluster components", zap.String("clusterID", cluster.ClusterID), zap.Error(err))
		}
		return result, err
	case o11y.DeployTypeV2PremiumResourcePool, o11y.DeployTypeV2PremiumReservedResourcePool, o11y.DeployTypeV2EssentialV2ResourcePool, o11y.DeployTypeV2ByocResourcePool:
		result, err := sources.resourcePool(user, clusterID)
		if err != nil {
			log.Error("Failed to get resource pool components", zap.String("clusterID", cluster.ClusterID), zap.Error(err))
		}
		return result, err
	case o11y.DeployTypeV2Premium, o11y.DeployTypeV2PremiumReserved, o11y.DeployTypeV2EssentialV2, o11y.DeployTypeV2Byoc:
		resourcePoolID, _ := strconv.ParseUint(cluster.ParentID, 10, 64)
		result, err := sources.premium(user, clusterID, resourcePoolID)
		if err != nil {
			log.Error("Failed to get premium cluster components", zap.String("clusterID", cluster.ClusterID), zap.Error(err))
		}
		return result, err
	default:
		log.Error("only support TiDB-X cluster", zap.String("clusterID", cluster.ClusterID))
		return nil, error2.ErrBadRequest.New("only support TiDB-X cluster")
	}
}

func isLegacyMockedTiCDCCluster(deployTypeV2 string) bool {
	switch deployTypeV2 {
	case o11y.DeployTypeV2Premium, o11y.DeployTypeV2PremiumReserved, o11y.DeployTypeV2EssentialV2, o11y.DeployTypeV2Byoc:
		return true
	default:
		return false
	}
}

func appendLegacyMockedTiCDC(result []*view.ResourcePoolComponent) []*view.ResourcePoolComponent {
	return append(result, &view.ResourcePoolComponent{
		Id:            "mocked",
		ComponentType: "ticdc",
		Version:       "",
		Phase:         "",
	})
}

func resourcePoolComponentsLogger(c *gin.Context) *zap.Logger {
	spanContext := trace.SpanContextFromContext(c.Request.Context())
	traceID := c.GetHeader("X-Trace-ID")
	if spanContext.HasTraceID() {
		traceID = spanContext.TraceID().String()
	}
	return cliniclog.WithContextL(c.Request.Context()).With(
		zap.String("trace_id", traceID),
		zap.String("api", "resource_pool_components"),
		zap.String("cluster_id", c.Param("cid")),
	)
}

// @Summary		Get resource pool components
// @Tags			ResourcePool
// @Description	Get resourcePool components for premium, premium reserved, essential v2, byoc, and byoc resource pool clusters
// @Produce		json
// @Param			x-csrf-token	header	string	true	"get value from login.ValidationResp response"
// @Param			oid				path	string	true	"organization id"
// @Param			cid				path	string	true	"cluster id"
// @Success		200				{array}	view.ResourcePoolComponent
// @Router			/orgs/{oid}/clusters/{cid}/resource-pool/components [get]
func (s *ClusterService) GetResourcePoolComponents(c *gin.Context) {
	user, _, cluster, ok, err := common.CanReadCluster(c, s.rbac, c.Param("cid"))
	if err != nil {
		_ = c.Error(error2.ErrNotFound.New("%s", err.Error()))
		return
	}
	redactionApplies := shouldRedactExternalDedicatedResourcePoolComponents(user, cluster)
	if !ok {
		if redactionApplies {
			resourcePoolComponentsLogger(c).Warn("resource pool components access denied")
		}
		_ = c.Error(error2.ErrForbidden.New("access cluster failed"))
		return
	}

	result, err := s.ListResourcePoolComponents(user, cluster)
	if err != nil {
		if redactionApplies {
			resourcePoolComponentsLogger(c).Error("list resource pool components failed",
				zap.String("deploy_type", cluster.DeployTypeV2),
				zap.Error(err))
		}
		_ = c.Error(err)
		return
	}
	result, redacted := redactExternalDedicatedResourcePoolComponents(user, cluster, result)
	// Keep the historical non-browser TiCDC decoration on the legacy route.
	if c.GetHeader("x-csrf-token") == "" && isLegacyMockedTiCDCCluster(cluster.DeployTypeV2) {
		result = appendLegacyMockedTiCDC(result)
	}

	if redacted {
		resourcePoolComponentsLogger(c).Info("resource pool components returned",
			zap.String("deploy_type", cluster.DeployTypeV2),
			zap.Bool("redacted", true),
			zap.Int("component_count", len(result)))
	}
	c.JSON(http.StatusOK, result)
}

// GetDataProxyResourcePoolComponents serves the cluster-internal canonical
// facade used by Clinic Data Proxy. orgId is the TiDB Cloud / DBAAS org ID,
// not Clinic's organizations.id.
func (s *ClusterService) GetDataProxyResourcePoolComponents(c *gin.Context) {
	orgID, err := strconv.ParseUint(c.Param("orgId"), 10, strconv.IntSize)
	if err != nil || orgID == 0 {
		_ = c.Error(error2.ErrBadRequest.New("invalid DBAAS organization id"))
		return
	}

	clusterID := c.Param("clusterId")
	if parsedClusterID, parseErr := strconv.ParseUint(clusterID, 10, 64); parseErr != nil || parsedClusterID == 0 {
		_ = c.Error(error2.ErrBadRequest.New("invalid cluster id"))
		return
	}

	user := session.GetUser(c)
	if user == nil {
		_ = c.Error(error2.ErrInternal.New("internal user context is missing"))
		return
	}

	cluster, err := model2.GetClusterWithDbaasIDs(clusterID, uint(orgID))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			_ = c.Error(error2.ErrNotFound.New("organization or cluster not found"))
			return
		}
		log.Error("failed to verify component cluster ownership",
			zap.String("clusterID", clusterID),
			zap.Uint64("dbaasOrgID", orgID),
			zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("failed to verify cluster ownership"))
		return
	}
	if cluster == nil {
		_ = c.Error(error2.ErrNotFound.New("organization or cluster not found"))
		return
	}

	result, err := s.ListResourcePoolComponents(user, cluster)
	if err != nil {
		_ = c.Error(err)
		return
	}

	// Deliberately do not append the legacy mocked TiCDC component here.
	c.JSON(http.StatusOK, result)
}
