// Copyright 2017, the Blazer authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package b2

import (
	"fmt"
	"html/template"
	"math"
	"net/http"
	"sort"
	"time"

	"github.com/Backblaze/blazer/internal/b2assets"
	"github.com/Backblaze/blazer/x/window"
)

// StatusInfo reports information about a client.
type StatusInfo struct {
	// Writers contains the status of all current uploads with progress.
	Writers map[string]*WriterStatus

	// Readers contains the status of all current downloads with progress.
	Readers map[string]*ReaderStatus

	// RPCs contains information about recently made RPC calls over the last
	// minute, five minutes, hour, and for all time.
	RPCs map[time.Duration]MethodList
}

// MethodList is an accumulation of RPC calls that have been made over a given
// period of time.
type MethodList []method

// CountByMethod returns the total RPC calls made per method.
func (ml MethodList) CountByMethod() map[string]int {
	r := make(map[string]int)
	for i := range ml {
		r[ml[i].name]++
	}
	return r
}

type method struct {
	name     string
	duration time.Duration
	status   int
}

type methodCounter struct {
	d time.Duration
	w *window.Window
}

func (mc methodCounter) record(m method) {
	mc.w.Insert([]method{m})
}

func (mc methodCounter) retrieve() MethodList {
	ms := mc.w.Reduce()
	return MethodList(ms.([]method))
}

func newMethodCounter(d, res time.Duration) methodCounter {
	r := func(i, j interface{}) interface{} {
		a, ok := i.([]method)
		if !ok {
			a = nil
		}
		b, ok := j.([]method)
		if !ok {
			b = nil
		}
		for _, m := range b {
			a = append(a, m)
		}
		return a
	}
	return methodCounter{
		d: d,
		w: window.New(d, res, r),
	}
}

// WriterStatus reports the status for each writer.
type WriterStatus struct {
	// Progress is a slice of completion ratios.  The index of a ratio is its
	// chunk id less one.
	Progress []float64
}

// ReaderStatus reports the status for each reader.
type ReaderStatus struct {
	// Progress is a slice of completion ratios.  The index of a ratio is its
	// chunk id less one.
	Progress []float64
}

// Status returns information about the current state of the client.
func (c *Client) Status() *StatusInfo {
	c.slock.Lock()
	defer c.slock.Unlock()

	si := &StatusInfo{
		Writers: make(map[string]*WriterStatus),
		Readers: make(map[string]*ReaderStatus),
		RPCs:    make(map[time.Duration]MethodList),
	}

	for name, w := range c.sWriters {
		si.Writers[name] = w.status()
	}

	for name, r := range c.sReaders {
		si.Readers[name] = r.status()
	}

	for _, c := range c.sMethods {
		si.RPCs[c.d] = c.retrieve()
	}

	return si
}

func (si *StatusInfo) table() map[string]map[string]int {
	r := make(map[string]map[string]int)
	for d, c := range si.RPCs {
		for _, m := range c {
			if _, ok := r[m.name]; !ok {
				r[m.name] = make(map[string]int)
			}
			dur := "all time"
			if d > 0 {
				dur = d.String()
			}
			r[m.name][dur]++
		}
	}
	return r
}

func (c *Client) addWriter(w *Writer) {
	c.slock.Lock()
	defer c.slock.Unlock()

	if c.sWriters == nil {
		c.sWriters = make(map[string]*Writer)
	}

	c.sWriters[fmt.Sprintf("%s/%s", w.o.b.Name(), w.name)] = w
}

func (c *Client) removeWriter(w *Writer) {
	c.slock.Lock()
	defer c.slock.Unlock()

	if c.sWriters == nil {
		return
	}

	delete(c.sWriters, fmt.Sprintf("%s/%s", w.o.b.Name(), w.name))
}

func (c *Client) addReader(r *Reader) {
	c.slock.Lock()
	defer c.slock.Unlock()

	if c.sReaders == nil {
		c.sReaders = make(map[string]*Reader)
	}

	c.sReaders[fmt.Sprintf("%s/%s", r.o.b.Name(), r.name)] = r
}

func (c *Client) removeReader(r *Reader) {
	c.slock.Lock()
	defer c.slock.Unlock()

	if c.sReaders == nil {
		return
	}

	delete(c.sReaders, fmt.Sprintf("%s/%s", r.o.b.Name(), r.name))
}

var (
	funcMap = template.FuncMap{
		"inc":    func(i int) int { return i + 1 },
		"lookUp": func(m map[string]int, s string) int { return m[s] },
		"pRange": func(i int) string {
			f := float64(i)
			min := int(math.Pow(2, f)) - 1
			max := min + int(math.Pow(2, f))
			return fmt.Sprintf("%v - %v", time.Duration(min)*time.Millisecond, time.Duration(max)*time.Millisecond)
		},
		"methods": func(si *StatusInfo) []string {
			methods := make(map[string]bool)
			for _, ms := range si.RPCs {
				for _, m := range ms {
					methods[m.name] = true
				}
			}
			var names []string
			for name := range methods {
				names = append(names, name)
			}
			sort.Strings(names)
			return names
		},
		"durations": func(si *StatusInfo) []string {
			var ds []time.Duration
			for d := range si.RPCs {
				ds = append(ds, d)
			}
			sort.Slice(ds, func(i, j int) bool { return ds[i] < ds[j] })
			var r []string
			for _, d := range ds {
				dur := "all time"
				if d > 0 {
					dur = d.String()
				}
				r = append(r, dur)
			}
			return r
		},
		"table": func(si *StatusInfo) map[string]map[string]int { return si.table() },
	}
	statusTemplate = template.Must(template.New("status").Funcs(funcMap).Parse(string(b2assets.MustAsset("data/status.html"))))
)

// ServeHTTP serves diagnostic information about the current state of the
// client; essentially everything available from Client.Status()
//
// ServeHTTP satisfies the http.Handler interface.  This means that a Client
// can be passed directly to a path via http.Handle (or on a custom ServeMux or
// a custom http.Server).
func (c *Client) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	info := c.Status()
	statusTemplate.Execute(rw, info)
}
