package v1

import (
	"context"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/spf13/viper"
	"google.golang.org/api/bigquery/v2"
	"google.golang.org/api/option"

	"github.com/pingcap-inc/pchub/internal/pkg/code"
	v1 "github.com/pingcap-inc/pchub/model/v1"
	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
)

const (
	SyncStartDate  = "2022-07-01"
	BigQuerySource = "gcp-digital-bi"
)

const (
	InternalGCPBillingTable     = "`gcp-digital-bi.gcp_billing_v2.gcp_billing_export_v1_01A169_D3D9BF_FB82A2`"
	FUllInternalGCPBillingTable = "`gcp_billing_v2.gcp_billing_export_v1_*`"
)

func getBQSrv(ctx context.Context) (*bigquery.Service, error) {
	saf := viper.GetString("bq.sa")
	f, err := os.Stat(saf)
	if err != nil || f.IsDir() {
		log.L(ctx).Error("Failed to get bg sa file: " + err.Error())

		return nil, errors.WithCode(code.ErrNoBQSAFile, "")
	}

	bqSrv, err := bigquery.NewService(ctx, option.WithCredentialsFile(saf))
	if err != nil {
		log.L(ctx).Error("Failed to new bg service " + err.Error())

		return nil, errors.WithCode(code.ErrBGServiceFailed, "")
	}

	return bqSrv, nil
}

func getAfterDateStr(fullUpdate bool) string {
	before45d := -1 * time.Hour * 24 * 45
	startDate := time.Now().Add(before45d).Format("2006-01-02")
	afterDate := startDate
	if fullUpdate {
		afterDate = SyncStartDate
	}

	return afterDate
}

func (s *billingService) SyncAWSByResourceIDs(ctx context.Context, bqSrv *bigquery.Service, ids *map[uint64]string, fullUpdate bool) {
	log.L(ctx).Info("BillingSrv.SyncAWSByResourceIDs called.")

	var wg sync.WaitGroup
	sem := make(chan struct{}, 10)

	for id, cid := range *ids {
		wg.Add(1)
		go func(id uint64, cid string) {
			sem <- struct{}{}
			defer func() { <-sem }()
			defer wg.Done()

			s.SyncAWSAccountBillingByID(ctx, bqSrv, id, cid, fullUpdate)
		}(id, cid)
	}

	wg.Wait()

	log.L(ctx).Info("Success to exec BillingSrv.SyncAWSByResourceIDs, sync " + strconv.Itoa(len(*ids)) + " resources.")
}

func constructAWSBillingQuery(cid, afterDate string) string {
	qb := strings.Builder{}
	qb.WriteString("(SELECT ")

	fullTableName := "aws_nonprod_billing." + cid + "_aws_billing_data"
	dayDem := "DATE(line_item_usage_start_date) as day"
	costMeasure := "ROUND(SUM(line_item_unblended_cost), 4) as cost"

	qb.WriteString(dayDem)
	qb.WriteString(", ")
	qb.WriteString(costMeasure)
	qb.WriteString(", line_item_product_code as product FROM ")
	qb.WriteString(fullTableName)
	qb.WriteString(" WHERE ")

	dateRange := "DATE(line_item_usage_start_date) >= '" + afterDate + "'"

	qb.WriteString(dateRange)
	qb.WriteString(" AND (line_item_line_item_type <> 'SavingsPlanNegation' AND line_item_product_code <> 'ComputeSavingsPlans')")
	qb.WriteString(" GROUP BY day, product HAVING cost <> 0 ORDER BY day ASC, cost DESC) UNION ALL (SELECT ")

	qb.WriteString(dayDem)
	qb.WriteString(", ")
	qb.WriteString(costMeasure)
	qb.WriteString(", 'SavingsPlans' as product FROM ")
	qb.WriteString(fullTableName)
	qb.WriteString(" WHERE ")
	qb.WriteString(dateRange)
	qb.WriteString(" AND (line_item_line_item_type = 'SavingsPlanNegation' OR line_item_product_code = 'ComputeSavingsPlans')")
	qb.WriteString(" GROUP BY day, product ORDER BY day ASC, cost DESC)")

	return qb.String()
}

func (s *billingService) SyncAWSAccountBillingByID(ctx context.Context, bqSrv *bigquery.Service, reourceID uint64, cid string, fullUpdate bool) {
	log.L(ctx).Info("BillingSrv.SyncAWSAccountBillingByID called, cloudInsID: " + cid)

	afterDate := getAfterDateStr(fullUpdate)
	qStr := constructAWSBillingQuery(cid, afterDate)

	s.SyncSingleBillingByID(ctx, bqSrv, reourceID, cid, qStr, afterDate)
}

func constructGCPBillingQuery(pid, afterDate string) string {
	qb := strings.Builder{}

	fullTableName := InternalGCPBillingTable
	if afterDate <= "2023-04-19" {
		fullTableName = FUllInternalGCPBillingTable
	}

	qb.WriteString("SELECT `date`, ROUND(SUM( (cost + credits_amount) * currency_conversion_rate ), 4) AS cost, service.description AS product, project_id AS projID FROM ( SELECT *  FROM ( SELECT sku.description AS product, DATE( usage_start_time ) AS date, project.id AS project_id, IF(( SELECT SUM(x) FROM UNNEST(ARRAY( SELECT amount FROM UNNEST(credits))) AS x) IS NULL, 0, ( SELECT SUM(x) FROM UNNEST(ARRAY( SELECT amount FROM UNNEST(credits))) AS x )) AS credits_amount, * FROM ") //nolint:lll // ignore long line

	qb.WriteString(fullTableName)
	qb.WriteString(" WHERE ")

	dateRange := "DATE(usage_start_time) >= '" + afterDate + "'"

	qb.WriteString(dateRange)
	qb.WriteString(" AND project.id = '" + pid + "'")
	qb.WriteString(") T )")
	qb.WriteString(" GROUP BY projID, product, date HAVING cost > 0 ORDER BY projID ASC, date ASC, cost DESC")

	return qb.String()
}

func (s *billingService) SyncGCPByResourceIDs(ctx context.Context, bqSrv *bigquery.Service, ids *map[uint64]string, fullUpdate bool) {
	log.L(ctx).Info("BillingSrv.SyncGCPByResourceIDs called.")

	var wg sync.WaitGroup
	sem := make(chan struct{}, 10)

	for id, cid := range *ids {
		wg.Add(1)
		go func(id uint64, cid string) {
			sem <- struct{}{}
			defer func() { <-sem }()
			defer wg.Done()

			s.SyncGCPProjectBillingByID(ctx, bqSrv, id, cid, fullUpdate)
		}(id, cid)
	}

	wg.Wait()

	log.L(ctx).Info("Success to exec BillingSrv.SyncGCPByResourceIDs, sync " + strconv.Itoa(len(*ids)) + " resources.")
}

func (s *billingService) SyncGCPProjectBillingByID(ctx context.Context, bqSrv *bigquery.Service, reourceID uint64, cid string, fullUpdate bool) {
	log.L(ctx).Info("BillingSrv.SyncGCPProjectBillingByID called, cloudInsID: " + cid)

	afterDate := getAfterDateStr(fullUpdate)
	qStr := constructGCPBillingQuery(cid, afterDate)

	s.SyncSingleBillingByID(ctx, bqSrv, reourceID, cid, qStr, afterDate)
}

func (s *billingService) SyncSingleBillingByID(ctx context.Context, bqSrv *bigquery.Service, reourceID uint64, cid, qStr, afterDate string) {
	useLegacySQL := false
	bgReq := &bigquery.QueryRequest{
		Query:        qStr,
		UseLegacySql: &useLegacySQL,
	}

	log.L(ctx).Info("Request data from bigquery with query: " + qStr)

	response, err := bqSrv.Jobs.Query(BigQuerySource, bgReq).Do()
	if err != nil {
		log.L(ctx).Error("Error when request data from bigquery with query: " + qStr + ", error: " + err.Error())

		return
	}

	news := make([]*v1.CloudProductCost, 0)
	for _, row := range response.Rows {
		day, dok := row.F[0].V.(string)
		cost, cok := row.F[1].V.(string)
		product, pok := row.F[2].V.(string)
		costF, err := strconv.ParseFloat(cost, 64)
		if !pok || !dok || !cok || err != nil {
			log.L(ctx).Errorf("Error when parse row data from bigquery, row: %v\n", row)

			continue
		}
		d, _ := time.Parse("2006-01-02", day)
		n := &v1.CloudProductCost{
			CreatedAt:   time.Now(),
			ResourceID:  reourceID,
			Date:        d.UTC(),
			Cost:        costF,
			ProductName: product,
		}
		news = append(news, n)
	}

	after, _ := time.Parse("2006-01-02", afterDate)
	err = s.store.CloudProductCost().SyncLatest(ctx, []uint64{reourceID}, after, news)
	if err != nil {
		log.L(ctx).Error("Error when call store.CloudProductCost.SyncLatest: " + err.Error())

		return
	}

	log.L(ctx).Info("Success to exec BillingSrv.SyncSingleBillingByID: " + cid)
}
