package costexplorer

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/cachehandler"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/model/billing"
	billingsnapshot "github.com/pingcap/dbaas/billing2/model/billing/snapshot"
	"github.com/pingcap/dbaas/billing2/utils/array"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
)

// InvoiceSummary holds essential details for a specific invoice.
type InvoiceSummary struct {
	SentDate    string
	PaymentCode string
}

// InvoicedCostExplorerModel handles queries based on invoice sent dates.
type InvoicedCostExplorerModel struct {
	CostExplorerModel
}

// NewInvoicedCostExplorerModel creates and returns a new InvoicedCostExplorerModel.
func NewInvoicedCostExplorerModel() *InvoicedCostExplorerModel {
	return &InvoicedCostExplorerModel{}
}

// GetInvoicesBySentDateRange retrieves invoices that were sent within a specified date range.
// It filters by tenant and billing configuration IDs if provided.
func (c *InvoicedCostExplorerModel) GetInvoicesBySentDateRange(tenantIDs []uint, billingConfigIDs []uint, year int, month int) ([]entity.Invoice, error) {
	var invoices []entity.Invoice
	filter := []string{"deleted_at is null", "status = ?", "sent_date >= ? AND sent_date < ?"}

	startDate := timeutils.GetMonthDateByYearMonth(year, month)
	endDate := startDate.AddDate(0, 1, 0)
	params := []interface{}{entity.InvoiceStatus_SENT, startDate, endDate}

	if len(tenantIDs) != 0 {
		filter = append(filter, "tenant_id IN (?)")
		params = append(params, tenantIDs)
	}
	if len(billingConfigIDs) != 0 {
		filter = append(filter, "binded_billing_config_id IN (?)")
		params = append(params, billingConfigIDs)
	}

	query := db.GetReadOnlyDB().Where(strings.Join(filter, " AND "), params...)

	err := query.Find(&invoices).Error
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	return invoices, nil
}

// InvoiceSummaryCache holds essential details for a specific invoice.
type InvoiceSummaryCache struct {
	SentDate    string
	PaymentCode string
}

// getInvoiceIDsAndDatesFromInvoices extracts a slice of invoice IDs and a map of invoice ID to invoice summary.
func getInvoiceIDsAndDatesFromInvoices(invoices []entity.Invoice) ([]int, map[uint]InvoiceSummaryCache) {
	var invoiceIDs []int
	invoiceMap := make(map[uint]InvoiceSummaryCache)
	for _, invoice := range invoices {
		invoiceIDs = append(invoiceIDs, int(invoice.ID))
		var summary InvoiceSummaryCache
		if invoice.PaymentCode != nil {
			summary.PaymentCode = *invoice.PaymentCode
		}
		if invoice.SentDate != nil {
			summary.SentDate = invoice.SentDate.Format("2006-01-02")
		}
		invoiceMap[invoice.ID] = summary
	}
	return invoiceIDs, invoiceMap
}

// GetPartnerBillsDetailsBySentDateWithCache retrieves detailed partner bills based on invoice sent dates, utilizing caching.
func (c *InvoicedCostExplorerModel) GetPartnerBillsDetailsBySentDateWithCache(tenantIDs []uint, billingConfigIDs []uint, year int, month int, tenantIDAsCustomer bool) (*forms.PartnerInvoicedBillsDetails, error) {
	funcName := "GetPartnerBillsDetailsBySentDate"
	cacheArgs := []interface{}{array.JoinUnitSlice(tenantIDs, "|"), array.JoinUnitSlice(billingConfigIDs, "|"), year, month, tenantIDAsCustomer}
	ret := &forms.PartnerInvoicedBillsDetails{}

	if found := cachehandler.Get().Get(funcName, ret, cacheArgs...); found {
		return ret, nil
	}

	ret, err := c.GetPartnerBillsDetailsBySentDate(tenantIDs, billingConfigIDs, year, month, tenantIDAsCustomer)
	if err != nil {
		return nil, err
	}

	cachehandler.Get().Set(funcName, ret, cacheArgs...)
	return ret, nil
}

func buildCostsDetailByInvoiceIDsQuery(tenantIDs []uint, billingConfigIDs []uint, year int, month int, invoiceIDs []int) (string, map[string]interface{}, time.Duration, bool) {
	filter := []string{}
	params := map[string]interface{}{}

	filter = append(filter, "status = @status")
	params["status"] = entity.UserUnitCostStatus_DONE
	filter = append(filter, "internal = @internal")
	params["internal"] = false

	if len(tenantIDs) != 0 {
		filter = append(filter, "tenant_id in @tenant_ids")
		params["tenant_ids"] = tenantIDs
	}
	if len(billingConfigIDs) != 0 {
		filter = append(filter, "binded_billing_config_id in @binded_billing_config_ids")
		params["binded_billing_config_ids"] = billingConfigIDs
	}
	if len(invoiceIDs) != 0 {
		filter = append(filter, "invoice_id in @invoice_ids")
		params["invoice_ids"] = invoiceIDs
	} else {
		queryTime := getCostQueryTime(year, month)
		filter = append(filter, "cost_time_stamp > @start_date")
		params["start_date"] = queryTime.StartDate.Unix() - 1
		filter = append(filter, "cost_time_stamp < @end_date")
		params["end_date"] = queryTime.EndDate.Unix()
	}

	disabledTenants := config.Get().GetStringSlice(config.DisableClusterDisplayTenants)
	shouldRemoveLabelsFromGroupBy := false

	if len(tenantIDs) > 0 {
		for _, tenantID := range tenantIDs {
			if slices.Contains(disabledTenants, fmt.Sprint(tenantID)) {
				shouldRemoveLabelsFromGroupBy = true
				break
			}
		}

		if shouldRemoveLabelsFromGroupBy && len(invoiceIDs) == 0 {
			cutoffDate := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
			queryTime := getCostQueryTime(year, month)
			if queryTime.StartDate.Before(cutoffDate) {
				shouldRemoveLabelsFromGroupBy = false
			}
		}
	}

	var query string
	if shouldRemoveLabelsFromGroupBy {
		query = fmt.Sprintf(`
	select tenant_id, binded_billing_config_id, substr(from_unixtime(cost_time_stamp), 1, 10 ) as billed_date, invoice_id, unit, consumed_source, sum(cost) as cost, sum(cost_price) as cost_price, sum(raw_price) as raw_price
	from user_unit_costs
	where %s
	group by tenant_id, binded_billing_config_id, invoice_id, unit, consumed_source, substr(from_unixtime(cost_time_stamp), 1, 10 )
	order by tenant_id, binded_billing_config_id, substr(from_unixtime(cost_time_stamp), 1, 10 );
	`, strings.Join(filter, " and "))
	} else {
		query = fmt.Sprintf(`
	select tenant_id, binded_billing_config_id, substr(from_unixtime(cost_time_stamp), 1, 10 ) as billed_date, invoice_id, unit, labels, consumed_source, sum(cost) as cost, sum(cost_price) as cost_price, sum(raw_price) as raw_price
	from user_unit_costs
	where %s
	group by tenant_id, binded_billing_config_id, invoice_id, unit, labels, consumed_source, substr(from_unixtime(cost_time_stamp), 1, 10 )
	order by tenant_id, binded_billing_config_id, substr(from_unixtime(cost_time_stamp), 1, 10 );
	`, strings.Join(filter, " and "))
	}

	queryTimeout := config.Get().GetDuration(config.PerDBQueryTimeoutSeconds) * time.Second
	return query, params, queryTimeout, shouldRemoveLabelsFromGroupBy
}

func forEachCostsDetailByInvoiceIDs(tenantIDs []uint, billingConfigIDs []uint, year int, month int, invoiceIDs []int, handle func(entity.CostsDetailsQuery) error) error {
	if len(invoiceIDs) != 0 && len(tenantIDs) == 1 && len(billingConfigIDs) == 1 {
		remainingInvoiceIDs := make([]int, 0, len(invoiceIDs))
		for _, invoiceID := range invoiceIDs {
			_, found, err := loadSnapshotInvoiceLink(tenantIDs[0], billingConfigIDs[0], uint(invoiceID))
			if err != nil {
				return err
			}
			if !found {
				remainingInvoiceIDs = append(remainingInvoiceIDs, invoiceID)
				continue
			}
			if err := forEachLinkedInvoiceDetailWithSnapshotOverlay(tenantIDs[0], billingConfigIDs[0], uint(invoiceID), year, month, handle); err != nil {
				return err
			}
		}
		if len(remainingInvoiceIDs) == 0 {
			return nil
		}
		invoiceIDs = remainingInvoiceIDs
	}
	query, params, queryTimeout, shouldRemoveLabelsFromGroupBy := buildCostsDetailByInvoiceIDsQuery(tenantIDs, billingConfigIDs, year, month, invoiceIDs)
	enrich := !shouldRemoveLabelsFromGroupBy

	// When no invoice IDs are given, the query falls back to a full month scan.
	// Split by day to keep each sub-query under the DB timeout.
	if len(invoiceIDs) == 0 {
		startDate := timeutils.GetMonthDateByYearMonth(year, month)
		endDate := startDate.AddDate(0, 1, 0)
		days := splitDateRangeByDay(startDate, endDate)
		if len(days) > 1 {
			splitCtx, splitCancel := context.WithTimeout(context.Background(),
				getSplitLoopTimeout(shouldRemoveLabelsFromGroupBy, time.Time{}))
			defer splitCancel()

			for _, day := range days {
				if err := splitCtx.Err(); err != nil {
					return billing_errors.NewSystemError(fmt.Errorf("invoice detail query cancelled: %w", err))
				}
				dayParams := copyCostQueryParams(params)
				dayParams["start_date"] = day.start.Unix() - 1
				dayParams["end_date"] = day.end.Unix()
				if err := streamCostsDetailRows(query, dayParams, queryTimeout, enrich, tenantIDs, handle); err != nil {
					return err
				}
			}
			return nil
		}
	}
	return streamCostsDetailRows(query, params, queryTimeout, enrich, tenantIDs, handle)
}

func forEachLinkedInvoiceDetailWithSnapshotOverlay(tenantID, billingConfigID, invoiceID uint, year, month int, handle func(entity.CostsDetailsQuery) error) error {
	var invoice entity.Invoice
	if err := db.GetDB().Where("id = ? AND tenant_id = ? AND binded_billing_config_id = ?", invoiceID, tenantID, billingConfigID).First(&invoice).Error; err != nil {
		return err
	}
	window, err := billingsnapshot.BuildInvoiceWindow(invoice.Type, invoice.InvoiceDate)
	if err != nil {
		return err
	}
	legacy := func(start, end time.Time, callback func(entity.CostsDetailsQuery) error) error {
		query, params, queryTimeout, shouldRemoveLabelsFromGroupBy := buildCostsDetailByInvoiceIDsQuery(
			[]uint{tenantID}, []uint{billingConfigID}, year, month, []int{int(invoiceID)})
		params["start_date"] = start.Unix() - 1
		params["end_date"] = end.Unix()
		return streamCostsDetailRows(query, params, queryTimeout, !shouldRemoveLabelsFromGroupBy, []uint{tenantID}, callback)
	}
	return forEachDetailWithSnapshotOverlay(tenantID, billingConfigID, window.Start, window.End, invoiceID, nil, legacy, handle)
}

func getCostsSliceWithMultiBillingConfigsByInvoiceIDs(tenantIDs []uint, billingConfigIDs []uint, year int, month int, invoiceIDs []int) ([]entity.CostsDetailsQuery, error) {
	var allDetailsCosts []entity.CostsDetailsQuery
	err := forEachCostsDetailByInvoiceIDs(tenantIDs, billingConfigIDs, year, month, invoiceIDs, func(cost entity.CostsDetailsQuery) error {
		allDetailsCosts = append(allDetailsCosts, cost)
		return nil
	})
	if err != nil {
		return nil, err
	}
	return allDetailsCosts, nil
}

// GetPartnerBillsDetailsBySentDate retrieves detailed partner bills based on invoice sent dates.
func (c *InvoicedCostExplorerModel) GetPartnerBillsDetailsBySentDate(tenantIDs []uint, billingConfigIDs []uint, year int, month int, tenantIDAsCustomer bool) (*forms.PartnerInvoicedBillsDetails, error) {
	invoices, err := c.GetInvoicesBySentDateRange(tenantIDs, billingConfigIDs, year, month)
	if err != nil {
		return nil, err
	}

	if len(invoices) == 0 {
		return &forms.PartnerInvoicedBillsDetails{Year: year, Month: month}, nil
	}

	invoiceIDs, invoiceMap := getInvoiceIDsAndDatesFromInvoices(invoices)

	units, err := (&billing.UnitModel{}).GetAllUnits()
	if err != nil {
		return nil, err
	}

	acc := newPartnerInvoicedBillsDetailsAccumulator(units, invoiceMap, tenantIDAsCustomer)
	err = forEachCostsDetailByInvoiceIDs(tenantIDs, billingConfigIDs, year, month, invoiceIDs, func(cost entity.CostsDetailsQuery) error {
		ok, addErr := acc.add(cost)
		if addErr != nil {
			return addErr
		}
		if !ok {
			return errCostsDetailServiceUnresolved
		}
		return nil
	})
	if errors.Is(err, errCostsDetailServiceUnresolved) {
		return &forms.PartnerInvoicedBillsDetails{Year: year, Month: month, BillsDetails: nil}, nil
	}
	if err != nil {
		return nil, err
	}

	details, err := acc.result()
	if err != nil {
		return nil, err
	}

	return &forms.PartnerInvoicedBillsDetails{
		Year:         year,
		Month:        month,
		BillsDetails: details,
	}, nil
}

// PartnerBillsDetailsFromUnitCostsWithInvoicedDate converts detailed unit costs to partner bill details,
// including the invoice's sent date and payment code.
func PartnerBillsDetailsFromUnitCostsWithInvoicedDate(allDetailsCosts []entity.CostsDetailsQuery, units []forms.UnitInfo, invoiceMap map[uint]InvoiceSummaryCache, tenantIDAsCustomer bool) ([]forms.PartnerInvoicedBillsDetail, error) {
	acc := newPartnerInvoicedBillsDetailsAccumulator(units, invoiceMap, tenantIDAsCustomer)
	for _, cost := range allDetailsCosts {
		ok, err := acc.add(cost)
		if err != nil {
			return nil, err
		}
		if !ok {
			return nil, nil
		}
	}
	return acc.result()
}

type partnerInvoicedBillsDetailsAccumulator struct {
	unitsCategory      map[string]string
	categoryLabels     map[string]entity.CategoryItemShow
	defaultCategory    string
	precision          int32
	tenantIDAsCustomer bool
	invoiceMap         map[uint]InvoiceSummaryCache
	uniqueKeyMap       map[string]int
	slice              []forms.PartnerInvoicedBillsDetail
}

func newPartnerInvoicedBillsDetailsAccumulator(units []forms.UnitInfo, invoiceMap map[uint]InvoiceSummaryCache, tenantIDAsCustomer bool) *partnerInvoicedBillsDetailsAccumulator {
	unitsCategory := map[string]string{}
	for _, unit := range units {
		unitsCategory[unit.UnitKey] = unit.Category
	}
	return &partnerInvoicedBillsDetailsAccumulator{
		unitsCategory:      unitsCategory,
		categoryLabels:     GetAllCategoryItemInfoWithCache(),
		defaultCategory:    config.Get().GetString(config.DefaultUnitCategory),
		precision:          config.Get().GetInt32(config.BillingCostsPrecisionDigits),
		tenantIDAsCustomer: tenantIDAsCustomer,
		invoiceMap:         invoiceMap,
		uniqueKeyMap:       map[string]int{},
	}
}

func (a *partnerInvoicedBillsDetailsAccumulator) add(cost entity.CostsDetailsQuery) (bool, error) {
	projectID := cost.Labels[constant.ProjectLabelKey]
	projectName := cost.Labels[constant.ProjectNameLabelKey]
	clusterID := cost.Labels[constant.ClusterLabelKey]
	clusterName := cost.Labels[constant.ClusterNameLabelKey]
	customerIDStr := cost.Labels[constant.MSPCustomerIDLabel]
	if a.tenantIDAsCustomer {
		customerIDStr = fmt.Sprint(cost.TenantID)
	}

	invoiceInfo := a.invoiceMap[cost.InvoiceID]
	invoicedDate := invoiceInfo.SentDate
	paymentCode := invoiceInfo.PaymentCode

	category := a.unitsCategory[cost.Unit]
	if category == "" {
		category = a.defaultCategory
	}

	service, ok := getServiceNameWithCategoryLabels(cost, category, a.categoryLabels)
	if !ok {
		return false, nil
	}
	if service == "Support Plan" {
		projectName = "All Projects"
		clusterName = "All Clusters"
	}

	var billableCost, creditUsage decimal.Decimal
	if cost.ConsumedSource == entity.ConsumedSource_Credits {
		creditUsage = cost.CostPrice
	} else {
		billableCost = cost.CostPrice
	}

	uniqueKey := fmt.Sprintf("%s%s%s%s%s%s%d%s", cost.BilledDate, projectName, clusterName, service, cost.ConsumedSource, customerIDStr, cost.InvoiceID, paymentCode)
	index, ok := a.uniqueKeyMap[uniqueKey]
	if ok {
		a.slice[index-1].RunningTotal = a.slice[index-1].RunningTotal.Add(cost.RawPrice)
		a.slice[index-1].TotalCost = a.slice[index-1].TotalCost.Add(billableCost)
		a.slice[index-1].Credits = a.slice[index-1].Credits.Add(creditUsage)
		return true, nil
	}

	a.slice = append(a.slice, forms.PartnerInvoicedBillsDetail{
		BillsDetail: forms.BillsDetail{
			BillsCosts: forms.BillsCosts{
				RunningTotal: cost.RawPrice,
				Credits:      creditUsage,
				TotalCost:    billableCost,
			},
			BilledDate:      cost.BilledDate,
			ProjectID:       projectID,
			ProjectName:     projectName,
			ClusterID:       clusterID,
			ClusterName:     clusterName,
			ServicePathName: service,
		},
		CustomerID:   customerIDStr,
		InvoicedDate: invoicedDate,
		PaymentCode:  paymentCode,
	})
	a.uniqueKeyMap[uniqueKey] = len(a.slice)
	return true, nil
}

func (a *partnerInvoicedBillsDetailsAccumulator) result() ([]forms.PartnerInvoicedBillsDetail, error) {
	customerNames := map[string]string{}
	for index, usageDetail := range a.slice {
		if usageDetail.CustomerID != "" {
			customerName, ok := customerNames[usageDetail.CustomerID]
			if !ok {
				parsed, err := strconv.ParseUint(usageDetail.CustomerID, 10, 64)
				if err != nil {
					return nil, err
				}
				customerID := uint(parsed)
				org, err := centralhandler.GetCentralHandler().GetTenant(customerID)
				if err != nil {
					return nil, err
				}
				customerName = org.Name
				customerNames[usageDetail.CustomerID] = customerName
			}
			a.slice[index].CustomerName = customerName
		}
		amount, creditUsage, billableCost := usageDetail.RunningTotal.Truncate(a.precision), usageDetail.Credits.Truncate(a.precision), usageDetail.TotalCost.Truncate(a.precision)
		a.slice[index].Discounts = amount.Sub(creditUsage).Sub(billableCost)
		a.slice[index].RunningTotalTruncatedLoss = a.slice[index].RunningTotal.Sub(amount)
		a.slice[index].RunningTotal = amount
		a.slice[index].TotalCostTruncatedLoss = a.slice[index].TotalCost.Sub(billableCost)
		a.slice[index].TotalCost = billableCost
		a.slice[index].CreditsTruncatedLoss = a.slice[index].Credits.Sub(creditUsage)
		a.slice[index].Credits = creditUsage
	}
	return a.slice, nil
}

// CalculatePartnerOpenAPIMonthlyCostBySentDateWithCache calculates monthly costs for a partner account
// based on the invoice sent date, with caching enabled.
func (c *InvoicedCostExplorerModel) CalculatePartnerOpenAPIMonthlyCostBySentDateWithCache(tenantIDs []uint, billingConfigIDs []uint, billedMonth time.Time, tenantIDAsCustomer bool) (*forms.PartnerBillsExplorer, error) {
	// First, get invoices that were sent within the specified month.
	invoices, err := c.GetInvoicesBySentDateRange(tenantIDs, billingConfigIDs, billedMonth.Year(), int(billedMonth.Month()))
	if err != nil {
		return nil, err
	}

	var invoiceIDs []uint
	for _, inv := range invoices {
		invoiceIDs = append(invoiceIDs, inv.ID)
	}

	// Build the cost explorer query using the retrieved invoice IDs.
	costQuery := &forms.CostExplorerQueryV2{
		InvoiceIDs: invoiceIDs,
	}

	costExplorerModel := &CostExplorerModel{}
	if tenantIDAsCustomer {
		costs, summaryByCustomer, err := costExplorerModel.CalculateCostV2MultiTenantsWithTenantCustomerSummary(tenantIDs, billingConfigIDs, costQuery)
		if err != nil {
			return nil, err
		}
		return costExplorerModel.toPartnerBillsExplorerWithCustomerSummary(costs, billedMonth, summaryByCustomer)
	}

	// Calculate costResult using the filtered query.
	costResult, err := costExplorerModel.CalculateCostV2MultiTenantsWithCache(tenantIDs, billingConfigIDs, costQuery)
	if err != nil {
		return nil, err
	}
	return c.toPartnerBillsExplorer(&costResult.ExternalCosts, billedMonth, tenantIDAsCustomer)
}
