package v1

import (
	"context"
	"sort"
	"strings"
	"time"

	v1 "github.com/pingcap-inc/pchub/model/v1"
	bfao "github.com/pingcap-inc/pchub/model/v1/fao/billing"

	"github.com/pingcap-inc/pchub/internal/apiserver/store"
	"github.com/pingcap-inc/pchub/internal/pkg/code"
	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
)

type billingService struct {
	store store.Factory
}

type BillingSrv interface {
	SyncWithBQ(ctx context.Context, fullUpdate bool) error
	ListGroupByResource(ctx context.Context, lParam *v1.ListParam, fao bfao.BillingFAO) (*v1.GetResBillingListData, error)
	CheckCostAlerts(ctx context.Context) error
}

var _ BillingSrv = (*billingService)(nil)

func newBilling(srv *service) *billingService {
	return &billingService{store: srv.store}
}

func (s *billingService) getDepartments(ctx context.Context, dptLarkID string) ([]string, error) {
	var dpts []string
	d, err := s.store.Department().GetByLarkID(ctx, dptLarkID)
	if err != nil {
		log.L(ctx).Error("Error when exec DepartmentStore.GetByLarkID, departmentLarkID：" + dptLarkID + "， err： " + err.Error())

		return nil, err
	}

	dpts = strings.Split(d.FlatChildrenLarkID, ",")
	dpts = append(dpts, dptLarkID)

	return dpts, nil
}

func (s *billingService) listCost(ctx context.Context, dpts []string, lParam *v1.ListParam, fao bfao.BillingFAO) ([]uint64, map[uint64]float64, int64, error) {
	rids := make([]uint64, 0)
	costMap := make(map[uint64]float64)
	r, rtotal, err := s.store.
		CloudProductCost().
		GetByCostOrder(ctx, &fao, &dpts, lParam)
	if err != nil {
		log.L(ctx).Error("Failed to exec BillingStore.GetByCostOrder: " + err.Error())

		return rids, costMap, 0, err
	}

	for _, rit := range *r {
		rids = append(rids, rit.ResourceID)
		costMap[rit.ResourceID] = rit.Cost
	}

	return rids, costMap, rtotal, nil
}

func genLatest14DayPlaceholders() map[time.Time]v1.DayCost {
	// cost map, key for 'date', value for 'cost'
	placeholders := make(map[time.Time]v1.DayCost)
	for i := 15; i >= 1; i-- {
		day := time.Now().Add(-time.Hour * 24 * time.Duration(i)).Format("2006-01-02")
		d, _ := time.Parse("2006-01-02", day)
		placeholders[d] = v1.DayCost{
			Date: d,
			Cost: 0,
		}
	}

	return placeholders
}

func (s *billingService) getLatest14DayCost(ctx context.Context, rids []uint64, latestCostMap *map[uint64]map[time.Time]v1.DayCost) error {
	lrs, err := s.store.CloudProductCost().GetLatest14dByRIDs(ctx, rids)
	if err != nil {
		log.L(ctx).Error("Failed to exec BillingStore.GetLatest14dByRIDs: " + err.Error())

		return err
	}

	for _, r := range lrs {
		if (*latestCostMap)[r.ResourceID] == nil {
			(*latestCostMap)[r.ResourceID] = genLatest14DayPlaceholders()
		}

		(*latestCostMap)[r.ResourceID][r.Date] = v1.DayCost{
			Date: r.Date,
			Cost: r.Cost,
		}
	}

	return nil
}

func convertCostMap2Slice(costMap map[time.Time]v1.DayCost) []*v1.DayCost {
	Latest14d := make([]*v1.DayCost, 0)
	for _, v := range costMap {
		Latest14d = append(Latest14d, &v1.DayCost{
			Date: v.Date,
			Cost: v.Cost,
		})
		sort.SliceStable(Latest14d, func(i, j int) bool {
			return Latest14d[i].Date.Before(Latest14d[j].Date)
		})
	}

	return Latest14d
}

//nolint:funlen,gocognit,gocyclo,cyclop // complex query
func (s *billingService) ListGroupByResource(ctx context.Context, lParam *v1.ListParam, fao bfao.BillingFAO) (*v1.GetResBillingListData, error) {
	log.L(ctx).Info("BillingSrv.GetCostByResourceIDs called.")

	var res v1.GetResBillingListData
	var dpts []string
	if fao.Filter.Department != "" {
		departmentIDs, err := s.getDepartments(ctx, fao.Filter.Department)
		if err != nil {
			return nil, err
		}
		dpts = departmentIDs
	}

	if fao.OrderBy.Field == bfao.OrderByFieldCost && fao.OrderBy.IsDesc { //nolint:nestif // complex
		rids, costMap, rtotal, err := s.listCost(ctx, dpts, lParam, fao)
		if err != nil {
			return nil, err
		}

		// resource map, key for 'id', value for 'CloudResource'
		crMap := make(map[uint64]*v1.CloudResource)
		// cost map, key for 'resourceID', value for 'cost' in certain period
		pcostMap := make(map[uint64]float64)
		// latest cost map, key for 'resourceID'
		// value for 'cost-map' in latest 14 days
		//   key for 'date', value for 'cost'
		latestCostMap := make(map[uint64]map[time.Time]v1.DayCost)
		if len(rids) > 0 {
			rs, err := s.store.CloudResource().GetByIDs(ctx, rids)
			if err != nil {
				log.L(ctx).Error("Failed to exec CloudResourceStore.GetByIDs: " + err.Error())

				return nil, err
			}
			for _, r := range *rs {
				rc := r
				crMap[r.ID] = &rc
			}

			// computed period cost
			if len(fao.Filter.CompareDateBetween) == 2 {
				prs, err := s.store.CloudProductCost().GetCostByRIDs(ctx, rids, fao.Filter.CompareDateBetween)
				if err != nil {
					log.L(ctx).Error("Failed to exec BillingStore.GetCostByRIDs: " + err.Error())

					return nil, err
				}
				for _, r := range *prs {
					pcostMap[r.ResourceID] = r.Cost
				}
			}

			// latest14days cost
			err = s.getLatest14DayCost(ctx, rids, &latestCostMap)
			if err != nil {
				return nil, err
			}
		}

		var ridsNoBill []uint64
		crNoBillMap := make(map[uint64]*v1.CloudResource)
		if int(rtotal) < lParam.Offset+lParam.Limit {
			needs := lParam.Offset + lParam.Limit - int(rtotal)
			li := lParam.Limit - len(rids)
			of := needs - li
			rsWithoutB, err := s.store.CloudResource().GetReourcesWithoutBilling(ctx, &fao, &dpts, of, li)
			if err != nil {
				coder := errors.ParseCoder(err)
				if coder.Code() != code.ErrRecordNotFound {
					e := errors.Wrap(err, "Error when exec CloudResourceStore.GetReourcesWithoutBilling")
					log.L(ctx).Error(e.Error())

					return nil, e
				}
			}

			for _, r := range *rsWithoutB {
				ridsNoBill = append(ridsNoBill, r.ID)
				rcopy := r
				crNoBillMap[r.ID] = &rcopy
			}

			// latest14days cost
			err = s.getLatest14DayCost(ctx, ridsNoBill, &latestCostMap)
			if err != nil {
				return nil, err
			}
		}

		total, err := s.store.CloudResource().GetTotal(ctx, &fao, &dpts)
		if err != nil {
			log.L(ctx).Error("Failed to exec CloudResourceStore.GetTotal: " + err.Error())

			return nil, err
		}

		res.Pagination = &v1.Pagination{
			Total: total,
			Page:  lParam.Offset/lParam.Limit + 1,
			PSize: lParam.Limit,
		}

		for _, rid := range rids {
			lp, ok := pcostMap[rid]
			lastPeriod := &lp
			if !ok {
				lastPeriod = nil
			}
			res.Items = append(res.Items, &v1.GetResBillingListDataIt{
				Resource: crMap[rid],
				Cost: &v1.CloudResourceCostOverview{
					Total:      costMap[rid],
					LastPeriod: lastPeriod,
					Latest14d:  convertCostMap2Slice(latestCostMap[rid]),
				},
			})
		}

		for _, nrid := range ridsNoBill {
			res.Items = append(res.Items, &v1.GetResBillingListDataIt{
				Resource: crNoBillMap[nrid],
				Cost: &v1.CloudResourceCostOverview{
					Latest14d: convertCostMap2Slice(latestCostMap[nrid]),
				},
			})
		}
	} else {
		return nil, errors.WithCode(code.ErrUnknown, "Only support order by cost DESC")
	}

	return &res, nil
}

// SyncWithBQ sync billing data from bigquery. Sync all if fullUpdate is true.
func (s *billingService) SyncWithBQ(ctx context.Context, fullUpdate bool) error {
	log.L(ctx).Info("SyncWithBQ (billing) called.")

	cloudrs := &cloudResourceService{store: s.store}
	crs, err := cloudrs.ListAll(ctx, []string{"Provider"})
	if err != nil {
		return err
	}

	bqSrv, err := getBQSrv(ctx)
	// bqSrv, err := bigquery.NewService(ctx) // use default sa
	if err != nil {
		return err
	}

	awsResourceIDs := make(map[uint64]string)
	gcpResourceIDs := make(map[uint64]string)
	for _, r := range *crs {
		if r.Provider.Code == "aws" {
			awsResourceIDs[r.ID] = r.CloudInsID
		}
		if r.Provider.Code == "gcp" {
			gcpResourceIDs[r.ID] = r.CloudInsID
		}
	}

	// sync aws
	s.SyncAWSByResourceIDs(ctx, bqSrv, &awsResourceIDs, fullUpdate)
	// sync gcp
	s.SyncGCPByResourceIDs(ctx, bqSrv, &gcpResourceIDs, fullUpdate)

	return nil
}
