package steps

import (
	"context"
	"fmt"
	"net/url"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/pingcap/test-infra/caselib/pkg/host"
	"github.com/pingcap/test-infra/caselib/pkg/utils"
	apiresource "github.com/pingcap/test-infra/common/model/resource"
	"github.com/pingcap/test-infra/sdk/core"
	"github.com/pingcap/test-infra/sdk/resource"
	"go.uber.org/zap"
)

type newLightingHostStep struct {
	res  *apiresource.ResourceHandle
	name string
}

func (n newLightingHostStep) String() string {
	return fmt.Sprintf("Initialize lightning host: name=%s lightning=%+v", n.name, n.res)
}

func (n newLightingHostStep) Execute(ctx context.Context) error {
	resolved, err := core.ResolveResourceHandle(n.res)
	if err != nil {
		return errors.Trace(err)
	}
	res := resolved.(resource.Lightning)
	lightning := host.NewLightningHost(res)
	GetInner(ctx).Board().LightningHost = lightning

	return nil
}

var _ Step = &newLightingHostStep{}

func NewLightningHost(name string, res *apiresource.ResourceHandle) Step {
	return &newLightingHostStep{
		res:  res,
		name: name,
	}
}

func (b *Builder) NewLightningHost(name string, res *apiresource.ResourceHandle) *Builder {
	b.steps = append(b.steps, NewLightningHost(name, res))
	return b
}

type LightningImportOpts struct {
	DataDir          string `json:"data-dir"`
	Backend          string `json:"backend"`
	SortedKvDir      string `json:"sorted-kv-dir"`
	Config           string `json:"config"`
	Timeout          string `json:"timeout"`
	Crc32Checksum    uint32 `json:"crc32-checksum"`
	TransferToConfig bool   `json:"transfer-to-config"`
}

type lightningImportStep struct {
	opts         *LightningImportOpts
	tidbRes      *apiresource.ResourceHandle
	lightningRes *apiresource.ResourceHandle
}

func (l lightningImportStep) String() string {
	return fmt.Sprintf("Lightning import from %s: options=%+v", l.lightningRes.Name, l.opts)
}

func (l lightningImportStep) Execute(ctx context.Context) error {
	lightningRes := utils.Try(core.ResolveResourceHandle(l.lightningRes)).(resource.Lightning)
	lightningHost := utils.Try(host.NewLightningHost(lightningRes)).(*host.LightningHost)
	tidbRes, err := core.ResolveResourceHandle(l.tidbRes)
	if err != nil {
		return err
	}
	tidb, ok := tidbRes.(resource.TiDBCluster)
	if !ok {
		log.Error("tidb not found", zap.Reflect("tidb", tidbRes))
		return errors.New("tidb not found")
	}

	return lightningHost.Import(NewImportOpts(l.opts, tidb), l.opts.Timeout)
}

type lightningTiUPImport struct {
	opts         *LightningImportOpts
	tidbRes      *apiresource.ResourceHandle
	lightningRes *apiresource.ResourceHandle
	version      *string
}

func (l *lightningTiUPImport) String() string {
	return fmt.Sprintf("tiup lightning import from %s: options=%+v", l.lightningRes.Name, l.opts)
}

func (l *lightningTiUPImport) Execute(ctx context.Context) error {
	lightningRes := utils.Try(core.ResolveResourceHandle(l.lightningRes)).(resource.Lightning)
	lightningHost := utils.Try(host.NewLightningHost(lightningRes)).(*host.LightningHost)
	tidbRes, err := core.ResolveResourceHandle(l.tidbRes)
	if err != nil {
		return err
	}
	tidb, ok := tidbRes.(resource.TiDBCluster)
	if !ok {
		log.Error("tidb not found", zap.Reflect("tidb", tidbRes))
		return errors.New("tidb not found")
	}

	return lightningHost.TiUPImport(NewImportOpts(l.opts, tidb), *l.version, l.opts.Timeout)
}

var _ Step = &lightningImportStep{}

func NewImportOpts(opts *LightningImportOpts, tidb resource.TiDBCluster) *host.ImportOpts {
	dbUrl := utils.Try(tidb.ServiceURL(resource.DBAddr)).(*url.URL)
	pdUrl := utils.Try(tidb.ServiceURL(resource.PDAddr)).(*url.URL)
	password, _ := dbUrl.User.Password()
	return &host.ImportOpts{
		DataDir:       opts.DataDir,
		Backend:       opts.Backend,
		SortedKvDir:   opts.SortedKvDir,
		PdUrl:         pdUrl.Host,
		Host:          dbUrl.Hostname(),
		Port:          dbUrl.Port(),
		User:          dbUrl.User.Username(),
		Pass:          password,
		Cfg:           opts.Config,
		TransferToCfg: opts.TransferToConfig,
	}
}

func (b *Builder) LightningImport(opts *LightningImportOpts, lightningRes, tidbRes *apiresource.ResourceHandle) *Builder {
	b.steps = append(b.steps, lightningImportStep{opts, tidbRes, lightningRes})
	return b
}

func NewLightningImport(opts *LightningImportOpts, lightningRes, tidbRes *apiresource.ResourceHandle) Step {
	return &lightningImportStep{opts, tidbRes, lightningRes}
}

func (b *Builder) TiUPLightningImport(opts *LightningImportOpts, lightningRes, tidbRes *apiresource.ResourceHandle, version *string) *Builder {
	b.steps = append(b.steps, &lightningTiUPImport{opts, tidbRes, lightningRes, version})
	return b
}

func NewTiUPLightningImport(opts *LightningImportOpts, lightningRes, tidbRes *apiresource.ResourceHandle, version *string) Step {
	return &lightningTiUPImport{opts, tidbRes, lightningRes, version}
}

type lightningImportFromDumpling struct {
	lightningImportStep
	dumplingOutput *string
}

func (l lightningImportFromDumpling) String() string {
	return fmt.Sprintf("Lightning import from dumpling output %s to %s", *l.dumplingOutput, l.lightningRes.Name)
}

func (l lightningImportFromDumpling) Execute(ctx context.Context) error {
	lightningRes := utils.Try(core.ResolveResourceHandle(l.lightningRes)).(resource.Lightning)
	lightningHost := utils.Try(host.NewLightningHost(lightningRes)).(*host.LightningHost)
	tidbRes, err := core.ResolveResourceHandle(l.tidbRes)
	if err != nil {
		return err
	}
	tidb, ok := tidbRes.(resource.TiDBCluster)
	if !ok {
		log.Error("tidb not found", zap.Reflect("tidb", tidbRes))
		return errors.New("tidb not found")
	}
	// replace lightning DataDir with dumpling output
	l.opts.DataDir = *l.dumplingOutput
	return lightningHost.Import(NewImportOpts(l.opts, tidb), l.opts.Timeout)
}

func (b *Builder) LightningImportFromDumpling(opts *LightningImportOpts, lightningRes, tidbRes *apiresource.ResourceHandle,
	dumplingOutput *string) *Builder {
	lightningImportStep := lightningImportStep{opts, tidbRes, lightningRes}
	b.steps = append(b.steps, lightningImportFromDumpling{lightningImportStep: lightningImportStep, dumplingOutput: dumplingOutput})
	return b
}
