package api

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"net/http"
	"slices"
	"sort"
	"strconv"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/auditloghandler"
	"github.com/pingcap/dbaas/billing2/handler/cachehandler"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/ratehandler"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/model/partner"
	"github.com/pingcap/dbaas/billing2/model/payment"
	"github.com/pingcap/dbaas/billing2/utils/ginutils"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/request"
	"github.com/pingcap/dbaas/billing2/utils/semaphore"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
)

type costExplorerAPI struct {
}

var costExploreSemInited bool
var concurrencyMutex = sync.Mutex{}
var costExploreSem, costExploreInternalSem, costExploreTrendsLongTimeSpanSem *semaphore.Semaphore

func InitCostExplorerSem() {
	if costExploreSemInited {
		return
	}
	concurrencyMutex.Lock()
	defer concurrencyMutex.Unlock()
	if costExploreSemInited {
		return
	}
	costExploreSem = semaphore.NewSemaphore(config.Get().GetInt(config.MaxCostExplorerQueryConcurrency))
	costExploreInternalSem = semaphore.NewSemaphore(config.Get().GetInt(config.MaxCostExplorerInternalQueryConcurrency))
	costExploreTrendsLongTimeSpanSem = semaphore.NewSemaphore(config.Get().GetInt(config.MaxCostExplorerTrendsLongTimeSpanQueryConcurrency))
	ratehandler.GetLimiter().SetLimit(CostExplorerTrendsTenantControlFuncName, ratehandler.Limit{
		Value:    config.Get().GetInt(config.CostExplorerTrendsQueryRequestTimesLimitPerTenant),
		TimeSpan: config.Get().GetDuration(config.CostExplorerTrendsQueryRequestTimesLimitPerTenantTimeSpanSeconds) * time.Second,
	})
}

// @Summary Cost Explorer
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param Query body forms.CostExplorerQuery true "Query"
// @Success 200 {object} forms.CostExplorerResult
// @Router /tenants/{tenant_id}/costexplorer [post]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer [post]
func (s *costExplorerAPI) QueryCost(c *gin.Context) (ginutils.HandlerFuncResult, error) {

	tenantID, err := getTenantIDFromURI(c)
	if err != nil {
		return nil, err
	}

	query := &forms.CostExplorerQuery{}
	if err := c.ShouldBindJSON(query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}

	model := &costexplorer.CostExplorerModel{}
	query.StartDate = query.StartDate.UTC()
	query.EndDate = query.EndDate.UTC()
	return model.CalculateCostWithCache(tenantID, 0, query)
}

// @Summary Internal Cost Explorer
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param Query body forms.CostExplorerInternalQuery true "Query"
// @Success 200 {object} forms.CostExplorerResult
// @Router /tenants/{tenant_id}/internal/costexplorer [post]
func (s *costExplorerAPI) InternalQueryCost(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	tenantID, err := getTenantIDFromURI(c)
	if err != nil {
		return nil, err
	}

	q := &forms.CostExplorerInternalQuery{}
	if err := c.ShouldBindJSON(q); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}

	model := &costexplorer.CostExplorerModel{}

	query := &forms.CostExplorerQuery{
		StartDate:  q.StartDate.UTC(),
		EndDate:    q.EndDate.UTC(),
		InvoiceID:  q.InvoiceID,
		Labels:     q.Labels,
		Units:      q.Units,
		Internal:   q.Internal,
		IgnoreBeta: q.IgnoreBeta,
	}
	return model.CalculateCostWithCache(tenantID, 0, query)
}

// @Summary Complete Cost Explorer
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param Query body forms.CostExplorerQueryV2 true "Query"
// @Success 200 {object} forms.CostExplorerResult
// @Router /tenants/{tenant_id}/internal/costexplorer/v2 [post]
func (s *costExplorerAPI) InternalQueryCostV2(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	tenantID, err := getTenantIDFromURI(c)
	if err != nil {
		return nil, err
	}

	query := &forms.CostExplorerQueryV2{}
	if err := c.ShouldBindJSON(query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	model := &costexplorer.CostExplorerModel{}
	return model.CalculateCostV2WithCache(tenantID, nil, query)
}

type MonthlyCostURI struct {
	TenantID uint `uri:"tenant_id" binding:"required"`
	Year     int  `uri:"year" binding:"required"`
	Month    int  `uri:"month" binding:"required"`
}

type OpenCostsQuery struct {
	StartDate string `form:"start_date" binding:"required"`
	EndDate   string `form:"end_date" binding:"required"`
}

type RecalculateCostExplorerURI struct {
	TenantID    uint `uri:"tenant_id" binding:"required"`
	SourceRunID uint `uri:"source_run_id" binding:"required"`
}

func isBillsEnabled(tenantID, billingConfigID uint) (bool, error) {
	billingConfigs, err := (&billingconfig.BillingConfigModel{}).GetBillingConfig(billingConfigID)
	if err != nil {
		return false, err
	}
	var partnerID uint
	switch billingConfigs.Type {
	case entity.BillingConfigType_MSPCustomer, entity.BillingConfigType_ResaleCustomer:
		tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
		if err != nil {
			return false, err
		}
		if !tenant.IsResaleCustomer && !tenant.IsMspCustomer {
			log.Warn("tenant is not resale or msp customer")
			return false, nil
		}
		partnerID64, err := strconv.ParseUint(tenant.ThirdPartyAccountCode, 10, 64)
		if err != nil {
			log.Error("parsing partner ID failed", zap.Error(err))
			return false, billing_errors.SystemError
		}
		partnerID = uint(partnerID64)
	case entity.BillingConfigType_MarketplaceResellerManagedCustomer:
		resellerBillingConfig, err := BillingConfigModel.GetBillingConfigByManagedTenantID(billingConfigs.TenantID)
		if err != nil {
			return false, err
		}
		partnerID = resellerBillingConfig.TenantID
	default:
		return true, nil
	}
	configsModel := &partner.Configs{}
	customerConfig, err := configsModel.GetCustomerPartnerConfigs(partnerID, billingConfigs.TenantID)
	if err != nil {
		return false, err
	}
	return customerConfig.EnableBillsView, nil
}

func ensureCostExplorerBillsVisible(c *gin.Context, tenantID uint, billingConfigID uint) error {
	if slices.Contains(config.Get().GetStringSlice(config.BillsInvisibleList), fmt.Sprintf("%d-%d", tenantID, billingConfigID)) {
		return billing_errors.NewInvalidInputError(errors.New("you are restricted view bills, please contact our support"))
	}
	billsEnabled, err := isBillsEnabled(tenantID, billingConfigID)
	if err != nil {
		return err
	}
	if !request.SourceFromOps(c.Request) && config.Get().GetBool(config.ViewBillsByConfig) && !billsEnabled {
		return billing_errors.ObjectNotExistsError
	}
	return nil
}

// @Summary Is Enabled Bills
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Success 200 {object} forms.BillsEnabled
// @Router /tenants/{tenant_id}/costexplorer/enabled [get]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/enabled [get]
func (s *costExplorerAPI) IsBillsEnabled(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	billsEnabled, err := isBillsEnabled(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)
	if err != nil {
		return nil, err
	}
	return forms.BillsEnabled{BillsEnabled: billsEnabled}, nil
}

// @Summary Cost Explorer Monthly
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param year path string true "year"
// @Param month path string true "month"
// @Param with_cppo_discount query bool false "with_cppo_discount"
// @Success 200 {object} forms.MonthlycostExplorerResult
// @Router /tenants/{tenant_id}/costexplorer/monthly_cost/year/{year}/month/{month} [get]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/monthly_cost/year/{year}/month/{month} [get]
func (s *costExplorerAPI) MonthlyCost(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	billsEnabled, err := isBillsEnabled(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)
	if err != nil {
		return nil, err
	}
	if !request.SourceFromOps(c.Request) && config.Get().GetBool(config.ViewBillsByConfig) && !billsEnabled {
		return nil, billing_errors.ObjectNotExistsError
	}
	if slices.Contains(config.Get().GetStringSlice(config.MonthlyBillsInvisibleList), fmt.Sprintf("%d-%d", billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)) {
		err := errors.New("you are restricted view bills, please contact our support")
		return nil, billing_errors.NewInvalidInputError(err)
	}
	input := &MonthlyCostURI{}
	err = c.ShouldBindUri(input)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	tenantID := input.TenantID

	model := &costexplorer.CostExplorerModel{}

	var costs *forms.CostExplorerResult
	isBigCustomer := slices.Contains(config.Get().GetStringSlice(config.MonthlyBillsCacheList), fmt.Sprintf("%d-%d", billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID))
	if isBigCustomer {
		costs, err = model.GetMonthlyCostFromCache(tenantID, billingCfgStruct.BillingConfigID, input.Year, input.Month)

		if err != nil {
			return nil, err
		}
	} else {
		costs, err = model.GetMonthlyCostWithCache(tenantID, billingCfgStruct.BillingConfigID, input.Year, input.Month)
		if err != nil {
			return nil, err
		}
	}

	lastMonthDate := timeutils.GetMonthDateByYearMonth(input.Year, input.Month).AddDate(0, -1, 0)

	invoiceGenerated := true

	invoiceModel := &payment.InvoiceModel{}
	_, err = invoiceModel.GetMonthlyInvoice(tenantID, billingCfgStruct.BillingConfigID, lastMonthDate)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			var res *forms.CostExplorerResult
			if isBigCustomer {
				res, err = model.GetMonthlyCostFromCache(tenantID, billingCfgStruct.BillingConfigID, lastMonthDate.Year(), int(lastMonthDate.Month()))
				if err != nil {
					return nil, err
				}
			} else {
				res, err = model.GetMonthlyCostWithCache(tenantID, billingCfgStruct.BillingConfigID, lastMonthDate.Year(), int(lastMonthDate.Month()))
				if err != nil {
					return nil, err
				}
			}

			// fot last month not need create invoice
			if res.UnbindedInvoiceCosts.CostPrice.Cmp(decimal.Zero) > 0 {
				invoiceGenerated = false
			}
		} else {
			return nil, err
		}
	}

	return &forms.MonthlycostExplorerResult{
		CostExplorerResult: *costs,
		InvoiceGenerated:   invoiceGenerated,
	}, nil
}

// @Bills Details
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param year path string true "year"
// @Param month path string true "month"
// @Param with_cppo_discount query bool false "with_cppo_discount"
// @Success 200 {object} forms.CostExplorerDetailsResult
// @Router /tenants/{tenant_id}/costexplorer/bills_details/year/{year}/month/{month} [get]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/bills_details/year/{year}/month/{month} [get]
func (s *costExplorerAPI) BillsDetails(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	input := &MonthlyCostURI{}
	err := c.ShouldBindUri(input)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	billingConfigID, err := getBillingConfigIDFromURI(c)
	if err != nil {
		return nil, err
	}
	if slices.Contains(config.Get().GetStringSlice(config.BillsInvisibleList), fmt.Sprintf("%d-%d", input.TenantID, billingConfigID)) {
		err := errors.New("you are restricted view bills, please contact our support")
		return nil, billing_errors.NewInvalidInputError(err)
	}
	billsEnabled, err := isBillsEnabled(input.TenantID, billingConfigID)
	if err != nil {
		return nil, err
	}
	if !request.SourceFromOps(c.Request) && config.Get().GetBool(config.ViewBillsByConfig) && !billsEnabled {
		return nil, billing_errors.ObjectNotExistsError
	}
	tenantID := input.TenantID
	defer func() {
		auditloghandler.Log(auditlog.EventShowBill, c, tenantID, nil, err)
	}()
	model := &costexplorer.CostExplorerModel{}
	costs, err := model.GetBillsDetailsWithCache(tenantID, billingConfigID, input.Year, input.Month)

	if err != nil {
		return nil, err
	}
	return costs, nil
}

// @Summary Cost Explorer Open Costs
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int true "billing_config_id"
// @Param start_date query string true "start date, YYYY-MM-DD"
// @Param end_date query string true "end date, YYYY-MM-DD"
// @Success 200 {object} forms.CostExplorerOpenCostsResponse
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/open_costs [get]
func (s *costExplorerAPI) OpenCosts(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	if err := ensureCostExplorerBillsVisible(c, billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID); err != nil {
		return nil, err
	}
	input := &OpenCostsQuery{}
	if err := c.ShouldBindQuery(input); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	start, end, err := parseOpenCostsWindow(input)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	defer func() {
		auditloghandler.Log(auditlog.EventShowBill, c, billingCfgStruct.TenantID, nil, err)
	}()
	return (&costexplorer.CostExplorerModel{}).CalculateOpenCostsForRange(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, start, end)
}

func parseOpenCostsWindow(input *OpenCostsQuery) (time.Time, time.Time, error) {
	if input == nil {
		return time.Time{}, time.Time{}, fmt.Errorf("open costs query is required")
	}
	start, err := parseOpenCostsDate(input.StartDate, "start_date")
	if err != nil {
		return time.Time{}, time.Time{}, err
	}
	endDay, err := parseOpenCostsDate(input.EndDate, "end_date")
	if err != nil {
		return time.Time{}, time.Time{}, err
	}
	end := endDay.AddDate(0, 0, 1)
	if !end.After(start) {
		return time.Time{}, time.Time{}, fmt.Errorf("end_date must be on or after start_date")
	}
	return start, end, nil
}

func parseOpenCostsDate(value string, name string) (time.Time, error) {
	date, err := time.ParseInLocation("2006-01-02", value, time.UTC)
	if err != nil {
		return time.Time{}, fmt.Errorf("invalid %s %q, expected YYYY-MM-DD", name, value)
	}
	return date, nil
}

// @Summary Cost Explorer Recalculate Run
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int true "billing_config_id"
// @Param source_run_id path int true "source_run_id"
// @Success 200 {object} forms.CostExplorerOpenCostsResponse
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/recalculate/{source_run_id} [get]
func (s *costExplorerAPI) RecalculateCosts(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	if err := ensureCostExplorerBillsVisible(c, billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID); err != nil {
		return nil, err
	}
	input := &RecalculateCostExplorerURI{}
	if err := c.ShouldBindUri(input); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	defer func() {
		auditloghandler.Log(auditlog.EventShowBill, c, billingCfgStruct.TenantID, nil, err)
	}()
	return (&costexplorer.CostExplorerModel{}).CalculateRecalculateRunCosts(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, input.SourceRunID)
}

// @Summary Cost Explorer Args
// @Schemes
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Success 200 {object} forms.CostExplorerArgs
// @Router /tenants/{tenant_id}/costexplorer/args [get]
func (s *costExplorerAPI) CostExplorerArgs(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	bcfg, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	tenantID := bcfg.TenantID
	if slices.Contains(config.Get().GetStringSlice(config.BillsInvisibleList), fmt.Sprintf("%d-%d", bcfg.TenantID, bcfg.BillingConfigID)) {
		err := errors.New("you are restricted view bills, please contact our support")
		return nil, billing_errors.NewInvalidInputError(err)
	}
	funcName := "CostExplorerArgs"
	var ret forms.CostExplorerArgs
	if cachehandler.Get().Get(funcName, &ret, tenantID) {
		return ret, nil
	}
	clusters, err := costexplorer.GetCostExplorerClusters(tenantID)
	if err != nil {
		return nil, err
	}
	projects, err := costexplorer.GetCostExplorerProjects(tenantID)
	if err != nil {
		return nil, err
	}
	providers, err := costexplorer.GetCostExplorerProviders()
	if err != nil {
		return nil, err
	}
	services, err := costexplorer.GetCostExplorerServices()
	if err != nil {
		return nil, err
	}
	productsSet := make(map[string]*struct{})
	for _, svc := range services {
		productsSet[*svc.Product] = nil
	}
	var products []string
	for p, _ := range productsSet {
		products = append(products, p)
	}
	ret = forms.CostExplorerArgs{
		Products:  products,
		Services:  services,
		Providers: providers,
		Clusters:  clusters,
		Projects:  projects,
	}
	cachehandler.Get().Set(funcName, &ret, tenantID)
	return &ret, nil
}

const CostExplorerTrendsTenantControlFuncName = "CostExplorerTrendsTenantControl"

func CostExplorerTrendsTenantControl(tenantID uint) error {
	funcName := CostExplorerTrendsTenantControlFuncName
	if !ratehandler.GetLimiter().Allow(funcName, tenantID) {
		return billing_errors.NewTooManyRequestError(errors.New("org requests out of limit, please try again later"))
	}
	return nil
}

func parseTrendsQuery(c *gin.Context, tenantID uint, isOpenAPI bool) (*forms.CostExploreTrendsInput, error) {
	err := CostExplorerTrendsTenantControl(tenantID)
	if err != nil {
		return nil, err
	}
	query := &forms.CostExploreTrendsInput{}
	if !isOpenAPI {
		if err := c.ShouldBindJSON(query); err != nil {
			return nil, billing_errors.NewInvalidInputError(err)
		}
	} else {
		trendsInput := &forms.TrendsInput{}
		if err := c.ShouldBindJSON(trendsInput); err != nil {
			return nil, billing_errors.NewInvalidInputError(err)
		}
		query.StartDate = trendsInput.Filter.TimeRange.StartDate
		query.EndDate = trendsInput.Filter.TimeRange.EndDate
		query.Services = trendsInput.Filter.Services
		query.Providers = trendsInput.Filter.Providers
		query.Projects = trendsInput.Filter.ProjectIDs
		query.Clusters = trendsInput.Filter.ClusterIDs
		query.Customer = trendsInput.Filter.CustomerIDs

		query.BreakdownBy = trendsInput.Groups.GroupBy
		query.TimeGroup = trendsInput.Groups.TimeGranularity
		if trendsInput.Groups.TimeGranularity != nil && *trendsInput.Groups.TimeGranularity == forms.CostExploreTrendsTimeGroupTotal {
			query.TimeGroup = nil
		}
	}

	if query.StartDate == nil {
		return nil, billing_errors.NewInvalidInputError(errors.New("you must assign start date"))
	}
	if query.EndDate == nil {
		query.EndDate = timeutils.GetNowAddr()
	}
	query.StartDate = timeutils.GetTimeAddr(query.StartDate.UTC())
	query.EndDate = timeutils.GetTimeAddr(query.EndDate.UTC())
	if !query.EndDate.After(*query.StartDate) {
		return nil, billing_errors.NewInvalidInputError(errors.New("end date must after start date"))
	}
	timeSpan := int(query.EndDate.Sub(*query.StartDate).Hours()) / 24
	if timeSpan > config.Get().GetInt(config.MaxCostExplorerTrendsQueryDaysSpan) {
		return nil, billing_errors.NewInvalidInputError(errors.New("the time span is too large"))
	}
	internal := false
	query.IgnoreBeta = true
	query.Internal = &internal
	return query, nil
}

// @Summary Cost Explorer Trends
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path string false "billing_config_id"
// @Param Query body forms.CostExploreTrendsInput true "Query"
// @Success 200 {object} forms.CostExploreTrendsOutput
// @Router /tenants/{tenant_id}/costexplorer/trends [post]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/trends [post]
func (s *costExplorerAPI) CostExplorerTrends(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	if slices.Contains(config.Get().GetStringSlice(config.BillsInvisibleList), fmt.Sprintf("%d-%d", billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)) {
		err := errors.New("you are restricted view bills, please contact our support")
		return nil, billing_errors.NewInvalidInputError(err)
	}
	billsEnabled, err := isBillsEnabled(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)
	if err != nil {
		return nil, err
	}
	if !request.SourceFromOps(c.Request) && config.Get().GetBool(config.ViewBillsByConfig) && !billsEnabled {
		return nil, billing_errors.ObjectNotExistsError
	}
	tenantID, billingConfigID := billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID
	query, err := parseTrendsQuery(c, tenantID, false)
	if err != nil {
		return nil, err
	}
	var ret *forms.CostExploreTrendsOutput
	timeSpan := int(query.EndDate.Sub(*query.StartDate).Hours()) / 24
	if timeSpan > config.Get().GetInt(config.CostExplorerTrendsLongTimeSpanQueryThresholdDays) {
		semaphore.ExecutionWithSemaphore(costExploreTrendsLongTimeSpanSem, func() {
			ret, err = costexplorer.CostTrends(tenantID, billingConfigID, query)
		})
	} else {
		ret, err = costexplorer.CostTrends(tenantID, billingConfigID, query)
	}
	return ret, err
}

// @Cost Explorer Trends CSV
// @Schemes
// @Accept json
// @Produce text/csv
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path string false "billing_config_id"
// @Param with_discounts_and_credits query bool false "display_discounts_and_credits"
// @Param Query body forms.CostExploreTrendsInput true "Query"
// @Success 200 {object} []byte
// @Router /tenants/{tenant_id}/costexplorer/trends/csv [post]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/trends/csv [post]
func (s *costExplorerAPI) CostExplorerTrendsCSVExport(c *gin.Context) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	tenantID, billingConfigID := billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID
	if slices.Contains(config.Get().GetStringSlice(config.BillsInvisibleList), fmt.Sprintf("%d-%d", billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)) {
		err := errors.New("you are restricted view bills, please contact our support")
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	billsEnabled, err := isBillsEnabled(tenantID, billingConfigID)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	if !request.SourceFromOps(c.Request) && config.Get().GetBool(config.ViewBillsByConfig) && !billsEnabled {
		ginutils.ReturnError(c, billing_errors.ObjectNotExistsError)
		return
	}
	trendsQueryInput, err := parseTrendsQuery(c, tenantID, false)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	var q struct {
		DisplayCreditsAndDiscountOnCSV bool `form:"with_discounts_and_credits"`
	}
	err = c.ShouldBindQuery(&q)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	query := &forms.CostExploreTrendsCSVInput{
		CostExploreTrendsInput:         *trendsQueryInput,
		DisplayCreditsAndDiscountOnCSV: q.DisplayCreditsAndDiscountOnCSV,
	}
	var ret []byte
	timeSpan := int(query.EndDate.Sub(*query.StartDate).Hours()) / 24
	if timeSpan > config.Get().GetInt(config.CostExplorerTrendsLongTimeSpanQueryThresholdDays) {
		semaphore.ExecutionWithSemaphore(costExploreTrendsLongTimeSpanSem, func() {
			ret, err = costexplorer.CostTrendsCSV(tenantID, billingConfigID, query)
		})
	} else {
		ret, err = costexplorer.CostTrendsCSV(tenantID, billingConfigID, query)
	}
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewSystemError(err))
		return
	}
	c.Data(http.StatusOK, "text/csv", ret)
}

func costsToCSV(costs *forms.CostExplorerResult) [][]string {
	res := [][]string{}
	invoiceCosts := costs.InvoiceCostsCosts
	headers := []string{}
	categorys := []string{}
	for category := range invoiceCosts.CategoryUnitsCosts {
		categorys = append(categorys, category)
	}
	sort.StringSlice(categorys).Sort()
	headers = append(headers, "Project")
	headers = append(headers, categorys...)
	headers = append(headers, "Total")

	res = append(res, headers)

	projects := []string{}
	for project := range invoiceCosts.LabelsUnitsCosts["project"] {
		projects = append(projects, project)
	}

	sort.StringSlice(projects).Sort()

	for _, project := range projects {
		projectCategoryCosts := invoiceCosts.LabelsUnitsCosts["project"][project]
		line := []string{}
		line = append(line, project)
		for _, category := range categorys {
			line = append(line, projectCategoryCosts.CategoryUnitsCosts[category].CostPrice.String())
		}
		line = append(line, projectCategoryCosts.CostPrice.String())
		res = append(res, line)
	}
	line := []string{}
	line = append(line, "Total")
	for _, category := range categorys {
		line = append(line, invoiceCosts.CategoryUnitsCosts[category].CostPrice.String())
	}
	line = append(line, invoiceCosts.CostPrice.String())
	res = append(res, line)
	return res
}

// @Summary Cost Explorer Monthly
// @Schemes
// @Accept json
// @Produce octet-stream
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param year path string true "year"
// @Param month path string true "month"
// @Router /tenants/{tenant_id}/costexplorer/monthly_cost/year/{year}/month/{month}/export [post]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/monthly_cost/year/{year}/month/{month}/export [post]
func (s *costExplorerAPI) MonthlyCostExport(c *gin.Context) {
	input := &MonthlyCostURI{}
	err := c.ShouldBindUri(input)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	billingConfigID, err := getBillingConfigIDFromURI(c)
	if err != nil {
		return
	}
	tenantID := input.TenantID
	billsEnabled, err := isBillsEnabled(tenantID, billingConfigID)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	if !request.SourceFromOps(c.Request) && config.Get().GetBool(config.ViewBillsByConfig) && !billsEnabled {
		ginutils.ReturnError(c, billing_errors.ObjectNotExistsError)
		return
	}
	model := &costexplorer.CostExplorerModel{}

	costs, err := model.GetMonthlyCostWithCache(tenantID, billingConfigID, input.Year, input.Month)

	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}

	csvResult := costsToCSV(costs)

	writer := &bytes.Buffer{}

	err = csv.NewWriter(writer).WriteAll(csvResult)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewSystemError(err))
		return
	}

	c.Data(http.StatusOK, "application/octet-stream", writer.Bytes())
}

// @Summary Monthly Bills Explorer Monthly
// @Schemes
// @Accept json
// @Produce octet-stream
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param year path string true "year"
// @Param month path string true "month"
// @Router /tenants/{tenant_id}/costexplorer/monthly_bills/year/{year}/month/{month}/export [post]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/costexplorer/monthly_bills/year/{year}/month/{month}/export [post]
func (s *costExplorerAPI) MonthlyBillsExport(c *gin.Context) {
	s.monthlyBillsExport(c)
}

// monthlyBillsExport builds the monthly bills CSV. Usage and Unit columns are
// appended at the end so the original columns keep their positions for
// downstream consumers that parse by index.
func (s *costExplorerAPI) monthlyBillsExport(c *gin.Context) {
	input := &MonthlyCostURI{}
	err := c.ShouldBindUri(input)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	billingConfigID, err := getBillingConfigIDFromURI(c)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	tenantID := input.TenantID
	if slices.Contains(config.Get().GetStringSlice(config.BillsInvisibleList), fmt.Sprintf("%d-%d", tenantID, billingConfigID)) {
		err := errors.New("you are restricted view bills, please contact our support")
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	billsEnabled, err := isBillsEnabled(tenantID, billingConfigID)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	if !request.SourceFromOps(c.Request) && config.Get().GetBool(config.ViewBillsByConfig) && !billsEnabled {
		ginutils.ReturnError(c, billing_errors.ObjectNotExistsError)
		return
	}
	defer func() {
		auditloghandler.Log(auditlog.EventDownloadBill, c, tenantID, nil, err)
	}()
	csvLine := [][]string{}
	var header1, header2, header3, header4, header5 []string

	header1 = append(header1, "Billing Period", timeutils.GetMonthPeriodByYearMonth(input.Year, input.Month))

	tenantInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	header2 = append(header2, "Organization Name", tenantInfo.Name)
	header3 = append(header3, "Organization ID", strconv.Itoa(int(tenantID)))
	header4 = nil
	header5 = append(header5, "Billed Date", "Service", "Cloud Provider", "Region", "Project Name",
		"Project ID", "Cluster ID", "Cluster Name", "Running Total", "Discounts", "Credits", "Total Cost",
		"Usage", "Unit")

	csvLine = append(csvLine, header1, header2, header3, header4, header5)

	model := &costexplorer.CostExplorerModel{}
	//precision := config.Get().GetInt32(config.BillingCostsPrecisionDigits)
	var precision int32 = 6
	exportUsageDetails, err := model.GetExportBillsDetailsWithCache(tenantID, billingConfigID, input.Year, input.Month)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}

	for _, usageDetail := range exportUsageDetails {
		var oneLine []string
		amount, creditUsage, BillableCost := usageDetail.Amount.Truncate(precision), usageDetail.CreditUsage.Truncate(precision), usageDetail.BillableCost.Truncate(precision)
		discountCost := amount.Sub(creditUsage).Sub(BillableCost)
		usageValue := formatExportUsageValue(usageDetail, precision)
		oneLine = append(oneLine, usageDetail.BilledDate, usageDetail.Service, usageDetail.CloudProvider, usageDetail.Region,
			usageDetail.ProjectName, usageDetail.ProjectID, usageDetail.ClusterID, usageDetail.ClusterName,
			amount.String(),
			discountCost.String(),
			creditUsage.String(),
			BillableCost.String(),
			usageValue, usageDetail.Unit)
		csvLine = append(csvLine, oneLine)

	}
	writer := &bytes.Buffer{}

	err = csv.NewWriter(writer).WriteAll(csvLine)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewSystemError(err))
		return
	}

	c.Data(http.StatusOK, "application/octet-stream", writer.Bytes())
}

func formatExportUsageValue(usageDetail forms.ExportUsageDetails, precision int32) string {
	if usageDetail.Unit == "" {
		return ""
	}
	return usageDetail.Usage.Truncate(precision).String()
}

// @Summary Monthly Bills Explorer Monthly
// @Schemes
// @Accept json
// @Produce octet-stream
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param year path string true "year"
// @Param month path string true "month"
// @Router /providers/alicloud/costexplorer/monthly_bills/year/{year}/month/{month}/export [post]
func (s *costExplorerAPI) AliCloudMarketplaceMonthlyBillsExport(c *gin.Context) {
	input := struct {
		Year  int `uri:"year" binding:"required"`
		Month int `uri:"month" binding:"required"`
	}{}
	err := c.ShouldBindUri(&input)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	csvLine := [][]string{}
	var header1, header2 []string

	header1 = append(header1, "Billing Period", timeutils.GetMonthPeriodByYearMonth(input.Year, input.Month))
	header2 = append(header2, "Customer ID", "Billed Date", "Service", "Region", "Cluster ID", "Total Cost")

	csvLine = append(csvLine, header1, header2)

	model := &costexplorer.CostExplorerModel{}
	invoices, err := invoiceModel.ListSentInvoicesByPaymentType(input.Year, input.Month, entity.PaymentType_AliCloud)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	billingConfigPaymentCodeMap := make(map[uint]string)
	var tenantIDs, billingConfigs []uint
	var invoiceIDs []int
	for _, inv := range invoices {
		if inv.PaymentCode == nil {
			err := errors.New("failed to get payment code on invoice")
			log.Error(err.Error(), zap.Error(err), zap.Uint("invoiceID", inv.InvoiceID))
			ginutils.ReturnError(c, err)
			return
		}
		if _, ok := billingConfigPaymentCodeMap[*inv.BindedBillingConfigID]; !ok {
			billingConfigPaymentCodeMap[*inv.BindedBillingConfigID] = *inv.PaymentCode
			tenantIDs = append(tenantIDs, inv.TenantID)
			billingConfigs = append(billingConfigs, *inv.BindedBillingConfigID)
		}
		invoiceIDs = append(invoiceIDs, int(inv.InvoiceID))
	}

	var precision int32 = 6
	exportUsageDetails, err := model.GetExportBillsDetailsByTenant(tenantIDs, billingConfigs, input.Year, input.Month, invoiceIDs)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}

	sort.Slice(exportUsageDetails, func(i, j int) bool {
		if exportUsageDetails[i].TenantID != exportUsageDetails[j].TenantID {
			return exportUsageDetails[i].TenantID < exportUsageDetails[j].TenantID
		}
		if exportUsageDetails[i].BillingConfigID != exportUsageDetails[j].BillingConfigID {
			return exportUsageDetails[i].BillingConfigID < exportUsageDetails[j].BillingConfigID
		}
		return exportUsageDetails[i].BilledDate < exportUsageDetails[j].BilledDate
	})
	for _, usageDetail := range exportUsageDetails {
		customerID, ok := billingConfigPaymentCodeMap[usageDetail.BillingConfigID]
		if !ok {
			p, err := paymentModel.GetPaymentMethodWithPaymentType(usageDetail.TenantID, usageDetail.BillingConfigID, entity.PaymentType_AliCloud)
			if err != nil {
				if errors.Is(err, billing_errors.ObjectNotExistsError) {
					log.Info("Ignore bills whose payment is not ali")
					continue
				}
				ginutils.ReturnError(c, err)
				return
			}
			billingConfigPaymentCodeMap[usageDetail.BillingConfigID] = p.PaymentCode
			customerID = p.PaymentCode
		}
		oneLine := []string{
			customerID, usageDetail.BilledDate, usageDetail.Service, usageDetail.Region, usageDetail.ClusterID, usageDetail.BillableCost.Truncate(precision).String(),
		}
		csvLine = append(csvLine, oneLine)
	}
	writer := &bytes.Buffer{}

	err = csv.NewWriter(writer).WriteAll(csvLine)
	if err != nil {
		ginutils.ReturnError(c, billing_errors.NewSystemError(err))
		return
	}

	c.Data(http.StatusOK, "application/octet-stream", writer.Bytes())
}

type SetTenantMonthlyCacheRequest struct {
	TenantID uint   `json:"tenant_id" binding:"required"`
	Month    string `json:"month" binding:"required"`
}

type SetTenantMonthlyCacheResponse struct {
	Message string `json:"message"`
}

func (s *costExplorerAPI) SetTenantMonthlyCache(c *gin.Context) {
	var req SetTenantMonthlyCacheRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	billedMonth, err := time.Parse("2006-01", req.Month)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid month format, should be YYYY-MM"})
		return
	}

	billingConfigModel := &billingconfig.BillingConfigModel{}
	billingConfig, err := billingConfigModel.GetActiveBillingConfig(req.TenantID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get billing config: %v", err)})
		return
	}

	model := &costexplorer.CostExplorerModel{}
	err = model.SetMonthlyCostToCache(req.TenantID, billingConfig.BillingConfigID, billedMonth.Year(), int(billedMonth.Month()))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, SetTenantMonthlyCacheResponse{
		Message: "Monthly cost cache set successfully",
	})
}
func InitializeCostExplorerRouter(router ginutils.MyRoute) {
	InitCostExplorerSem()
	s := &costExplorerAPI{}
	router.Handle("POST", "/providers/alicloud/costexplorer/monthly_bills/year/:year/month/:month/export", s.AliCloudMarketplaceMonthlyBillsExport)

	router.HandleWithErrorAndSemaphore("POST", "/tenants/:tenant_id/costexplorer", s.QueryCost, costExploreSem)
	router.HandleWithErrorAndSemaphore("POST", "/tenants/:tenant_id/internal/costexplorer", s.InternalQueryCost, costExploreInternalSem)
	router.HandleWithErrorAndSemaphore("POST", "/tenants/:tenant_id/internal/costexplorer/v2", s.InternalQueryCostV2, costExploreInternalSem)

	router.HandleWithError("GET", "/tenants/:tenant_id/costexplorer/enabled", s.IsBillsEnabled)
	router.HandleWithError("GET", "/tenants/:tenant_id/costexplorer/args", s.CostExplorerArgs)
	router.HandleWithErrorAndSemaphore("GET", "/tenants/:tenant_id/costexplorer/monthly_cost/year/:year/month/:month", s.MonthlyCost, costExploreSem)
	router.HandleWithErrorAndSemaphore("GET", "/tenants/:tenant_id/costexplorer/bills_details/year/:year/month/:month", s.BillsDetails, costExploreSem)
	router.HandleWithErrorAndSemaphore("POST", "/tenants/:tenant_id/costexplorer/trends", s.CostExplorerTrends, costExploreSem)
	router.HandleWithSemaphore("POST", "/tenants/:tenant_id/costexplorer/monthly_cost/year/:year/month/:month/export", s.MonthlyCostExport, costExploreSem)
	router.HandleWithSemaphore("POST", "/tenants/:tenant_id/costexplorer/monthly_bills/year/:year/month/:month/export", s.MonthlyBillsExport, costExploreSem)
	router.HandleWithSemaphore("POST", "/tenants/:tenant_id/costexplorer/trends/csv", s.CostExplorerTrendsCSVExport, costExploreSem)
	router.Handle("POST", "/setTenantMonthlyCache", s.SetTenantMonthlyCache)
	// for billing config add api
	router.HandleWithError("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/enabled", s.IsBillsEnabled)
	router.HandleWithErrorAndSemaphore("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer", s.QueryCost, costExploreSem)
	router.HandleWithErrorAndSemaphore("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/monthly_cost/year/:year/month/:month", s.MonthlyCost, costExploreSem)
	router.HandleWithErrorAndSemaphore("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/bills_details/year/:year/month/:month", s.BillsDetails, costExploreSem)
	router.HandleWithErrorAndSemaphore("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/open_costs", s.OpenCosts, costExploreSem)
	router.HandleWithErrorAndSemaphore("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/recalculate/:source_run_id", s.RecalculateCosts, costExploreSem)
	router.HandleWithErrorAndSemaphore("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/trends", s.CostExplorerTrends, costExploreSem)
	router.HandleWithSemaphore("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/monthly_cost/year/:year/month/:month/export", s.MonthlyCostExport, costExploreSem)
	router.HandleWithSemaphore("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/monthly_bills/year/:year/month/:month/export", s.MonthlyBillsExport, costExploreSem)
	router.HandleWithSemaphore("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/costexplorer/trends/csv", s.CostExplorerTrendsCSVExport, costExploreSem)
}
