package metadata

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	"github.com/pingcap/log"
	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	grpcmetadata "google.golang.org/grpc/metadata"
)

func ExtractRequestMetaWithCheckPermission(ctx context.Context) (*RequestMeta, error) {
	var err error
	reqMeta, err := extractRequestMeta(ctx, validProjectExtractor)
	if err != nil {
		return nil, err
	}

	if !reqMeta.GetIsCheckPermission() {
		return nil, errors.New("check permission is not set to true")
	}

	return reqMeta, nil
}

func ExtractRequestMeta(ctx context.Context) (*RequestMeta, error) {
	return extractRequestMeta(ctx, validProjectExtractor)
}

// extractRequestMeta is a shared implementation that tries to read metadata
// from the incoming context first, then the outgoing context. It logs warnings for
// failures and returns the first successful result. If both attempts fail, the final
// error is returned. It avoids calling the getter with a nil MD.
func extractRequestMeta(ctx context.Context, extractor func(grpcmetadata.MD) (*RequestMeta, error)) (*RequestMeta, error) {
	var errs []string

	if md, ok := grpcmetadata.FromIncomingContext(ctx); ok && md != nil {
		out, err := extractor(md)
		if err == nil {
			return out, nil
		}
		tilog.Warn(ctx, "extract original request from incoming context failed", zap.Error(err))
		errs = append(errs, fmt.Sprintf("incoming: %v", err))
	}

	if md, ok := grpcmetadata.FromOutgoingContext(ctx); ok && md != nil {
		out, err := extractor(md)
		if err == nil {
			return out, nil
		}
		tilog.Warn(ctx, "extract original request from outgoing context failed", zap.Error(err))
		errs = append(errs, fmt.Sprintf("outgoing: %v", err))
	}

	if len(errs) > 0 {
		return nil, fmt.Errorf("grpc metadata parse failed (%s)", strings.Join(errs, "; "))
	}

	return nil, errors.New("grpc metadata not found in incoming or outgoing context")
}

func validProjectExtractor(md grpcmetadata.MD) (*RequestMeta, error) {
	if md == nil {
		return nil, errors.New("not found md from grpc metadata")
	}

	reqMeta := extract(md)

	if reqMeta.OrgID == 0 || reqMeta.UserID == "" {
		return nil, errors.New("cannot get org id and user id from the grpc request")
	}

	if reqMeta.NoProjectRequired {
		log.Debug("no project required in grpc metadata", zap.Any("reqMeta", reqMeta))
	} else {
		if len(reqMeta.ValidProjectList) == 0 || reqMeta.DefaultProjectID == 0 {
			return nil, errors.New("cannot get project id from the grpc request, or valid projects less than 1")
		}

		if !lo.Contains(reqMeta.ValidProjectList, reqMeta.DefaultProjectID) {
			return nil, errors.New("default project id is not in the valid project list")
		}
	}

	return reqMeta, nil
}

func noErrorExtractor(md grpcmetadata.MD) (*RequestMeta, error) {
	if md == nil {
		return nil, errors.New("not found md from grpc metadata")
	}

	reqMeta := extract(md)

	if reqMeta.OrgID == 0 && reqMeta.DefaultProjectID == 0 && reqMeta.ClusterID == 0 && reqMeta.ResourceID == "" {
		return nil, errors.New("missing original request meta")
	}

	return reqMeta, nil
}

func extract(md grpcmetadata.MD) *RequestMeta {
	reqMeta := &RequestMeta{ValidProjectList: make([]uint64, 0)}

	// Get org id from grpc metadata
	if ids, ok := md[OrgIDHeaderKey]; ok {
		if len(ids) == 1 {
			if id, err := strconv.ParseUint(ids[0], 10, 64); err == nil {
				reqMeta.OrgID = id
			}
		}
	}

	// Get project id from grpc metadata
	if ids, ok := md[DefaultProjectIDHeaderKey]; ok {
		if len(ids) == 1 {
			if id, err := strconv.ParseUint(ids[0], 10, 64); err == nil {
				reqMeta.DefaultProjectID = id
			}
		}
	}

	// Get user id from grpc metadata
	if ids, ok := md[UserIDHeaderKey]; ok {
		if len(ids) == 1 {
			reqMeta.UserID = ids[0]
		}
	}

	// Get user email
	if emails, ok := md[UserEmailHeaderKey]; ok {
		if len(emails) == 1 {
			reqMeta.Email = emails[0]
		}
	}

	// Get api key
	if keys, ok := md[ApiKeyHeaderKey]; ok {
		if len(keys) == 1 {
			reqMeta.ApiPublicKey = keys[0]
		}
	}
	// Get api key id
	if keyIds, ok := md[ApiKeyHeaderKeyId]; ok {
		if len(keyIds) == 1 {
			if id, err := strconv.ParseUint(keyIds[0], 10, 64); err == nil {
				reqMeta.ApiPublicKeyId = id
			}
		}
	}

	// Get access token
	if tokens, ok := md[AccessTokenKey]; ok {
		if len(tokens) == 1 {
			reqMeta.AccessToken = tokens[0]
		}
	}

	// Get Cluster id
	if ids, ok := md[ClusterIDHeaderKey]; ok {
		if len(ids) == 1 {
			if id, err := strconv.ParseUint(ids[0], 10, 64); err == nil {
				reqMeta.ClusterID = id
			}
		}
	}

	if validProjects, ok := md[ProjectListHeaderKey]; ok {
		if len(validProjects) == 1 {
			validProjects[0] = strings.TrimPrefix(validProjects[0], "[")
			validProjects[0] = strings.TrimSuffix(validProjects[0], "]")
			validProjects[0] = strings.TrimSpace(validProjects[0])

			idStrs := strings.SplitSeq(validProjects[0], ",")
			for idStr := range idStrs {
				id, err := strconv.ParseUint(idStr, 10, 64)
				if err != nil {
					continue
				}
				reqMeta.ValidProjectList = append(reqMeta.ValidProjectList, id)
			}
		}
	}

	if checkPermission, ok := md[IsCheckPermissionKey]; ok {
		if len(checkPermission) == 1 {
			isCheck, err := strconv.ParseBool(checkPermission[0])
			if err != nil {
				reqMeta.IsCheckPermission = lo.ToPtr(false)
			} else {
				reqMeta.IsCheckPermission = lo.ToPtr(isCheck)
			}
		}
	}

	if resourceID, ok := md[ResourceIDKey]; ok {
		if len(resourceID) == 1 {
			reqMeta.ResourceID = resourceID[0]
		}
	}

	if ignoreCustomStatusID, ok := md[IgnoreCustomStatusIDKey]; ok {
		if len(ignoreCustomStatusID) == 1 {
			if id, err := strconv.ParseUint(ignoreCustomStatusID[0], 10, 64); err == nil {
				reqMeta.IgnoreCustomStatusID = id
			}
		}
	}

	if noProjectRequired, ok := md[NoProjectRequiredKey]; ok {
		if len(noProjectRequired) == 1 && noProjectRequired[0] == "true" {
			reqMeta.NoProjectRequired = true
		}
	}

	return reqMeta
}
