package stress

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"net/http"
	"sync"
	"time"

	"github.com/influxdata/influxdb/client/v2"
)

const backoffInterval = time.Duration(500 * time.Millisecond)

// AbstractTag is a struct that abstractly
// defines a tag
type AbstractTag struct {
	Key   string `toml:"key"`
	Value string `toml:"value"`
}

// AbstractTags is a slice of abstract tags
type AbstractTags []AbstractTag

// Template returns a templated string of tags
func (t AbstractTags) Template() string {
	var buf bytes.Buffer
	for i, tag := range t {
		if i == 0 {
			buf.Write([]byte(fmt.Sprintf("%v=%v-%%v,", tag.Key, tag.Value)))
		} else {
			buf.Write([]byte(fmt.Sprintf("%v=%v,", tag.Key, tag.Value)))
		}
	}

	b := buf.Bytes()
	b = b[0 : len(b)-1]

	return string(b)
}

// AbstractField is a struct that abstractly
// defines a field
type AbstractField struct {
	Key  string `toml:"key"`
	Type string `toml:"type"`
}

// AbstractFields is a slice of abstract fields
type AbstractFields []AbstractField

// Template returns a templated string of fields
func (f AbstractFields) Template() (string, []string) {
	var buf bytes.Buffer
	a := make([]string, len(f))
	for i, field := range f {
		buf.Write([]byte(fmt.Sprintf("%v=%%v,", field.Key)))
		a[i] = field.Type
	}

	b := buf.Bytes()
	b = b[0 : len(b)-1]

	return string(b), a
}

// BasicPointGenerator implements the PointGenerator interface
type BasicPointGenerator struct {
	PointCount  int            `toml:"point_count"`
	Tick        string         `toml:"tick"`
	Jitter      bool           `toml:"jitter"`
	Measurement string         `toml:"measurement"`
	SeriesCount int            `toml:"series_count"`
	Tags        AbstractTags   `toml:"tag"`
	Fields      AbstractFields `toml:"field"`
	StartDate   string         `toml:"start_date"`
	Precision   string         `toml:"precision"`
	time        time.Time
	mu          sync.Mutex
}

// typeArr accepts a string array of types and
// returns an array of equal length where each
// element of the array is an instance of the type
// expressed in the string array.
func typeArr(a []string) []interface{} {
	i := make([]interface{}, len(a))
	for j, ty := range a {
		var t string
		switch ty {
		case "float64":
			t = fmt.Sprintf("%v", time.Now().Nanosecond()%1000)
		case "int":
			t = fmt.Sprintf("%vi", time.Now().Nanosecond()%1000)
		case "bool":
			b := time.Now().Nanosecond()%2 == 1
			t = fmt.Sprintf("%t", b)
		default:
			t = fmt.Sprintf("%v", time.Now().Nanosecond()%1000)
		}
		i[j] = t
	}

	return i
}

func (b *BasicPointGenerator) timestamp(t time.Time) int64 {
	var n int64

	if b.Precision == "s" {
		n = t.Unix()
	} else {
		n = t.UnixNano()
	}

	return n
}

// Template returns a function that returns a pointer to a Pnt.
func (b *BasicPointGenerator) Template() func(i int, t time.Time) *Pnt {
	ts := b.Tags.Template()
	fs, fa := b.Fields.Template()
	tmplt := fmt.Sprintf("%v,%v %v %%v", b.Measurement, ts, fs)

	return func(i int, t time.Time) *Pnt {
		p := &Pnt{}
		arr := []interface{}{i}
		arr = append(arr, typeArr(fa)...)
		arr = append(arr, b.timestamp(t))

		str := fmt.Sprintf(tmplt, arr...)
		p.Set([]byte(str))
		return p
	}
}

// Pnt is a struct that implements the Point interface.
type Pnt struct {
	line []byte
}

// Set sets the internal state for a Pnt.
func (p *Pnt) Set(b []byte) {
	p.line = b
}

// Next generates very simple points very
// efficiently.
// TODO: Take this out
func (p *Pnt) Next(i int, t time.Time) {
	p.line = []byte(fmt.Sprintf("a,b=c-%v v=%v", i, i))
}

// Line returns a byte array for a point
// in line protocol format.
func (p Pnt) Line() []byte {
	return p.line
}

// Graphite returns a byte array for a point
// in graphite format.
func (p Pnt) Graphite() []byte {
	// TODO: Implement
	return []byte("")
}

// OpenJSON returns a byte array for a point
// in opentsdb json format
func (p Pnt) OpenJSON() []byte {
	// TODO: Implement
	return []byte("")
}

// OpenTelnet returns a byte array for a point
// in opentsdb-telnet format
func (p Pnt) OpenTelnet() []byte {
	// TODO: Implement
	return []byte("")
}

// Generate returns a point channel. Implements the
// Generate method for the PointGenerator interface
func (b *BasicPointGenerator) Generate() (<-chan Point, error) {
	// TODO: should be 1.5x batch size
	c := make(chan Point, 15000)

	tmplt := b.Template()

	go func(c chan Point) {
		defer close(c)

		var start time.Time
		var err error
		if b.StartDate == "now" {
			start = time.Now()
		} else {
			start, err = time.Parse("2006-Jan-02", b.StartDate)
		}
		if err != nil {
			fmt.Println(err)
			return
		}

		b.mu.Lock()
		b.time = start
		b.mu.Unlock()

		tick, err := time.ParseDuration(b.Tick)
		if err != nil {
			fmt.Println(err)
			return
		}

		for i := 0; i < b.PointCount; i++ {
			b.mu.Lock()
			b.time = b.time.Add(tick)
			b.mu.Unlock()

			for j := 0; j < b.SeriesCount; j++ {
				p := tmplt(j, b.time)

				c <- *p
			}
		}
	}(c)

	return c, nil
}

// Time returns the timestamp for the latest points
// that are being generated. Implements the Time method
// for the PointGenerator interface.
func (b *BasicPointGenerator) Time() time.Time {
	defer b.mu.Unlock()
	b.mu.Lock()
	t := b.time
	return t
}

// BasicClient implements the InfluxClient
// interface.
type BasicClient struct {
	Enabled         bool     `toml:"enabled"`
	Addresses       []string `toml:"addresses"`
	Database        string   `toml:"database"`
	RetentionPolicy string   `toml:"retention-policy"`
	Precision       string   `toml:"precision"`
	BatchSize       int      `toml:"batch_size"`
	BatchInterval   string   `toml:"batch_interval"`
	Concurrency     int      `toml:"concurrency"`
	SSL             bool     `toml:"ssl"`
	Format          string   `toml:"format"`

	addrId   int
	r        chan<- response
	interval time.Duration
}

func (c *BasicClient) retry(b []byte, backoff time.Duration) {
	bo := backoff + backoffInterval
	rs, err := c.send(b)
	time.Sleep(c.interval)

	c.r <- rs
	if !rs.Success() || err != nil {
		time.Sleep(bo)
		c.retry(b, bo)
	}
}

// Batch groups together points
func (c *BasicClient) Batch(ps <-chan Point, r chan<- response) error {
	if !c.Enabled {
		return nil
	}
	instanceURLs := make([]string, len(c.Addresses))
	for i := 0; i < len(c.Addresses); i++ {
		instanceURLs[i] = fmt.Sprintf("http://%v/write?db=%v&rp=%v&precision=%v", c.Addresses[i], c.Database, c.RetentionPolicy, c.Precision)
	}

	c.Addresses = instanceURLs

	c.r = r
	var buf bytes.Buffer
	var wg sync.WaitGroup
	counter := NewConcurrencyLimiter(c.Concurrency)

	interval, err := time.ParseDuration(c.BatchInterval)
	if err != nil {
		return err
	}
	c.interval = interval

	ctr := 0

	writeBatch := func(b []byte) {
		wg.Add(1)
		counter.Increment()
		go func(byt []byte) {
			c.retry(byt, time.Duration(1))
			counter.Decrement()
			wg.Done()
		}(b)

	}

	for p := range ps {
		b := p.Line()
		c.addrId = ctr % len(c.Addresses)
		ctr++

		buf.Write(b)
		buf.Write([]byte("\n"))

		if ctr%c.BatchSize == 0 && ctr != 0 {
			b := buf.Bytes()
			if len(b) == 0 {
				continue
			}
			// Trimming the trailing newline character
			b = b[0 : len(b)-1]

			writeBatch(b)
			var temp bytes.Buffer
			buf = temp
		}
	}
	// Write out any remaining points
	b := buf.Bytes()
	if len(b) > 0 {
		writeBatch(b)
	}

	wg.Wait()

	return nil
}

// post sends a post request with a payload of points
func post(url string, datatype string, data io.Reader) (*http.Response, error) {
	resp, err := http.Post(url, datatype, data)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
		err := errors.New(string(body))
		return nil, err
	}

	return resp, nil
}

// Send calls post and returns a response
func (c *BasicClient) send(b []byte) (response, error) {

	t := NewTimer()
	resp, err := post(c.Addresses[c.addrId], "", bytes.NewBuffer(b))
	t.StopTimer()
	if err != nil {
		return response{Timer: t}, err
	}

	r := response{
		Resp:  resp,
		Time:  time.Now(),
		Timer: t,
	}

	return r, nil
}

// BasicQuery implements the QueryGenerator interface
type BasicQuery struct {
	Template   Query `toml:"template"`
	QueryCount int   `toml:"query_count"`
	time       time.Time
}

// QueryGenerate returns a Query channel
func (q *BasicQuery) QueryGenerate(now func() time.Time) (<-chan Query, error) {
	c := make(chan Query)

	go func(chan Query) {
		defer close(c)

		for i := 0; i < q.QueryCount; i++ {
			c <- Query(fmt.Sprintf(string(q.Template), i))
		}

	}(c)

	return c, nil
}

// SetTime sets the internal state of time
func (q *BasicQuery) SetTime(t time.Time) {
	q.time = t
}

// BasicQueryClient implements the QueryClient interface
type BasicQueryClient struct {
	Enabled       bool     `toml:"enabled"`
	Addresses     []string `toml:"addresses"`
	Database      string   `toml:"database"`
	QueryInterval string   `toml:"query_interval"`
	Concurrency   int      `toml:"concurrency"`
	clients       []client.Client
	addrId        int
}

// Init initializes the InfluxDB client
func (b *BasicQueryClient) Init() error {

	for _, a := range b.Addresses {
		cl, err := client.NewHTTPClient(client.HTTPConfig{
			Addr: fmt.Sprintf("http://%v", a),
		})

		if err != nil {
			return err
		}

		b.clients = append(b.clients, cl)
	}

	return nil
}

// Query runs the query
func (b *BasicQueryClient) Query(cmd Query) (response, error) {
	q := client.Query{
		Command:  string(cmd),
		Database: b.Database,
	}

	t := NewTimer()
	_, err := b.clients[b.addrId].Query(q)
	t.StopTimer()

	if err != nil {
		return response{Timer: t}, err
	}

	// Needs actual response type
	r := response{
		Time:  time.Now(),
		Timer: t,
	}

	return r, nil

}

// Exec listens to the query channel an executes queries as they come in
func (b *BasicQueryClient) Exec(qs <-chan Query, r chan<- response) error {
	if !b.Enabled {
		return nil
	}
	var wg sync.WaitGroup
	counter := NewConcurrencyLimiter(b.Concurrency)

	b.Init()

	interval, err := time.ParseDuration(b.QueryInterval)
	if err != nil {
		return err
	}

	ctr := 0

	for q := range qs {
		b.addrId = ctr % len(b.Addresses)
		ctr++

		wg.Add(1)
		counter.Increment()
		func(q Query) {
			defer wg.Done()
			qr, _ := b.Query(q)
			r <- qr
			time.Sleep(interval)
			counter.Decrement()
		}(q)
	}

	wg.Wait()

	return nil
}

// resetDB will drop an create a new database on an existing
// InfluxDB instance.
func resetDB(c client.Client, database string) error {
	_, err := c.Query(client.Query{
		Command: fmt.Sprintf("DROP DATABASE %s", database),
	})
	if err != nil {
		return err
	}

	_, err = c.Query(client.Query{
		Command: fmt.Sprintf("CREATE DATABASE %s", database),
	})
	return err
}

// BasicProvisioner implements the Provisioner
// interface.
type BasicProvisioner struct {
	Enabled       bool   `toml:"enabled"`
	Address       string `toml:"address"`
	Database      string `toml:"database"`
	ResetDatabase bool   `toml:"reset_database"`
}

// Provision runs the resetDB function.
func (b *BasicProvisioner) Provision() error {
	if !b.Enabled {
		return nil
	}

	cl, err := client.NewHTTPClient(client.HTTPConfig{
		Addr: fmt.Sprintf("http://%v", b.Address),
	})

	if err != nil {
		return err
	}

	if b.ResetDatabase {
		resetDB(cl, b.Database)
	}

	return nil
}

type BroadcastChannel struct {
	chs []chan response
	wg  sync.WaitGroup
	fns []func(t *Timer)
}

func NewBroadcastChannel() *BroadcastChannel {
	chs := make([]chan response, 0)
	return &BroadcastChannel{chs: chs}
}

func (b *BroadcastChannel) Register(fn responseHandler) {
	ch := make(chan response)

	b.chs = append(b.chs, ch)

	f := func(t *Timer) {
		go fn(ch, t)
	}

	b.fns = append(b.fns, f)
}

func (b *BroadcastChannel) Broadcast(r response) {

	b.wg.Add(1)
	for _, ch := range b.chs {
		b.wg.Add(1)
		go func(ch chan response) {
			ch <- r
			b.wg.Done()
		}(ch)
	}
	b.wg.Done()
}

func (b *BroadcastChannel) Close() {
	b.wg.Wait()
	for _, ch := range b.chs {
		close(ch)
		// Workaround
		time.Sleep(1 * time.Second)
	}
}

func (b *BroadcastChannel) Handle(rs <-chan response, t *Timer) {

	// Start all of the handlers
	for _, fn := range b.fns {
		fn(t)
	}

	for i := range rs {
		b.Broadcast(i)
	}
	b.Close()
}

// BasicWriteHandler handles write responses.
func (b *BasicClient) BasicWriteHandler(rs <-chan response, wt *Timer) {
	n := 0
	success := 0
	fail := 0

	s := time.Duration(0)

	for t := range rs {

		n++

		if t.Success() {
			success++
		} else {
			fail++
		}

		s += t.Timer.Elapsed()

	}

	if n == 0 {
		return
	}

	fmt.Printf("Total Requests: %v\n", n)
	fmt.Printf("	Success: %v\n", success)
	fmt.Printf("	Fail: %v\n", fail)
	fmt.Printf("Average Response Time: %v\n", s/time.Duration(n))
	fmt.Printf("Points Per Second: %v\n\n", int(float64(n)*float64(b.BatchSize)/float64(wt.Elapsed().Seconds())))
}

// BasicReadHandler handles read responses.
func (b *BasicQueryClient) BasicReadHandler(r <-chan response, rt *Timer) {
	n := 0
	s := time.Duration(0)
	for t := range r {
		n++
		s += t.Timer.Elapsed()
	}

	if n == 0 {
		return
	}

	fmt.Printf("Total Queries: %v\n", n)
	fmt.Printf("Average Query Response Time: %v\n\n", s/time.Duration(n))
}

func (o *outputConfig) HTTPHandler(method string) func(r <-chan response, rt *Timer) {
	return func(r <-chan response, rt *Timer) {
		c, _ := client.NewHTTPClient(client.HTTPConfig{
			Addr: o.addr,
		})
		bp, _ := client.NewBatchPoints(client.BatchPointsConfig{
			Database:        o.database,
			RetentionPolicy: o.retentionPolicy,
			Precision:       "ns",
		})
		for p := range r {
			tags := make(map[string]string, len(o.tags))
			for k, v := range o.tags {
				tags[k] = v
			}
			tags["method"] = method
			fields := map[string]interface{}{
				"response_time": float64(p.Timer.Elapsed()),
			}
			pt, _ := client.NewPoint("performance", tags, fields, p.Time)
			bp.AddPoint(pt)
			if len(bp.Points())%1000 == 0 && len(bp.Points()) != 0 {
				c.Write(bp)
				bp, _ = client.NewBatchPoints(client.BatchPointsConfig{
					Database:        o.database,
					RetentionPolicy: o.retentionPolicy,
					Precision:       "ns",
				})
			}
		}

		if len(bp.Points()) != 0 {
			c.Write(bp)
		}
	}
}
