// Package adminapigrpc add by blacknc <blacknc@163.com> at 2024/01/03 10:30:07
package adminapigrpc

import (
	"context"
	"dataservices/commons/constants"
	"dataservices/commons/ginparser"
	"dataservices/commons/pagetoken"
	adminapiv1beta1 "dataservices/idl/pbgen/proto/v1beta1"
	dto "dataservices/internal/dto/dataapi"
	dto2 "dataservices/internal/dto/dataapi_gateway"
	"dataservices/internal/entity"
	service "dataservices/internal/service/central"
	"fmt"
	"strconv"

	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
	"github.com/tidbcloud/tiinf/errs"
	"google.golang.org/protobuf/types/known/emptypb"
)

type EndpointsService struct {
	adminapiv1beta1.UnimplementedEndpointsServiceServer
}

func (s *EndpointsService) ListEndpoints(ctx context.Context, req *adminapiv1beta1.ListEndpointsRequest) (*adminapiv1beta1.ListEndpointsResponse, error) {
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}

	dataapp, err := service.GetDataApp(ctx, innerAppId)
	if err != nil {
		return nil, err
	}

	token := pagetoken.PageToken[uint64]{}
	if req.PageToken != "" {
		if err := token.DecodeFromString(req.PageToken); err != nil {
			return nil, errs.NewBadRequest("invalid page token", err)
		}
	}
	pageSize := req.PageSize
	if req.PageSize == 0 {
		pageSize = constants.AdminApiDefaultPageSize
	} else if req.PageSize < 0 || req.PageSize > constants.AdminApiDefaultPageSize {
		return nil, errs.NewBadRequest(fmt.Sprintf("invalid page size, the effective range is: [0,%d]", constants.AdminApiDefaultPageSize), nil)
	}

	endpointList, err := service.ListEndpointsWithPreID(ctx, innerAppId, token.Prev, int(pageSize)+1)
	if err != nil {
		return nil, err
	}

	resp := new(adminapiv1beta1.ListEndpointsResponse)
	if len(endpointList) == int(pageSize)+1 {
		endpointList = endpointList[:pageSize]
		token.Prev = endpointList[pageSize-1].ID
		resp.NextPageToken, _ = token.EncodeToString()
	}

	if err := service.LoadEndpointPublishID(ctx, endpointList...); err != nil {
		return nil, err
	}

	for _, endpoint := range endpointList {
		resp.Endpoints = append(resp.Endpoints, convertEndpointToProtoFormat(dataapp, endpoint))
	}
	return resp, nil
}

func (s *EndpointsService) CreateEndpoint(ctx context.Context, req *adminapiv1beta1.CreateEndpointRequest) (*adminapiv1beta1.Endpoint, error) {
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}

	_, createdBy, err := ginparser.GetCreatedByFromGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("invalid request public key", err)
	}

	dataapp, err := service.GetDataApp(ctx, innerAppId)
	if err != nil {
		return nil, err
	}
	if dataapp.AppType == entity.DataAppTypeChat2Query {
		return nil, errs.NewBadRequest("Creating endpoints under the chat2query app is not allowed", nil)
	}

	var clusterID uint64
	if req.Endpoint.ClusterId != "" {
		clusterID, err = strconv.ParseUint(req.Endpoint.ClusterId, 10, 64)
		if err != nil {
			return nil, errs.NewBadRequest("Invalid cluster id", err)
		}
	}

	if clusterID > 0 {
		if err := service.CheckDataAppHasCluster(ctx, innerAppId, clusterID); err != nil {
			return nil, err
		}
	}

	settings := buildEndpointSettingsFromProtoFormat(req.Endpoint.Settings)
	params := buildEndpointParamsFromProtoFormat(req.Endpoint.Params)
	endpoint, err := service.SaveEndpoint(ctx, innerAppId, entity.CreateEndpointParam{
		Name:        req.Endpoint.DisplayName,
		Description: req.Endpoint.Description,
		Endpoint:    req.Endpoint.Path,
		Method:      req.Endpoint.Method,
		ClusterId:   clusterID,
		SqlTemplate: req.Endpoint.SqlTemplate,
		Settings:    *settings,
		Args:        params,
		Tag:         req.Endpoint.Tag,
		Batch:       req.Endpoint.BatchOperation,
	}, createdBy, nil)
	if err != nil {
		return nil, err
	}

	return convertEndpointToProtoFormat(dataapp, endpoint), nil
}

func (s *EndpointsService) GetEndpoint(ctx context.Context, req *adminapiv1beta1.GetEndpointRequest) (*adminapiv1beta1.Endpoint, error) {
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}
	params := ginparser.ParseAdminApiPathParameters(ctx, req.Name)
	if params.EndpointId == 0 {
		return nil, errs.NewBadRequest("invalid endpoint id", nil)
	}
	dataapp, err := service.GetDataApp(ctx, innerAppId)
	if err != nil {
		return nil, err
	}
	endpoint, err := service.GetEndpoint(ctx, params.EndpointId)
	if err != nil {
		return nil, err
	}
	if err := service.LoadEndpointPublishID(ctx, endpoint); err != nil {
		return nil, err
	}
	return convertEndpointToProtoFormat(dataapp, endpoint), nil
}

func (s *EndpointsService) UpdateEndpoint(ctx context.Context, req *adminapiv1beta1.UpdateEndpointRequest) (*adminapiv1beta1.Endpoint, error) {
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}

	params := ginparser.ParseAdminApiPathParameters(ctx, req.Endpoint.Name)
	if params.EndpointId == 0 {
		return nil, errs.NewBadRequest("invalid endpoint id", nil)
	}

	_, createdBy, err := ginparser.GetCreatedByFromGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("invalid request public key", err)
	}

	dataapp, err := service.GetDataApp(ctx, innerAppId)
	if err != nil {
		return nil, err
	}

	var clusterID uint64
	if req.Endpoint.ClusterId != "" {
		clusterID, err = strconv.ParseUint(req.Endpoint.ClusterId, 10, 64)
		if err != nil {
			return nil, errs.NewBadRequest("Invalid cluster id", err)
		}
	}

	if clusterID > 0 {
		if err := service.CheckDataAppHasCluster(ctx, innerAppId, clusterID); err != nil {
			return nil, err
		}
	}

	var allowedKeys map[string]any
	endpoint, err := service.GetEndpoint(ctx, params.EndpointId)
	if err != nil {
		return nil, err
	} else if endpoint.IsSystemDataEndpoint() || endpoint.IsChat2QueryEndpoint() {
		allowedKeys = map[string]any{"settings": true}
	}

	endpointSettings := buildEndpointSettingsFromProtoFormat(req.Endpoint.Settings)
	endpointParams := buildEndpointParamsFromProtoFormat(req.Endpoint.Params)
	_, err = service.SaveEndpoint(ctx, innerAppId, entity.CreateEndpointParam{
		EndpointID:  params.EndpointId,
		Name:        req.Endpoint.DisplayName,
		Description: req.Endpoint.Description,
		Endpoint:    req.Endpoint.Path,
		Method:      req.Endpoint.Method,
		ClusterId:   clusterID,
		SqlTemplate: req.Endpoint.SqlTemplate,
		Settings:    *endpointSettings,
		Args:        endpointParams,
		Tag:         req.Endpoint.Tag,
		Batch:       req.Endpoint.BatchOperation,
	}, createdBy, allowedKeys)
	if err != nil {
		return nil, err
	}

	endpoint, err = service.GetEndpoint(ctx, params.EndpointId)
	if err != nil {
		return nil, err
	}

	return convertEndpointToProtoFormat(dataapp, endpoint), nil
}

func (s *EndpointsService) DeleteEndpoint(ctx context.Context, req *adminapiv1beta1.DeleteEndpointRequest) (*emptypb.Empty, error) {
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}
	params := ginparser.ParseAdminApiPathParameters(ctx, req.Name)
	if params.EndpointId == 0 {
		return nil, errs.NewBadRequest("invalid endpoint id", nil)
	}
	_, createdBy, err := ginparser.GetCreatedByFromGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("invalid request public key", err)
	}
	err = service.DeleteEndpoint(ctx, innerAppId, params.EndpointId, createdBy)
	if err != nil {
		return nil, err
	}
	return new(emptypb.Empty), nil
}

func (s *EndpointsService) TestEndpoint(ctx context.Context, req *adminapiv1beta1.TestEndpointRequest) (*adminapiv1beta1.TestEndpointResponse, error) {
	innerAppId, err := ginparser.GetRequestInnerAppIdGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("get innerAppId failed", err)
	}

	params := ginparser.ParseAdminApiPathParameters(ctx, req.Name)
	if params.EndpointId == 0 {
		return nil, errs.NewBadRequest("invalid endpoint id", nil)
	}

	_type, createdBy, err := ginparser.GetCreatedByFromGrpc(ctx)
	if err != nil {
		return nil, errs.NewBadRequest("invalid request public key", err)
	}

	dataapp, err := service.GetDataApp(ctx, innerAppId)
	if err != nil {
		return nil, err
	}
	var argsArr [][]entity.TestEndpointArgs
	for _, arg := range req.Args {
		var args []entity.TestEndpointArgs
		for k, v := range arg.Items {
			args = append(args, entity.TestEndpointArgs{
				Name:  k,
				Value: v,
			})
		}
		argsArr = append(argsArr, args)
	}
	var credType = dto2.TypeCredentialApikey
	if _type == dataapi.CrtType_EMAIL {
		credType = dto2.TypeCredentialEmail
	}

	res, err := service.PublishTest(ctx, entity.TestEndpointParamV2{
		Args:       argsArr,
		ProjectId:  dataapp.ProjectId,
		AppId:      innerAppId,
		EndpointId: params.EndpointId,
		CredType:   credType,
		CredKey:    createdBy,
		Operator:   createdBy,
	})
	if err != nil {
		return nil, err
	}
	return convertEndpointResultToProtoFormat(res), nil
}

func convertEndpointResultToProtoFormat(res *dto.EndpointResult) *adminapiv1beta1.TestEndpointResponse {
	var columns []*adminapiv1beta1.TestEndpointResponse_Column
	var rows []*adminapiv1beta1.TestEndpointResponse_Row
	for _, column := range res.Data.Columns {
		columns = append(columns, &adminapiv1beta1.TestEndpointResponse_Column{
			Col:      column.Col,
			Nullable: column.Nullable,
			DataType: column.DataType,
		})
	}
	for _, row := range res.Data.Rows {
		rows = append(rows, &adminapiv1beta1.TestEndpointResponse_Row{Items: row})
	}
	result := &adminapiv1beta1.TestEndpointResponse{
		Type: res.Type,
		Data: &adminapiv1beta1.TestEndpointResponse_Data{
			Columns: columns,
			Rows:    rows,
			Result: &adminapiv1beta1.TestEndpointResponse_Result{
				Code: int32(res.Data.Result.Code),
				// result message
				Message: res.Data.Result.Message,
				// request start timestamp
				StartMs: res.Data.Result.StartMs,
				// request end timestamp
				EndMs: res.Data.Result.EndMs,
				// request total latency
				Latency: res.Data.Result.Latency,
				// request total count
				RowCount: int32(res.Data.Result.RowCount),
				// request affected row count
				RowAffect: int32(res.Data.Result.RowAffect),
				// request max row limit
				Limit: int32(res.Data.Result.Limit),
			},
		},
	}
	return result
}

func convertEndpointToProtoFormat(dataapp *entity.DataApp, endpoint *entity.Endpoint) *adminapiv1beta1.Endpoint {
	var (
		status    string
		clusterID string
	)
	if endpoint.PublishID > 0 {
		status = "deployed"
	} else {
		status = "draft"
	}
	if !endpoint.IsSystemDataEndpoint() && endpoint.ClusterId > 0 {
		clusterID = strconv.FormatUint(endpoint.ClusterId, 10)
	}
	return &adminapiv1beta1.Endpoint{
		// Unique name of endpoint, format: dataApps/{data_app}/endpoints/{endpoint}
		Name: fmt.Sprintf("dataApps/%s/endpoints/%d", dataapp.ExternalAppID, endpoint.ID),
		// Endpoint deployment status: deployed/draft
		Status: status,
		// Endpoint display name
		DisplayName: endpoint.Name,
		// Endpoint description
		Description: endpoint.Description,
		// Http Path, example: /v1/endpoint1
		Path: endpoint.Endpoint,
		// Http method, example: GET/POST/DELETE
		Method:    endpoint.Method,
		ClusterId: clusterID,
		// Endpoint parameters
		Params:   convertEndpointParamsToProtoFormat(endpoint.GetParams()),
		Settings: convertEndpointSettingsToProtoFormat(endpoint.GetSettings()),
		Tag:      endpoint.Tag,
		// Is support batch operation
		BatchOperation: endpoint.IsBatch(),
		SqlTemplate:    endpoint.SqlTemplate,
		Type:           entity.EndpointInnerTypeToGithubType(endpoint.Type),
		// Result format type
		ReturnType: "json",
		CreatedAt:  endpoint.CreatedAt.FormatToStr(),
		UpdatedAt:  endpoint.UpdatedAt.FormatToStr(),
	}
}

func convertEndpointParamsToProtoFormat(params []entity.EndpointArgs) []*adminapiv1beta1.EndpointParams {
	if len(params) == 0 {
		return nil
	}
	result := make([]*adminapiv1beta1.EndpointParams, 0, len(params))
	for _, param := range params {
		result = append(result, &adminapiv1beta1.EndpointParams{
			Name:         param.Name,
			Type:         param.Type,
			Required:     param.Required == 1,
			DefaultValue: param.Default,
			Description:  param.Description,
			ItemType:     param.ItemType,
			Enum:         param.Enum,
		})
	}
	return result
}
func buildEndpointParamsFromProtoFormat(params []*adminapiv1beta1.EndpointParams) []entity.EndpointArgs {
	result := make([]entity.EndpointArgs, 0, len(params))
	for _, param := range params {
		result = append(result, entity.EndpointArgs{
			Name:        param.Name,
			Type:        param.Type,
			ItemType:    param.ItemType,
			Default:     param.DefaultValue,
			Description: param.Description,
			Enum:        param.Enum,
		})
		if param.Required {
			result[len(result)-1].Required = 1
		}
	}
	return result
}

func convertEndpointSettingsToProtoFormat(settings *entity.EndpointSettings) *adminapiv1beta1.EndpointSettings {
	if settings == nil {
		return nil
	}
	return &adminapiv1beta1.EndpointSettings{
		Timeout:           int32(settings.TimeOut),
		RowLimit:          int32(settings.RowsLimit),
		PaginationEnabled: settings.EnablePagination == 1,
		CacheEnabled:      settings.IsCacheEnabled(),
		CacheTtl:          int32(settings.CacheTTL),
	}
}
func buildEndpointSettingsFromProtoFormat(settings *adminapiv1beta1.EndpointSettings) *entity.EndpointSettings {
	result := &entity.EndpointSettings{}
	if settings == nil {
		return result
	}
	result.TimeOut = int(settings.Timeout)
	result.RowsLimit = int(settings.RowLimit)
	result.CacheTTL = int(settings.CacheTtl)
	if settings.PaginationEnabled {
		result.EnablePagination = 1
	}
	if settings.CacheEnabled {
		result.CacheEnabled = 1
	}
	return result
}
