package impl

import (
	"crypto/md5"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"time"

	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/billing"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"go.uber.org/zap"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
)

type MspHandlerImpl struct {
}

func (*MspHandlerImpl) Setup(tenant *forms.Tenant) (string, error) {
	if tenant.ThirdPartyAccountProvider == constant.MSPAccountProvider { // for directly created customer
		return tenant.ThirdPartyAccountCode, nil
	}
	scheduleInfo, err := centralhandler.GetCentralHandler().GetMspCustomerScheduleInfo(tenant.TenantID) // for the switched by schedule customers
	if err != nil {
		return "", err
	}
	return fmt.Sprint(scheduleInfo.MspTenantID), nil
}

func getHashString(cost entity.DateUnitCostSummary, extra ...string) string {
	keys := make([]string, 0, 32)
	for key := range cost.Labels {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	hasher := md5.New()
	for _, key := range keys {
		_, _ = hasher.Write([]byte(key + cost.Labels[key]))
	}
	hasher.Write([]byte(cost.Unit))
	hasher.Write([]byte(strconv.Itoa(cost.Cost)))
	hasher.Write([]byte(strconv.FormatInt(cost.Date.Unix(), 10)))
	for _, e := range extra {
		hasher.Write([]byte(e))
	}

	hashBytes := hasher.Sum(nil)
	return fmt.Sprintf("%x", hashBytes)

}

func (*MspHandlerImpl) Pay(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoice *forms.Invoice) (*forms.Invoice, error) {
	_tenantID, err := strconv.ParseUint(paymentInfo.PaymentCode, 10, 0)
	if err != nil {
		return nil, err
	}
	tenantID := uint(_tenantID)
	measureModel := &billing.MeasureModel{}
	err = (&costexplorer.CostExplorerModel{}).ForEachTransferCostByInvoiceId(invoice.TenantID, invoice.InvoiceID, func(cost entity.DateUnitCostSummary) error {
		if cost.Labels == nil {
			cost.Labels = make(map[string]string)
		}
		if cost.ConsumedSource == entity.ConsumedSource_Credits {
			return nil
		}
		// For partner-managed customers (ResaleCustomer / MSPCustomer) the
		// support plan's 4 units (FixedPrice / PercentagePrice /
		// CommintmentPrice / TieredPrice) must not be re-transferred
		// from the customer to the partner here:
		//
		//   - ResaleCustomer: SendReseller* (called from
		//     model/marketing/support_plan.go via the customer's own
		//     worker) already pushed the support fee to the partner.
		//     The customer-side row you see at this Pay call would
		//     double-count.
		//   - MSPCustomer: the support plan is bound on the PARTNER
		//     (see MspCustomerSyncSupportPlan), not the customer. The
		//     cost was already produced and lives on the partner's
		//     user_unit_costs, not the customer's. A customer-side
		//     row of these SKUs in a Pay transfer is therefore
		//     anomalous — skipping avoids double-counting in either
		//     direction.
		//
		// CPPO is handled separately by
		// AWSCPPOCustomerInvoiceTransferHandlerImpl.Pay (see
		// handler/paymenthandler/impl/aws_cppo.go), which uses the
		// same skip pattern but a different code path.
		switch cost.Unit {
		case constant.FixedPrice_SKU,
			constant.PercentagePrice_SKU,
			constant.CommintmentPrice_SKU,
			constant.TieredPrice_SKU:
			reason := "support cost already on partner books (SendReseller* for ResaleCustomer; partner's own SendNormalMetering for MSPCustomer)"
			log.Info("ignore support fee transfer",
				zap.Uint("customerTenantID", invoice.TenantID),
				zap.Uint("mspTenantID", tenantID),
				zap.String("unit", cost.Unit),
				zap.String("reason", reason))
			return nil
		}
		customerIDStr := strconv.FormatUint(uint64(invoice.TenantID), 10)
		return transferMergedCost(measureModel, tenantID, invoice.InvoiceID,
			constant.MSPCustomerIDLabel, customerIDStr, cost,
			func(costTimeStamp int64) (uint, error) {
				return measureModel.GetBillingConfigIDByTimeStamp(tenantID, &forms.UnitCosts{CostTimeStamp: costTimeStamp}, false)
			})
	})
	if err != nil {
		return nil, err
	}
	return invoice, nil
}

// transferMergedCost pushes a merged user_unit_costs row to the partner tenant
// via SecondaryMeasure. When the row carries a complete MergedLabelsHistory
// (sum of entry costs equals the row cost), each original raw contribution is
// replayed with its own labels so per-chunk pricing (e.g. PriceType_IntervalValue)
// is computed correctly instead of on the merged row's stale first-chunk labels.
// Otherwise — history empty, or incomplete because enable-merged-labels-history
// was toggled mid-row-lifecycle — it falls back to a single merged-row transfer
// (the legacy behavior) so no contribution is silently dropped.
func transferMergedCost(measureModel *billing.MeasureModel, targetTenantID, invoiceID uint,
	customerIDLabel, customerIDStr string, cost entity.DateUnitCostSummary,
	resolveBillingConfigID func(costTimeStamp int64) (uint, error)) error {
	if !historyIsComplete(cost) {
		return transferOneEntry(measureModel, targetTenantID, invoiceID, customerIDLabel, customerIDStr,
			cost.Unit, cost.Cost, cost.Date, cost.Labels, "", resolveBillingConfigID)
	}
	for _, entry := range cost.MergedLabelsHistory {
		if err := transferOneEntry(measureModel, targetTenantID, invoiceID, customerIDLabel, customerIDStr,
			cost.Unit, entry.Cost, time.Unix(entry.CostTimeStamp, 0), entry.Labels, entry.CostID, resolveBillingConfigID); err != nil {
			return err
		}
	}
	return nil
}

// historyIsComplete reports whether cost.MergedLabelsHistory covers the full row
// cost — i.e. every raw contribution folded into the merged row was recorded
// exactly once. Empty or partial history (enable-merged-labels-history toggled
// mid-row-lifecycle) must fall back to the legacy single-row transfer so no
// contribution is silently dropped or double-transferred.
func historyIsComplete(cost entity.DateUnitCostSummary) bool {
	if len(cost.MergedLabelsHistory) == 0 {
		return false
	}
	historySum := 0
	for _, entry := range cost.MergedLabelsHistory {
		historySum += entry.Cost
	}
	return historySum == cost.Cost
}

// buildTransferMeasure assembles the partner-side UnitCosts for one (cost, labels)
// snapshot: a defensive copy of the labels plus the customer attribution label,
// and the deterministic costID (see getHashString). hashDate and sourceCostID
// select the hash domain — the legacy day-granularity domain for the merged-row
// fallback, the full-timestamp plus source-costID domain for replayed entries.
func buildTransferMeasure(invoiceID uint, customerIDLabel, customerIDStr, unit string,
	cost int, hashDate time.Time, labels map[string]string, sourceCostID string) *forms.UnitCosts {
	// Defensive copy so we never mutate the caller's labels map.
	entryLabels := make(map[string]string, len(labels)+1)
	for k, v := range labels {
		entryLabels[k] = v
	}
	entryLabels[customerIDLabel] = customerIDStr

	return &forms.UnitCosts{
		CostID: fmt.Sprintf("%s-%s", strconv.FormatUint(uint64(invoiceID), 10),
			getHashString(entity.DateUnitCostSummary{
				UnitCostSummary: entity.UnitCostSummary{
					Unit:   unit,
					Labels: entryLabels,
					Cost:   cost,
				},
				Date: hashDate,
			}, sourceCostID)),
		CostTimeStamp: hashDate.Unix(),
		Unit:          unit,
		Cost:          cost,
		Labels:        entryLabels,
	}
}

// transferOneEntry pushes one (cost, labels) snapshot through SecondaryMeasure on
// the partner tenant. costID is deterministic so a retried invoice Pay skips
// entries that were already transferred (the measure pipeline treats a repeated
// costID as already-executed). hashDate keeps the costID byte-compatible with the
// legacy single-row transfer for merged rows without history, and disambiguates
// replay entries that share unit/labels/cost within the same invoice. sourceCostID
// is the original raw costID of the contribution ("" for the legacy fallback
// transfer) — it is folded into the replay hash so distinct raw rows can never
// collide, and logged for traceability back to the customer-side raw cost.
func transferOneEntry(measureModel *billing.MeasureModel, targetTenantID, invoiceID uint,
	customerIDLabel, customerIDStr, unit string, cost int, hashDate time.Time,
	labels map[string]string, sourceCostID string, resolveBillingConfigID func(costTimeStamp int64) (uint, error)) error {

	measure := buildTransferMeasure(invoiceID, customerIDLabel, customerIDStr, unit, cost, hashDate, labels, sourceCostID)
	billingConfigID, err := resolveBillingConfigID(measure.CostTimeStamp)
	if err != nil {
		return err
	}
	costOut, err := measureModel.SecondaryMeasure(targetTenantID, billingConfigID, measure)
	if err != nil {
		log.Error("secondary measure cost failed ",
			zap.Uint("targetTenantID", targetTenantID),
			zap.Uint("billingConfigID", billingConfigID),
			zap.String("sourceCostID", sourceCostID),
			zap.Any("measure", measure),
			zap.Error(err))
		return err
	}
	log.Info("ProduceSecondaryMeasure ", zap.String("sourceCostID", sourceCostID), zap.Any("costOut", costOut))
	return nil
}

func (*MspHandlerImpl) PayMultiInvoice(paymentInfo *forms.PaymentMethod, billingCompany *forms.BillingCompany, invoices []forms.Invoice) ([]forms.Invoice, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("can not send msp multi invoice"))
}

func (*MspHandlerImpl) Payable(tenantID uint, userCode string) (bool, error) {
	return true, nil
}

func (*MspHandlerImpl) NeedMultiInvoiceSendOnce() bool {
	return false
}

func (h *MspHandlerImpl) GetThirdPartyName() string {
	return entity.PaymentType_MSP
}

func (h *MspHandlerImpl) EnableReconciliation() bool {
	return false
}

func (h *MspHandlerImpl) GetDailyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("don't need"))
}

func (h *MspHandlerImpl) GetMonthlyRevenue(date time.Time) ([]*paymenthandler.TenantRevenue, error) {
	return nil, billing_errors.NewNotImplementedError(errors.New("don't need"))
}

func (h *MspHandlerImpl) EnableTaxCalculation() bool {
	return false
}

func (h *MspHandlerImpl) EnableHourlySending(tenantID, billingConfigID uint) bool {
	return true
}

func (h *MspHandlerImpl) EnableDailySending(tenantID, billingConfigID uint) (bool, int) {
	return false, 0
}

func (h *MspHandlerImpl) NeedInMonthInvoice() bool {
	return false
}

func (h *MspHandlerImpl) NeedSetInvoiceAutoConfirm() bool {
	return true
}
