package handlers

import (
	stderrors "errors"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/gin-gonic/gin"
	"github.com/pingcap-inc/clinic/clinic_data_service/pkg/clustercontext"
	"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"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
)

const (
	defaultMetadataPageSize = 50
	maxMetadataPageSize     = 200
	maxMetadataPage         = 1_000_000
	maxResourceIDLength     = 255
)

// ClusterMetadataHandler exposes canonical metadata without calling legacy
// Clinic HTTP APIs.
type ClusterMetadataHandler struct {
	repository     clustercontext.Repository
	userRepository clustercontext.UserRepository
}

func NewClusterMetadataHandler(repository clustercontext.Repository) *ClusterMetadataHandler {
	if repository == nil {
		repository = clustercontext.NewUnavailableRepository()
	}
	return &ClusterMetadataHandler{
		repository:     repository,
		userRepository: clustercontext.NewUserScopedRepository(nil, nil),
	}
}

// NewClusterMetadataHandlerWithUserRepository creates a handler whose public
// list route is constrained by the authenticated user's HTTP API-visible clusters.
func NewClusterMetadataHandlerWithUserRepository(repository clustercontext.Repository, userRepository clustercontext.UserRepository) *ClusterMetadataHandler {
	handler := NewClusterMetadataHandler(repository)
	if userRepository != nil {
		handler.userRepository = userRepository
	}
	return handler
}

// GetCluster returns canonical metadata for an internal/global exact lookup.
//
// @Summary      Get cluster metadata by cluster ID
// @Tags         cluster-metadata
// @Produce      json
// @Param        clusterId path string true "Opaque TiDB Cloud cluster ID"
// @Success      200 {object} clustercontext.Cluster
// @Failure      400 {object} errors.APIError
// @Failure      401 {object} errors.APIError
// @Failure      403 {object} errors.APIError
// @Failure      404 {object} errors.APIError
// @Failure      500 {object} errors.APIError
// @Failure      503 {object} errors.APIError
// @Failure      504 {object} errors.APIError
// @Security     BearerAuth
// @Router       /data-proxy/api/v1/clusters/{clusterId} [get]
func (h *ClusterMetadataHandler) GetCluster(c *gin.Context) {
	start, rec := beginMetadataRequest(c, "cluster_metadata_exact")
	defer func() { observability.RecordDataProxy(c, start, rec) }()

	clusterID := c.Param("clusterId")
	if err := validateResourceID(clusterID); err != nil {
		log.Info(c.Request.Context(), "Invalid cluster ID", zap.Error(err))
		rec.ErrCode = "INVALID_CLUSTER_ID"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}

	cluster, err := h.repository.GetByClusterID(c.Request.Context(), clusterID)
	if err != nil {
		writeClusterRepositoryError(c, rec, err, clusterID)
		return
	}
	applyClusterMetadataObservability(rec, cluster)
	c.JSON(http.StatusOK, cluster)
}

// GetOrgCluster returns canonical metadata after external org ownership check.
//
// @Summary      Get org-scoped cluster metadata
// @Tags         cluster-metadata
// @Produce      json
// @Param        orgId path string true "TiDB Cloud/DBAAS org ID"
// @Param        clusterId path string true "Opaque TiDB Cloud cluster ID"
// @Success      200 {object} clustercontext.Cluster
// @Failure      400 {object} errors.APIError
// @Failure      401 {object} errors.APIError
// @Failure      403 {object} errors.APIError
// @Failure      404 {object} errors.APIError
// @Failure      500 {object} errors.APIError
// @Failure      503 {object} errors.APIError
// @Failure      504 {object} errors.APIError
// @Security     BearerAuth
// @Router       /data-proxy/api/v1/orgs/{orgId}/clusters/{clusterId} [get]
func (h *ClusterMetadataHandler) GetOrgCluster(c *gin.Context) {
	start, rec := beginMetadataRequest(c, "cluster_metadata_detail")
	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
	}

	cluster, err := h.repository.GetByOrgAndClusterID(c.Request.Context(), orgID, clusterID)
	if err != nil {
		writeClusterRepositoryError(c, rec, err, clusterID)
		return
	}
	applyClusterMetadataObservability(rec, cluster)
	c.JSON(http.StatusOK, cluster)
}

// ListOrgClusters returns a page of canonical clusters for one external org.
//
// @Summary      List clusters in a TiDB Cloud org
// @Tags         cluster-metadata
// @Produce      json
// @Param        orgId path string true "TiDB Cloud/DBAAS org ID"
// @Param        page query int false "1-based page" default(1) minimum(1) maximum(1000000)
// @Param        pageSize query int false "Page size, max 200" default(50) minimum(1) maximum(200)
// @Param        query query string false "Cluster ID or name contains"
// @Param        showDeleted query bool false "Include deleted clusters" default(false)
// @Success      200 {object} clustercontext.Page
// @Failure      400 {object} errors.APIError
// @Failure      401 {object} errors.APIError
// @Failure      403 {object} errors.APIError
// @Failure      500 {object} errors.APIError
// @Failure      503 {object} errors.APIError
// @Failure      504 {object} errors.APIError
// @Security     BearerAuth
// @Router       /data-proxy/api/v1/orgs/{orgId}/clusters [get]
func (h *ClusterMetadataHandler) ListOrgClusters(c *gin.Context) {
	start, rec := beginMetadataRequest(c, "cluster_metadata_list")
	defer func() { observability.RecordDataProxy(c, start, rec) }()

	orgID, ok := parseOrgID(c, rec)
	if !ok {
		return
	}
	opts, err := parseMetadataListOptions(c)
	if err != nil {
		rec.ErrCode = "INVALID_PAGINATION"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}

	page, err := h.repository.ListByOrg(c.Request.Context(), orgID, opts)
	if err != nil {
		writeClusterRepositoryError(c, rec, err, "")
		return
	}
	c.JSON(http.StatusOK, page)
}

// ListOrgClustersByUser returns only the clusters that the current bearer
// token can access. The canonical response shape is preserved for clients.
func (h *ClusterMetadataHandler) ListOrgClustersByUser(c *gin.Context) {
	start, rec := beginMetadataRequest(c, "cluster_metadata_user_list")
	defer func() { observability.RecordDataProxy(c, start, rec) }()

	orgID, ok := parseOrgID(c, rec)
	if !ok {
		return
	}
	opts, err := parseUserMetadataListOptions(c)
	if err != nil {
		rec.ErrCode = "INVALID_PAGINATION"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", err.Error(), nil)
		return
	}
	authorization := c.GetHeader("Authorization")
	if strings.TrimSpace(authorization) == "" {
		rec.ErrCode = "UNAUTHENTICATED"
		writeResourceError(c, http.StatusUnauthorized, "Unauthenticated", "Authorization header is required", nil)
		return
	}
	page, err := h.userRepository.ListByOrgForUser(c.Request.Context(), orgID, opts, authorization, c.Request.Header)
	if err != nil {
		writeClusterRepositoryError(c, rec, err, "")
		return
	}
	c.JSON(http.StatusOK, page)
}

func (h *ClusterMetadataHandler) RegisterRoutes(r *gin.Engine) {
	group := r.Group("/data-proxy/api/v1")
	group.GET("/clusters/:clusterId", h.GetCluster)
	group.GET("/orgs/:orgId/clusters", h.ListOrgClusters)
	group.GET("/orgs/:orgId/clusters/:clusterId", h.GetOrgCluster)

	publicGroup := r.Group("/api/v1/orgs/:orgId/data-proxy")
	publicGroup.GET("/clusters", h.ListOrgClustersByUser)
	publicGroup.GET("/clusters/:clusterId", h.GetOrgCluster)
}

func beginMetadataRequest(c *gin.Context, endpoint string) (time.Time, *observability.DataProxyRecord) {
	return time.Now(), &observability.DataProxyRecord{
		Endpoint: endpoint,
		Caller:   observability.CallerFromGin(c),
	}
}

func parseOrgID(c *gin.Context, rec *observability.DataProxyRecord) (uint64, bool) {
	raw := c.Param("orgId")
	orgID, err := strconv.ParseUint(raw, 10, 64)
	if err != nil || orgID == 0 {
		rec.ErrCode = "INVALID_ORG_ID"
		writeResourceError(c, http.StatusBadRequest, "InvalidArgument", "orgId must be a positive TiDB Cloud/DBAAS org ID", nil)
		return 0, false
	}
	return orgID, true
}

func parseMetadataListOptions(c *gin.Context) (clustercontext.ListOptions, error) {
	page, err := parsePositiveInt(c.Query("page"), 1, 1, maxMetadataPage)
	if err != nil {
		return clustercontext.ListOptions{}, stderrors.New("page must be between 1 and 1000000")
	}
	pageSize, err := parsePositiveInt(c.Query("pageSize"), defaultMetadataPageSize, 1, maxMetadataPageSize)
	if err != nil {
		return clustercontext.ListOptions{}, stderrors.New("pageSize must be between 1 and 200")
	}
	showDeleted := false
	if raw := c.Query("showDeleted"); raw != "" {
		showDeleted, err = strconv.ParseBool(raw)
		if err != nil {
			return clustercontext.ListOptions{}, stderrors.New("showDeleted must be true or false")
		}
	}
	query := strings.TrimSpace(c.Query("query"))
	if len(query) > maxResourceIDLength {
		return clustercontext.ListOptions{}, stderrors.New("query must not exceed 255 bytes")
	}
	return clustercontext.ListOptions{
		Page:        page,
		PageSize:    pageSize,
		Query:       query,
		ShowDeleted: showDeleted,
	}, nil
}

func parseUserMetadataListOptions(c *gin.Context) (clustercontext.ListOptions, error) {
	opts, err := parseMetadataListOptions(c)
	if err != nil {
		return clustercontext.ListOptions{}, err
	}
	if opts.Query != "" {
		return clustercontext.ListOptions{}, stderrors.New("query is not supported for user-scoped cluster lists")
	}
	if opts.ShowDeleted {
		return clustercontext.ListOptions{}, stderrors.New("showDeleted=true is not supported for user-scoped cluster lists")
	}
	return opts, nil
}

func parsePositiveInt(raw string, defaultValue, minValue, maxValue int) (int, error) {
	if raw == "" {
		return defaultValue, nil
	}
	value, err := strconv.Atoi(raw)
	if err != nil || value < minValue || (maxValue > 0 && value > maxValue) {
		return 0, stderrors.New("integer out of range")
	}
	return value, nil
}

func validateResourceID(value string) error {
	if value == "" {
		return stderrors.New("clusterId is required")
	}
	if len(value) > maxResourceIDLength {
		return stderrors.New("clusterId must not exceed 255 bytes")
	}
	if strings.Contains(value, "/") {
		return stderrors.New("clusterId must not contain slash characters")
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return stderrors.New("clusterId must not contain control characters")
		}
	}
	return nil
}

func writeClusterRepositoryError(c *gin.Context, rec *observability.DataProxyRecord, err error, clusterID string) {
	ctx := c.Request.Context()
	switch {
	case stderrors.Is(err, clustercontext.ErrNotFound):
		rec.ErrCode = "CLUSTER_NOT_FOUND"
		writeResourceError(c, http.StatusNotFound, "NotFound", "cluster not found", nil)
	case stderrors.Is(err, clustercontext.ErrUnavailable):
		rec.ErrCode = "METADATA_UNAVAILABLE"
		writeResourceError(c, http.StatusServiceUnavailable, "Unavailable", "cluster metadata is temporarily unavailable", nil)
	case stderrors.Is(err, clustercontext.ErrTimeout):
		rec.ErrCode = "METADATA_TIMEOUT"
		writeResourceError(c, http.StatusGatewayTimeout, "DeadlineExceeded", "cluster metadata request timed out", nil)
	case stderrors.Is(err, clustercontext.ErrUnauthenticated):
		rec.ErrCode = "UNAUTHENTICATED"
		writeResourceError(c, http.StatusUnauthorized, "Unauthenticated", "authentication is required", nil)
	case stderrors.Is(err, clustercontext.ErrPermissionDenied):
		rec.ErrCode = "PERMISSION_DENIED"
		writeResourceError(c, http.StatusForbidden, "PermissionDenied", "permission denied", nil)
	case stderrors.Is(err, clustercontext.ErrResourceExhausted):
		rec.ErrCode = "RESOURCE_EXHAUSTED"
		writeResourceError(c, http.StatusTooManyRequests, "ResourceExhausted", "cluster metadata service rate limit exceeded", nil)
	case stderrors.Is(err, clustercontext.ErrBadGateway):
		rec.ErrCode = "METADATA_BAD_GATEWAY"
		writeResourceError(c, http.StatusBadGateway, "BadGateway", "cluster metadata service returned an invalid response", nil)
	default:
		rec.ErrCode = "METADATA_QUERY_FAILED"
		log.Error(ctx, "cluster metadata query failed", zap.String("cluster_id", clusterID), zap.Error(err))
		writeResourceError(c, http.StatusInternalServerError, "InternalError", "failed to query cluster metadata", nil)
	}
}

func applyClusterMetadataObservability(rec *observability.DataProxyRecord, cluster *clustercontext.Cluster) {
	if rec == nil || cluster == nil {
		return
	}
	rec.Provider = cluster.Provider
	rec.Region = cluster.Region
	rec.DeployTypeV2 = cluster.DeployTypeV2
}

func writeResourceError(c *gin.Context, status int, code, message string, details any) {
	requestID := c.Writer.Header().Get("X-Trace-ID")
	if requestID == "" {
		spanContext := trace.SpanContextFromContext(c.Request.Context())
		if spanContext.HasTraceID() {
			requestID = spanContext.TraceID().String()
		}
	}
	c.JSON(status, errors.APIError{
		Code:      code,
		Message:   message,
		RequestID: requestID,
		Details:   details,
	})
}
