package v1

import (
	"context"
	"encoding/json"
	"strconv"
	"time"

	"github.com/pingcap-inc/pchub/internal/apiserver/store"
	"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"
)

type cloudResourceService struct {
	store store.Factory
}

type CloudResourceSrv interface {
	ListAll(ctx context.Context, preloads []string) (*[]v1.CloudResource, error)
	Detail(ctx context.Context, mid string) (*v1.GetCloudResourceDetailData, error)
	NameCheck(ctx context.Context, providerCode, name string) (bool, error)
	GetByManagementID(ctx context.Context, mid string) (*v1.CloudResource, error)
	UpdateInfo(ctx context.Context, r *v1.CloudResource) error
	Search(ctx context.Context, searchInfo string) ([]*v1.CloudResource, error)
	DetailMembers(ctx context.Context, id uint64, provider string) (*[]v1.GetCloudResourceDetailMemIt, error)
}

var _ CloudResourceSrv = (*cloudResourceService)(nil)

func newCloudResource(srv *service) *cloudResourceService {
	return &cloudResourceService{store: srv.store}
}

func (cr *cloudResourceService) UpdateInfo(ctx context.Context, r *v1.CloudResource) error {
	log.L(ctx).Info("CloudResourceSrv.UpdateInfo called")

	return cr.store.CloudResource().Update(ctx, r)
}

func (cr *cloudResourceService) GetByManagementID(ctx context.Context, mid string) (*v1.CloudResource, error) {
	log.L(ctx).Info("CloudResourceSrv.GetByManagementID called, mid: " + mid)

	r, err := cr.store.CloudResource().Get(ctx, &v1.CloudResource{ManagementID: mid})
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			e := errors.Wrap(err, "error when exec CloudResourceStore.Get")
			log.L(ctx).Error(e.Error())

			return nil, e
		}

		return nil, errors.WithCode(code.ErrResourceNotFound, "cloud resource for managementID: "+mid+" not found")
	}

	return r, nil
}

func (cr *cloudResourceService) ListAll(ctx context.Context, preloads []string) (*[]v1.CloudResource, error) {
	log.L(ctx).Info("Service:CloudResourceSrv.ListAll called.")

	crs, err := cr.store.CloudResource().ListAll(ctx, preloads)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when exec Store:CloudResource.ListAll").Error())

			return nil, errors.Wrap(err, "Error when list cloud resource from db")
		}
	}

	return crs, nil
}

func (cr *cloudResourceService) Detail(ctx context.Context, mid string) (*v1.GetCloudResourceDetailData, error) {
	log.L(ctx).Info("CloudResourceSrv.Detail called, mid: " + mid)

	r, err := cr.store.CloudResource().Get(ctx, &v1.CloudResource{ManagementID: mid})
	if err != nil {
		return nil, err
	}

	dpts, err := cr.store.Department().GetChainByLarkID(ctx, r.DepartLarkID)
	if err != nil {
		return nil, err
	}

	mems, err := cr.DetailMembers(ctx, r.ID, r.Provider.Name)
	if err != nil {
		return nil, err
	}

	ow, err := cr.DetailOwner(ctx, r.ID)
	if err != nil {
		return nil, err
	}

	b, err := cr.DetailBilling(ctx, r.ID)
	if err != nil {
		return nil, err
	}

	hm, err := cr.DetailHistoricalInvoices(ctx, r.ID)
	if err != nil {
		return nil, err
	}

	bp, err := cr.DetailBudgetPlanning(ctx, r.ID)
	if err != nil {
		return nil, err
	}

	al, err := cr.DetailAlert(ctx, r.ID)
	if err != nil {
		return nil, err
	}

	tas := make([]v1.TrustAdvisor, 0)
	if r.Provider.Code == "aws" {
		tas, err = cr.store.TrustAdvisor().ListSecurityItems(ctx, r.ID)
		if err != nil {
			log.L(ctx).Error("Failed to exec TrustAdvisorStore.ListSecurityItems: " + err.Error())
		}
	}

	res := &v1.GetCloudResourceDetailData{
		Resource:           r,
		Departments:        *dpts,
		Members:            *mems,
		Owner:              *ow,
		DailyCost:          *b,
		HistoricalInvoices: *hm,
		BudgetPlanning:     *bp,
		Alerts:             *al,
		SecurityAdvisors:   tas,
	}

	return res, nil
}

//nolint:gocognit,gocyclo,cyclop // accpetaable
func (cr *cloudResourceService) DetailAlert(ctx context.Context, id uint64) (*[]v1.Alert, error) {
	alerts, err := cr.store.CostAlert().ListByResourceID(ctx, id)
	if err != nil {
		log.L(ctx).Error("Failed to exec CostAlertStore.ListByResourceID: " + err.Error())

		return nil, err
	}

	res := make([]v1.Alert, 0, len(*alerts))
	for _, a := range *alerts {
		item := v1.Alert{
			ID:           a.ID,
			Threshold:    a.Threshold,
			Type:         a.Type,
			ThresholdNum: a.Threshold,
			Options:      a.Options,
		}
		if a.StartDate.Valid {
			item.StartDate = a.StartDate.Time.Format("2006-01-02")
		}
		if a.EndDate.Valid {
			item.EndDate = a.EndDate.Time.Format("2006-01-02")
		}
		if a.Type == 2 {
			bg, err := cr.store.CloudBudget().GetMonthlyBudgetByResourecID(ctx, time.Now().UTC().Format("2006-01"), id)
			if err != nil {
				log.L(ctx).Error("Failed to exec CloudBudgetStore.GetMonthlyBudgetByResourecID: " + err.Error())

				return nil, err
			}
			item.ThresholdNum = bg * a.Threshold
		}
		if a.Receivers != "" {
			var rs []v1.CostAlertReceiver
			if err := json.Unmarshal([]byte(a.Receivers), &rs); err != nil {
				log.L(ctx).Error("Failed to unmarshal receivers: " + err.Error())

				return nil, err
			}

			userLarkIDs := make([]string, 0)
			chatlarkIDs := make([]string, 0)
			for _, r := range rs {
				if r.LarkUserID != "" {
					userLarkIDs = append(userLarkIDs, r.LarkUserID)
				}
				if r.LarkChatID != "" {
					chatlarkIDs = append(chatlarkIDs, r.LarkChatID)
				}
			}

			users, err := cr.store.User().MGetByLarkIDs(ctx, userLarkIDs)
			if err != nil {
				log.L(ctx).Error("Failed to exec UserStore.MGetByLarkIDs: " + err.Error())

				return nil, err
			}

			chats, err := cr.store.LarkChat().MGetByLarkIDs(ctx, chatlarkIDs)
			if err != nil {
				log.L(ctx).Error("Failed to exec LarkChatStore.MGetByLarkIDs: " + err.Error())

				return nil, err
			}

			for _, u := range users {
				n := u.EnName
				if n == "" {
					n = u.Name
				}
				item.Receivers = append(item.Receivers, &v1.AlertReceiver{
					Type:   "user",
					Name:   n,
					Avatar: string(u.Avatars),
					LarkID: u.LarkUserID,
				})
			}
			for _, c := range chats {
				item.Receivers = append(item.Receivers, &v1.AlertReceiver{
					Type:   "chat",
					Name:   c.Name,
					Avatar: c.Avatar,
					LarkID: c.ChatLarkID,
				})
			}
		}
		res = append(res, item)
	}

	return &res, nil
}

func (cr *cloudResourceService) DetailMembers(ctx context.Context, id uint64, provider string) (*[]v1.GetCloudResourceDetailMemIt, error) {
	ms, err := cr.store.ResourceMemberAccess().ListByResourceID(ctx, id)
	if err != nil {
		log.L(ctx).Error("Failed to exec ResourceMemberAccessStore.ListByResourceID: " + err.Error())

		return nil, err
	}

	mems := make([]v1.GetCloudResourceDetailMemIt, 0, len(ms))
	for _, m := range ms {
		mems = append(mems, v1.GetCloudResourceDetailMemIt{
			ID:     m.ID,
			Member: m.Member,
			Role:   m.Role,
		})
	}

	return &mems, nil
}

func (cr *cloudResourceService) DetailOwner(ctx context.Context, id uint64) (*v1.User, error) {
	re, err := cr.store.CloudResource().GetByIDs(ctx, []uint64{id})
	if err != nil {
		log.L(ctx).Error("Failed to exec CloudResourceStore.GetByIDs: " + err.Error())

		return nil, err
	}

	if len(*re) == 0 {
		er := errors.WithCode(code.ErrResourceNotFound, "Cloud resource not found")
		log.L(ctx).Error(er.Error())

		return nil, er
	}
	u := (*re)[0].Owner

	return &u, nil
}

func (cr *cloudResourceService) DetailBilling(ctx context.Context, id uint64) (*[]v1.DailyProductCost, error) {
	dataRange := []string{
		time.Now().Add(-time.Hour * 24 * 30).Format("2006-01-02"),
		time.Now().Format("2006-01-02"),
	}

	re, err := cr.store.CloudProductCost().ListByDateRange(ctx, id, dataRange)
	if err != nil {
		log.L(ctx).Error("Failed to exec BillingStore.ListByDateRange: " + err.Error())

		return nil, err
	}

	res := make([]v1.DailyProductCost, 0, len(*re))
	for _, r := range *re {
		if r.Date.After(time.Now()) {
			continue
		}

		res = append(res, r)
	}

	return &res, nil
}

//nolint:gocognit // acceptacble
func (cr *cloudResourceService) DetailHistoricalInvoices(ctx context.Context, id uint64) (*[]v1.InvoiceWithDate, error) {
	const LIT = 12

	cre, err := cr.store.CloudProductCost().ListPeriodByResourecID(ctx, "month", LIT, id)
	if err != nil {
		log.L(ctx).Error("Failed to exec BillingStore.ListPeriodByResourecID: " + err.Error())

		return nil, err
	}

	bre, err := cr.store.CloudBudget().ListByResourecID(ctx, LIT, id)
	if err != nil {
		log.L(ctx).Error("Failed to exec CloudBudgetStore.ListByResourecID: " + err.Error())

		return nil, err
	}

	t := time.Now()
	ty := t.Year()
	tm := int(t.Month())
	var re []v1.InvoiceWithDate
	for i := 0; i < LIT; i++ {
		curTM := tm - i
		curTY := ty
		if tm-i < 1 {
			curTY = ty - 1
			curTM = 12 + tm - i
		}

		tmstr := strconv.Itoa(curTM)
		if curTM < 10 {
			tmstr = "0" + tmstr
		}

		dateStr := strconv.Itoa(curTY) + "-" + tmstr

		if dateStr < "2022-07" {
			break
		}

		r := v1.InvoiceWithDate{
			DateStr: dateStr,
		}
		isValid := false
		for _, c := range *cre {
			if c.DateStr == dateStr {
				r.Cost = c.Cost
				isValid = true
			}
			if c.DateStr < dateStr {
				break
			}
		}
		for _, b := range *bre {
			if b.DateStr == dateStr {
				r.Budget = b.Budget
				isValid = true
			}
			if b.DateStr < dateStr {
				break
			}
		}
		if isValid {
			re = append(re, r)
		}
	}

	return &re, nil
}

func (cr *cloudResourceService) DetailBudgetPlanning(ctx context.Context, id uint64) (*[]v1.CloudResourceBudget, error) {
	re, err := cr.store.CloudBudget().ListPlanningByResourceID(ctx, id)
	if err != nil {
		log.L(ctx).Error("Failed to exec CloudBudgetStore.ListPlanningByResourceID: " + err.Error())

		return nil, err
	}

	return re, nil
}

func (cr *cloudResourceService) NameCheck(ctx context.Context, providerIDStr, name string) (bool, error) {
	log.L(ctx).Info("CloudResourceSrv.NameCheck called, providerID: " + providerIDStr + ", name: " + name)

	providerID, err := strconv.ParseUint(providerIDStr, 10, 64)
	if err != nil {
		log.L(ctx).Error("failed to parse providerID into uint64: " + err.Error())

		return false, errors.WithCode(code.ErrValidation, "providerID can not be parsed into uint64")
	}

	_, err = cr.store.CloudResource().Get(ctx, &v1.CloudResource{
		Name:       name,
		ProviderID: providerID,
	})
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			e := errors.Wrap(err, "Error when exec CloudResourceStore.Get")
			log.L(ctx).Error(e.Error())

			return false, e
		}

		return true, nil
	}

	return false, nil
}

func (cr *cloudResourceService) Search(ctx context.Context, searchInfo string) ([]*v1.CloudResource, error) {
	log.L(ctx).Info("Service:CloudResourceSrv.Search called.")

	res, err := cr.store.CloudResource().Search(ctx, searchInfo)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when exec Store:CloudResource.Search").Error())

			return nil, errors.Wrap(err, "Error when search cloud resource from db")
		}
	}

	return res, nil
}
