package core

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"

	"github.com/pkg/errors"
	"go.uber.org/zap"

	. "github.com/pingcap/test-infra/common/model/resource"
)

const (
	envPrefix         = "TI_"
	envStrParamPrefix = envPrefix + "PARAM_STR_"
	envResParamPrefix = envPrefix + "PARAM_RES_"
	envArchivePath    = envPrefix + "ARCHIVE"
	envOutputPath     = envPrefix + "OUTPUT"
	envExecID         = envPrefix + "EXEC_ID"
	envRMSEndpoint    = envPrefix + "ENDPOINT_RMS"
	envTCMSEndpoint   = envPrefix + "ENDPOINT_TCMS"
)

type TestContext interface {
	context.Context
	Logger() *zap.Logger

	ExecInfo() ExecutionInfo
	Param(name string, optionalDefault ...string) string
	Resource(name string) Resource
}

type ExecutionInfo struct {
	CaseID     string
	CaseExecID string
	PlanID     string
	PlanExecID string

	RMSEndpoint  string
	TCMSEndpoint string
}

type Resource interface {
	Handle() ResourceHandle
}

type ResourceGetter interface {
	Get() (Resource, error)
}

type ResourceProvider func(handle ResourceHandle) ResourceGetter

var (
	providers = map[string]ResourceProvider{}
	lock      sync.Mutex
)

func RegisterProvider(name string, provider ResourceProvider) {
	lock.Lock()
	providers[name] = provider
	lock.Unlock()
}

func GetProvider(name string) (provider ResourceProvider, exists bool) {
	lock.Lock()
	provider, exists = providers[name]
	lock.Unlock()
	return
}

func Fail(reason ...string) {
	appendOutput(reason)
	panic(1)
}

func Fail2(reason ...string) {
	appendOutput(reason)
	panic(2)
}

func Skip(reason ...string) {
	appendOutput(reason)
	panic(3)
}

func Timeout(reason ...string) {
	appendOutput(reason)
	panic(4)
}

func Cancelled(reason ...string) {
	appendOutput(reason)
	panic(5)
}

func ArchivePath() string { return os.Getenv(envArchivePath) }

func CreateFileForArchive(path string) (*os.File, error) {
	path = filepath.Join(ArchivePath(), path)
	_ = os.MkdirAll(filepath.Dir(path), 0755)
	return os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
}

// TODO@codingpoet: remove appendOutput and related apis
func appendOutput(messages []string) {
	out := os.Stderr
	for _, line := range messages {
		fmt.Fprintln(out, line)
	}
	if outPath := os.Getenv(envOutputPath); len(outPath) > 0 {
		outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
		if err == nil {
			defer outFile.Close()
			out = outFile
		}
		for _, line := range messages {
			fmt.Fprintln(out, line)
		}
	}
}

func BuildContext() (TestContext, error) {
	ctx := context.Background()
	logCfg := zap.NewDevelopmentConfig()
	logCfg.OutputPaths = []string{"stdout"}
	if archivePath := ArchivePath(); len(archivePath) > 0 {
		_ = os.MkdirAll(archivePath, 0755)
		logCfg.OutputPaths = append(logCfg.OutputPaths, filepath.Join(archivePath, "test.log"))
	}
	logger, err := logCfg.Build()
	if err != nil {
		return nil, errors.Wrap(err, "build logger")
	}
	if execID := os.Getenv(envExecID); len(execID) > 0 {
		logger = logger.Named(execID)
	}
	return &testContext{
		Context: ctx,
		logger:  logger,
	}, nil
}

type testContext struct {
	context.Context
	logger *zap.Logger
}

func (c *testContext) Logger() *zap.Logger { return c.logger }

func (c *testContext) ExecInfo() ExecutionInfo {
	// TODO: read other fields from TCMS API
	return ExecutionInfo{
		CaseExecID:   os.Getenv(envExecID),
		RMSEndpoint:  os.Getenv(envRMSEndpoint),
		TCMSEndpoint: os.Getenv(envTCMSEndpoint),
	}
}

func (c *testContext) Param(name string, optionalDefault ...string) string {
	v, ok := os.LookupEnv(envStrParamPrefix + name)
	if !ok && len(optionalDefault) > 0 {
		return optionalDefault[0]
	}
	return v
}

func (c *testContext) Resource(name string) Resource {
	handle, ok := os.LookupEnv(envResParamPrefix + name)
	if !ok {
		Fail2("undefined resource: " + name)
	}
	return resolveResource(handle)
}

func decodeHandle(handle string) ResourceHandle {
	var h ResourceHandle
	if err := json.Unmarshal([]byte(handle), &h); err != nil {
		Fail2(fmt.Sprintf("decode handle %q: %v", handle, err))
	}
	return h
}

func resolveResource(handle string) Resource {
	h := decodeHandle(handle)
	res, err := ResolveResourceHandle(&h)
	if err != nil {
		Fail2(fmt.Sprintf("get resource by handle %q: %v", handle, err))
	}
	return res
}

func ResolveResourceHandle(handle *ResourceHandle) (Resource, error) {
	p, ok := GetProvider(handle.Provider)
	if !ok {
		return nil, fmt.Errorf("unregistered resource provider: %s, check it on: https://github.com/pingcap/test-infra/discussions/116", handle.Provider)
	}

	return p(*handle).Get()
}
