package ginparser

import (
	"bytes"
	"context"
	"dataservices/commons/constants"
	"dataservices/commons/grpc"
	"dataservices/internal/entity"
	"encoding/json"
	"io"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"

	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/pkgv2/auth/kong"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
	"google.golang.org/grpc/metadata"
)

const (
	APP_RESOURCE_PLURAL        = "dataApps"
	APIKEY_RESOURCE_PLURAL     = "apiKeys"
	DATASOURCE_RESOURCE_PLURAL = "dataSources"
	ENDPOINT_PLURAL            = "endpoints"
	DEPLOYMENT_PLURAL          = "deployments"
)

type AdminApiCommonParams struct {
	ExternalAppId string
	ApiKeyId      uint64
	ClusterId     uint64
	EndpointId    uint64
	DeploymentId  uint64
}

// Admin API path format is like `/v1beta1/{parent=apps/*}/apiKeys`.
// We need to parse resource id from path directly instead of depending on gin's standard method.
// The input path could be `/v1beta1/apps/123/apiKeys` or `/apps/123`, then this method parse `123` from the input
func ParseAdminApiPathParameters(ctx context.Context, path string) *AdminApiCommonParams {
	temp := strings.Split(path, "/")
	parts := make([]string, 0)
	for _, t := range temp {
		if t == "" {
			continue
		}
		parts = append(parts, t)
	}
	result := new(AdminApiCommonParams)
	for i := 0; i < len(parts); i++ {
		part := strings.Split(parts[i], "?")
		l := part[0]
		if l == APP_RESOURCE_PLURAL && i < len(parts)-1 {
			result.ExternalAppId = parts[i+1]
		} else if l == APP_RESOURCE_PLURAL && i == len(parts)-1 {
			result.ExternalAppId = entity.DataAppAdminAPICreateOrList
		} else if l == APIKEY_RESOURCE_PLURAL && i < len(parts)-1 {
			id, err := strconv.ParseUint(parts[i+1], 10, 64)
			if err != nil {
				tilog.Error(ctx, "parse apiKeyId failed", zap.Error(err))
				continue
			}
			result.ApiKeyId = id
		} else if parts[i] == DATASOURCE_RESOURCE_PLURAL && i < len(parts)-1 {
			id, err := strconv.ParseUint(parts[i+1], 10, 64)
			if err != nil {
				tilog.Error(ctx, "parse dataSourceId failed", zap.Error(err))
				continue
			}
			result.ClusterId = id

		} else if parts[i] == ENDPOINT_PLURAL && i < len(parts)-1 {
			id, err := strconv.ParseUint(parts[i+1], 10, 64)
			if err != nil {
				tilog.Error(ctx, "parse endpoint id failed", zap.Error(err))
				continue
			}
			result.EndpointId = id
		} else if parts[i] == DEPLOYMENT_PLURAL && i < len(parts)-1 {
			id, err := strconv.ParseUint(parts[i+1], 10, 64)
			if err != nil {
				tilog.Error(ctx, "parse deployment id failed", zap.Error(err))
				continue
			}
			result.DeploymentId = id
		}
	}
	return result
}

type Org4Middleware struct {
	//OrgId string `json:"org_id"`
	ProjectId string `json:"projectId"`
}

func ParseOrgIdByCreateDataAppOrListDataApp(c *gin.Context, ctx context.Context) (string, error) {
	if c.Request.Method == http.MethodPost {
		var buf bytes.Buffer
		tee := io.TeeReader(c.Request.Body, &buf)
		body, err := ioutil.ReadAll(tee)
		if err != nil {
			tilog.Error(ctx, "ParseOrgIdByCreateDataAppOrListDataApp failed. Read body failed", zap.Error(err))
			return "", err
		}
		var dataapp Org4Middleware
		err = json.Unmarshal(body, &dataapp)
		if err != nil {
			tilog.Error(ctx, "ParseOrgIdByCreateDataAppOrListDataApp failed. Error occurred during unmarshaling. Error: %s", zap.Error(err))
			return "", err
		}
		orgId, err := getOrgIdFromProjectId(ctx, dataapp.ProjectId)
		if err != nil {
			return "", err
		}
		// reset body
		c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
		return orgId, nil
	} else if c.Request.Method == http.MethodGet {
		projectIdStr := c.Query("projectId")
		orgId, err := getOrgIdFromProjectId(ctx, projectIdStr)
		if err != nil {
			return "", err
		}
		return orgId, nil
	}
	return "", errs.NewInternalUnknown("get org id failed", nil)
}

func GetRequestPublicKeyGin(c *gin.Context) (string, error) {
	authType := c.GetHeader(constants.RequestHeaderAuthTypeKey)
	if authType != string(kong.AuthTypeOpenAPI) {
		return "", nil
	}
	authContent := c.GetHeader(constants.RequestHeaderAuthContentKey)
	var result kong.OpenAPIAuthResult
	err := json.Unmarshal([]byte(authContent), &result)
	if err != nil {
		return "", err
	}
	return result.PublicKey, nil
}

func GetCreatedByFromGrpc(ctx context.Context) (dataapi.CrtType, string, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return 0, "", errs.NewInternalUnknown("get metadata from grpc context failed", nil)
	}

	idInfo, ok := md[constants.RequestHeaderIdentityInfoKey]
	if !ok {
		return 0, "", errs.NewInternalUnknown("get identity info from grpc context failed", nil)
	}

	var info auth.AuthnIdentityInfo
	err := json.Unmarshal([]byte(idInfo[0]), &info)
	if err != nil {
		return 0, "", errs.NewInternalUnknown("unmarshal identity info failed", err)
	}

	switch info.IdentityType {
	case auth.AuthnIdentityTypeAccessToken:
		return dataapi.CrtType_EMAIL, info.AccessTokenRsp.AccessTokenIdentity.UserEmail, nil
	case auth.AuthnIdentityTypeApiKeyAK:
		return dataapi.CrtType_APIKEY, info.ApiKeyRsp.ApiKey.AccessKey, nil
	case auth.AuthnIdentityTypeUserToken:
		return dataapi.CrtType_EMAIL, info.UserTokenRsp.User.Email, nil
	default:
		return 0, "", errs.NewBadRequest("invalid identity type", nil)
	}
}

func GetRequestPublicKeyGrpc(ctx context.Context) (string, error) {
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		if authContent, ok := md[constants.RequestHeaderAuthContentKey]; ok {
			var result kong.OpenAPIAuthResult
			err := json.Unmarshal([]byte(authContent[0]), &result)
			if err != nil {
				return "", err
			}
			return result.PublicKey, nil
		}
	}
	return "", nil
}

func GetRequestInnerAppIdGrpc(ctx context.Context) (uint64, error) {
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		if idStr, ok := md[constants.RequestHeaderInnerAppId]; ok {
			return strconv.ParseUint(idStr[0], 10, 64)
		}
	}
	return 0, errs.NewInternalUnknown("get inner data app id failed", nil)
}

func GetRequestInnerOrgIdGrpc(ctx context.Context) (string, error) {
	if md, ok := metadata.FromIncomingContext(ctx); ok {
		if idStr, ok := md[constants.RequestHeaderInnerOrgId]; ok {
			return idStr[0], nil
		}
	}
	return "", errs.NewInternalUnknown("get inner org id failed", nil)
}

func getOrgIdFromProjectId(ctx context.Context, projectIdStr string) (string, error) {
	projectId, err := strconv.ParseUint(projectIdStr, 10, 64)
	if err != nil {
		tilog.Error(ctx, "ParseOrgIdByCreateDataAppOrListDataApp failed. ProjectID parse failed", zap.Error(err))
		return "", err
	}
	project, err := grpc.GetProjectByID(ctx, &account.GetProjectByIDRequest{Id: projectId})
	if err != nil {
		tilog.Error(ctx, "ParseOrgIdByCreateDataAppOrListDataApp failed. Get project grpc failed", zap.Error(err))
		return "", err
	}
	orgId := strconv.FormatUint(project.Project.TenantId, 10)
	return orgId, nil
}
