package dataapi

import (
	"context"
	"dataservices/commons/constants"
	"dataservices/commons/contextvar"
	"dataservices/commons/logger"
	"dataservices/commons/utils"
	"dataservices/dbhubsdk"
	dto "dataservices/internal/dto/dataapi"
	"dataservices/internal/entity"
	centralSvc "dataservices/internal/service/central"
	service "dataservices/internal/service/dataapi"
	"strconv"

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

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type dataAPIService struct {
	dataapi.UnsafeDataAPIServiceServer
}

func NewDataAPIServiceService() dataapi.DataAPIServiceServer {
	return &dataAPIService{}
}

var (
	apikeySource = map[dataapi.APIKeySource]string{
		dataapi.APIKeySource_DataAPI_Source_Chat2Query_Console: entity.DataAPISourceChat2QueryConsole,
		dataapi.APIKeySource_DataAPI_Source_DataAPI_Console:    entity.DataAPISourceDataAPIConsole,
		dataapi.APIKeySource_DataAPI_Source_Vercel_Console:     entity.DataAPISourceVercelConsole,
	}

	stmtTypeMap = map[dbhubsdk.StmtType]dataapi.StmtType{
		dbhubsdk.SELECT:       dataapi.StmtType_Select,
		dbhubsdk.INSERT:       dataapi.StmtType_Insert,
		dbhubsdk.UPDATE:       dataapi.StmtType_Update,
		dbhubsdk.DELETE:       dataapi.StmtType_Delete,
		dbhubsdk.DROPDATABASE: dataapi.StmtType_DropDatabase,
		dbhubsdk.DROPTABLE:    dataapi.StmtType_DropTable,
		dbhubsdk.DROPINDEX:    dataapi.StmtType_DropIndex,
		dbhubsdk.ALTERTABLE:   dataapi.StmtType_AlterTable,
		dbhubsdk.SHOW:         dataapi.StmtType_Show,
		dbhubsdk.EXPLAIN:      dataapi.StmtType_Explain,
		dbhubsdk.USE:          dataapi.StmtType_Use,
		dbhubsdk.OTHER:        dataapi.StmtType_Other,
	}
)

func (c *dataAPIService) GetClusterMeta(ctx context.Context, in *dataapi.GetClusterMetaReq) (*dataapi.GetClusterMetaResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logRequest(ctx, in, "GetClusterMeta begin")
	res, err := service.GetClusterMeta(ctx, in)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-GetClusterMeta failed.")
		return nil, err
	}
	resp := dto.ConvertGetClusterMetaResp(res)
	return resp, nil

}

func (c *dataAPIService) GetDatabaseMeta(ctx context.Context, in *dataapi.GetDatabaseMetaReq) (*dataapi.GetDatabaseMetaResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logRequest(ctx, in, "GetDatabaseMeta begin")
	res, err := service.GetDatabaseMeta(ctx, in)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-GetDatabaseMeta failed.")
		return nil, err
	}
	resp := dto.ConvertGetDatabaseMetaResp(res, in.Database)
	return resp, nil

}
func (c *dataAPIService) ExecSqlWithSession(ctx context.Context, in *dataapi.BasicEndpointReq) (*dataapi.ExecSqlResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logRequest(ctx, in, "ExecSqlWithSession begin")
	res, err := service.ExecSql(ctx, in)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-ExecSqlWithSession internal server failed.")
		return nil, err
	}
	if res.Code != 0 {
		logger.Error(ctx, "DS-Central-Dataapi-grpc-ExecSqlWithSession db execution failed.",
			zap.String("sql", in.Sql),
			zap.Strings("args", in.Args),
			zap.String("errcode", strconv.FormatUint(uint64(res.Code), 10)),
			zap.String("errmsg", res.Message),
		)
	} else {
		logger.Info(ctx, "ExecSqlWithSession succeed.",
			zap.String("sql", in.Sql),
			zap.Strings("args", in.Args))
	}
	resp := dto.ConvertExecSqlResp(res)
	return resp, nil
}

func (c *dataAPIService) ExecSqlDirectly(ctx context.Context, in *dataapi.BasicEndpointReq) (*dataapi.ExecSqlResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logRequest(ctx, in, "ExecSqlDirectly begin")
	res, err := service.ExecSql(ctx, in)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-ExecSqlDirectly failed.")
		return nil, err
	}
	logger.Info(ctx, "ExecSqlDirectly succeed.",
		zap.String("sql", in.Sql),
		zap.Strings("args", in.Args))
	resp := dto.ConvertExecSqlResp(res)
	return resp, nil
}

func (c *dataAPIService) CreateSession(ctx context.Context, in *dataapi.CreateSessionReq) (*dataapi.CreateSessionResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logRequest(ctx, in, "CreateSession begin")
	sessionId, err := service.CreateSession(ctx, in)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-createSession failed.")
		return nil, errs.NewBadRequest("createSession failed, err:"+err.Error(), err)
	}
	logger.Info(ctx, "createSession succeed.", zap.Uint64("expire_sec", in.ExpireSec))
	resp := &dataapi.CreateSessionResp{
		SessionId: sessionId,
	}
	return resp, nil
}

func (c *dataAPIService) CloseSession(ctx context.Context, in *dataapi.CloseSessionReq) (*dataapi.CloseSessionResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logRequest(ctx, in, "CloseSession begin")
	err := service.CloseSession(ctx, in)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-closeSession failed.")
		return nil, errs.NewBadRequest("closeSession failed.", err)
	}
	logger.Info(ctx, "closeSession succeed.")
	resp := &dataapi.CloseSessionResp{}
	return resp, nil
}

func (c *dataAPIService) CreateAPIKey(ctx context.Context, in *dataapi.CreateAPIKeyReq) (*dataapi.CreateAPIKeyResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "CreateAPIKey begin", zap.Any("request", in))
	apikey, err := centralSvc.RbacCreateApikey(ctx, in.DataappId, in.Description, constants.ApikeyRoleOwner, apikeySource[in.Source], in.CreatedBy, constants.ApikeyDefaultRateLimitRpm, account.OperatorType_OPERATOR_TYPE_USER, 0)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-CreateAPIKey failed.")
		return nil, errs.NewBadRequest("CreateAPIKey failed.", err)
	}
	logger.Info(ctx, "CreateAPIKey succeed.", zap.Any("apikey", apikey))
	resp := &dataapi.CreateAPIKeyResp{
		AccessKey: apikey.AccessKey,
		SecretKey: apikey.SecretKey,
		Source:    apikey.Source,
		Name:      apikey.Name,
	}
	return resp, nil
}

func (c *dataAPIService) DeleteAPIKey(ctx context.Context, in *dataapi.DeleteAPIKeyReq) (*dataapi.DeleteAPIKeyResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "DeleteAPIKey begin", zap.Any("request", in))
	err := centralSvc.RbacDeleteApikey(ctx, in.DataappId, in.ApikeyId, constants.ApikeyRoleOwner, in.CreatedBy, account.OperatorType_OPERATOR_TYPE_USER)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-DeleteAPIKey failed.")
		return nil, errs.NewBadRequest("DeleteAPIKey failed.", err)
	}
	logger.Info(ctx, "DeleteAPIKey succeed.")
	return new(dataapi.DeleteAPIKeyResp), nil
}

func (c *dataAPIService) ListAPIKey(ctx context.Context, in *dataapi.ListAPIKeyReq) (*dataapi.ListAPIKeyResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "ListAPIKey begin", zap.Any("request", in))
	listApiKeysResp, err := centralSvc.RbacListApiKeys(ctx, in.DataappId, false)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-ListAPIKey failed.")
		return nil, errs.NewBadRequest("ListAPIKey failed.", err)
	}
	resp := dto.ConvertListAPIKeysResp(listApiKeysResp)
	logger.Info(ctx, "ListAPIKey succeed.", zap.Any("apikeys", listApiKeysResp))
	return resp, nil
}

func (c *dataAPIService) AuthAPIKey(ctx context.Context, in *dataapi.AuthAPIKeyReq) (*dataapi.AuthAPIKeyResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "AuthAPIKey begin", zap.Any("request", in))
	authResp, err := centralSvc.AuthApiKey(ctx, in.DataappExtId, in.ApiKey)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-AuthAPIKey failed.")
		return nil, errs.NewBadRequest("AuthAPIKey failed.", err)
	}
	resp := &dataapi.AuthAPIKeyResp{
		AuthFlag: authResp,
	}
	logger.Info(ctx, "AuthAPIKey succeed.", zap.Any("authResult", authResp))
	return resp, nil
}

func (c *dataAPIService) CreateDataApp(ctx context.Context, in *dataapi.CreateDataAppReq) (*dataapi.CreateDataAppResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "CreateDataApp begin", zap.Any("request", in))
	req := &entity.CreateDataAppParam{AppType: entity.DataAppType(in.AppType)}
	appInfo, endpoints, err := centralSvc.CreateDataApp(ctx, in.OrgId, in.ProjectId, in.CreatedBy, req)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-CreateDataApp failed.")
		return nil, errs.NewBadRequest("CreateDataApp failed.", err)
	}
	logger.Info(ctx, "CreateDataApp succeed", zap.Uint64("app_id", appInfo.ID))
	resp := new(dataapi.CreateDataAppResp)
	resp.AppInfo = dto.ConvertDataAppInfo(appInfo)
	resp.Endpoints = dto.ConvertEndpointInfo(endpoints)
	return resp, nil
}

func (c *dataAPIService) ShowDataApp(ctx context.Context, in *dataapi.ShowDataAppReq) (*dataapi.ShowDataAppResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "ShowDataApp begin", zap.Any("request", in))
	if in.AppExtId == "" && in.AppId == 0 {
		return nil, errs.NewBadRequest("ShowDataApp params illegal.", nil)
	}
	var (
		appInfo *entity.DataApp
		err     error
	)
	if in.AppExtId != "" {
		appInfo, err = centralSvc.GetDataAppByExtId(ctx, in.AppExtId)
	} else {
		appInfo, err = centralSvc.GetDataApp(ctx, in.AppId)
	}
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-ShowDataApp failed.", zap.Uint64("app_id", in.AppId))
		return nil, err
	}
	resp := new(dataapi.ShowDataAppResp)
	resp.AppInfo = dto.ConvertDataAppInfo(appInfo)
	resp.AppInfo.ServiceUrl = centralSvc.GenAPPServiceUrl(appInfo, "", nil)
	return resp, nil
}

func (c *dataAPIService) ListDataApp(ctx context.Context, in *dataapi.ListDataAppReq) (*dataapi.ListDataAppResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "ListDataApp begin", zap.Any("request", in))
	if in.ProjectId == 0 {
		return nil, errs.NewBadRequest("ListDataApp params illegal.", nil)
	}

	appList, err := centralSvc.ListDataApps(ctx, in.ProjectId, in.AppTypes...)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-ListDataApp failed.")
		return nil, errs.NewBadRequest("ListDataApp failed.", err)
	}
	resp := new(dataapi.ListDataAppResp)
	for _, appInfo := range appList {
		resp.AppList = append(resp.AppList, dto.ConvertDataAppInfo(appInfo))
	}
	return resp, nil
}

func (c *dataAPIService) DeleteDataApp(ctx context.Context, in *dataapi.DeleteDataAppReq) (*dataapi.DeleteDataAppResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "DeleteDataApp begin", zap.Any("request", in))
	err := centralSvc.DeleteDataApp(ctx, in.AppId, in.Operator, account.OperatorType_OPERATOR_TYPE_USER)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-DeleteDataApp failed.", zap.Uint64("app_id", in.AppId), zap.String("user", in.Operator))
		return nil, errs.NewBadRequest("DeleteDataApp failed.", err)
	}
	logger.Info(ctx, "DeleteDataApp succeed", zap.Uint64("app_id", in.AppId))
	resp := new(dataapi.DeleteDataAppResp)
	return resp, nil
}

func (c *dataAPIService) ShowAPIKey(ctx context.Context, in *dataapi.ShowAPIKeyReq) (*dataapi.ShowAPIKeyResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "ShowAPIKey begin", zap.Any("request", in))
	apikey, err := centralSvc.ShowApiKey(ctx, in.ApikeyId)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-ShowAPIKey failed.", zap.Uint64("apikey_id", in.ApikeyId))
		return nil, errs.NewBadRequest("ShowAPIKey failed.", err)
	}
	logger.Info(ctx, "ShowAPIKey succeed", zap.Uint64("apikey_id", in.ApikeyId))
	resp := &dataapi.ShowAPIKeyResp{RawKey: apikey}
	return resp, nil
}

func (c *dataAPIService) ParseOneStmt(ctx context.Context, in *dataapi.ParseOneStmtReq) (*dataapi.ParseOneStmtResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "ParseOneStmt begin", zap.Any("request", in))
	if in.Sql == "" {
		logger.Warn(ctx, "DS-Central-Dataapi-grpc-ParseOneStmt sql is required")
		return nil, errs.NewBadRequest("ParseOneStmt sql is required", nil)
	}
	stmtType, err := service.ParseOneStmt(ctx, in.Sql)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-ParseOneStmt failed", zap.String("sql", in.Sql))
		return nil, errs.NewBadRequest("ParseOneStmt failed, err:"+err.Error(), nil)
	}
	resp := new(dataapi.ParseOneStmtResp)
	resp.StmtType = stmtTypeMap[stmtType]
	return resp, nil
}

func (c *dataAPIService) EndpointCodeExample(ctx context.Context, in *dataapi.EndpointCodeExampleReq) (*dataapi.EndpointCodeExampleResp, error) {
	tilog.Info(ctx, "EndpointCodeExample begin", zap.Any("request", in))
	if in.ProjectId == 0 || in.EndpointType == "" {
		return nil, errs.NewBadRequest("Got invalid arguments", nil)
	}

	// list app
	appList, err := centralSvc.ListDataApps(ctx, in.ProjectId, string(entity.DataAppTypeChat2Query))
	if err != nil {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample list dataapps failed", zap.Error(err))
		return nil, err
	}
	if len(appList) == 0 {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample Chat2query dataapp not found by project")
		return nil, errs.NewBadRequest("Chat2query dataapp not found by project", nil)
	}
	// list endpoint
	endpointList, err := centralSvc.ListEndpoints(ctx, appList[0].ID, in.EndpointType)
	if err != nil {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample list endpoints failed", zap.Error(err))
		return nil, err
	}
	if len(endpointList) == 0 {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample Chat2query dataapp endpoint not found by project")
		return nil, errs.NewBadRequest("Chat2query dataapp endpoint not found by project", nil)
	}
	// list apikey
	apikeyList, err := centralSvc.RbacListApiKeys(ctx, appList[0].ID, false)
	if err != nil {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample RbacListApiKeys failed", zap.Error(err))
		return nil, err
	}
	if len(apikeyList) == 0 {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample Chat2query dataapp apikey not found by project")
		return nil, errs.NewBadRequest("Chat2query dataapp apikey not found by project", nil)
	}
	codeExample, err := centralSvc.CodeExample(ctx, endpointList[0].ID, entity.CodeExampleInput{
		ClusterID: strconv.FormatUint(in.ClusterId, 10),
		AK:        apikeyList[0].AccessKey,
		SK:        apikeyList[0].SecretKey,
	})
	if err != nil {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample CodeExample failed", zap.Error(err))
		return nil, err
	}

	res := new(dataapi.EndpointCodeExampleResp)
	if len(codeExample.Prod) == 0 {
		tilog.Error(ctx, "DS-Central-Dataapi-grpc-EndpointCodeExample Chat2query dataapp no deployed endpoint found by project")
		return nil, errs.NewBadRequest("Chat2query dataapp no deployed endpoint found by project", nil)
	}
	res.Code = codeExample.Prod[0].Code
	return res, nil
}

func (c *dataAPIService) GetClusterStatus(ctx context.Context, in *dataapi.GetClusterStatusReq) (*dataapi.GetClusterStatusResp, error) {
	ctx = WithLoggerContext(ctx, in)
	logger.Info(ctx, "GetClusterStatus begin", zap.Any("request", in))
	if len(in.ClusterIds) == 0 {
		return nil, errs.NewBadRequest("Got invalid arguments", nil)
	}
	status, err := centralSvc.GetClusterStatus(ctx, in.ClusterIds)
	if err != nil {
		logger.LogWithError(ctx, err, "DS-Central-Dataapi-grpc-GetClusterStatus failed")
		return nil, err
	}
	return &dataapi.GetClusterStatusResp{
		Status: status,
	}, nil
}

func logRequest(ctx context.Context, req interface{}, msg string) {
	tilog.Info(ctx, msg, logger.Redact("request", req))
}

func WithLoggerContext(ctx context.Context, in any) context.Context {
	l := tilog.WithContextL(ctx)
	if e, ok := in.(interface{ GetOrgId() uint64 }); ok {
		l = l.With(zap.Uint64("orgId", e.GetOrgId()))
	}
	if e, ok := in.(interface{ GetProjectId() uint64 }); ok {
		l = l.With(zap.Uint64("projectId", e.GetProjectId()))
	}
	if e, ok := in.(interface{ GetClusterId() uint64 }); ok {
		l = l.With(zap.Uint64("clusterId", e.GetClusterId()))
	}
	if e, ok := in.(interface{ GetSessionId() string }); ok {
		l = l.With(zap.String("sessionId", e.GetSessionId()))
	}
	if e, ok := in.(interface{ GetSource() string }); ok {
		l = l.With(zap.String("source", e.GetSource()))
	}
	if e, ok := in.(interface{ GetCredential() *dataapi.Credential }); ok {
		l = l.With(
			zap.String("cretType", e.GetCredential().GetType().String()),
			logger.Redact("cretAccount", e.GetCredential().GetAccountId()),
		)
		l = l.With(zap.String("cretKeyMd5WithSalt", utils.Md5WithDefaultSalt(e.GetCredential().GetKey())))
	}
	if e, ok := in.(interface{ GetDataappId() uint64 }); ok {
		l = l.With(zap.Uint64("dataappId", e.GetDataappId()))
	}
	if e, ok := in.(interface{ GetDataappExtId() string }); ok {
		l = l.With(zap.String("dataappExtId", e.GetDataappExtId()))
	}
	if e, ok := in.(interface{ GetCreatedBy() string }); ok {
		l = l.With(logger.Redact("createdBy", e.GetCreatedBy()))
	}
	if e, ok := in.(interface{ GetSource() dataapi.APIKeySource }); ok {
		l = l.With(zap.String("source", e.GetSource().String()))
	}
	if e, ok := in.(interface{ GetApikeyId() uint64 }); ok {
		l = l.With(zap.Uint64("apikeyId", e.GetApikeyId()))
	}
	if e, ok := in.(interface{ GetApiKey() string }); ok {
		l = l.With(logger.Redact("apikey", e.GetApiKey()))
	}
	return contextvar.WithLogger(ctx, l)
}
