package handlers

import (
	"context"
	stderrors "errors"
	"fmt"
	"net"
	"net/http"

	"github.com/gin-gonic/gin"
	"go.uber.org/zap"

	clinic_internal_api "github.com/pingcap-inc/clinic/clinic_data_service/pkg/clinic-internal-api"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/errors"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/log"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/observability"
)

type componentsLister interface {
	ListComponents(context.Context, uint64, string) ([]*clinic_internal_api.ResourcePoolComponent, error)
}

// ComponentsHandler exposes Clinic-owned topology through Data Proxy without
// duplicating deploy-type routing or DBAAS component aggregation.
type ComponentsHandler struct {
	client componentsLister
}

func NewComponentsHandler(client componentsLister) *ComponentsHandler {
	return &ComponentsHandler{client: client}
}

// ListComponents returns the canonical component array for a cluster.
//
//	@Summary	List cluster components
//	@Tags		topology
//	@Produce	json
//	@Param		orgId		path		string	true	"TiDB Cloud/DBAAS org ID"
//	@Param		clusterId	path		string	true	"Opaque TiDB Cloud cluster ID"
//	@Success	200			{array}		clinic_internal_api.ResourcePoolComponent
//	@Failure	400			{object}	errors.APIError
//	@Failure	401			{object}	errors.APIError
//	@Failure	403			{object}	errors.APIError
//	@Failure	404			{object}	errors.APIError
//	@Failure	502			{object}	errors.APIError
//	@Failure	503			{object}	errors.APIError
//	@Failure	504			{object}	errors.APIError
//	@Security	BearerAuth
//	@Router		/data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId}/components [get]
func (h *ComponentsHandler) ListComponents(c *gin.Context) {
	start, rec := beginMetadataRequest(c, "cluster_components")
	rec.QueryType = "components"
	defer func() { observability.RecordDataProxy(c, start, rec) }()

	orgID, ok := parseOrgID(c, rec)
	if !ok {
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateResourceID(clusterID); err != nil {
		rec.ErrCode = "INVALID_CLUSTER_ID"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}
	if h.client == nil {
		rec.ErrCode = "CLINIC_INTERNAL_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "component service is temporarily unavailable", nil)
		return
	}

	components, err := h.client.ListComponents(c.Request.Context(), orgID, clusterID)
	if err != nil {
		h.writeClinicComponentsError(c, rec, err, clusterID)
		return
	}
	if components == nil {
		components = make([]*clinic_internal_api.ResourcePoolComponent, 0)
	}
	c.JSON(http.StatusOK, components)
}

func (h *ComponentsHandler) RegisterRoutes(r *gin.Engine) {
	group := r.Group("/data-proxy/api/v1")
	group.GET("/orgs/:orgId/clusters/:clusterId/components", h.ListComponents)
	r.GET("/api/v1/orgs/:orgId/data-proxy/clusters/:clusterId/components", h.ListComponents)
}

func (h *ComponentsHandler) writeClinicComponentsError(c *gin.Context, rec *observability.DataProxyRecord, err error, clusterID string) {
	ctx := c.Request.Context()
	switch {
	case stderrors.Is(err, context.Canceled):
		rec.ErrCode = "CLINIC_INTERNAL_CANCELED"
		writeResourceError(c, clientClosedStatus, "Canceled", "component request was canceled", nil)
		return
	case stderrors.Is(err, context.DeadlineExceeded):
		rec.ErrCode = "CLINIC_INTERNAL_TIMEOUT"
		writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "component service request timed out", nil)
		return
	}
	var networkError net.Error
	if stderrors.As(err, &networkError) {
		if networkError.Timeout() {
			rec.ErrCode = "CLINIC_INTERNAL_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "component service request timed out", nil)
			return
		}
		rec.ErrCode = "CLINIC_INTERNAL_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "component service is temporarily unavailable", nil)
		return
	}

	var upstreamError *clinic_internal_api.UpstreamError
	if stderrors.As(err, &upstreamError) {
		log.Error(ctx, "Clinic internal components request failed",
			zap.String("cluster_id", clusterID),
			zap.Int("upstream_status", upstreamError.StatusCode))
		switch upstreamError.StatusCode {
		case http.StatusBadRequest:
			rec.ErrCode = "COMPONENTS_UNSUPPORTED"
			writeResourceError(c, http.StatusBadRequest, "InvalidArgument", "cluster components are not supported", nil)
		case http.StatusNotFound:
			rec.ErrCode = "CLUSTER_NOT_FOUND"
			writeResourceError(c, http.StatusNotFound, "NotFound", "cluster not found", nil)
		case http.StatusRequestTimeout, http.StatusGatewayTimeout:
			rec.ErrCode = "CLINIC_INTERNAL_TIMEOUT"
			writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "component service request timed out", nil)
		case http.StatusServiceUnavailable:
			rec.ErrCode = "COMPONENTS_UNAVAILABLE"
			writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "component service is temporarily unavailable", nil)
		default:
			rec.ErrCode = "CLINIC_INTERNAL_ERROR"
			writeResourceError(c, http.StatusBadGateway, "BadGateway", "Clinic returned an invalid component response", nil)
		}
		return
	}

	rec.ErrCode = "CLINIC_INTERNAL_BAD_RESPONSE"
	log.Error(ctx, "Clinic internal component response could not be processed",
		zap.String("cluster_id", clusterID),
		zap.String("error_type", fmt.Sprintf("%T", err)))
	writeResourceError(c, http.StatusBadGateway, "BadGateway", "Clinic returned an invalid component response", nil)
}

// Keep the qualified APIError import visible to the OpenAPI comment parser.
var _ errors.APIError
