package v1

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"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"
)

func (s *billingService) CheckCostAlerts(ctx context.Context) error {
	log.L(ctx).Info("BillingSrv.CheckCostAlerts called.")

	// get all resource alerts
	alerts, err := s.store.CostAlert().ListAllActives(ctx)
	if err != nil {
		log.L(ctx).Error("Error when call store.CostAlert.ListAll: " + err.Error())

		return err
	}
	gAlerts := make(map[string][]v1.CostAlertRule)
	for _, ar := range *alerts {
		curtG := strconv.FormatUint(ar.ResourceID, 10) + "-" + strconv.Itoa(ar.Type)
		gAlerts[curtG] = append(gAlerts[curtG], ar)
	}
	// loop all alerts, check the effect
	var wg sync.WaitGroup
	ps := make(chan struct{}, 10)
	for g, gar := range gAlerts {
		wg.Add(1)
		go func(g string, gar []v1.CostAlertRule) {
			ps <- struct{}{}
			defer func() { <-ps }()
			defer wg.Done()

			s.checkGroupCostAlert(ctx, g, gar)
		}(g, gar)
	}

	wg.Wait()

	log.L(ctx).Info("Success to exec BillingSrv.CheckCostAlerts.")

	return nil
}

func (s *billingService) checkGroupCostAlert(ctx context.Context, g string, gar []v1.CostAlertRule) {
	log.L(ctx).Info("BillingSrv.checkGroupCostAlert called for alert rule group: " + g)

	if len(gar) <= 0 || gar[0].ResourceID <= 0 {
		return
	}
	rid := gar[0].ResourceID
	alertType := gar[0].Type

	switch alertType {
	case v1.TypeBaseMonthBudget, v1.TypeMonthAbs:
		s.checkMonthlyCostGroupAlerts(ctx, rid, gar)
	case v1.TypeDailyAbs:
		s.checkDailyCostGroupAlerts(ctx, rid, gar)
	case v1.TypeBaseDailyAverage:
		s.checkBaseDailyAverageGroupAlerts(ctx, rid, gar)
	}
}

func (s *billingService) checkDailyCostGroupAlerts(ctx context.Context, rid uint64, gar []v1.CostAlertRule) {
	t := time.Now().UTC()
	todayStart := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
	beforenow7d := todayStart.AddDate(0, 0, -7)
	today := todayStart.Format("2006-01-02")
	before7d := beforenow7d.Format("2006-01-02")
	costs7d, err := s.store.CloudProductCost().ListDayCostByDateRange(ctx, rid, []string{before7d, today})
	if err != nil {
		log.L(ctx).Error("Error when call store.CloudProductCost.ListDayCostByDateRange: " + err.Error())

		return
	}

	if costs7d == nil || len(*costs7d) <= 0 {
		return
	}

	sort.SliceStable(gar, func(i, j int) bool {
		return gar[i].Threshold > gar[j].Threshold
	})

	for _, costDay := range *costs7d {
		if costDay.Cost <= 0.0001 {
			continue
		}

		for _, ar := range gar {
			if ar.StartDate.Valid && ar.StartDate.Time.After(costDay.Date) || ar.EndDate.Valid && ar.EndDate.Time.Before(costDay.Date) {
				continue
			}

			hit := s.checkDailyCostAlert(ctx, ar, costDay)
			if hit {
				break
			}
		}
	}
}

func (s *billingService) checkDailyCostAlert(ctx context.Context, ar v1.CostAlertRule, dailyCost v1.DailyTotalCost) bool {
	log.L(ctx).Info("BillingSrv.checkDailyCostAlert called for alert rule with ID: " + strconv.Itoa(int(ar.ID)))

	alertRuleShadow := fmt.Sprintf("AlertID_%d-ResourceID_%d-Type_%d-ThresholdCost_%.1f-Date_%s", ar.ID, ar.Resource.ID, ar.Type, ar.Threshold, dailyCost.Date.Format("2006-01-02"))
	overCost := dailyCost.Cost > ar.Threshold
	alertRec, notFound, err := s.findExistAlertRecord(ctx, alertRuleShadow)
	if err != nil {
		return false
	}

	if overCost && notFound {
		// create alert record
		newARecord := &v1.AlertRecord{
			CreatedAt:       time.Now().UTC(),
			AlertRuleShadow: alertRuleShadow,
			ExpiredAt:       time.Now().UTC(),
		}
		err = s.store.AlertRecord().Create(ctx, newARecord)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Create: " + err.Error())

			return false
		}
		larkMsgShadow, err := s.sendLarkAlertMsg(ctx, ar, dailyCost.Cost, ar.Threshold, newARecord.ID, " on "+dailyCost.Date.Format("2006-01-02"), " (over daily critical line)", "")
		if err != nil {
			log.L(ctx).Error("Error when call BillingSrv.sendLarkAlertMsg: " + err.Error())

			return false
		}
		newARecord.LarkMsgShadow = larkMsgShadow
		// nextDay := time.Now().UTC().AddDate(0, 0, 1)
		nextY := time.Now().UTC().AddDate(1, 0, 0)
		newARecord.ExpiredAt = nextY

		err = s.store.AlertRecord().Update(ctx, newARecord)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Update: " + err.Error())

			return false
		}

		return true
	} else if !overCost && alertRec != nil && alertRec.ID > 0 {
		// del alert record
		err = s.store.AlertRecord().Delete(ctx, alertRec)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Delete: " + err.Error())

			return false
		}

		return false
	} else if overCost && alertRec != nil && alertRec.ID > 0 {
		// hit prev, do nothing
		return true
	}

	return false
}

func (s *billingService) checkBaseDailyAverageGroupAlerts(ctx context.Context, rid uint64, gar []v1.CostAlertRule) {
	t := time.Now().UTC()
	todayStart := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
	beforenow7d := todayStart.AddDate(0, 0, -7)
	today := todayStart.Format("2006-01-02")
	before7d := beforenow7d.Format("2006-01-02")
	costs7d, err := s.store.CloudProductCost().ListDayCostByDateRange(ctx, rid, []string{before7d, today})
	if err != nil {
		log.L(ctx).Error("Error when call store.CloudProductCost.ListDayCostByDateRange: " + err.Error())

		return
	}

	if costs7d == nil || len(*costs7d) <= 0 {
		return
	}

	sort.SliceStable(gar, func(i, j int) bool {
		return gar[i].Threshold > gar[j].Threshold
	})

	for _, costDay := range *costs7d {
		if costDay.Cost <= 0.0001 {
			continue
		}

		for _, ar := range gar {
			if ar.StartDate.Valid && ar.StartDate.Time.After(costDay.Date) || ar.EndDate.Valid && ar.EndDate.Time.Before(costDay.Date) {
				continue
			}

			hit := s.checkBaseDailyAverageAlert(ctx, ar, costDay)
			if hit {
				break
			}
		}
	}
}

type BaseDailyAverageAlertOpt struct {
	AverageDays int `json:"averageDays"`
}

func (s *billingService) computeBaseDailyAverage(ctx context.Context, ar v1.CostAlertRule, dailyCost v1.DailyTotalCost) (float64, int, error) {
	targetD := dailyCost.Date
	var opt BaseDailyAverageAlertOpt
	if err := json.Unmarshal([]byte(ar.Options), &opt); err != nil {
		log.L(ctx).Error("Error when unmarshal alert options: " + err.Error())

		return 0, 0, err
	}
	days := opt.AverageDays
	if days < 1 {
		days = 7
	}
	prevD := targetD.AddDate(0, 0, -1).Format("2006-01-02")
	prevDdays := targetD.AddDate(0, 0, -days).Format("2006-01-02")
	total, err := s.store.CloudProductCost().TotalCost(ctx, dailyCost.ResourceID, []string{prevDdays, prevD})
	if err != nil {
		log.L(ctx).Error("Error when call CloudProductCostStore.AverageCost: " + err.Error())

		return 0, 0, err
	}

	return total / float64(days), days, nil
}

func (s *billingService) checkBaseDailyAverageAlert(ctx context.Context, ar v1.CostAlertRule, dailyCost v1.DailyTotalCost) bool {
	log.L(ctx).Info("BillingSrv.checkBaseDailyAverageAlert called for alert rule with ID: " + strconv.Itoa(int(ar.ID)))

	alertRuleShadow := fmt.Sprintf("AlertID_%d-ResourceID_%d-Type_%d-Threshold_%.1f-Date_%s", ar.ID, ar.Resource.ID, ar.Type, ar.Threshold, dailyCost.Date.Format("2006-01-02"))
	alertRec, notFound, err := s.findExistAlertRecord(ctx, alertRuleShadow)
	if err != nil {
		return false
	}

	aver, days, err := s.computeBaseDailyAverage(ctx, ar, dailyCost)
	if err != nil {
		return false
	}
	overCost := dailyCost.Cost/aver > ar.Threshold

	if overCost && notFound {
		// create alert record
		newARecord := &v1.AlertRecord{
			CreatedAt:       time.Now().UTC(),
			AlertRuleShadow: alertRuleShadow,
			ExpiredAt:       time.Now().UTC(),
		}
		err = s.store.AlertRecord().Create(ctx, newARecord)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Create: " + err.Error())

			return false
		}
		reasonInfo := fmt.Sprintf(" (over %.1f%% of the latest-%ddays' average)", ar.Threshold*100, days)
		larkMsgShadow, err := s.sendLarkAlertMsg(ctx, ar, dailyCost.Cost, aver*ar.Threshold, newARecord.ID, " on "+dailyCost.Date.Format("2006-01-02"), reasonInfo, "")
		if err != nil {
			log.L(ctx).Error("Error when call BillingSrv.sendLarkAlertMsg: " + err.Error())

			return false
		}
		newARecord.LarkMsgShadow = larkMsgShadow
		// nextDay := time.Now().UTC().AddDate(0, 0, 1)
		nextY := time.Now().UTC().AddDate(1, 0, 0)
		newARecord.ExpiredAt = nextY

		err = s.store.AlertRecord().Update(ctx, newARecord)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Update: " + err.Error())

			return false
		}

		return true
	} else if !overCost && alertRec != nil && alertRec.ID > 0 {
		// del alert record
		err = s.store.AlertRecord().Delete(ctx, alertRec)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Delete: " + err.Error())

			return false
		}

		return false
	} else if overCost && alertRec != nil && alertRec.ID > 0 {
		// hit prev, do nothing
		return true
	}

	return false
}

func (s *billingService) findExistAlertRecord(ctx context.Context, alertRuleShadow string) (*v1.AlertRecord, bool, error) {
	alertRec, err := s.store.AlertRecord().GetValidByRule(ctx, alertRuleShadow)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			e := errors.Wrap(err, "Error when exec AlertRecordStore.GetValidByRule")
			log.L(ctx).Error(e.Error())

			return nil, true, e
		}

		return nil, true, nil
	}

	return alertRec, false, nil
}

func (s *billingService) checkMonthlyCostGroupAlerts(ctx context.Context, rid uint64, gar []v1.CostAlertRule) {
	curYM := time.Now().UTC().Format("2006-01")
	curMonCost, err := s.store.CloudProductCost().GetMonthlyCostByResourecID(ctx, curYM, rid)
	if err != nil {
		log.L(ctx).Error("Error when call store.CloudProductCost.GetMonthlyCostByResourecID: " + err.Error())

		return
	}

	if curMonCost <= 0.0001 {
		return
	}

	sort.SliceStable(gar, func(i, j int) bool {
		return gar[i].Threshold > gar[j].Threshold
	})

	for _, ar := range gar {
		hit := s.checkCostAlert(ctx, ar, curYM, curMonCost)
		if hit {
			break
		}
	}
}

//nolint:gocognit,gocyclo,cyclop // acceptable complexity
func (s *billingService) checkCostAlert(ctx context.Context, ar v1.CostAlertRule, curYM string, curMonCost float64) bool {
	log.L(ctx).Info("BillingSrv.checkCostAlert called for alert rule with ID: " + strconv.Itoa(int(ar.ID)))

	thresholdCost := ar.Threshold
	if ar.Type == v1.TypeBaseMonthBudget {
		bg, err := s.store.CloudBudget().GetMonthlyBudgetByResourecID(ctx, curYM, ar.Resource.ID)
		if err != nil {
			log.L(ctx).Error("Error when call store.CloudBudget.GetMonthlyBudgetByResourecID: " + err.Error())

			return false
		}

		thresholdCost = ar.Threshold * bg
	}
	alertRuleShadow := fmt.Sprintf("AlertID_%d-ResourceID_%d-Type_%d-ThresholdCost_%.1f", ar.ID, ar.Resource.ID, ar.Type, thresholdCost)
	overCost := curMonCost > thresholdCost

	alertRec, notFound, err := s.findExistAlertRecord(ctx, alertRuleShadow)
	if err != nil {
		return false
	}

	d := time.Now().UTC().AddDate(0, 0, 1)
	d2 := time.Now().UTC().AddDate(0, 1, 0)
	d3 := time.Date(d2.Year(), d2.Month(), 1, 0, 0, 0, 0, time.UTC)
	expired := d
	if d3.Before(d) {
		expired = d3
	}

	if overCost && notFound {
		// create alert record
		newARecord := &v1.AlertRecord{
			CreatedAt:       time.Now().UTC(),
			AlertRuleShadow: alertRuleShadow,
			ExpiredAt:       time.Now().UTC(),
		}
		err = s.store.AlertRecord().Create(ctx, newARecord)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Create: " + err.Error())

			return false
		}
		reasonInfo := ""
		if ar.Type == v1.TypeBaseMonthBudget {
			reasonInfo = fmt.Sprintf(" (over %.1f%% of budget)", ar.Threshold*100)
		}
		larkMsgShadow, err := s.sendLarkAlertMsg(ctx, ar, curMonCost, thresholdCost, newARecord.ID, " this month", reasonInfo, " until the end of this month")
		if err != nil {
			log.L(ctx).Error("Error when call BillingSrv.sendLarkAlertMsg: " + err.Error())

			return false
		}
		newARecord.LarkMsgShadow = larkMsgShadow
		newARecord.ExpiredAt = expired
		err = s.store.AlertRecord().Update(ctx, newARecord)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Update: " + err.Error())

			return false
		}

		return true
	} else if !overCost && alertRec != nil && alertRec.ID > 0 {
		// del alert record
		err = s.store.AlertRecord().Delete(ctx, alertRec)
		if err != nil {
			log.L(ctx).Error("Error when call AlertRecordStore.Delete: " + err.Error())

			return false
		}

		return false
	} else if overCost && alertRec != nil && alertRec.ID > 0 {
		// hit prev, do nothing
		return true
	}

	return false
}

type LarkMsg struct {
	ReceiveID string `json:"receive_id"`
	MsgType   string `json:"msg_type"`
	Content   string `json:"content"`
}

//nolint:funlen // acceptable
func (s *billingService) sendLarkAlertMsg(ctx context.Context, ar v1.CostAlertRule, actualCost, thresholdCost float64, arecord uint64, timeInfo, reasonInfo, blockTime string) (string, error) {
	log.L(ctx).Info("BillingSrv.sendLarkAlertMsg called.")

	provider, err := s.store.CloudProvider().GetByID(ctx, ar.Resource.ProviderID)
	if err != nil {
		return "", errors.Wrap(err, "Error when exec CloudProviderStore.GetByID")
	}
	term := "Account"
	if provider.Code == "gcp" {
		term = "Project"
	}

	alertCont := fmt.Sprintf(`
{
  "config": {
    "wide_screen_mode": true
  },
  "elements": [
    {
      "fields": [
        {
          "is_short": false,
          "text": {
            "content": "**%s Name**\n%s",
            "tag": "lark_md"
          }
        },
        {
          "is_short": false,
          "text": {
            "content": "**%s ID**\n%s",
            "tag": "lark_md"
          }
        }
      ],
      "tag": "div"
    },
    {
      "fields": [
        {
          "is_short": false,
          "text": {
            "content": "**Alert Content**\nThis %s has cost **<font color='red'>$%.1f</font>%s, more than <font color='red'>$%.1f</font>%s**.",
            "tag": "lark_md"
          }
        }
      ],
      "tag": "div"
    },
    {
      "actions": [
        {
          "tag": "button",
          "text": {
            "content": "Confirm in PCH",
            "tag": "plain_text"
          },
          "type": "primary",
          "url": "https://pch.pingcap.net/resource/self-managed/%s?ard=%d"
        }
      ],
      "tag": "action"
    },
		{
      "tag": "hr"
    },
		{
			"elements": [
        {
          "content": "*Click this button to block the same alert%s.\n*PCH can only be accessed through a desktop browser.",
          "tag": "lark_md"
        }
      ],
			"tag": "note"
		}
  ],
  "header": {
    "template": "red",
    "title": {
      "content": "Billing Alert: %s %s '%s'",
      "tag": "plain_text"
    }
  }
}
`, term, ar.Resource.Name, term, ar.Resource.CloudInsID, term, actualCost, timeInfo, thresholdCost, reasonInfo, ar.Resource.ManagementID, arecord, blockTime, provider.Name, term, ar.Resource.Name)

	receiverLarkIDs := map[string]string{ar.Resource.OwnerLarkID: "user_id"}
	if ar.Receivers != "" {
		var receivers []v1.CostAlertReceiver
		if err := json.Unmarshal([]byte(ar.Receivers), &receivers); err == nil {
			for _, rec := range receivers {
				if rec.LarkUserID != "" {
					receiverLarkIDs[rec.LarkUserID] = "user_id"
				}
				if rec.LarkChatID != "" {
					receiverLarkIDs[rec.LarkChatID] = "chat_id"
				}
			}
		} else {
			log.L(ctx).Error("Error when unmarshal alert receivers: " + err.Error())
		}
	}

	larkMsgIDs := make([]string, 0, len(receiverLarkIDs))
	for receID, receType := range receiverLarkIDs {
		reqBody := LarkMsg{
			MsgType:   "interactive",
			ReceiveID: receID,
			Content:   alertCont,
		}
		reqb, err := json.Marshal(&reqBody)
		if err != nil {
			log.L(ctx).Error(err.Error())

			return "", errors.WithCode(code.ErrEncodingJSON, err.Error())
		}
		commonService := &commonService{store: s.store}
		msgRes, err := commonService.GetDataByReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/im/v1/messages?receive_id_type="+receType, bytes.NewReader(reqb), nil)
		if err != nil {
			log.L(ctx).Error(err.Error())

			continue
		}
		var msgResMap map[string]interface{}
		if err := json.Unmarshal(msgRes, &msgResMap); err != nil {
			log.L(ctx).Errorf("Cannot unmarshal lark msg result, err: %s", err.Error())

			continue
		}
		larkMsgIDs = append(larkMsgIDs, msgResMap["message_id"].(string))
	}

	msgStr := strings.Join(larkMsgIDs, ",")

	return msgStr, nil
}
