package v1

import (
	"context"
	"fmt"
	"regexp"
	"strings"
	"sync"

	"github.com/pingcap-inc/pingcat/internal/apiserver/store"
	v1 "github.com/pingcap-inc/pingcat/model/v1"
	"go.uber.org/zap"

	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
)

// Declare package-level variables.
var (
	router *simpleLLMRouter
	once   sync.Once
	// todo: Convert to a complete plug-in mechanism, including plug-in registration, etc.
	appPluginsHub = map[string]AppPlugin{
		"datalens plugin": NewDataLensPlugin(),
		"mock plugin":     NewMockPlugin(),
	}
)

// handleRoute2APP is a method that routes an incoming message to the appropriate application plugin.
// It first removes any mentions of the bot from the message and then determines if there is a
// hardcoded route (e.g., if the message starts with "/q " or "/query ") or if it needs to
// use the simple LLM router to find the appropriate action based on the message content.
//
// If an action is found and matches a registered plugin in the appPluginsHub, it will call the
// HandleLarkMsg method of the matched plugin with the incoming message, returning true to
// indicate that a matching plugin was found and the message was handled.
//
// If no matching plugin is found, the function returns false (may write an answer), and the caller can
// handle the message in other ways (or use the answer).
//
// Parameters:
// - c: The context.Context that is passed through for logging, cancellation, and deadline purposes.
// - me: The *v1.LarkMsgEvent containing the message event that needs to be routed.
// - msg: The string containing the actual message content.
//
// Returns:
// - A bool indicating whether a matching plugin was found and the message was handled.
func (s *ChatService) handleRoute2APP(c context.Context, me *v1.LarkMsgEvent, msg string) bool {
	trimStr := strings.TrimSpace(msg)
	atReg := regexp.MustCompile(`@_user_\d+(\s+|$)`)
	rmAtStr := atReg.ReplaceAllString(trimStr, "")
	rmAtStr = strings.TrimSpace(rmAtStr)

	var (
		action string
		answer string
		err    error
	)

	// hard route based on request keyword
	if strings.HasPrefix(rmAtStr, "/q ") || strings.HasPrefix(rmAtStr, "/query ") {
		action = "datalens plugin"
		if app, ok := appPluginsHub["datalens plugin"]; ok {
			_ = app.HandleLarkMsg(c, me, trimStr, s.store)

			return true
		}
	} else if strings.HasPrefix(rmAtStr, "/route ") { // for manual testing temporarily
		// soft route based on LLM analysis
		r := GetRouter()
		// answer is from the LLM that can be used to send to user, and action is the app name if it exists
		action, answer, err = r.Route(c, rmAtStr, s.simpleLLMWrapper)
		if action != "" {
			for name, app := range appPluginsHub {
				if strings.Contains(strings.ToLower(action), strings.ToLower(name)) {
					_ = app.HandleLarkMsg(c, me, trimStr, s.store)
					return true
				}
			}
		}
	} else if strings.HasPrefix(rmAtStr, "/kb ") {
		kbReg := regexp.MustCompile(`^/kb\s+`)
		rmKBStr := kbReg.ReplaceAllString(rmAtStr, "")
		rmKBStr = strings.TrimSpace(rmKBStr)

		kbSrv := NewOncallBotChatAPP(s.store)
		kbSrv.HandleLarkMsg(c, me, rmKBStr)

		return true
	} else if strings.HasPrefix(rmAtStr, "/it ") {
		itReg := regexp.MustCompile(`^/it\s+`)
		rmITStr := itReg.ReplaceAllString(rmAtStr, "")
		rmITStr = strings.TrimSpace(rmITStr)

		itSrv := NewITChatAPP(s.store)
		itSrv.HandleLarkMsg(c, me, rmITStr)

		return true
	}

	log.L(c).Info("router: no app found, fallthrough to base router", zap.String("answer", answer), zap.Error(err))
	return false
}

// Currently there is no independent openai client that does not couple other properties, so this function is simply constructed for llm route
// ToDo: needs to be improved in the future.
func (s *ChatService) simpleLLMWrapper(ctx context.Context, prompt string) (string, error) {
	gptMsgs := []*v1.GPTChatMessage{
		{
			Content: prompt,
			Role:    "user",
		},
	}

	gptSrv := NewGPT(s.store)
	resp, err := gptSrv.ChatCompletion(ctx, gptMsgs, nil)
	if err != nil {
		return "", errors.Wrap(err, "error when request openai api.")
	}

	return resp.Content, nil
}

// GetRouter function returns the singleton instance.
func GetRouter() *simpleLLMRouter {
	once.Do(func() {
		router = NewSimpleLLMRouter()
	})
	return router
}

type simpleLLMRouter struct {
	routePrompt string
}

func NewSimpleLLMRouter() *simpleLLMRouter {
	return &simpleLLMRouter{
		routePrompt: customeRoutePromptTemplate(),
	}
}

func (r *simpleLLMRouter) Route(ctx context.Context, msg string, llm func(ctx context.Context, prompt string) (string, error)) (action, answer string, err error) {
	log.L(ctx).Info("simple llm router called")

	planPrompt := fmt.Sprintf(r.routePrompt, msg)
	log.L(ctx).Info("simple llm router", zap.String("plan prompt", planPrompt))

	predict, err := llm(ctx, planPrompt)
	if err != nil {
		return "", "", err
	}
	log.L(ctx).Info("simple llm router", zap.String("plan predict", predict))
	/*
		predict example:
			Thought: This is a simple test to check whether the plugin route is working properly or not.
			Action: should use mock plugin
			Action Input: None
			Observation: The mock plugin successfully routed and returned a mocked response.
			Thought: The route is working properly.
			Final Answer: Arrr, the plugin route be in workin' order, matey!
	*/

	actionPattern := regexp.MustCompile(`(?mi)^Action: (.+)$`)
	finalAnswerPattern := regexp.MustCompile(`(?mi)^Final Answer: (.+)$`)

	actions := actionPattern.FindAllStringSubmatch(predict, -1)
	log.L(ctx).Infof("simple llm router, actions %+v", actions)
	if len(actions) > 0 {
		// Avoid too big search space and hallucinatory reasoning, only take the first one
		action = strings.TrimSpace(actions[0][1])
	}

	finalAnswer := finalAnswerPattern.FindStringSubmatch(predict)
	if finalAnswer != nil {
		answer = finalAnswer[1]
	}

	return action, answer, nil
}

func (r *simpleLLMRouter) Name() string {
	return "simple LLM Router"
}

// AppPlugin a simple version plugin definition.
type AppPlugin interface {
	Name() string
	Description() string
	HandleLarkMsg(c context.Context, me *v1.LarkMsgEvent, msg string, s store.Factory) error
}

// dataLensPlugin is a plugin for datalens app.
type dataLensPlugin struct {
	name        string
	description string
}

func NewDataLensPlugin() AppPlugin {
	return &dataLensPlugin{
		name:        "datalens plugin",
		description: "only useful for when you need to answer questions about the TiDB company knowledge (employee, product, data)",
	}
}

func (app *dataLensPlugin) Name() string {
	return app.name
}

func (app *dataLensPlugin) Description() string {
	return app.name + ": " + app.description
}

func (app *dataLensPlugin) HandleLarkMsg(c context.Context, me *v1.LarkMsgEvent, msg string, s store.Factory) error {
	queryDB := NewQueryDBChatAPP(s)
	queryDB.HandleLarkMsg(c, me, msg)
	return nil
}

// MockPlugin is a plugin for testing.
type MockPlugin struct{}

func NewMockPlugin() AppPlugin {
	return &MockPlugin{}
}

func (app *MockPlugin) Name() string {
	return "mock plugin"
}

func (app *MockPlugin) Description() string {
	return "mock plugin: only useful for when you need to test whether the plugin route works. If you don't see the cypher `mock mock`, please don't use this plugin"
}

func (app *MockPlugin) HandleLarkMsg(ctx context.Context, me *v1.LarkMsgEvent, msg string, s store.Factory) error {
	log.L(ctx).Info("mock plugin called", zap.String("msg", msg))
	if strings.HasPrefix(msg, "fail") {
		return errors.New("mock plugin failed")
	}

	chatSrv := NewChat(s)
	_, _ = chatSrv.SendTextLarkMsg(ctx, me, "mock plugin called")

	return nil
}

func customeRoutePromptTemplate() string {
	var toolsDescription strings.Builder
	var toolsName []string

	for _, app := range appPluginsHub {
		toolsDescription.WriteString(app.Description())
		toolsDescription.WriteString("\n")
		toolsName = append(toolsName, app.Name())
	}

	promptTemplate := `Answer the following questions as best you can. You have access to the following tools:

%s

If there is no tool available, you can use your own knowledge to answer the question.

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [%s]. Don't take the useless tools.
Action Input: the input to the action
Observation: the result of the action
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin! Remember to speak as a pirate when giving your final answer.

Question: %%s`

	return fmt.Sprintf(promptTemplate, toolsDescription.String(), strings.Join(toolsName, ", "))
}
