package file_download

import (
	"context"
	"fmt"
	"math"
	"net/url"
	"strconv"
	"strings"

	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/ngm/file_download/api"
	"github.com/pingcap-inc/clinic/pkg/ngm/storage"
)

type StorageImpl struct {
	env     string
	service string
	sf      storage.StorageFactory
}

var _ api.Query = (*StorageImpl)(nil)

func NewStorageImpl(env, service string, sf storage.StorageFactory) *StorageImpl {
	return &StorageImpl{
		env,
		service,
		sf,
	}
}

func (impl *StorageImpl) List(ctx context.Context, param *api.ListParam) (*api.ListData, error) {
	bkt := bucketName(impl.env, param.Provider, param.Region)
	store, err := impl.sf.Storage(ctx, param.Provider, param.Region)
	if err != nil {
		return nil, err
	}

	// This is a temporary solution to handle plan_replayer service. (to support multi-ac)
	if impl.service == "plan_replayer" {
		dir := fmt.Sprintf("0/%s/%s/%s/plan_replayer/", param.OrgID, param.ProjectID, param.ClusterID)
		metas, err := store.Objects(ctx, bkt, dir)
		if err != nil {
			log.Error("list objects failed", zap.Error(err))
			return nil, err
		}
		data := &api.ListData{Records: make([]api.ListFileRecord, 0)}
		for _, meta := range metas {
			ts, instance, fname := parseMetaFromKey(strings.TrimPrefix(meta.Key, dir))
			if !param.InRange(ts) {
				continue
			}
			if !param.MatchInstance(instance) {
				continue
			}
			fInfo := api.FileMetaInfo{
				Filename:         fname,
				Key:              meta.Key,
				DownloadPath:     downloadPath(param.Provider, param.Region, meta.Key),
				DownloadURL:      downloadPath(param.Provider, param.Region, meta.Key),
				Size:             meta.Size,
				CreatedAtUnixSec: ts,
			}
			if len(data.Records) == 0 || data.Records[len(data.Records)-1].Instance != instance {
				data.Records = append(data.Records, api.ListFileRecord{Instance: instance, Files: []api.FileMetaInfo{fInfo}})
			} else {
				data.Records[len(data.Records)-1].Files = append(data.Records[len(data.Records)-1].Files, fInfo)
			}
		}
		return data, nil
	}

	dir := genS3Prefix(param.OrgID, param.ProjectID, param.ClusterID, impl.service)
	prefix := dir + rangeTsPrefix(param.StartUnixSec, param.EndUnixSec)
	metas, err := store.Objects(ctx, bkt, prefix)
	if err != nil {
		log.Error("list objects failed", zap.Error(err))
		return nil, err
	}
	data := &api.ListData{Records: make([]api.ListFileRecord, 0)}
	for _, meta := range metas {
		ts, instance, fname := parseMetaFromKey(strings.TrimPrefix(meta.Key, dir))
		if !param.InRange(ts) {
			continue
		}
		if !param.MatchInstance(instance) {
			continue
		}
		fInfo := api.FileMetaInfo{
			Filename:         fname,
			Key:              meta.Key,
			DownloadPath:     downloadPath(param.Provider, param.Region, meta.Key),
			DownloadURL:      downloadPath(param.Provider, param.Region, meta.Key),
			Size:             meta.Size,
			CreatedAtUnixSec: ts,
		}
		if len(data.Records) == 0 || data.Records[len(data.Records)-1].Instance != instance {
			data.Records = append(data.Records, api.ListFileRecord{Instance: instance, Files: []api.FileMetaInfo{fInfo}})
		} else {
			data.Records[len(data.Records)-1].Files = append(data.Records[len(data.Records)-1].Files, fInfo)
		}
	}
	return data, nil
}

func (impl *StorageImpl) Download(ctx context.Context, param *api.DownloadParam) (*api.DownloadData, error) {
	bkt := bucketName(impl.env, param.Provider, param.Region)
	store, err := impl.sf.Storage(ctx, param.Provider, param.Region)
	if err != nil {
		return nil, err
	}

	log.Info("input", zap.String("key", param.Key), zap.String("region", param.Region))

	output, err := store.Object(ctx, bkt, param.Key)
	if err != nil {
		log.Error("get objects failed", zap.Error(err))
		return nil, err
	}
	s := strings.Split(param.Key, "/")
	fname := s[len(s)-1]
	extraHeaders := map[string]string{
		"Content-Disposition": fmt.Sprintf(`attachment; filename="%s"`, fname),
		"Content-Length":      fmt.Sprintf("%d", *output.ContentLength),
	}
	if output.ETag != nil {
		extraHeaders["ETag"] = *output.ETag
	}
	if output.ContentRange != nil {
		extraHeaders["Content-Range"] = *output.ContentRange
	}
	if output.AcceptRanges != nil {
		extraHeaders["Accept-Range"] = *output.AcceptRanges
	}

	data := &api.DownloadData{
		ContentType:   *output.ContentType,
		ContentLength: *output.ContentLength,
		Reader:        output.Reader,
		Headers:       extraHeaders,
	}
	return data, nil
}

func bucketName(env, provider, region string) string {
	switch env {
	case "prod":
		return fmt.Sprintf("o11y-prod-shared-%s", region)
	default:
		if provider == "aws" && region == "us-west-2" {
			return fmt.Sprintf("o11y-prod-shared-%s-staging", region)
		} else {
			return fmt.Sprintf("o11y-test-shared-%s", region)
		}
	}
}

func genS3Prefix(orgID, projectID, clusterID, service string) string {
	return fmt.Sprintf("0/%s/%s/%s/%s/tidb/", orgID, projectID, clusterID, service)
}

func rangeTsPrefix(beginTime, endTime int64) string {
	if beginTime <= 0 || endTime <= 0 {
		return ""
	}
	for n := 2; n <= 9; n++ {
		base := beginTime - beginTime%int64(math.Pow10(n))
		if base <= beginTime && (base+int64(math.Pow10(n)-1)) >= endTime {
			baseStr := strconv.Itoa(int(base))
			return baseStr[:len(baseStr)-n]
		}
	}
	return ""
}

func parseMetaFromKey(key string) (int64, string, string) {
	s := strings.Split(key, "/")
	if len(s) != 3 && len(s) != 4 {
		return 0, "", ""
	}
	var tsStr, instance, fname string
	if len(s) == 3 {
		tsStr, instance, fname = s[0], s[1], s[2]
	} else {
		tsStr, instance, fname = s[1], s[2], s[3]
	}
	ts, err := strconv.ParseInt(tsStr, 10, 64)
	if err != nil {
		return 0, instance, fname
	}
	return ts, instance, fname
}

func downloadPath(provider, region, key string) string {
	q := fmt.Sprintf("provider=%s&region=%s&key=%s", provider, region, url.QueryEscape(key))
	return fmt.Sprintf("/ngm/api/v1/oom_record/files?%s", q)
}
