package cmdstr

import (
	"encoding/json"
	"flag"
	"strings"
	"time"
)

var (
	bettingIdentifier = "betting"
	qhAdIdentifier    = "qhad" // Corresponding to workload oncall2929.
)

type GoycsbSubcmd string

var (
	GoycsbLoad GoycsbSubcmd = "load"
	GoycsbRun  GoycsbSubcmd = "run"
)

// TODO: move jsonDuration to util package.
// ref: https://github.com/tikv/pd/blob/master/pkg/typeutil/duration.go
type jsonDuration struct {
	time.Duration
}

func (j *jsonDuration) UnmarshalJSON(b []byte) error {
	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}
	dur, err := time.ParseDuration(s)
	if err != nil {
		return err
	}
	*j = jsonDuration{dur}
	return nil
}

type GoycsbMySQLDBOptions struct {
	Host string `json:"host"`
	// url.URL.Port() also returns a string object.
	Port string `json:"port"`
}

// BindFlagSet convert attributes to flags and bind them to a flagSet.
func (opts *GoycsbMySQLDBOptions) BindFlagSet(flagSet *flag.FlagSet, prefix string) {
	p := func(name string) string {
		return f("%s.%s", prefix, name)
	}
	flagSet.StringVar(&opts.Host, p("mysql.host"), "127.0.0.1", "")
	flagSet.StringVar(&opts.Port, p("mysql.port"), "4000", "")
}

// GoycsbCommonOptions defines commonly used options for go-ycsb.
type GoycsbCommonOptions struct {
	MySQL GoycsbMySQLDBOptions `json:"mysql"`

	Threads int `json:"threads"`

	OperationCount int64 `json:"operationCount"`
	RecordCount    int64 `json:"recordCount"`
}

// BindFlagSet convert attributes to flags and bind them to a flagSet.
// Flag name is formated as {prefix}.{flagName}. You can use workload name as a prefix.
func (opts *GoycsbCommonOptions) BindFlagSet(flagSet *flag.FlagSet, prefix string) {
	p := func(name string) string {
		return f("%s.%s", prefix, name)
	}
	opts.MySQL.BindFlagSet(flagSet, prefix)
	flagSet.IntVar(&opts.Threads, p("threads"), 100, "")
	flagSet.Int64Var(&opts.OperationCount, p("operationcount"), 0, "")
	flagSet.Int64Var(&opts.RecordCount, p("recordcount"), 0, "")
}

// Goycsb helps build a command string for the workload.
type Goycsb struct {
	path       string
	subcommand string
	args       []string
	options    []string
}

// NewGoycsb create a Goycsb cmdstr object.
func NewGoycsb(path string) *Goycsb {
	return &Goycsb{path: path}
}

// String implements CmdBuilder.String interface.
func (b *Goycsb) String() string {
	arr := []string{}
	arr = append(arr, b.path)
	arr = append(arr, b.subcommand)
	arr = append(arr, b.args...)
	arr = append(arr, b.options...)
	return strings.Join(arr, " ")
}

// Run refers to the go-ycsb subcommand `run`.
func (b *Goycsb) Run(db string) *Goycsb {
	b.subcommand = "run"
	b.args = []string{db}
	return b
}

// Load refers to the go-ycsb subcommand `load`.
func (b *Goycsb) Load(db string) *Goycsb {
	b.subcommand = "load"
	b.args = []string{db}
	return b
}

// Options parse all options from GoycsbCommonOptions.
func (b *Goycsb) CommonOptions(p *GoycsbCommonOptions) *Goycsb {
	b.prop(f("mysql.host=%s", p.MySQL.Host))
	b.prop(f("mysql.port=%s", p.MySQL.Port))
	b.prop(f("threadcount=%d", p.Threads))
	b.prop(f("operationcount=%d", p.OperationCount))
	b.prop(f("recordcount=%d", p.RecordCount))
	return b
}

// PropertyFile refers to the go-ycsb option `-P --property_file`.
func (b *Goycsb) PropertyFile(f string) *Goycsb {
	b.options = append(b.options, []string{"-P", f}...)
	return b
}

// prop appends a property to options.
func (b *Goycsb) prop(opt string) {
	b.options = append(b.options, []string{"-p", opt}...)
}

// BettingOptions defines commonly used options for go-ycsb betting workload.
type BettingOptions struct {
	GoycsbCommonOptions

	DatabaseProportion float64 `json:"databaseProportion"`
	DBNamePrefix       string  `json:"dbNamePrefix"`
	TableCount         int     `json:"tableCount"`

	UnitNamePrefix   string `json:"unitNamePrefix"`
	UnitCount        int    `json:"unitCount"`
	UnitDistribution string `json:"unitDistribution"`

	Clients int `json:"clients"`
}

// BindFlagSet convert attributes to flags and bind them to a flagSet.
func (opts *BettingOptions) BindFlagSet(flagSet *flag.FlagSet) {
	prefix := bettingIdentifier
	p := func(name string) string {
		return f("%s.%s", prefix, name)
	}

	opts.GoycsbCommonOptions.BindFlagSet(flagSet, prefix)

	flagSet.Float64Var(&opts.DatabaseProportion, p("databaseproportion"), 1, "")
	flagSet.StringVar(&opts.DBNamePrefix, p("dbnameprefix"), "betting_", "")
	flagSet.IntVar(&opts.TableCount, p("tablecount"), 1000, "")

	flagSet.StringVar(&opts.UnitNamePrefix, p("unitnameprefix"), "unit_", "")
	flagSet.IntVar(&opts.UnitCount, p("unitcount"), 10, "")
	flagSet.StringVar(&opts.UnitDistribution, p("unitdistribution"), "zipfian", "")

	flagSet.IntVar(&opts.Clients, p("clients"), 1, "")
}

func (b *Goycsb) BettingOptions(p *BettingOptions) *Goycsb {
	b.PropertyFile(f("workloads/%s", bettingIdentifier))

	b.CommonOptions(&p.GoycsbCommonOptions)

	b.prop(f("tablecount=%d", p.TableCount))
	b.prop(f("dbnameprefix=%s", p.DBNamePrefix))
	b.prop(f("databaseproportions=%f", p.DatabaseProportion))
	b.prop(f("tablecount=%d", p.TableCount))

	// Map UnitCount to unitscount.
	b.prop(f("unitscount=%d", p.UnitCount))
	b.prop(f("unitnameprefix=%s", p.UnitNamePrefix))
	b.prop(f("unitdistribution=%s", p.UnitDistribution))

	// Betting does not support clients property temporarily.
	// b.prop(f("client=%d", p.Clients))
	return b
}

// QHAdOptions defines commonly used options for go-ycsb oncall2929 work load.
// Use QHAd as the workload name because it is more semantic.
type QHAdOptions struct {
	GoycsbCommonOptions

	DBName                string       `json:"dbName"`
	InitialPartitionCount int          `json:"initialPartitionCount"`
	AutoCommit            bool         `json:"autoCommit"`
	TableCount            int          `json:"tableCount"`
	TableNamePrefix       string       `json:"tableNamePrefix"`
	DropPartitionInterval jsonDuration `json:"dropPartitionInterval"`
	ExecTime              jsonDuration `json:"execTime"`
}

// BindFlagSet convert attributes to flags and bind them to a flagSet.
func (opts *QHAdOptions) BindFlagSet(flagSet *flag.FlagSet) {
	prefix := qhAdIdentifier
	p := func(name string) string {
		return f("%s.%s", prefix, name)
	}

	opts.GoycsbCommonOptions.BindFlagSet(flagSet, prefix)

	flagSet.IntVar(&opts.InitialPartitionCount, p("initialpartitioncount"), 10, "")
	flagSet.BoolVar(&opts.AutoCommit, p("autocommit"), true, "")
	flagSet.StringVar(&opts.DBName, p("databasename"), "qhad", "")
	flagSet.IntVar(&opts.TableCount, p("tablecount"), 10, "")
	flagSet.StringVar(&opts.TableNamePrefix, p("tablenameprefix"), "qhad_", "")
	flagSet.DurationVar(&opts.DropPartitionInterval.Duration, p("droppartitioninterval"), 2*time.Minute, "")
	flagSet.DurationVar(&opts.ExecTime.Duration, p("exectime"), 1*time.Minute, "")
}

func (b *Goycsb) QHAdOptions(p *QHAdOptions) *Goycsb {
	b.PropertyFile("workloads/oncall2929")
	b.CommonOptions(&p.GoycsbCommonOptions)

	var autocommit int
	if p.AutoCommit {
		autocommit = 1
	} else {
		autocommit = 0
	}
	b.prop(f("initialpartitioncount=%d", p.InitialPartitionCount))
	b.prop(f("autommit=%d", autocommit))
	b.prop(f("databasename=%s", p.DBName))
	b.prop(f("tablecount=%d", p.TableCount))
	b.prop(f("tablenameprefix=%s", p.TableNamePrefix))
	b.prop(f("droppartitioninterval=%d", int64(p.DropPartitionInterval.Seconds())))
	b.prop(f("exectime=%d", int64(p.ExecTime.Seconds())))
	return b
}
