package actions

import (
	"fmt"
	"io"
	"math/rand"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/tidbcloud/cluster-service-ng/pkg/template/render"
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

type MockActionBase struct {
	ActionName string

	// all fields below can be empty, default values will be auto used
	MockErrorRate float64
	MockElapsed   time.Duration
	Logger        io.Writer
	Indent        string
	Title         string
}

func (ka *MockActionBase) Name() string {
	return ka.ActionName
}

func (ka *MockActionBase) PrintAction(action string, outputKeys, outputVals []string) error {
	outputKeys = append(outputKeys, "action")
	outputVals = append(outputVals, action)
	outputKeys = append(outputKeys, "mock-elapsed")
	outputVals = append(outputVals, ka.MockElapsed.String())

	title := ka.Title
	if title == "" {
		title = "MockAction"
	}

	indent := ka.Indent
	if indent == "" {
		indent = "    "
	}

	output := fmt.Sprintf("[%s]\n", title)
	for i, key := range outputKeys {
		output += fmt.Sprintf("%s%s:", indent, key)
		val := outputVals[i]
		if val == "" {
			val = "(empty)"
		}
		valLines := strings.Split(val, "\n")
		if len(valLines) == 1 {
			output += " " + val
		} else {
			var vals string
			for _, line := range valLines {
				vals += indent + indent + line + "\n"
			}
			output += vals
		}
	}

	writer := ka.Logger
	if writer == nil {
		writer = os.Stdout
	}
	_, err := writer.Write([]byte(output))
	return err
}

func (ka *MockActionBase) Invoke(args ActionArgs) error {
	return ka.MockInvoke()
}

func (ka *MockActionBase) MockInvoke() error {
	time.Sleep(ka.MockElapsed)
	n := int64(float64(rand.Intn(100)))
	rate := int64(ka.MockErrorRate * 100)
	if n < rate {
		return fmt.Errorf("hit mock error, rate %v(percent), roll %v", rate, n)
	}
	return nil
}

type MockServiceApiAction struct {
	*MockActionBase
	ServiceEndpoint string
	Method          string
}

func (sa *MockServiceApiAction) Invoke(args ActionArgs) error {
	err := sa.MockInvoke()
	if err != nil {
		return err
	}
	var argsStr string
	if args != nil {
		argsStr = fmt.Sprintf("%v", args)
	} else {
		argsStr = "(empty)"
	}
	err = sa.PrintAction(
		"api call",
		[]string{"service", "method", "args"},
		[]string{sa.ServiceEndpoint, sa.Method, argsStr})
	if err != nil {
		return err
	}
	return nil
}

type MockBuiltinAction struct {
	*MockActionBase
	FunctionName string
	Function     any
}

func (ba *MockBuiltinAction) Invoke(args ActionArgs) error {
	err := ba.MockInvoke()
	if err != nil {
		return err
	}
	var argsStr string
	if args != nil {
		argsStr = fmt.Sprintf("%v", args)
	} else {
		argsStr = "(empty)"
	}
	err = ba.PrintAction(
		"invoke builtin action function",
		[]string{"function", "args"},
		[]string{ba.FunctionName, argsStr})
	if err != nil {
		return err
	}
	return nil
}

type MockK8sActionExpectCrWhenFinished struct {
	K8sEndPoint     string
	CrFetchPath     string
	CrKeyPath       string
	ExpectOneOfVals []any
}

type MockK8sAction struct {
	*MockActionBase
	Action               K8sActionType
	K8sEndpoint          string
	YamlTemplateName     string
	Templates            *render.TemplateLibrary
	ExpectCrWhenFinished *MockK8sActionExpectCrWhenFinished
}

func (ka *MockK8sAction) Invoke(args ActionArgs) error {
	templArgs := map[string]string{}
	for k, v := range args {
		templArgs[k] = v.String()
	}
	template, err := ka.Templates.Render(ka.YamlTemplateName, templArgs)
	if err != nil {
		return err
	}
	err = ka.PrintAction(
		K8sActionTypeStr(ka.Action),
		[]string{"k8s-endpoint", "content"},
		[]string{ka.K8sEndpoint, template})
	if err != nil {
		return err
	}
	return ka.MockInvoke()
}

type MockAsyncAction struct {
	ActionName  string
	MockElapsed time.Duration

	mu        sync.Mutex
	startTime time.Time
}

func NewMockAsyncAction(name string, mockElapsed time.Duration) *MockAsyncAction {
	return &MockAsyncAction{
		ActionName:  name,
		MockElapsed: mockElapsed,
	}
}

func (maa *MockAsyncAction) Name() string {
	return maa.ActionName
}

func (maa *MockAsyncAction) Start(args ActionArgs) error {
	maa.mu.Lock()
	defer maa.mu.Unlock()
	maa.startTime = time.Now()
	return nil
}

func (maa *MockAsyncAction) CheckFinish(args ActionArgs) (bool, error) {
	maa.mu.Lock()
	defer maa.mu.Unlock()
	if time.Since(maa.startTime) > maa.MockElapsed {
		return true, nil
	}
	return false, nil
}
