package main

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"os/user"
	"sort"
	"time"
)

const (
	yasmTimeout = 10

	yasmHosts = "ASEARCH"

	yasmAddr         = "yasm.yandex-team.ru"
	yasmPushAddr     = "localhost:11005"
	golovanUserAgent = "balancer yasmtrending"

	yasmHistURL     = "/hist/series"
	yasmMetainfoURL = "/metainfo/signals"
	yasmRtURL       = "/rt/"
)

type RequstType int

const (
	GET RequstType = iota
	POST
)

type YasmTags struct {
	Itype string `yaml:"itype"`
	Ctype string `yaml:"ctype"`
	Prj   string `yaml:"prj"`
}

type YasmAPIInterface interface {
	Client() *http.Client
	Hostname() string
	User() string
	Timeout() time.Duration
}

type YasmHTTPApi struct {
	client   http.Client
	hostname string
	user     string
	timeout  time.Duration
}

func (t YasmHTTPApi) Client() *http.Client {
	return &t.client
}

func (t YasmHTTPApi) Hostname() string {
	return t.hostname
}

func (t YasmHTTPApi) User() string {
	return t.user
}

func (t YasmHTTPApi) Timeout() time.Duration {
	return t.timeout
}

type YasmAgentHTTPApi struct {
	client   http.Client
	hostname string
	user     string
	timeout  time.Duration
}

func (t YasmAgentHTTPApi) Client() *http.Client {
	return &t.client
}

func (t YasmAgentHTTPApi) Hostname() string {
	return t.hostname
}

func (t YasmAgentHTTPApi) User() string {
	return t.user
}

func (t YasmAgentHTTPApi) Timeout() time.Duration {
	return t.timeout
}

// Two type of API with no intersection by methods
func NewYasmHTTPApi() (*YasmHTTPApi, error) {
	client := http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	hostname, err := os.Hostname()
	if err != nil {
		return nil, err
	}

	username, err := user.Current()
	if err != nil {
		return nil, err
	}

	return &YasmHTTPApi{client, hostname, username.Username, time.Duration(yasmTimeout)}, nil
}

func NewYasmAgentHTTPApi() (*YasmAgentHTTPApi, error) {
	client := http.Client{
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	hostname, err := os.Hostname()
	if err != nil {
		return nil, err
	}

	username, err := user.Current()
	if err != nil {
		return nil, err
	}

	return &YasmAgentHTTPApi{client, hostname, username.Username, time.Duration(yasmTimeout)}, nil
}

func parseSignalsByPattern(data []byte) ([]string, error) {
	topLevel := make(map[string]interface{})

	err := json.Unmarshal(data, &topLevel)
	if err != nil {
		return nil, err
	}

	if st, ok := topLevel["status"]; !ok || st != "ok" {
		return nil, fmt.Errorf("json has no correct status field [%s]", string(data))
	}

	if _, ok := topLevel["response"]; !ok {
		return nil, fmt.Errorf("json has no correct response field [%s]", string(data))
	}

	response := topLevel["response"].(map[string]interface{})
	result, ok := response["result"].([]interface{})
	if !ok {
		return nil, fmt.Errorf("json has no correct result field [%s]", string(data))
	}

	ret := make([]string, len(result))
	for i, value := range result {
		if v, ok := value.(string); !ok {
			return nil, fmt.Errorf("json has incorrent type of item in result field [%s]", string(data))
		} else {
			ret[i] = v
		}
	}

	return ret, nil
}

func (t YasmHTTPApi) RequestSignalsByPattern(tags YasmTags, pattern string) ([]string, error) {
	url := fmt.Sprintf("http://%s%s?ctype=%s&itype=%s&prj=%s&signal_pattern=%s",
		yasmAddr, yasmMetainfoURL, tags.Ctype, tags.Itype, tags.Prj, pattern)

	data, err := t.makeRequest(GET, url, nil, nil)
	if err != nil {
		return nil, err
	}

	return parseSignalsByPattern(data)
}

type YasmJSONHistRequest struct {
	Name    string   `json:"name"`
	ID      string   `json:"id"`
	Host    string   `json:"host"`
	St      int64    `json:"st"`
	Et      int64    `json:"et"`
	Period  int      `json:"period"`
	Signals []string `json:"signals"`
}

type YasmJSONHistRootRequest struct {
	CtxList []YasmJSONHistRequest `json:"ctxList"`
}

type YasmJSONHistResponse struct {
	signal   string
	timeline []float64
	values   []float64
}

func prepareHistJSON(period int, startTime int64, endTime int64, signals []string) ([]byte, error) {
	// Request as slice of requests
	jsonStruct := YasmJSONHistRootRequest{[]YasmJSONHistRequest{YasmJSONHistRequest{
		"hist",
		fmt.Sprintf("%s:%d_%d_%d", yasmHosts, startTime, endTime, period),
		yasmHosts,
		startTime,
		endTime,
		period,
		signals,
	}}}

	jsonValue, err := json.Marshal(jsonStruct)
	if err != nil {
		return nil, err
	}

	return jsonValue, nil
}

func parseHistJSON(jsonData []byte, requestID string) ([]YasmJSONHistResponse, error) {
	topLevel := make(map[string]interface{})

	err := json.Unmarshal(jsonData, &topLevel)
	if err != nil {
		return nil, err
	}

	if st, ok := topLevel["status"]; !ok || st != "ok" {
		return nil, fmt.Errorf("json has no correct status field [%s]", string(jsonData))
	}

	if _, ok := topLevel["response"]; !ok {
		return nil, fmt.Errorf("json has no correct response field [%s]", string(jsonData))
	}

	response := topLevel["response"].(map[string]interface{})
	item, ok := response[requestID].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("json has no correct %s field [%s]", requestID, string(jsonData))
	}

	content, ok := item["content"].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("json has no correct content field [%s]", string(jsonData))
	}

	var timeline []float64
	_, ok = content["timeline"].([]interface{})
	if !ok {
		return nil, fmt.Errorf("json has no correct timeline field [%s]", string(jsonData))
	}

	for _, k := range content["timeline"].([]interface{}) {
		_, ok = k.(float64)
		if !ok {
			return nil, fmt.Errorf("json has no correct timeline field, incorrect value type [%s]", string(jsonData))
		}

		timeline = append(timeline, k.(float64))
	}
	sort.Float64s(timeline)

	values, ok := content["values"].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("json has no correct values field [%s]", string(jsonData))
	}

	ret := make([]YasmJSONHistResponse, 0)
	for k := range values {
		var vals []float64
		for _, v := range values[k].([]interface{}) {
			_, ok = v.(float64)
			if !ok {
				return nil, fmt.Errorf("json has no correct values field, incorrect value type [%s]", string(jsonData))
			}

			vals = append(vals, v.(float64))
		}
		sort.Float64s(vals)

		ret = append(ret, YasmJSONHistResponse{k, timeline, vals})
	}

	return ret, nil
}

func (t YasmHTTPApi) RequestHist(period int, startTime int64, endTime int64, signals []string) ([]YasmJSONHistResponse, error) {
	url := fmt.Sprintf("http://%s%s", yasmAddr, yasmHistURL)

	data, err := prepareHistJSON(period, startTime, endTime, signals)
	if err != nil {
		return nil, err
	}

	req, err := t.makeRequest(POST, url, data, nil)
	if err != nil {
		return nil, err
	}

	ret, err := parseHistJSON(req, fmt.Sprintf("%s:%d_%d_%d", yasmHosts, startTime, endTime, period))
	if err != nil {
		return nil, err
	}

	return ret, nil
}

type YasmJSONRtRootRequest struct {
	Signals []string `json:"signals"`
}

type YasmJSONRtResponse struct {
	signal    string
	timestamp int64
	errors    []string
	value     *float64
}

func prepareRtJSON(signals []string) ([]byte, error) {
	jsonStruct := YasmJSONRtRootRequest{
		Signals: signals,
	}

	jsonValue, err := json.Marshal(jsonStruct)
	if err != nil {
		return nil, err
	}

	return jsonValue, nil
}

func prepareRtHash(signals []string) string {
	md5Hash := md5.New()
	for _, i := range signals {
		md5Hash.Write([]byte(i))
	}

	return hex.EncodeToString(md5Hash.Sum([]byte{})[:])
}

func parseRtJSON(jsonData []byte) ([]YasmJSONRtResponse, error) {
	topLevel := make(map[string]interface{})

	err := json.Unmarshal(jsonData, &topLevel)
	if err != nil {
		return nil, err
	}

	if st, ok := topLevel["status"]; !ok || st != "ok" {
		return nil, fmt.Errorf("json has no correct status field [%s]", string(jsonData))
	}

	if _, ok := topLevel["response"]; !ok {
		return nil, fmt.Errorf("json has no correct response field [%s]", string(jsonData))
	}

	response, ok := topLevel["response"].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("json has no correct response field type list [%s]", string(jsonData))
	}

	ret := make([]YasmJSONRtResponse, 0)
	for k, v := range response {
		signal := k
		var timestamp float64
		var errorsList []string
		var value *float64

		item, ok := v.([]interface{})
		if !ok {
			continue
		}

		if len(item) == 0 {
			continue
		}

		info, ok := item[0].(map[string]interface{})
		if !ok {
			return nil, fmt.Errorf("json has incorrect structure [%s]", string(jsonData))
		}

		if _, ok := info["timestamp"]; !ok {
			return nil, fmt.Errorf("json has no correct response field timestamp [%s]", string(jsonData))
		}

		if timestamp, ok = info["timestamp"].(float64); !ok {
			return nil, fmt.Errorf("could convert timestamp field to int64 [%s]", string(jsonData))
		}

		if _, ok := info["errors"]; !ok {
			return nil, fmt.Errorf("json has no correct response field errors [%s]", string(jsonData))
		}

		if errorsIfaceList, ok := info["errors"].([]interface{}); !ok {
			return nil, fmt.Errorf("could convert errors field to array] [%s]", string(jsonData))
		} else {
			for _, i := range errorsIfaceList {
				if x, ok := i.(string); !ok {
					return nil, fmt.Errorf("could convert errors field to string] [%s]", string(jsonData))
				} else {
					errorsList = append(errorsList, x)
				}
			}
		}

		if _, ok := info["value"]; !ok {
			return nil, fmt.Errorf("json has no correct response field value [%s]", string(jsonData))
		}

		if info["value"] != nil {
			if v, ok := info["value"].(float64); !ok {
				return nil, fmt.Errorf("could convert value field to float [%s]", string(jsonData))
			} else {
				value = &v
			}
		}

		ret = append(ret, YasmJSONRtResponse{
			signal:    signal,
			timestamp: int64(timestamp),
			errors:    errorsList,
			value:     value,
		})
	}

	return ret, nil
}

func (t YasmHTTPApi) RequestRt(signals []string) ([]YasmJSONRtResponse, error) {
	url := fmt.Sprintf("http://%s%s", yasmAddr, yasmRtURL)

	data, err := prepareRtJSON(signals)
	if err != nil {
		return nil, fmt.Errorf("request RT format error: %w", err)
	}

	rtHash := prepareRtHash(signals)

	req, err := t.makeRequest(POST, url, data, map[string]string{"X-Golovan-Rt-Request": rtHash})
	if err != nil {
		return nil, fmt.Errorf("request RT request error: %w", err)
	}

	ret, err := parseRtJSON(req)
	if err != nil {
		return nil, fmt.Errorf("request RT parse error: %w", err)
	}

	return ret, nil
}

type YasmPushJSONRequest struct {
	Name string            `json:"name"`
	Tags map[string]string `json:"tags"`
	Val  float64           `json:"val"`
}

func (t YasmAgentHTTPApi) PushData(data YasmPushJSONRequest) ([]byte, error) {
	url := fmt.Sprintf("http://%s", yasmPushAddr)

	body, err := json.Marshal([]YasmPushJSONRequest{data})
	if err != nil {
		return nil, err
	}

	resp, err := t.makeRequest(POST, url, body, nil)
	if err != nil {
		return nil, err
	}

	return resp, nil
}

func (t YasmHTTPApi) makeRequest(requestType RequstType, url string, data []byte, hdrs map[string]string) ([]byte, error) {
	return makeRequest(t, requestType, url, data, hdrs)
}

func (t YasmAgentHTTPApi) makeRequest(requestType RequstType, url string, data []byte, hdrs map[string]string) ([]byte, error) {
	return makeRequest(t, requestType, url, data, hdrs)
}

func makeRequest(t YasmAPIInterface, requestType RequstType, url string, data []byte, hdrs map[string]string) ([]byte, error) {
	var req *http.Request
	var err error

	switch requestType {
	case GET:
		req, err = http.NewRequest("GET", url, nil)
	case POST:
		req, err = http.NewRequest("POST", url, bytes.NewBuffer(data))
	default:
		return nil, errors.New("incorrect request method")
	}

	if err != nil {
		return nil, errors.New("could not create request")
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", golovanUserAgent)
	req.Header.Set("X-Golovan-Hostname", t.Hostname())
	req.Header.Set("X-Golovan-Username", t.User())
	if len(hdrs) > 0 {
		for k, v := range hdrs {
			req.Header.Set(k, v)
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), t.Timeout()*time.Second)
	defer cancel()

	resp, err := t.Client().Do(req.WithContext(ctx))
	if err != nil {
		return nil, err
	}

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

	err = resp.Body.Close()
	if err != nil {
		return nil, err
	}

	return body, nil
}
