package host

import (
	"net/url"
	"strings"

	"github.com/pingcap/errors"
	"github.com/pingcap/test-infra/caselib/pkg/command"
	brmodel "github.com/pingcap/test-infra/caselib/pkg/model/br"
	"github.com/pingcap/test-infra/sdk/resource"
)

type BackupOpts struct {
	Storage            string            `json:"storage"`
	Timeout            string            `json:"timeout"`
	Crc32Checksum      uint32            `json:"crc32-checksum"`
	BackupOptions      []string          `json:"backup-options"`
	RestoreOptions     []string          `json:"restore-options"`
	StorageIncremental string            `json:"storage-incremental"`
	Stdout             string            `json:"-"`
	Stderr             string            `json:"-"`
	Env                map[string]string `json:"env"` // to inject environment variable to the command
}

type BackupFullOpts struct {
	BackupOpts
	Filter             string   `json:"filter"`
	BackupFullOptions  []string `json:"backup-full-options"`
	RestoreFullOptions []string `json:"restore-full-options"`
}

type BackupDbOpts struct {
	BackupOpts
	Database         string   `json:"database"`
	BackupDbOptions  []string `json:"backup-db-options"`
	RestoreDbOptions []string `json:"restore-db-options"`
}

type BackupTableOpts struct {
	BackupDbOpts
	Table               string   `json:"table"`
	BackupTableOptions  []string `json:"backup-table-options"`
	RestoreTableOptions []string `json:"restore-table-options"`
}

type BrHost struct {
	Host
}

type BackupLogOpts struct {
	BackupOpts
	TaskName         string   `json:"task-name"`
	StartTs          string   `json:"start-ts"`
	GcTtl            string   `json:"gc-ttl"`
	BackupLogOptions []string `json:"backup-log-options"`
}

type RestorePointOpts struct {
	BackupOpts
	FullBackupStorage   string   `json:"full-backup-storage"`
	StartTs             string   `json:"start-ts"`
	RestoredTs          string   `json:"restored-ts"`
	RestorePointOptions []string `json:"restore-point-options"`
}

func (b *BrHost) Br(env map[string]string, args ...string) *command.BrCmd {
	println(env)
	return &command.BrCmd{
		ExecCmd: command.ExecCmd{
			Command: "/br",
			Options: args,
			ParentCmd: command.SetEnv{
				"env": env,
			},
		},
	}
}

func (b *BrHost) TiUPBr(env map[string]string, version string, args ...string) *command.BrCmd {
	println(env)
	var cmd string
	if version == "" {
		cmd = "tiup br"
	} else {
		cmd = "tiup br:" + version
	}
	return &command.BrCmd{
		ExecCmd: command.ExecCmd{
			Command: cmd,
			Options: args,
			ParentCmd: command.SetEnv{
				"env": env,
			},
		},
	}
}

func (b *BrHost) BackupFull(pdUrl *url.URL, opts *BackupFullOpts) error {
	args := []string{"-s", opts.Storage, "-u", pdUrl.String()}
	if opts.Filter != "" {
		args = append(args, "-f", opts.Filter)
	}
	args = append(args, opts.BackupFullOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Backup(opts.BackupOptions...).Full(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR backup full failed")
		return errors.New("BR backup full failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) TiUPBackupFull(pdUrl *url.URL, version string, opts *BackupFullOpts) error {
	args := []string{"-s", opts.Storage, "-u", pdUrl.String()}
	if opts.Filter != "" {
		args = append(args, "-f", opts.Filter)
	}
	args = append(args, opts.BackupFullOptions...)
	res, err := b.ExecCmd(b.TiUPBr(opts.Env, version).Backup(opts.BackupOptions...).Full(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR backup full failed")
		return errors.New("BR backup full failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) BackupDb(pdUrl *url.URL, opts *BackupDbOpts) error {
	args := append([]string{"-s", opts.Storage, "-u", pdUrl.String(), "--db", opts.Database}, opts.BackupDbOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Backup(opts.BackupOptions...).Db(args...).Compose(), opts.Timeout)
	if err != nil {
		return err
	}
	if res.ExitCode != 0 {
		logger.Error("BR backup db failed")
		return errors.New("BR backup db failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) BackupTable(pdUrl *url.URL, opts *BackupTableOpts) error {
	args := append([]string{"-s", opts.Storage, "-u", pdUrl.String(), "--db", opts.Database, "--table", opts.Table}, opts.BackupTableOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Backup(opts.BackupOptions...).Table(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR backup table failed")
		return errors.New("BR backup table failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) BackupLog(operation brmodel.OperateBrLogType, pdUrl *url.URL, opts *BackupLogOpts) error {
	args := []string{operation.String(), "--storage", opts.Storage, "--pd", pdUrl.String()}
	if opts.TaskName != "" {
		args = append(args, "--task-name", opts.TaskName)
	}
	if operation == brmodel.Start && opts.StartTs != "" {
		args = append(args, "--start-ts", opts.StartTs)
	}
	if operation == brmodel.Pause && opts.GcTtl != "" {
		args = append(args, "--gc-ttl", opts.GcTtl)
	}
	args = append(args, opts.BackupLogOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Log(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR log failed")
		return errors.New("BR log failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil

}

func (b *BrHost) RestoreFull(pdUrl *url.URL, opts *BackupFullOpts) error {
	args := []string{"-s", opts.Storage, "-u", pdUrl.String()}
	if opts.Filter != "" {
		args = append(args, "-f", opts.Filter)
	}
	args = append(args, opts.RestoreFullOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Restore(opts.RestoreOptions...).Full(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR restore full failed")
		return errors.New("BR restore full failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) TiUPRestoreFull(pdUrl *url.URL, version string, opts *BackupFullOpts) error {
	args := []string{"-s", opts.Storage, "-u", pdUrl.String()}
	if opts.Filter != "" {
		args = append(args, "-f", opts.Filter)
	}
	args = append(args, opts.RestoreFullOptions...)
	res, err := b.ExecCmd(b.TiUPBr(opts.Env, version).Restore(opts.RestoreOptions...).Full(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR restore full failed")
		return errors.New("BR restore full failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) RestoreDb(pdUrl *url.URL, opts *BackupDbOpts) error {
	args := append([]string{"-s", opts.Storage, "-u", pdUrl.String(), "--db", opts.Database}, opts.RestoreDbOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Restore(opts.RestoreOptions...).Db(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR restore db failed")
		return errors.New("BR restore db failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) RestoreTable(pdUrl *url.URL, opts *BackupTableOpts) error {
	args := append([]string{"-s", opts.Storage, "-u", pdUrl.String(), "--db", opts.Database, "--table", opts.Table}, opts.RestoreTableOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Restore(opts.RestoreOptions...).Table(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR restore table failed")
		return errors.New("BR restore table failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) RestorePoint(pdUrl *url.URL, opts *RestorePointOpts) error {
	args := []string{"--storage", opts.Storage, "--pd", pdUrl.String()}
	if opts.RestoredTs != "" {
		args = append(args, "--restored-ts", opts.RestoredTs)
	}
	if opts.StartTs != "" {
		args = append(args, "--start-ts", opts.StartTs)
	}
	if opts.FullBackupStorage != "" {
		args = append(args, "--full-backup-storage", opts.FullBackupStorage)
	}
	args = append(args, opts.RestorePointOptions...)
	res, err := b.ExecCmd(b.Br(opts.Env).Restore(opts.RestoreOptions...).Point(args...).Compose(), opts.Timeout)
	if err != nil {
		return errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR restore point failed")
		return errors.New("BR restore point failed")
	}
	opts.Stdout = res.Stdout
	opts.Stderr = res.Stderr
	return nil
}

func (b *BrHost) GetTsFromBackup(backupLocation *string) (string, error) {
	args := []string{"--field", "end-version", "-s", *backupLocation}
	tmp := make(map[string]string)
	res, err := b.ExecCmd(b.Br(tmp).Validate().Decode(args...).Compose(), "60s")
	if err != nil {
		return "", errors.Trace(err)
	}
	if res.ExitCode != 0 {
		logger.Error("BR GetTsFromBackup failed")
		return "", errors.New("BR GetTsFromBackup failed")
	}

	return strings.Trim(res.Stdout, "\n"), nil
}

func NewBrHost(res resource.WorkloadNode) *BrHost {
	return &BrHost{Host{res}}
}
