package main

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/hashicorp/golang-lru/v2/expirable"
	"github.com/samber/lo"
)

const CLINIC_ENDPOINT = "https://clinic.pingcap.com"

type ClinicClient struct {
	client      *http.Client
	credentials *expirable.LRU[string, http.Header]
	responses   *expirable.LRU[string, []byte]
}

type UserProfile struct {
	Email string `json:"email"`
}

// do NOT use int64: https://github.com/go-gota/gota/issues/22
type Cluster struct {
	ClusterCreatedAt     int    `json:"clusterCreatedAt,omitempty" dataframe:"cluster_created_at"`
	ClusterDataPlaneName string `json:"clusterDataPlaneName,omitempty" dataframe:"-"`
	ClusterDeletedAt     int    `json:"clusterDeletedAt,omitempty" dataframe:"-"`
	ClusterDeployType    string `json:"clusterDeployType,omitempty" dataframe:"-"`
	ClusterDuration      string `json:"clusterDuration,omitempty" dataframe:"-"`
	ClusterID            string `json:"clusterID" dataframe:"cluster_id"`
	ClusterK8sNamespace  string `json:"clusterK8sNamespace,omitempty" dataframe:"-"`
	ClusterName          string `json:"clusterName" dataframe:"cluster_name"`
	ClusterNetworkName   string `json:"clusterNetworkName,omitempty" dataframe:"-"`
	ClusterProviderName  string `json:"clusterProviderName,omitempty" dataframe:"provider"`
	ClusterRegionName    string `json:"clusterRegionName,omitempty" dataframe:"region"`
	ClusterShootName     string `json:"clusterShootName,omitempty" dataframe:"-"`
	ClusterStatus        string `json:"clusterStatus,omitempty" dataframe:"-"`
	ClusterType          string `json:"clusterType,omitempty" dataframe:"-"`
	ClusterVersion       string `json:"clusterVersion,omitempty" dataframe:"version"`
	OrgID                string `json:"orgID" dataframe:"-"`
	ProjectID            string `json:"projectID" dataframe:"-"`
	TenantID             string `json:"tenantID" dataframe:"tenant_id"`
	TenantName           string `json:"tenantName,omitempty" dataframe:"tenant_name"`
	TenantPlan           string `json:"tenantPlan,omitempty" dataframe:"-"`
}

// do NOT use int64: https://github.com/go-gota/gota/issues/22
type ClusterEvent struct {
	TenantID  string `dataframe:"tenant_id"`
	ClusterID string `dataframe:"cluster_id"`
	Name      string `dataframe:"name"`
	Status    string `dataframe:"status"`
	Time      int    `dataframe:"time"`
	Summary   string `dataframe:"summary"`
}

type ResourceQuota struct {
	AccountOrProject string
	Region           string
	Resource         string
	Limit            float64
	Used             float64
	Usage            float64
}

type PrometheusMetric struct {
	Results map[string]struct {
		Frames []struct {
			Data struct {
				Values [][]float64 `json:"values"`
			} `json:"data"`
			Schema struct {
				Name string `json:"name"`
			} `json:"schema"`
		} `json:"frames"`
	} `json:"results"`
}

func flatten(ms map[string]any) map[string]any {
	result := map[string]any{}
	for k, v := range ms {
		var sub map[string]any

		if m, ok := v.(map[string]any); ok {
			sub = flatten(m)
		} else if m, ok := v.(map[any]any); ok {
			fixM := map[string]any{}
			for k, v := range m {
				if sk, ok := k.(string); ok {
					fixM[sk] = v
				}
			}
			sub = flatten(fixM)
		} else {
			result[k] = v
			continue
		}

		for sk, sv := range sub {
			result[k+"."+sk] = sv
		}
	}
	return result
}

func NewClient() *ClinicClient {
	return &ClinicClient{
		client:      &http.Client{},
		credentials: expirable.NewLRU[string, http.Header](16, nil, time.Hour*24),
		responses:   expirable.NewLRU[string, []byte](0, nil, time.Hour*1),
	}
}

func (c *ClinicClient) AddCredential(h http.Header) {
	credential := http.Header{}
	credential.Set("cookie", h.Get("x-cookie"))
	credential.Set("x-csrf-token", h.Get("x-csrf-token"))
	credential.Set("content-type", "application/json")
	user, err := c.whoami(credential)
	if err != nil {
		return
	}
	c.credentials.Add(user, credential)
	fmt.Println("credential of", user, "added")
}

func (c *ClinicClient) DoWithCredential(r *http.Request) (res *http.Response, err error) {
	if r.Method == http.MethodGet {
		if dump, ok := c.responses.Get(r.URL.String()); ok {
			return http.ReadResponse(bufio.NewReader(bytes.NewReader(dump)), nil)
		}

		defer func() {
			if err == nil && res.StatusCode < 300 {
				if dump, err := httputil.DumpResponse(res, true); err == nil {
					c.responses.Add(r.URL.String(), dump)
				}
			}
		}()
	}

	for _, k := range c.credentials.Keys() {
		header, ok := c.credentials.Get(k)
		if !ok {
			continue
		}

		for hn := range header {
			r.Header.Set(hn, header.Get(hn))
		}
		res, err = c.client.Do(r)
		if err != nil {
			return nil, err
		}
		if res.StatusCode == 401 {
			c.credentials.Remove(k)
			continue
		}
		return res, nil
	}

	return nil, errors.New("no valid credential")
}

func (c *ClinicClient) BatchDoWithCredential(rs []*http.Request) ([]*http.Response, []error) {
	var mu sync.Mutex
	var wg sync.WaitGroup
	xs, es := make([]*http.Response, len(rs)), make([]error, len(rs))

	for idx := 0; idx < len(rs); idx++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			x, e := c.DoWithCredential(rs[i])
			mu.Lock()
			xs[i], es[i] = x, e
			mu.Unlock()
		}(idx)
	}
	wg.Wait()

	return xs, es
}

func (c *ClinicClient) whoami(credential http.Header) (string, error) {
	url := CLINIC_ENDPOINT + "/clinic/api/v1/user/profile"
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return "", err
	}
	req.Header = credential

	res, err := c.client.Do(req)
	if err != nil {
		return "", err
	}
	defer res.Body.Close()

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

	if res.StatusCode != 200 {
		return "", errors.New(string(body))
	}

	var user UserProfile
	if err := json.Unmarshal(body, &user); err != nil {
		return "", err
	}
	return user.Email, nil
}

func (c *ClinicClient) ListClusters(query url.Values) ([]Cluster, uint, error) {
	url := CLINIC_ENDPOINT + "/clinic/api/v1/dashboard/clusters?" + query.Encode()

	r, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return nil, 0, err
	}

	res, err := c.DoWithCredential(r)
	if err != nil {
		return nil, 0, err
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, 0, err
	}
	if res.StatusCode != 200 {
		return nil, 0, errors.New(string(body))
	}

	var payload struct {
		Items []Cluster `json:"items"`
		Total uint      `json:"total"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, 0, err
	}
	return payload.Items, payload.Total, nil
}

func (c *ClinicClient) ClusterProxyAvailability(
	ctx context.Context, clusters []Cluster,
) ([]bool, []error) {
	rs := lo.Map(clusters, func(cluster Cluster, _ int) *http.Request {
		url := CLINIC_ENDPOINT + "/clinic/api/v1/dashboard/proxy/orgs/" + cluster.TenantID +
			"/clusters/" + cluster.ClusterID + "/pd/dashboard/api/info/whoami"

		r, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
		return r
	})

	xs, errs := c.BatchDoWithCredential(rs)
	ys := make([]bool, len(xs))

	for idx, res := range xs {
		// if this request failed
		if errs[idx] != nil {
			continue
		}
		defer res.Body.Close()

		var body []byte
		if body, errs[idx] = ioutil.ReadAll(res.Body); errs[idx] != nil {
			continue
		}

		if res.StatusCode != 200 {
			errs[idx] = errors.New(string(body))
			continue
		}

		ys[idx] = true
	}

	return ys, errs
}

func (c *ClinicClient) tiFlashConfig(ctx context.Context, clusters []Cluster) ([][][]string, []error) {
	rs := lo.Map(clusters, func(cluster Cluster, _ int) *http.Request {
		url := CLINIC_ENDPOINT + "/clinic/api/v1/dashboard/proxy/orgs/" + cluster.TenantID +
			"/clusters/" + cluster.ClusterID + "/tiflash/config"

		r, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
		return r
	})

	xs, errs := c.BatchDoWithCredential(rs)
	xss := make([][][]string, len(xs))

	for idx, res := range xs {
		// if this request failed
		if errs[idx] != nil {
			continue
		}
		defer res.Body.Close()

		var body []byte
		body, errs[idx] = ioutil.ReadAll(res.Body)
		if errs[idx] != nil {
			continue
		}

		if res.StatusCode != 200 {
			errs[idx] = errors.New(string(body))
			continue
		}

		var payload map[string]any
		if errs[idx] = json.Unmarshal(body, &payload); errs[idx] != nil {
			continue
		}

		configs := flatten(payload)
		xss[idx] = [][]string{}
		for k, v := range configs {
			xss[idx] = append(xss[idx], []string{
				"tiflash_config", k, fmt.Sprint(v),
			})
		}
	}

	return xss, errs
}

func (c *ClinicClient) ClusterConfig(ctx context.Context, clusters []Cluster) ([][][]string, []error) {
	rs := lo.Map(clusters, func(cluster Cluster, _ int) *http.Request {
		url := CLINIC_ENDPOINT + "/clinic/api/v1/dashboard/proxy/orgs/" + cluster.TenantID +
			"/clusters/" + cluster.ClusterID + "/pd/dashboard/api/configuration/all"

		r, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
		return r
	})

	xs, errs := c.BatchDoWithCredential(rs)
	ys, _ := c.tiFlashConfig(ctx, clusters)
	xss := make([][][]string, len(xs))

	for idx, res := range xs {
		// if this request failed
		if errs[idx] != nil {
			continue
		}
		defer res.Body.Close()

		var body []byte
		body, errs[idx] = ioutil.ReadAll(res.Body)
		if errs[idx] != nil {
			continue
		}
		if res.StatusCode != 200 {
			errs[idx] = errors.New(string(body))
			continue
		}

		var payload struct {
			Items map[string][]struct {
				Name  string `json:"id"`
				Value any    `json:"value"`
			} `json:"items"`
		}
		if errs[idx] = json.Unmarshal(body, &payload); errs[idx] != nil {
			continue
		}

		xss[idx] = [][]string{}
		for component, items := range payload.Items {
			for _, item := range items {
				xss[idx] = append(xss[idx], []string{
					component, item.Name, fmt.Sprint(item.Value),
				})
			}
		}
		xss[idx] = append(xss[idx], ys[idx]...)
	}

	return xss, errs
}

func (c *ClinicClient) queryDS(queries []map[string]any, from, to int64, header map[string]string) (*PrometheusMetric, error) {
	url := CLINIC_ENDPOINT + "/grafana/api/ds/query"

	payload, err := json.Marshal(map[string]any{
		"queries": queries,
		"from":    fmt.Sprint(from),
		"to":      fmt.Sprint(to),
	})
	if err != nil {
		return nil, err
	}

	r, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payload))
	for k, v := range header {
		r.Header.Set(k, v)
	}
	res, err := c.DoWithCredential(r)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	if res.StatusCode != 200 {
		return nil, errors.New(string(body))
	}

	var metric PrometheusMetric
	if err := json.Unmarshal(body, &metric); err != nil {
		return nil, err
	}
	return &metric, nil
}

func (c *ClinicClient) accountQuota(queries []map[string]any) ([]*ResourceQuota, error) {
	ts := time.Now().Unix() * 1000
	metric, err := c.queryDS(queries, ts, ts, nil)
	if err != nil {
		return nil, err
	}

	used_map := map[string]float64{}
	for _, frame := range metric.Results["used"].Frames {
		used_map[frame.Schema.Name] = frame.Data.Values[1][0]
	}

	limit_map := map[string]float64{}
	for _, frame := range metric.Results["limit"].Frames {
		limit_map[frame.Schema.Name] = frame.Data.Values[1][0]
	}

	usage_map := map[string]float64{}
	for _, frame := range metric.Results["usage"].Frames {
		usage_map[frame.Schema.Name] = frame.Data.Values[1][0]
	}

	xs := []*ResourceQuota{}
	for name := range usage_map {
		ss := strings.Split(name, "/")
		xs = append(xs, &ResourceQuota{
			AccountOrProject: ss[0],
			Region:           ss[1],
			Resource:         ss[2],
			Used:             used_map[name],
			Limit:            limit_map[name],
			Usage:            usage_map[name],
		})
	}

	return xs, nil
}

func (c *ClinicClient) AWSAccountQuota() ([]*ResourceQuota, error) {
	ds := map[string]string{
		"uid":  "Ak6VpZD4z",
		"type": "prometheus",
	}
	return c.accountQuota([]map[string]any{
		map[string]any{
			"refId":        "used",
			"datasource":   ds,
			"instant":      true,
			"expr":         "sum(awsaccount_quota_usage{}) by (aws_account,exported_region,resource)",
			"legendFormat": "{{aws_account}}/{{exported_region}}/{{resource}}",
		},
		map[string]any{
			"refId":        "limit",
			"datasource":   ds,
			"instant":      true,
			"expr":         "sum(awsaccount_quota_limit{}) by (aws_account,exported_region,resource)",
			"legendFormat": "{{aws_account}}/{{exported_region}}/{{resource}}",
		},
		map[string]any{
			"refId":      "usage",
			"datasource": ds,
			"instant":    true,
			"expr": "sum(awsaccount_quota_usage{}) by (aws_account,exported_region,resource) /" +
				" sum(awsaccount_quota_limit{}) by (aws_account,exported_region,resource)",
			"legendFormat": "{{aws_account}}/{{exported_region}}/{{resource}}",
		},
	})
}

func (c *ClinicClient) GCPAccountQuota() ([]*ResourceQuota, error) {
	ds := map[string]string{
		"uid":  "zk64tWv4z",
		"type": "prometheus",
	}
	return c.accountQuota([]map[string]any{
		map[string]any{
			"refId":        "used",
			"datasource":   ds,
			"instant":      true,
			"expr":         "sum(gcpproject_quota_usage{gcp_project=~\"dbaas-prod\",control_plane_info=\"seed/gcp-us-west1\"}) by (gcp_project,exported_region,metric)",
			"legendFormat": "{{gcp_project}}/{{exported_region}}/{{metric}}",
		},
		map[string]any{
			"refId":        "limit",
			"datasource":   ds,
			"instant":      true,
			"expr":         "sum(gcpproject_quota_limit{gcp_project=~\"dbaas-prod\",control_plane_info=\"seed/gcp-us-west1\"}) by (gcp_project,exported_region,metric)",
			"legendFormat": "{{gcp_project}}/{{exported_region}}/{{metric}}",
		},
		map[string]any{
			"refId":      "usage",
			"datasource": ds,
			"instant":    true,
			"expr": "sum(gcpproject_quota_usage{gcp_project=~\"dbaas-prod\",control_plane_info=\"seed/gcp-us-west1\"}) by (gcp_project,exported_region,metric) /" +
				" sum(gcpproject_quota_limit{gcp_project=~\"dbaas-prod\",control_plane_info=\"seed/gcp-us-west1\"}) by (gcp_project,exported_region,metric)",
			"legendFormat": "{{gcp_project}}/{{exported_region}}/{{metric}}",
		},
	})
}

// Recent events from eventbus
func (c *ClinicClient) ClusterEvents(
	ctx context.Context, clusters []Cluster, begin_ts, end_ts int64,
) ([]ClusterEvent, []error) {
	type task_t struct {
		cluster  Cluster
		start_ts int64
		stop_ts  int64
	}

	tasks := []task_t{}

	// init tasks for every day
	step := int64(24 * 60 * 60)
	for _, cluster := range clusters {
		for start_ts := begin_ts - begin_ts%step; start_ts < end_ts; start_ts += step {
			stop_ts := start_ts + step
			if stop_ts > end_ts {
				stop_ts = end_ts
			}
			tasks = append(tasks, task_t{
				cluster:  cluster,
				start_ts: start_ts,
				stop_ts:  stop_ts,
			})
		}
	}

	limit := 100
	batch_size := 10
	events := []ClusterEvent{}
	error_list := []error{}
	for len(tasks) > 0 {
		this_batch := []task_t{}
		if len(tasks) > batch_size {
			this_batch = tasks[:batch_size]
			tasks = tasks[batch_size:]
		} else {
			this_batch = tasks
			tasks = nil
		}

		rs := lo.Map(this_batch, func(task task_t, _ int) *http.Request {
			url := fmt.Sprintf(
				"%s/clinic/api/v1/activityhub/applications/%s/targets/%s/activities?begin_ts=%d&end_ts=%d&limit=%d",
				CLINIC_ENDPOINT, task.cluster.OrgID, task.cluster.ClusterID, task.start_ts, task.stop_ts, limit,
			)

			r, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
			return r
		})

		xs, errs := c.BatchDoWithCredential(rs)
		for idx, res := range xs {
			// if this request failed
			if errs[idx] != nil {
				continue
			}
			defer res.Body.Close()

			var body []byte
			if body, errs[idx] = ioutil.ReadAll(res.Body); errs[idx] != nil {
				continue
			}
			if res.StatusCode != 200 {
				errs[idx] = errors.New(string(body))
				continue
			}

			var data struct {
				Activities []struct {
					TenantID  string `json:"application_id"`
					ClusterID string `json:"target_id"`
					Name      string `json:"name"`
					Time      int64  `json:"calibration_time"`
					Payload   struct {
						Summary string `json:"summary"`
					} `json:"payload"`
					Tags struct {
						Status string `json:"status"`
					} `json:"tags,omitempty"`
				} `json:"activities"`
			}
			if errs[idx] = json.Unmarshal(body, &data); errs[idx] != nil {
				continue
			}

			for _, act := range data.Activities {
				events = append(events, ClusterEvent{
					TenantID:  act.TenantID,
					ClusterID: act.ClusterID,
					Name:      act.Name,
					Status:    act.Tags.Status,
					Time:      int(act.Time * 1000),
					Summary:   act.Payload.Summary,
				})
			}

			// means the response is full, should continue with new time range
			if len(data.Activities) == limit {
				this_batch[idx].stop_ts = data.Activities[limit-1].Time
				tasks = append(tasks, this_batch[idx])
			}
		}
		error_list = append(error_list, errs...)
	}
	return events, error_list
}
