//go:build service
// +build service

package utils

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"

	"github.com/ajg/form"
	"github.com/andygrunwald/go-jira"
	"github.com/aws/aws-sdk-go-v2/aws"
	v2Config "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/ddliu/go-httpclient"
	"github.com/tidbcloud/observability/pkg/common/constants"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
	"gopkg.in/yaml.v2"
)

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"`
	JiraURL            string     `yaml:"jiraURL"`
	JiraToken          string     `yaml:"jiraToken"`
}

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

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 Auth0Resp struct {
	AccessToken string `json:"access_token" form:"access_token"`
}

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

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

func GetConfigInfo() *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 {
}

// QueryClusters queries clusters with the given parameters and returns the response
func QueryClusters(req message.QueryClustersReq) (*message.QueryClustersResp, error) {
	request := QueryReq(req)

	httpResp, err := http.DefaultClient.Do(request)
	if err != nil {
		return nil, fmt.Errorf("failed to execute query request: %w", err)
	}
	defer httpResp.Body.Close()

	if httpResp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(httpResp.Body)
		return nil, fmt.Errorf("query failed with status %d: %s", httpResp.StatusCode, string(body))
	}

	respContent, err := io.ReadAll(httpResp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	var queryResp message.QueryClustersResp
	err = UnmarshalRespData(respContent, &queryResp)
	if err != nil {
		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
	}

	return &queryResp, nil
}

func QueryReq(req message.QueryClustersReq) *http.Request {
	r := GetRegion()
	queryString := fmt.Sprintf("%s%s/clusters/?vendor=%s&region=%s&tenantID=%s&projectID=%s&applicationID=%s&limit=%d&continueToken=%s",
		r.ServerURL, ApiPrefix, req.Vendor, req.Region,
		req.TenantID, req.ProjectID, req.ApplicationID,
		req.Limit, req.ContinueToken)
	if req.BizType != "" {
		queryString = fmt.Sprintf("%s&bizType=%s", queryString, 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(err)
	}
	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 RequestPrometheus(url string) *QueryResponse {
	req, _ := http.NewRequest("GET", url, nil)
	resp, err := http.DefaultClient.Do(req)

	if err != nil {
		logs := fmt.Sprintf("request prometheus ERROR: %v, url: %s", err, url)
		panic(logs)
	}

	if resp.StatusCode != http.StatusOK {
		panic(resp.Status)
	}
	// unmarshal resp
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	var res = &QueryResponse{}
	err = json.Unmarshal(respContent, res)
	if err != nil {
		panic(err)
	}

	return res
}

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

func PostAlertManager(domain string, alert Alert) (*httpclient.Response, error) {
	api := fmt.Sprintf("%s/api/v1/alerts", domain)
	req := make([]*Alert, 0)
	req = append(req, &alert)
	body, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	headers := map[string]string{
		"Content-Type": "application/json",
	}
	resp, err := httpclient.WithHeaders(headers).PostJson(api, body)
	return resp, err
}

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 time.Time `json:"clusterCreatedAt"`
	ClusterDuration string `json:"clusterDuration"`
	//ClusterDeletedAt time.Time `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 GetTiDBCluster(cluster structs.ClusterBaseInfo) *TiDBCluster {
	var api string
	var clinicToken string
	if strings.HasPrefix(cluster.BizType, "staging") {
		api = clinicClusters2URL("staging-", GetConfigInfo().ClinicURL, cluster.ApplicationID)
		clinicToken = GetConfigInfo().ClinicTokenStaging
	} else {
		api = clinicClusters2URL("", GetConfigInfo().ClinicURL, cluster.ApplicationID)
		clinicToken = GetConfigInfo().ClinicToken
	}
	if GetConfigInfo().Env == "staging" {
		api = clinicClusters2URL("staging-", GetConfigInfo().ClinicURL, cluster.ApplicationID)
		clinicToken = GetConfigInfo().ClinicTokenStaging
	}
	// request of querying tidb clusters
	request, _ := http.NewRequest("GET", api, nil)
	WithHeaderV2(request, clinicToken)

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

	clustersResp := &QueryTiDBClusterResp{}
	respContent, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}
	err = json.Unmarshal(respContent, clustersResp)
	if err != nil {
		panic(err)
	}
	if len(clustersResp.Items) == 0 {
		return nil
	}
	return &clustersResp.Items[0]
}

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

func GetTidbClusterStatus(tidbCluster *TiDBCluster, tenantID, applicationID string) (clinicStatus string, hasShoot bool) {
	if tidbCluster == nil {
		return "", false
	}
	hasShoot = false
	if tidbCluster.TenantID == tenantID && tidbCluster.ClusterID == applicationID {
		if len(tidbCluster.ClusterShootName) > 0 {
			hasShoot = true
		}
		return tidbCluster.ClusterStatus, hasShoot
	}
	return "", hasShoot
}

var jiraProject = map[string]string{
	"dev":     "O11YDEV",
	"staging": "O11YSTAG",
	"prod":    "O11Y",
}

var jiraTag = map[string]string{
	"dev":     "DEV",
	"staging": "STAGING",
	"prod":    "PROD",
}

func PostToJira(des string, title string, priority string) (*http.Response, error) {
	issue := &jira.Issue{
		Fields: &jira.IssueFields{
			Project: jira.Project{
				Key: jiraProject[GetConfigInfo().Env],
			},
			Type: jira.IssueType{
				Name: "Task",
			},
			Description: des,
			Summary:     fmt.Sprintf("[%s]%s", jiraTag[GetConfigInfo().Env], title),
			Priority:    &jira.Priority{Name: priority},
			Components: []*jira.Component{
				{
					Name: "observability-regional-server",
				},
			},
		},
	}
	api := GetConfigInfo().JiraURL
	body, _ := json.Marshal(issue)
	request, _ := http.NewRequest("POST", api, strings.NewReader(string(body)))
	WithHeaderV2(request, GetConfigInfo().JiraToken)

	resp, err := http.DefaultClient.Do(request)

	return resp, err
}

func NewClient(region string) (*S3Storage, error) {
	cfg, err := v2Config.LoadDefaultConfig(context.TODO(),
		v2Config.WithRegion(region))
	if err != nil {
		return nil, err
	}
	s := &S3Storage{
		S3Client: s3.NewFromConfig(cfg),
	}
	return s, err
}

type S3Storage struct {
	S3Client *s3.Client
}

// DownloadFile gets an object from a bucket and stores it in a local file.
func (s *S3Storage) DownloadFile(bucketName string, objectKey string) ([]byte, error) {
	result, err := s.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
		Bucket: aws.String(bucketName),
		Key:    aws.String(objectKey),
	})
	if err != nil {
		fmt.Printf("get object %s error: %v\n", objectKey, err)
		return nil, err
	}
	defer result.Body.Close()

	body, err := io.ReadAll(result.Body)
	//data := map[string]ClusterStatus{}
	//err = json.Unmarshal(body, &data)
	return body, err
}

func GetCheckpoint(bucketName string, objectKey string, s *S3Storage) (map[string]string, error) {
	body, err := s.DownloadFile(bucketName, objectKey)
	if err != nil {
		return nil, err
	}
	data := map[string]string{}
	err = json.Unmarshal(body, &data)
	if err != nil {
		return nil, err
	}
	return data, nil
}

func GetWrongStatusClusters(bucketName string, objectKey string, s *S3Storage) (map[string]ClusterStatus, error) {
	body, err := s.DownloadFile(bucketName, objectKey)
	if err != nil {
		return nil, err
	}
	data := map[string]ClusterStatus{}
	err = json.Unmarshal(body, &data)
	if err != nil {
		return nil, err
	}
	return data, nil
}

func (s *S3Storage) UploadObject(bucket string, key string, data []byte) error {
	_, err := s.S3Client.PutObject(context.TODO(), &s3.PutObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
		Body:   bytes.NewReader(data),
	})

	return err
}

type ClusterStatus struct {
	BaseInfo          structs.ClusterBaseInfo
	O11yClusterID     string `json:"O11YClusterID"`
	O11yClusterStatus string `json:"O11YClusterStatus"`
	TiDBClusterID     string `json:"TiDBClusterID"`
	TiDBClusterStatus string `json:"TiDBClusterStatus"`
	//LastDetectTime    string `json:"LastDetectTime"`
}
