package auth

import (
	"bytes"
	"encoding/json"
	"github.com/tidbcloud/dataflow-service/internal/cmd/bootstrap/server/wrapper"
	"github.com/tidbcloud/dataflow-service/pkg/parse"
	"io"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/dataflow-service/pkg/log"
	"go.uber.org/zap"
)

var extractors = []authParamExtractor{
	httpQueryParamsExtractorSingleton,
	httpPathParamsExtractorSingleton,
	httpBodyParamsExtractorSingleton,
}

var httpQueryParamsExtractorSingleton = &httpQueryParamsExtractor{}
var httpPathParamsExtractorSingleton = &httpPathParamsExtractor{}
var httpBodyParamsExtractorSingleton = &httpBodyParamsExtractor{}

func ExtractorAuthParam(ctx *gin.Context) {
	for _, extractor := range extractors {
		extractor.Extractor(ctx)
	}
}

type authParamExtractor interface {
	Extractor(ctx *gin.Context)
}

type queryParamDefine struct {
	OrgID     uint64 `form:"org_id"`
	ProjectID uint64 `form:"project_id"`
	ClusterID uint64 `form:"cluster_id"`
}

type httpQueryParamsExtractor struct{}

func (h *httpQueryParamsExtractor) Extractor(ctx *gin.Context) {
	param := &queryParamDefine{}
	if err := ctx.ShouldBindQuery(param); err != nil {
		log.GetGinLogger(ctx).Warn("failed to bind query param", zap.Error(err))
		return
	}
	if param.OrgID != 0 {
		setContextParam(ctx, OrgID, param.OrgID)
	}
	if param.ProjectID != 0 {
		setContextParam(ctx, ProjectID, param.ProjectID)
	}
	if param.ClusterID != 0 {
		setContextParam(ctx, ClusterID, param.ClusterID)
	}
}

func setContextParam(ctx *gin.Context, key string, value any) {
	if _, exists := ctx.Get(key); !exists {
		ctx.Set(key, value)
	}
}

type httpPathParamsExtractor struct{}

func (h *httpPathParamsExtractor) Extractor(ctx *gin.Context) {
	path := ctx.Request.URL.Path
	if orgID, exist := parse.ParseOrgIDFromPath(path); exist {
		setContextParam(ctx, OrgID, orgID)
	}
	if projectID, exist := parse.ParseProjectIDFromPath(path); exist {
		setContextParam(ctx, ProjectID, projectID)
	}
	if clusterID, exist := parse.ParseClusterIDFromPath(path); exist {
		setContextParam(ctx, ClusterID, clusterID)
	}
}

type bodyParamDefine struct {
	Labels map[string]string `json:"labels"`
}

type httpBodyParamsExtractor struct{}

func (h *httpBodyParamsExtractor) Extractor(ctx *gin.Context) {
	if ctx.Request.Body == http.NoBody {
		return
	}
	var originalBodyBytes []byte
	originalBodyBytes, err := io.ReadAll(ctx.Request.Body)
	if err != nil {
		log.GetGinLogger(ctx).Error("failed to read body", zap.Error(err))
		wrapper.NewAPIError(ctx, http.StatusInternalServerError, InternalError)
		return
	}
	ctx.Request.Body = io.NopCloser(bytes.NewBuffer(originalBodyBytes))
	if len(originalBodyBytes) == 0 {
		return
	}
	param := &bodyParamDefine{}
	if err = json.Unmarshal(originalBodyBytes, param); err != nil {
		log.GetGinLogger(ctx).Warn("failed to bind body param", zap.Error(err))
	}

	if len(param.Labels) == 0 {
		return
	}
	if val, ok := param.Labels[OrgID]; ok && val != "" {
		if num := h.convertToUint64(ctx, val); num != 0 {
			setContextParam(ctx, OrgID, num)
		}
	}
	if val, ok := param.Labels[ProjectID]; ok && val != "" {
		if num := h.convertToUint64(ctx, val); num != 0 {
			setContextParam(ctx, ProjectID, num)
		}
	}
	if val, ok := param.Labels[ClusterID]; ok && val != "" {
		if num := h.convertToUint64(ctx, val); num != 0 {
			setContextParam(ctx, ClusterID, num)
		}
	}
}

func (h *httpBodyParamsExtractor) convertToUint64(ctx *gin.Context, val string) uint64 {
	num, err := strconv.ParseUint(val, 10, 64)
	if err != nil {
		log.GetGinLogger(ctx).Warn("failed to parse id", zap.Error(err))
		return 0
	}
	return num
}
