package service

import (
	"context"
	"dataservices/commons/grpc"
	"dataservices/commons/http"
	"dataservices/commons/logger"
	"dataservices/dbhubsdk"
	dto "dataservices/internal/dto/dataapi_gateway"
	"regexp"

	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/core-svc/idl/pbgen/proto/core"
	"github.com/tidbcloud/dataservices/idl/pbgen/proto/dataapi"
	"github.com/tidbcloud/tiinf/errs"
)

var emialRegexp = regexp.MustCompile(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`)

func CreateSession(ctx context.Context, in *dataapi.CreateSessionReq) (sessionId string, err error) {

	clusterInfo, err := grpc.GetClusterWithoutPermCheck(ctx, in.ClusterId)
	if err != nil {
		return "", err
	}

	req := &dto.CreateSessionReq{
		ProjectId: in.ProjectId,
		ClusterId: in.ClusterId,
		Database:  in.Database,
		ExpireSec: in.ExpireSec,
	}
	if in.Credential != nil {
		req.Credential.Type = in.Credential.Type.String()
		req.Credential.Key = in.Credential.Key
		req.Credential.AccountID = in.Credential.AccountId
		if in.Credential.Type == dataapi.CrtType_AUTH0 && req.Credential.AccountID == "" {
			resp, err := grpc.GetUserByToken(ctx, &account.GetUserByTokenRequest{
				Token: req.Credential.Key,
			})
			if err != nil {
				return "", err
			}
			req.Credential.AccountID = resp.User.Email
		}
	}
	resp, err := http.CreateSessionWithProvider(ctx, clusterInfo.RegionName, http.ProviderForRegionalHostPrefix(clusterInfo.CloudProviderName, clusterInfo.GetDeployType()), req)
	if err != nil {
		return "", err
	}
	return resp.SessionId, nil
}

func CloseSession(ctx context.Context, in *dataapi.CloseSessionReq) error {

	clusterInfo, err := grpc.GetClusterWithoutPermCheck(ctx, in.ClusterId)
	if err != nil {
		return err
	}

	req := &dto.CloseSessionReq{
		SessionId: in.SessionId,
		ClusterId: in.ClusterId,
		ProjectId: in.ProjectId,
	}
	if in.Credential != nil {
		req.Credential.Type = in.Credential.Type.String()
		req.Credential.Key = in.Credential.Key
		req.Credential.AccountID = in.Credential.AccountId
		if in.Credential.Type == dataapi.CrtType_AUTH0 && req.Credential.AccountID == "" {
			resp, err := grpc.GetUserByToken(ctx, &account.GetUserByTokenRequest{
				Token: req.Credential.Key,
			})
			if err != nil {
				return err
			}
			req.Credential.AccountID = resp.User.Email
		}
	}
	return http.CloseSessionWithProvider(ctx, clusterInfo.RegionName, http.ProviderForRegionalHostPrefix(clusterInfo.CloudProviderName, clusterInfo.GetDeployType()), req)
}

func GetClusterMeta(ctx context.Context, in *dataapi.GetClusterMetaReq) (*dbhubsdk.QueryRes, error) {
	sql := "select table_name, column_name, table_schema from INFORMATION_SCHEMA.COLUMNS where table_schema not in ('INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA', 'mysql')"
	return ExecSql(ctx, &dataapi.BasicEndpointReq{
		ProjectId:  in.ProjectId,
		ClusterId:  in.ClusterId,
		Database:   "INFORMATION_SCHEMA",
		Credential: in.Credential,
		Sql:        sql,
	})
}

func GetDatabaseMeta(ctx context.Context, in *dataapi.GetDatabaseMetaReq) (*dbhubsdk.QueryRes, error) {
	sql := "select table_name, column_name from INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA='" + in.Database + "'"
	return ExecSql(ctx, &dataapi.BasicEndpointReq{
		ProjectId:  in.ProjectId,
		ClusterId:  in.ClusterId,
		Database:   "INFORMATION_SCHEMA",
		Credential: in.Credential,
		Sql:        sql,
	})
}

func ExecSql(ctx context.Context, in *dataapi.BasicEndpointReq) (*dbhubsdk.QueryRes, error) {

	if in.SessionId == "" && (in.ProjectId == 0 || in.ClusterId == 0) {
		return nil, errs.NewBadRequest("exec sql got invalid arguments", nil)
	}

	clusterInfo, err := grpc.GetClusterWithoutPermCheck(ctx, in.ClusterId)
	if err != nil {
		return nil, err
	}

	req := &dto.ExecSqlReq{
		ProjectId:           in.ProjectId,
		ClusterId:           in.ClusterId,
		SessionId:           in.SessionId,
		Database:            in.Database,
		Sql:                 in.Sql,
		LimitRows:           in.LimitRows,
		TimeoutMs:           in.TimeoutMs,
		ReturnAllStmtResult: in.ReturnAllStmtResult,
		Source:              in.Source,
	}
	crtType, crtKey, accountID, err := getCredential(ctx, in, clusterInfo)
	if err != nil {
		logger.Warn(ctx, "first get meta failed! Because serverless not support!")
		in.Credential.Key = "ServerlessNotSupport"
		crtType, crtKey, accountID, err = getCredential(ctx, in, clusterInfo)
		if err != nil {
			return nil, err
		}
	}
	req.Credential.Type = crtType.String()
	req.Credential.Key = crtKey
	req.Credential.AccountID = accountID

	res, err := http.ExecSqlWithProvider(ctx, clusterInfo.RegionName, http.ProviderForRegionalHostPrefix(clusterInfo.CloudProviderName, clusterInfo.GetDeployType()), req)
	if err != nil {
		return nil, err
	}
	return convertSqlRespToDBHubRes(res), nil
}

func ParseOneStmt(ctx context.Context, sql string) (dbhubsdk.StmtType, error) {
	return dbhubsdk.GetStmtTypeBySql(ctx, sql)
}

func getCredential(ctx context.Context, in *dataapi.BasicEndpointReq, clusterInfo *core.Cluster) (dataapi.CrtType, string, string, error) {
	var (
		crtType   dataapi.CrtType
		crtKey    string
		accountID string
		err       error
	)
	if in.Credential != nil {
		crtType = in.Credential.Type
		crtKey = in.Credential.Key
		switch crtType {
		case dataapi.CrtType_MIXED:
			switch {
			case isEmail(crtKey):
				res, err := grpc.GetJwtByUser(ctx, &acl.GetJwtByUserReq{
					ClusterId: in.ClusterId,
					Email:     crtKey,
				})
				if err != nil {
					return 0, "", "", err
				}
				accountID = crtKey
				crtType = dataapi.CrtType_ACCOUNT
				crtKey = dbhubsdk.EncryptDBAccount(res.DbUser, res.Jwt)
			case isRbacKey(crtKey):
				crtType = dataapi.CrtType_APIKEY
			default:
				crtKey, err = getPlaygroundAccount(ctx, clusterInfo)
				if err != nil {
					return 0, "", "", err
				}
				crtType = dataapi.CrtType_ACCOUNT
			}
		case dataapi.CrtType_AUTH0:
			resp, err := grpc.GetUserByToken(ctx, &account.GetUserByTokenRequest{
				Token: crtKey,
			})
			if err != nil {
				return 0, "", "", err
			}
			accountID = resp.User.Email
		}
	} else {
		crtType = dataapi.CrtType_ACCOUNT
		crtKey, err = getPlaygroundAccount(ctx, clusterInfo)
		if err != nil {
			return 0, "", "", err
		}
	}
	return crtType, crtKey, accountID, nil
}

func convertSqlRespToDBHubRes(res *dto.ExecSqlResp) *dbhubsdk.QueryRes {

	queryRes := new(dbhubsdk.QueryRes)
	queryRes.Code = uint16(res.Data.Result.Code)
	queryRes.Message = res.Data.Result.Message
	if queryRes.Code != 200 {
		return queryRes
	}
	queryRes.Code = 0

	for _, column := range res.Data.Columns {
		queryRes.Columns = append(queryRes.Columns, &dbhubsdk.Column{
			Col:      column.Col,
			DataType: column.DataType,
			Nullable: column.Nullable,
		})
	}
	for _, row := range res.Data.Rows {
		items := make([]string, 0, len(row))
		for _, column := range res.Data.Columns {
			items = append(items, row[column.Col])
		}
		queryRes.Rows = append(queryRes.Rows, items)
	}
	queryRes.RowNum = res.Data.Result.RowCount
	queryRes.Affect = res.Data.Result.RowAffect
	queryRes.StartMs = res.Data.Result.StartMs
	queryRes.EndMs = res.Data.Result.EndMs
	queryRes.Databases = res.Data.Result.Databases
	queryRes.WarnCount = res.Data.Result.WarnCount
	queryRes.WarnMessages = res.Data.Result.WarnMessages
	return queryRes
}

func getPlaygroundAccount(ctx context.Context, clusterInfo *core.Cluster) (string, error) {
	if clusterInfo.GetDeployType() == core.DeployType_DEPLOY_TYPE_DEDICATED {
		return "", errs.NewBadRequest("the dedicated cluster does not support this operation", nil)
	}
	userMeta, err := dbhubsdk.Serverless.InfraApi.GetUserMeta(ctx, clusterInfo.ProjectId, clusterInfo.Id)
	if err != nil {
		return "", errs.NewBadRequest("Failed to get cluster information, Please check if the cluster is available.", err)
	}
	return dbhubsdk.EncryptDBAccount(userMeta.ServerlessUser, userMeta.ServerlessPasswd), nil
}

func isEmail(email string) bool {
	return emialRegexp.MatchString(email)
}

func isRbacKey(key string) bool {
	return !isEmail(key) && len(key) == 8
}
