package checkernextgen

import (
	"context"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/observability/test/ft/common/objstore"
)

const MAX_UPLOAD_LATENCY_SECS = 300

type DataType string

const (
	DataTypeMetrics           DataType = "metrics"
	DataTypeTopsql            DataType = "topsql"
	DataTypeLokiLogs          DataType = "loki_logs"
	DataTypeS3Logs            DataType = "s3_logs"
	DataTypeContinuousProfile DataType = "continuous_profile"
	DataTypeKeyVisualizer     DataType = "key_visualizer"
	DataTypeSlowLogs          DataType = "slow_logs"
)

func CheckAll(ctx context.Context, cluster ClusterInfo, tr TimeRange, authToken string, whenNotFound func(DataType) error) (bool, error) {
	if !cluster.Valid() {
		return false, fmt.Errorf("invalid cluster info: %v", cluster)
	}
	if !tr.Valid() {
		return false, fmt.Errorf("invalid time range: %v", tr)
	}
	if whenNotFound == nil {
		whenNotFound = func(DataType) error { return nil }
	}

	slog.Info("check metrics", "startTs", tr.StartTs, "endTs", tr.EndTs)
	ok, err := QueryMetricsCase(cluster, tr.StartTs, tr.EndTs, authToken)
	if err != nil {
		return false, err
	}
	if !ok {
		return false, whenNotFound(DataTypeMetrics)
	}

	slog.Info("check topsql", "startTs", tr.StartTs, "endTs", tr.EndTs)
	ok, err = QueryTopsqlCase(cluster, tr.StartTs, tr.EndTs, authToken)
	if err != nil {
		return false, err
	}
	if !ok {
		return false, whenNotFound(DataTypeTopsql)
	}

	// slog.Info("check loki logs", "startTs", tr.StartTs, "endTs", tr.EndTs)
	// ok, err = QueryLogsCase(cluster, tr.StartTs, tr.EndTs)
	// if err != nil {
	// 	return false, err
	// }
	// if !ok {
	// 	return false, whenNotFound(DataTypeLokiLogs)
	// }

	store, err := NewStorageFromCluster(ctx, cluster)
	if err != nil {
		return false, err
	}

	slog.Info("check s3 logs", "startTs", tr.StartTs, "endTs", tr.EndTs)
	ok, err = HasS3Logs(ctx, store, cluster, tr)
	if err != nil {
		return false, err
	}
	if !ok {
		return false, whenNotFound(DataTypeS3Logs)
	}

	slog.Info("check slowlogs", "startTs", tr.StartTs, "endTs", tr.EndTs)
	ok, err = HasSlowLogs(ctx, store, cluster, tr)
	if err != nil {
		return false, err
	}
	if !ok {
		return false, whenNotFound(DataTypeSlowLogs)
	}

	// slog.Info("check continuous profiles", "startTs", tr.StartTs, "endTs", tr.EndTs)
	// ok, err = HasContinuousProfiles(ctx, store, cluster, tr)
	// if err != nil {
	// 	return false, err
	// }
	// if !ok {
	// 	return false, whenNotFound(DataTypeContinuousProfile)
	// }
	// slog.Info("check key visualizer files", "startTs", tr.StartTs, "endTs", tr.EndTs)
	// ok, err = HasKeyVisualizerFiles(ctx, store, cluster, tr)
	// if err != nil {
	// 	return false, err
	// }
	// if !ok {
	// 	return false, whenNotFound(DataTypeKeyVisualizer)
	// }
	return true, nil
}

func NewStorageFromCluster(ctx context.Context, cluster ClusterInfo) (objstore.Storage, error) {
	switch strings.ToLower(cluster.Provider) {
	case "aws":
		return objstore.NewAwsStorage(ctx, cluster.Region, "")
	case "gcp":
		return objstore.NewGcpStorage(ctx)
	case "azure":
		return objstore.NewAzureStorage(ctx, cluster.AzureStorageAccountName())
	default:
		return nil, fmt.Errorf("unsupported provider: %s", cluster.Provider)
	}
}

func HasS3Logs(ctx context.Context, store objstore.Storage, cluster ClusterInfo, tr TimeRange) (bool, error) {
	bucket := cluster.BucketName()
	keyPath := cluster.KeyPrefixS3Logs()
	for chunkTs := tr.StartTs - tr.StartTs%1000; chunkTs < tr.EndTs; chunkTs += 1000 {
		prefix := fmt.Sprintf("%s%d", keyPath, chunkTs/1000)
		objs, err := store.Objects(ctx, bucket, prefix)
		if err != nil {
			return false, err
		}
		for _, obj := range objs {
			filename := strings.TrimPrefix(obj.Key, keyPath)
			var ts int
			if n, err := fmt.Sscanf(filename, "%d-", &ts); n != 1 || err != nil {
				continue // invalid filename
			}
			if ts < tr.StartTs {
				continue // old file
			}
			if ts >= tr.EndTs+MAX_UPLOAD_LATENCY_SECS { // Log files have batch duration.
				continue // too new file
			}
			slog.Info("s3 logs found", "filename", filename)
			return true, nil
		}
	}
	return false, nil
}

func HasContinuousProfiles(ctx context.Context, store objstore.Storage, cluster ClusterInfo, tr TimeRange) (bool, error) {
	bucket := cluster.BucketName()
	keyPath := cluster.KeyPrefixContinuousProfiles()
	for chunkTs := tr.StartTs - tr.StartTs%1000; chunkTs < tr.EndTs; chunkTs += 1000 {
		prefix := fmt.Sprintf("%s%d", keyPath, chunkTs/1000)
		objs, err := store.Objects(ctx, bucket, prefix)
		if err != nil {
			return false, err
		}
		for _, obj := range objs {
			filename := strings.TrimPrefix(obj.Key, keyPath)
			var ts int
			if n, err := fmt.Sscanf(filename, "%d-", &ts); n != 1 || err != nil {
				continue // invalid filename
			}
			if ts < tr.StartTs {
				continue // old file
			}
			if ts > tr.EndTs {
				continue // new file
			}
			slog.Info("continuous profiles found", "filename", filename)
			return true, nil
		}
	}
	return false, nil
}

func HasKeyVisualizerFiles(ctx context.Context, store objstore.Storage, cluster ClusterInfo, tr TimeRange) (bool, error) {
	bucket := cluster.BucketName()
	keyPath := cluster.KeyPrefixKeyVisualizerFiles()
	prefixes, err := tr.FormatedHourlyPrefixes("2006010215")
	if err != nil {
		return false, err
	}
	for _, prefix := range prefixes {
		keyPrefix := keyPath + prefix
		objs, err := store.Objects(ctx, bucket, keyPrefix)
		if err != nil {
			return false, err
		}
		for _, obj := range objs {
			filename := strings.TrimPrefix(obj.Key, keyPath)
			// aws: 202410290904.json.slog.gz
			// gcp: 202410290904.json.log
			filename = strings.TrimSuffix(filename, ".gz")
			filename = strings.TrimSuffix(filename, ".json.log")
			minutelyTm, err := time.Parse("200601021504", filename)
			if err != nil {
				continue // invalid filename
			}
			ts := int(minutelyTm.Unix())
			if ts < tr.StartTs {
				continue // old file
			}
			if ts > tr.EndTs {
				continue // new file
			}
			slog.Info("key visualizer files found", "filename", filename)
			return true, nil
		}
	}
	return false, nil
}

func HasSlowLogs(ctx context.Context, store objstore.Storage, cluster ClusterInfo, tr TimeRange) (bool, error) {
	bucket := cluster.BucketName()
	keyPath := cluster.KeyPrefixSlowLogs()
	objs, err := store.Objects(ctx, bucket, keyPath)
	if err != nil {
		return false, err
	}
	for _, obj := range objs {
		filename := strings.TrimPrefix(obj.Key, keyPath)
		// filename: 1736411230-12574584-6d1c-4a86-9af2-f17e9e16821d.slog.gz
		ts := filename[:strings.Index(filename, "-")]
		tsInt, err := strconv.Atoi(ts)
		if err != nil {
			continue // invalid filename
		}
		if tsInt < tr.StartTs {
			continue // old file
		}
		if tsInt > tr.EndTs {
			continue // new file
		}
		slog.Info("slow logs found", "filename", filename)
		return true, nil
	}
	return false, nil
}

type ClusterInfo struct {
	Provider  string `yaml:"provider"`
	Env       string `yaml:"env"`
	Region    string `yaml:"region"`
	TenantID  string `yaml:"tenant"`
	ClusterID string `yaml:"cluster"`
	// O11yID    string `yaml:"o11y_id"`
}

func (c ClusterInfo) Valid() bool {
	return c.Provider != "" &&
		c.Env != "" &&
		c.Region != "" &&
		c.TenantID != "" &&
		c.ClusterID != ""
}

func (c ClusterInfo) BizType() string {
	switch strings.ToLower(c.Env) {
	case "dev":
		return "dev-tidbcloud-nextgen"
	case "staging":
		return "staging-tidbcloud-nextgen"
	default:
		return "tidbcloud-nextgen"
	}
}

func (c ClusterInfo) BucketName() string {
	// switch strings.ToLower(c.Env) {
	// case "dev":
	// 	return fmt.Sprintf("o11y-dev-shared-%s", c.Region)
	// case "prod":
	// 	return fmt.Sprintf("o11y-prod-shared-%s", c.Region)
	// default:
	// 	return fmt.Sprintf("o11y-test-shared-%s", c.Region)
	// }
	return fmt.Sprintf("o11y-prod-shared-%s", c.Region)
}

func (c ClusterInfo) DSDomain() string {
	// switch c.Env {
	// case "prod":
	// 	return fmt.Sprintf("www.ds.%s.%s.observability.tidbcloud.com", c.Region, c.Provider)
	// case "staging":
	// 	return fmt.Sprintf("www.ds.%s.%s.observability-staging.pingcap.cloud", c.Region, c.Provider)
	// default:
	// 	return fmt.Sprintf("www.ds.%s.%s.observability-dev.pingcap.cloud", c.Region, c.Provider)
	// }
	return fmt.Sprintf("www.ds.%s.%s.observability.tidbcloud.com", c.Region, c.Provider)
}

func (c ClusterInfo) LokiAPIAddress() string {
	return ""
}

func (c ClusterInfo) MetricsQueryRangeURL() string {
	return fmt.Sprintf("https://%s/external/metrics/%s/tenant/%s/application/%s/api/v1/query_range",
		c.DSDomain(),
		c.BizType(),
		c.TenantID,
		c.ClusterID)
}

func (c ClusterInfo) KeyPrefixS3Logs() string {
	return fmt.Sprintf("0/next-gen/%v/logs/tidb/", c.ClusterID)
}

func (c ClusterInfo) KeyPrefixContinuousProfiles() string {
	return fmt.Sprintf("0/next-gen/%v/profiles/", c.ClusterID)
}

func (c ClusterInfo) KeyPrefixKeyVisualizerFiles() string {
	return fmt.Sprintf("0/next-gen/%v/regions/", c.ClusterID)
}

func (c ClusterInfo) KeyPrefixSlowLogs() string {
	return fmt.Sprintf("0/next-gen/%v/logs/slowlog/", c.ClusterID)
}

func (c ClusterInfo) AzureStorageAccountName() string {
	return fmt.Sprintf("o11yprod%s", c.Region)
}

type TimeRange struct {
	StartTs int `yaml:"start"` // if 0 means now-1h
	EndTs   int `yaml:"end"`   // if 0 means now
}

func (tr *TimeRange) Valid() bool {
	if tr.StartTs > 0 && tr.EndTs > 0 {
		return true
	}
	if tr.StartTs == 0 && tr.EndTs == 0 {
		tr.EndTs = int(time.Now().Unix())
		tr.StartTs = tr.EndTs - 3600
		return true
	}
	return false
}

func (tr TimeRange) FormatedHourlyPrefixes(format string) ([]string, error) {
	start := time.Unix(int64(tr.StartTs), 0)
	end := time.Unix(int64(tr.EndTs), 0)
	var prefixes []string
	for t := start; !t.After(end); t = t.Add(time.Hour) {
		prefix := t.UTC().Format(format)
		prefixes = append(prefixes, prefix)
	}
	return prefixes, nil
}
