//go:build snapshot
// +build snapshot

package utils

import (
	"cloud.google.com/go/storage"
	"context"
	"encoding/json"
	"fmt"
	"github.com/ajg/form"
	v2Aws "github.com/aws/aws-sdk-go-v2/aws"
	v2Config "github.com/aws/aws-sdk-go-v2/config"
	v2S3 "github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"google.golang.org/api/iterator"
	"gopkg.in/yaml.v2"
	"io"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strings"
	"time"
)

type ConfigInfo struct {
	Auth0              Auth0Req   `yaml:"auth0"`
	Region             RegionInfo `yaml:"region"`
	ClinicToken        string     `yaml:"clinicToken"`
	ClinicTokenStaging string     `yaml:"clinicTokenStaging"`
	ClinicURL          string     `yaml:"clinicURL"`
	Env                string     `yaml:"env"`
}

type RegionInfo struct {
	ServerURL string              `yaml:"serverURL"`
	Region    map[string][]string `yaml:"region"`
	BizType   []string            `yaml:"bizType"`
	OldPath   []string            `yaml:"oldPath"`
}

type Auth0Req struct {
	GrantType    string `yaml:"grantType" json:"grant_type" form:"grant_type"`
	ClientID     string `yaml:"clientID" json:"client_id" form:"client_id"`
	ClientSecret string `yaml:"clientSecret" json:"client_secret" form:"client_secret"`
	Audience     string `yaml:"audience" json:"audience" form:"audience"`
}

type Interval struct {
	Day    int `yaml:"day"`
	Hour   int `yaml:"hour"`
	Minute int `yaml:"minute"`
}

type Auth0Resp struct {
	AccessToken string `json:"access_token" form:"access_token"`
}

var config *ConfigInfo
var token string
var ApiPrefix = "/api/v1"
var s3RequestCount int
var gcsRequestCount int

func GetRegion() *RegionInfo {
	if config == nil {
		panic("config is nil")
	}
	return &config.Region
}

func GetConfig() *ConfigInfo {
	if config == nil {
		panic("config is nil")
	}
	return config
}

func DefaultAuth0Req() Auth0Req {
	return config.Auth0
}

func PrepareToken() {
	body, err := form.EncodeToString(DefaultAuth0Req())
	if err != nil {
		panic(err)
	}
	resp, err := http.Post("https://tidb-soc2.us.auth0.com/oauth/token",
		"application/x-www-form-urlencoded",
		strings.NewReader(body))
	if err != nil {
		panic(err)
	}
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	auth0Resp := &Auth0Resp{}
	err = json.Unmarshal(respContent, auth0Resp)
	if err != nil {
		panic(err)
	}
	token = auth0Resp.AccessToken
}

func PrepareConfig(path string) {
	in, err := os.ReadFile(path)
	if err != nil {
		panic(err)
	}
	var c ConfigInfo
	err = yaml.Unmarshal(in, &c)
	if err != nil {
		panic(err)
	}
	config = &c
}

type QueryResponse struct {
	Status    string `json:"status"`
	IsPartial bool   `json:"isPartial"`
	Data      Data   `json:"data"`
}

type Data struct {
	ResultType string   `json:"resultType"`
	Result     []Result `json:"result"`
}

type Result struct {
	Metric map[string]string `json:"metric"`
	Value  []interface{}     `json:"value"`
}

type AlertRequest struct {
	Labels       map[string]string `json:"labels"`
	Annotations  map[string]string `json:"annotations"`
	GeneratorURL string            `json:"generatorURL" validate:"optional"`
	StartsAt     string            `json:"startsAt" validate:"optional"`
}

type AlertPostResponse struct {
}

func QueryCluster(queryReq message.QueryClustersReq) *message.QueryClustersResp {
	request := QueryReq(queryReq)
	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	clustersResp := &message.QueryClustersResp{}
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	err = UnmarshalRespData(respContent, clustersResp)
	if err != nil {
		panic(err)
	}
	return clustersResp
}

func QueryReq(req message.QueryClustersReq) *http.Request {
	r := GetRegion()
	queryString := fmt.Sprintf("%s%s/clusters/?vendor=%s&region=%s&bizType=%s",
		r.ServerURL, ApiPrefix, req.Vendor, req.Region, req.BizType)
	request, _ := http.NewRequest("GET", queryString, nil)
	WithHeader(request)
	return request
}

func GetClusterDetail(req message.DetailClusterReq) *structs.ObservabilityCluster {
	request := DetailReq(req)
	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	if resp.StatusCode == http.StatusNotFound {
		return nil
	}
	clustersResp := &message.DetailClusterResp{}
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	err = UnmarshalRespData(respContent, clustersResp)
	if err != nil {
		panic(fmt.Sprintf("respContent [%s]: %s", string(respContent[:]), err.Error()))
	}
	return &clustersResp.ObservabilityCluster
}

func DetailReq(req message.DetailClusterReq) *http.Request {
	r := GetRegion()
	api := fmt.Sprintf("%s%s/clusters/%s?vendor=%s&region=%s",
		r.ServerURL, ApiPrefix, req.ID, req.Vendor, req.Region)
	body, _ := json.Marshal(req)
	request, _ := http.NewRequest("GET", api, strings.NewReader(string(body)))
	WithHeader(request)
	return request
}

func WithHeader(request *http.Request) {
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set(constants.AuthorizationHeader, fmt.Sprintf("%s %s", constants.BearerPrefix, token))
}

func UnmarshalRespData(data []byte, v interface{}) error {
	type Result struct {
		Code int         `json:"code"`
		Data interface{} `json:"data"`
	}
	result := &Result{
		Data: v,
	}
	return json.Unmarshal(data, result)
}

func PostAlertManager(domain string, req []AlertRequest) *AlertPostResponse {
	api := fmt.Sprintf("%s%s/alerts",
		domain, ApiPrefix)
	body, _ := json.Marshal(req)
	request, _ := http.NewRequest("POST", api, strings.NewReader(string(body)))
	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	alertResp := &AlertPostResponse{}
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	err = json.Unmarshal(respContent, alertResp)
	if err != nil {
		panic(err)
	}
	return alertResp
}

func DefaultSessionV2(region string) (v2Aws.Config, error) {
	cfg, err := v2Config.LoadDefaultConfig(context.TODO(),
		v2Config.WithRegion(region))
	if err != nil {
		fmt.Errorf("Failed to create session, %v", err)
	}

	return cfg, err
}

func GetMinutesSinceLastS3Snapshot(cluster *structs.ObservabilityCluster, cfg v2Aws.Config, prefix string, clusterBaseInfo structs.ClusterBaseInfo) (lastModifiedTime, createdAt time.Time,
	minutesToLastModified, minutesSinceCreated int, curTime time.Time, lastBlock, clinicStatus, msg string) {

	lastBlock, msg = GetLastS3Block(cluster, cfg, prefix, []string{}, v2Aws.String(""))

	tidbClusters := QueryTiDBCluster(clusterBaseInfo)
	clinicStatus = GetClinicStatus(tidbClusters, cluster.TenantID, cluster.ApplicationID)
	createdAt = GetClinicCreatedAt(tidbClusters, cluster.TenantID, cluster.ApplicationID)
	curTime = time.Now()
	minutesSinceCreated = int(curTime.Sub(createdAt).Minutes())

	if lastBlock == "" {
		if !createdAt.IsZero() {
			lastModifiedTime = createdAt
			curTime = time.Now()
			minutesToLastModified = int(curTime.Sub(lastModifiedTime).Minutes())
			return
		} else {
			//fmt.Println("[warning no cluster]", cluster.ID, cluster.StorageInfo[0].BucketName, prefix)
			msg = "warning no cluster"
			return
		}
	}

	minutesToLastModified = int(curTime.Sub(time.Time{}).Minutes())
	s3RequestCount++
	objects, err := v2S3.NewFromConfig(cfg).ListObjects(context.TODO(), &v2S3.ListObjectsInput{
		Bucket: v2Aws.String(cluster.StorageInfo[0].BucketName),
		Prefix: v2Aws.String(fmt.Sprintf("%s%s/", prefix, lastBlock)),
	})
	if err != nil {
		panic(err)
	}

	for _, o := range objects.Contents {

		tempMinutesToLastModified := int(curTime.Sub(*o.LastModified).Minutes())
		if tempMinutesToLastModified < minutesToLastModified {
			lastModifiedTime = *o.LastModified
			minutesToLastModified = tempMinutesToLastModified
		}
	}
	return
}

func GetMinutesSinceLastGcsSnapshot(cluster *structs.ObservabilityCluster, prefix string, clusterBaseInfo structs.ClusterBaseInfo) (lastModifiedTime, createdAt time.Time,
	minutesToLastModified, minutesSinceCreated int, curTime time.Time, lastBlock, clinicStatus, msg string) {
	ctx := context.Background()

	client, err := storage.NewClient(context.TODO())
	if err != nil {
		panic(err)
	}
	defer client.Close()

	ctx, cancel := context.WithTimeout(ctx, time.Second*10)
	defer cancel()

	bucket := client.Bucket(cluster.StorageInfo[0].BucketName)
	gcsRequestCount++
	objects := bucket.Objects(context.TODO(), &storage.Query{
		Prefix: prefix,
	})

	tidbClusters := QueryTiDBCluster(clusterBaseInfo)
	clinicStatus = GetClinicStatus(tidbClusters, cluster.TenantID, cluster.ApplicationID)
	createdAt = GetClinicCreatedAt(tidbClusters, cluster.TenantID, cluster.ApplicationID)
	curTime = time.Now()
	minutesSinceCreated = int(curTime.Sub(createdAt).Minutes())

	minutesToLastModified = int(curTime.Sub(time.Time{}).Minutes())
	for {
		attrs, err := objects.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			panic(err)
		}
		tempMinutesToLastModified := int(curTime.Sub(attrs.Updated).Minutes())
		//fmt.Println("object name: ", attrs.Name, ", last modified: ", attrs.Updated, ", minutesToLastModified: ", tempMinutesToLastModified)
		if tempMinutesToLastModified < minutesToLastModified {
			lastModifiedTime = attrs.Updated
			minutesToLastModified = tempMinutesToLastModified
			lastBlock = attrs.Name
		}
	}

	if minutesToLastModified == int(curTime.Sub(time.Time{}).Minutes()) {
		if !createdAt.IsZero() {
			lastModifiedTime = createdAt
			curTime = time.Now()
			minutesToLastModified = int(curTime.Sub(lastModifiedTime).Minutes())
			return
		} else {
			msg = "warning no cluster"
			return
		}
	}

	return
}

type Cookie struct {
	Cookie     string `yaml:"Cookie"json:"Cookie"`
	XCsrfToken string `yaml:"x-csrf-token"json:"x-csrf-token"`
}

type QueryTiDBClusterResp struct {
	Total int64         `json:"total"`
	Items []TiDBCluster `json:"items"`
}

type TiDBCluster struct {
	TenantID         string `json:"tenantID"`
	TenantName       string `json:"tenantName"`
	TenantPlan       string `json:"tenantPlan"`
	ClusterID        string `json:"clusterID"`
	ClusterName      string `json:"clusterName"`
	ClusterCreatedAt int64  `json:"clusterCreatedAt"`
	ClusterDuration  string `json:"clusterDuration"`
	ClusterDeletedAt int64  `json:"clusterDeletedAt"`

	ClusterStatus       string `json:"clusterStatus"`
	ClusterDeployType   string `json:"clusterDeployType"`
	ClusterRegionName   string `json:"clusterRegionName"`
	ClusterProviderName string `json:"clusterProviderName"`
	ClusterShootName    string `json:"clusterShootName"`
	ClusterK8sNamespace string `json:"ClusterK8sNamespace"`
	ClusterNetworkName  string `json:"ClusterNetworkName"`
	ClusterType         string `json:"clusterType"`
}

func clinicClusters2URL(host, clinicURL, applicationID string) string {
	q := url.Values{}
	q.Set("cluster_id", applicationID)
	q.Set("sort", "")
	q.Set("order", "")
	q.Set("limit", "1")
	q.Set("page", "1")
	return fmt.Sprintf("https://%s%s/clinic/api/v1/dashboard/clusters2?%s", host, clinicURL, q.Encode())
}

func QueryTiDBCluster(cluster structs.ClusterBaseInfo) *[]TiDBCluster {
	var api string
	var clinicToken string
	if strings.HasPrefix(cluster.BizType, "staging") {
		api = clinicClusters2URL("staging-", GetConfig().ClinicURL, cluster.ApplicationID)
		clinicToken = GetConfig().ClinicTokenStaging
	} else {
		api = clinicClusters2URL("", GetConfig().ClinicURL, cluster.ApplicationID)
		clinicToken = GetConfig().ClinicToken
	}
	if GetConfig().Env == "staging" {
		api = clinicClusters2URL("staging-", GetConfig().ClinicURL, cluster.ApplicationID)
		clinicToken = GetConfig().ClinicTokenStaging
	}

	// request of querying tidb clusters
	request, _ := http.NewRequest("GET", api, nil)
	WithHeaderV3(request, clinicToken)
	//fmt.Println("request: ", request)

	// get response
	resp, err := http.DefaultClient.Do(request)
	if err != nil {
		panic(err)
	}
	//fmt.Println(resp, "\n", resp.Body) // get empty body
	if resp.StatusCode != http.StatusOK {
		panic("request failed")
	}

	clustersResp := &QueryTiDBClusterResp{}
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	//fmt.Println(string(respContent))
	err = json.Unmarshal(respContent, clustersResp)
	if err != nil {
		panic(err)
	}
	//fmt.Println(clustersResp)
	if clustersResp.Items == nil {
		panic("body is empty")
	}
	if err != nil {
		panic(err)
	}
	return &clustersResp.Items
}

func WithHeaderV3(request *http.Request, clinicToken string) {
	request.Header.Set("Content-Type", "application/json")
	request.Header.Set("Accept", "application/json, text/plain, */*")
	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", clinicToken))
	request.Header.Set("referer", "https://clinic.pingcap.com/portal/")
	request.Header.Set("sec-fetch-dest", "empty")
	request.Header.Set("sec-fetch-mode", "cors")
	request.Header.Set("sec-fetch-site", "same-origin")
	request.Header.Set("accept-encoding", "gzip, deflate, br")
	request.Header.Set("authority", "clinic.pingcap.com")
	request.Header.Set("scheme", "https")
}

func GenerateS3Prefix(cluster *structs.ObservabilityCluster) string {
	r := GetRegion()
	if Contains(r.OldPath, fmt.Sprintf("%s/%s/%s", cluster.TenantID, cluster.ProjectID, cluster.ApplicationID)) {
		return fmt.Sprintf("%s/tenant=%s/project=%s/application=%s/metrics/", cluster.StorageInfo[0].BucketName, cluster.TenantID, cluster.ProjectID, cluster.ApplicationID)
	} else {
		return fmt.Sprintf("%s/%d/%s/%s/%s/metrics/", cluster.StorageInfo[0].BucketName,
			cluster.ClusterBaseInfo.DataPropertyRights, cluster.TenantID, cluster.ProjectID, cluster.ApplicationID)
	}
}

func Contains(elems []string, v string) bool {
	for _, s := range elems {
		if v == s {
			return true
		}
	}
	return false
}

func GetClinicStatus(tidbClusters *[]TiDBCluster, tenantID, applicationID string) (clinicStatus string) {
	for _, tidbCluster := range *tidbClusters {
		if tidbCluster.TenantID == tenantID && tidbCluster.ClusterID == applicationID {
			return tidbCluster.ClusterStatus
		}
	}
	return
}

func GetClinicCreatedAt(tidbClusters *[]TiDBCluster, tenantID, applicationID string) (createdAt time.Time) {
	for _, tidbCluster := range *tidbClusters {
		if tidbCluster.TenantID == tenantID && tidbCluster.ClusterID == applicationID {
			return time.Unix(0, tidbCluster.ClusterCreatedAt*int64(time.Millisecond))
		}
	}
	return
}

func GetLastS3Block(cluster *structs.ObservabilityCluster, cfg v2Aws.Config, prefix string, lastSubPrefixes []string, marker *string) (lastBlock, msg string) {
	s3RequestCount++
	objects, err := v2S3.NewFromConfig(cfg).ListObjects(context.TODO(), &v2S3.ListObjectsInput{
		Bucket:    v2Aws.String(cluster.StorageInfo[0].BucketName),
		Prefix:    v2Aws.String(prefix),
		Delimiter: v2Aws.String("/"),
		Marker:    marker,
	})
	if err != nil {
		//fmt.Sprintf("[warning no bucket]%s,%s:%v", cluster.StorageInfo[0].BucketName, prefix, err)
		return "", "warning no bucket"
	}

	var subPrefixes []string
	for _, commonPrefix := range objects.CommonPrefixes {
		subPrefixFullPathArray := strings.Split(*commonPrefix.Prefix, "/")
		subPrefixes = append(subPrefixes, subPrefixFullPathArray[len(subPrefixFullPathArray)-2])
	}
	if len(subPrefixes) == 0 {
		if len(lastSubPrefixes) == 0 {
			return
		} else {
			return lastSubPrefixes[len(lastSubPrefixes)-1], ""
		}
	}

	sort.Strings(subPrefixes)
	if len(subPrefixes) == 1000 || objects.IsTruncated {
		return GetLastS3Block(cluster, cfg, prefix, subPrefixes, objects.NextMarker)
	} else {
		return subPrefixes[len(subPrefixes)-1], ""
	}
}

func GetS3RequestCount() int {
	return s3RequestCount
}

func GetGcsRequestCount() int {
	return gcsRequestCount
}
