// Package service add by blacknc <blacknc@163.com> at 2023/02/01 11:41:28
package service

import (
	"context"
	"encoding/json"
	"errors"
	"strconv"
	"time"

	"dataapps/commons/config"
	"dataapps/commons/grpc"
	dto "dataapps/internal/dto/sqleditor"
	entity "dataapps/internal/entity/sqleditor"

	"github.com/tidbcloud/aipower/idl/pbgen/proto/api"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/tiinf/errs"
	tierr "github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

func CheckInteractReqeustValid(req *dto.InteractRequest) error {
	if req.Question == "" {
		return errs.NewBadRequest("question is empty", nil)
	}
	/*
		if req.BotType == "" {
			return errs.NewBadRequest("BotType is empty", nil)
		}
	*/
	return nil
}

func InteractBot(ctx context.Context, req *dto.InteractRequest, createdBy string) (*dto.InteractResponse, error) {
	startTime := time.Now()
	clusterID, _ := strconv.ParseUint(req.ClusterID, 10, 64)
	botReq := &api.ChatBotReq{
		BotType:               req.BotType,
		OrgId:                 req.OrgID,
		ProjectId:             req.ProjectID,
		ClusterId:             clusterID,
		Database:              req.Database,
		Question:              req.Question,
		Caller:                createdBy,
		PromptTemplate:        req.PromptTemplate,
		PromptTemplateVersion: req.PromptTemplateVersion,
		QuestionNums:          int32(req.QuestionNum),
		Tables:                req.Tables,
		SampleData:            req.SampleData,
		DatabaseSchema:        req.DatabaseSchema,
		Async:                 req.Async,
	}
	botRsp, botErr := grpc.ChatBot(ctx, botReq)
	if botErr != nil && errors.Is(botErr, tierr.ErrInternalGRPC) {
		for i := 0; i < chatBotRetryCount; i++ {
			botRsp, botErr = grpc.ChatBot(ctx, botReq)
			if botErr == nil || !errors.Is(botErr, tierr.ErrInternalGRPC) {
				break
			}
		}
	}

	var (
		errorMessage string
		status       int8 = 1
		endTime           = time.Now()
		executeTime       = endTime.Sub(startTime).Milliseconds()
	)

	if botErr != nil {
		status = 2
		if newErr, ok := errs.AsTiError(botErr); ok {
			errorMessage = strconv.FormatInt(newErr.BizCode(), 10) + "-" + newErr.ErrMsg()
		} else {
			errorMessage = botErr.Error()
		}
	} else if botRsp.GetCode() != 0 {
		status = 2
		// get errorMessage
		errorMessage = strconv.FormatInt(botRsp.BaseResp.ErrCode, 10) + "-" + botRsp.BaseResp.ErrMsg
	}

	// add gen_sql querylog
	go func() {
		defer func() {
			if err := recover(); err != nil {
				tilog.Error(ctx, "InteractBot add gen_sql querylog panic", zap.Any("err", err))
			}
		}()
		botType := req.BotType
		if botType == "" {
			botType = "chat2sql"
		}
		promptVersion := botRsp.GetPromptVersion()
		if promptVersion == "" {
			promptVersion = botType + "-default"
		}
		sqlContent := botRsp.GetCompletion()
		if botType == "chat2chart" && status == 1 {
			chat2ChartInfo := dto.Chat2ChartInfo{}
			if jerr := json.Unmarshal([]byte(botRsp.GetCompletion()), &chat2ChartInfo); jerr != nil {
				tilog.Error(ctx, "Parse Chat2ChartInfo failed. ", zap.String("bot response completeion", botRsp.GetCompletion()))
				return
			}
			sqlContent = chat2ChartInfo.Sql
		}
		oplogParam := entity.SqlEditorOplog{
			OrgId:         req.OrgID,
			ProjectId:     req.ProjectID,
			ClusterId:     clusterID,
			SessionId:     req.SessionID,
			Chat2QuerySId: botRsp.GetSessionId(),
			SqlFileId:     req.SqlFileID,
			EventType:     string(entity.EventTypeGenSql),
			UserQuestion:  req.Question,
			SqlContent:    sqlContent,
			StartTime:     startTime.Format("2006-01-02 15:04:05"),
			EndTime:       endTime.Format("2006-01-02 15:04:05"),
			Duration:      executeTime,
			Status:        status,
			ErrorMessage:  errorMessage,
			CreatedBy:     createdBy,
			PromptVersion: promptVersion,
			Databases:     botRsp.GetDatabase(),
			Channel:       botType,
		}

		if err := CreateOplog(ctx, oplogParam); err != nil {
			tilog.Error(ctx, "CreateOplog failed", zap.Error(err), zap.String("event_type", string(entity.EventTypeGenSql)))
		}
		// chat2chart、chat2data accept sql automatic
		if botType != "chat2sql" && status == 1 {
			oplogParam.EventType = string(entity.EventTypeSqlAccept)
			oplogParam.IsAccept = true
			if err := CreateOplog(ctx, oplogParam); err != nil {
				tilog.Error(ctx, "CreateOplog failed", zap.Error(err), zap.String("event_type", string(entity.EventTypeSqlAccept)))
			}
		}
	}()

	if botErr != nil {
		tilog.Error(ctx, "InteractBot.ChatBot failed", zap.Any("err", botErr), zap.Any("botReq", botReq))
		return nil, botErr
	}

	return &dto.InteractResponse{
		SessionID:     botRsp.GetSessionId(),
		Question:      req.Question,
		Prompt:        botRsp.GetPrompt(),
		PromptVersion: botRsp.GetPromptVersion(),
		Response:      botRsp.GetCompletion(),
		Code:          int(botRsp.GetCode()),
		StartTime:     startTime.Format("2006-01-02 15:04:05"),
		EndTime:       endTime.Format("2006-01-02 15:04:05"),
		StartTimeMS:   startTime.UnixMilli(),
		EndTimeMS:     endTime.UnixMilli(),
		ExecuteTime:   strconv.FormatInt(executeTime, 10),
		TokenUsed:     botRsp.GetTokenUsed(),
		Database:      botRsp.GetDatabase(),
		JobID:         botRsp.JobId,
	}, nil
}

func ChatBotWithParam(ctx context.Context, botType string, param *dto.Chat2QueryParam) (*dto.InteractResponse, error) {
	conf := config.Global().ServerConfig

	tilog.Info(ctx, "chat bot with param", zap.Any("param", param), zap.Bool("enableStreaming", conf.EnableStreaming))
	req := &dto.InteractRequest{
		BotType:               botType,
		OrgID:                 param.OrgID,
		ProjectID:             param.ProjectID,
		ClusterID:             strconv.FormatUint(param.ClusterID, 10),
		Database:              param.Database,
		Question:              param.Instruction,
		PromptTemplate:        param.PromptTemplate,
		PromptTemplateVersion: param.PromptTemplateVersion,
		QuestionNum:           param.QuestionNum,
		Tables:                param.Tables,
		SampleData:            param.SampleData,
		DatabaseSchema:        param.DatabaseSchema,
		Async:                 conf.EnableStreaming && param.Async, // enable it in server side, and set from request
	}
	rsp, err := InteractBot(ctx, req, param.Apikey)
	if err != nil {
		tilog.Error(ctx, "call InteractBot failed", zap.Any("req", req), zap.Any("err", err))
		return nil, err
	}
	if rsp.Code != chatBotOKCode {
		tilog.Warn(ctx, "call InteractBot failed, code not 0", zap.Any("req", req), zap.Any("rsp", rsp))
		return rsp, bizerr.Restore(bizerr.ErrCodeGeneralBusiness, "gen sql by instruction fail")
	}
	if !req.Async && rsp.Response == "" {
		tilog.Warn(ctx, "call InteractBot fail, completion is empty", zap.Any("req", req), zap.Any("rsp", rsp))
		return rsp, bizerr.Restore(bizerr.ErrCodeGeneralBusiness, "gen sql by instruction is empty")
	}

	tilog.Info(ctx, "call InteractBot succeed", zap.Any("req", req), zap.Any("rsp", rsp))
	return rsp, nil
}
