package workers

import (
	"errors"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"

	"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/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/model/payment"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/concurrency"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"go.uber.org/zap"
)

const billingPermanentFreeOrgsFeatureFlag = "billing_permenent_free_orgs"

func createMonthInvoice(tenantID uint, billingConfigID uint, monthStartTime time.Time, invoiceType string, invoiceStatus string) {
	invoiceModel := &payment.InvoiceModel{}

	log.Info("try  to create normal monthly invoice", zap.Uint("TenantID", tenantID), zap.Time("monthStartTime", monthStartTime), zap.String("invoiceType", invoiceType), zap.String("invoiceStatus", invoiceStatus))

	invoice, err := invoiceModel.CreateInvoiceByTypeStatus(tenantID, billingConfigID, monthStartTime, invoiceType, invoiceStatus)

	if err != nil {
		if !errors.Is(err, billing_errors.InvoiceMonthlyNoCosts) && !errors.Is(err, billing_errors.ObjectAlreadyExists) {
			log.Error("failed to create invoice in worker", zap.Uint("TenantID", tenantID), zap.String("invoiceType", invoiceType), zap.String("invoiceStatus", invoiceStatus), zap.Error(err))
		}
		log.Info("no monthly costs to create invoice", zap.Uint("TenantID", tenantID), zap.Time("monthStartTime", monthStartTime), zap.String("invoiceType", invoiceType), zap.String("invoiceStatus", invoiceStatus))
		return
	}
	log.Info("created tenant monthly invoice", zap.Uint("TenantID", tenantID), zap.Time("invoice date", invoice.StartDate), zap.String("invoiceType", invoiceType), zap.String("invoiceStatus", invoiceStatus))
}

func createInvoice(tenantID uint, isPartner bool, paymentMethod string, billingConfigID uint, suspend bool) {
	billingConfig, err := (&billingconfig.BillingConfigModel{}).GetBillingConfig(billingConfigID)
	if err != nil {
		log.Error("failed to get billing config on creating invoice", zap.Error(err), zap.Uint("billingConfigID", billingConfigID))
	}
	invoiceModel := &payment.InvoiceModel{}
	now := timeutils.GetNow()
	invoiceHours := config.Get().GetInt(config.Invoicehours)
	if isPartner {
		invoiceHours += config.Get().GetInt(config.PartnerTenantInvoicehours)
	}
	monthStartTime := timeutils.GetMonthDateForMonthlyWorkerByHours(now, invoiceHours)
	provider, err := paymenthandler.GetPaymentHandler().GetProvider(paymentMethod)
	if err != nil {
		log.Error("failed to get provider", zap.Error(err))
		return
	}
	needInMonthInvoice := provider.NeedInMonthInvoice()
	needHourlyInvoice := provider.EnableHourlySending(tenantID, billingConfigID)
	needDailyInvoice, delayDays := provider.EnableDailySending(tenantID, billingConfigID)

	invoiceStatus := entity.InvoiceStatus_GENERATED
	if provider.NeedSetInvoiceAutoConfirm() {
		invoiceStatus = entity.InvoiceStatus_CONFIRMED
	}

	if suspend {
		invoiceStatus = entity.InvoiceStatus_SUSPENDED
	}

	if (billingConfig.Status == entity.BillingConfigStatus_Closed || billingConfig.Status == entity.BillingConfigStatus_Closing) && (billingConfig.EndActiveTime != nil && now.Sub(*billingConfig.EndActiveTime) > time.Duration(invoiceHours)*time.Hour) {
		createMonthInvoice(tenantID, billingConfigID, timeutils.GetMonthDate(*billingConfig.EndActiveTime), entity.InvoiceType_MONTHLY, invoiceStatus)
		createMonthInvoice(tenantID, billingConfigID, timeutils.GetLastMonth(*billingConfig.EndActiveTime), entity.InvoiceType_MONTHLY, invoiceStatus)
	}
	log.Info("try to create invoice", zap.String("paymentMethod", paymentMethod), zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfigID))
	var invoice *forms.Invoice
	if needHourlyInvoice {
		hourlyInvoiceStartTime := now.Add(-time.Hour * config.Get().GetDuration(config.HourlyInvoiceIssueDelayHours))
		log.Info("try to create hourly invoice", zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfigID), zap.Time("hourlyInvoiceStartTime", hourlyInvoiceStartTime))
		invoice, err = invoiceModel.CreateHourlyInvoice(tenantID, billingConfigID, hourlyInvoiceStartTime, invoiceStatus)
	} else if needDailyInvoice {
		issueDate := timeutils.GetDayDate(now).AddDate(0, 0, -delayDays)
		log.Info("try to create daily invoice", zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfigID), zap.Time("issueDate", issueDate), zap.String("status", invoiceStatus))
		invoice, err = invoiceModel.CreateDailyInvoice(tenantID, billingConfigID, issueDate, invoiceStatus)
	} else if needInMonthInvoice {
		log.Info("try to create inmonth invoice", zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfigID))
		inMonthInvoiceHours := config.Get().GetInt(config.InMonthInvoiceHours)
		InMonthInvoiceStartTime := timeutils.GetMonthDate(now.Add(time.Hour*time.Duration(inMonthInvoiceHours))).AddDate(0, -1, 0)
		invoice, err = invoiceModel.CreateInvoiceByTypeStatus(tenantID, billingConfigID, InMonthInvoiceStartTime, entity.InvoiceType_INMONTH, invoiceStatus)
	}
	if err != nil {
		if !errors.Is(err, billing_errors.InvoiceMonthlyNoCosts) && !errors.Is(err, billing_errors.ObjectAlreadyExists) {
			log.Error("failed to create invoice in worker", zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfigID), zap.Error(err))
		}
		log.Info("skip create no costs invoice", zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfigID))
	}
	if invoice != nil {
		log.Info("created tenant invoice", zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfigID), zap.Time("invoiceDate", invoice.StartDate), zap.String("type", invoice.Type), zap.Uint("invoiceID", invoice.InvoiceID))
	}
	createMonthInvoice(tenantID, billingConfigID, monthStartTime, entity.InvoiceType_MONTHLY, invoiceStatus)
}

func createBigCustomerInvoice(tenantID uint, paymentMethod string, billingConfigID uint) {
	log.Info("createBigCustomerInvoice todo", zap.Uint("TenantID", tenantID), zap.Uint("billingConfigID", billingConfigID))
}

func sendInvoice(tenantID uint, billingConfigID uint) {
	invoiceModel := &payment.InvoiceModel{}
	err := invoiceModel.SendAllUnsentInvoice(tenantID, billingConfigID)
	if err != nil {
		log.Error("failed to send all unsent invoices", zap.Uint("tenantID", tenantID), zap.Error(err))
		return
	}
}

var (
	CreateInvoiceProfileHistogram = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Namespace: "dbaas",
		Subsystem: "billing",
		Name:      "create_invoice_profile",
	}, []string{"stage"})
)

func CreateInvoiceWorkerByTenant(tenantID uint) {
	start := timeutils.GetNow()
	defer func() {
		stage := "CreateInvoiceByTenant"
		end := timeutils.GetNow()
		diff := end.Sub(start)
		CreateInvoiceProfileHistogram.WithLabelValues(stage).Observe(diff.Seconds())
		log.Info("CreateInvoiceByTenant Profiling", zap.String("Stage", stage), zap.Duration("Time Cost", diff))
	}()
	paymentModel := &paymentsub.PaymentModel{}
	tenant, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		log.Error("failed to get tenant from account.", zap.Error(err), zap.Uint("tenantID", tenantID))
		return
	}
	if tenant.Status != centralhandler.TenantStatusActive {
		log.Info("not active tenant, ignore to create invoice", zap.Uint("tenantID", tenantID))
		return
	}
	permanentFree, err := centralhandler.IsHitFeatureFlag(uint64(tenantID), billingPermanentFreeOrgsFeatureFlag)
	if err != nil {
		log.Error("failed to check permanent free feature flag, skip invoice generation", zap.Error(err), zap.Uint("tenantID", tenantID))
		return
	}
	model := &billingconfig.BillingConfigModel{}
	billingConfigs, err := model.GetUseBillingConfigsWithCache(tenantID)
	if err != nil {
		log.Error("failed to get tenant billingConfigs from db.", zap.Error(err), zap.Uint("tenantID", tenantID))
		return
	}
	log.Info("CreateInvoiceWorkerByTenant get billingConfigs ", zap.Uint("tenantID", tenantID), zap.Int("billingConfigs_len", len(billingConfigs)))
	managingBillingConfigs, err := model.GetManagingBillingConfigEntities(tenantID)
	if err != nil {
		log.Error("failed to get tenant billingConfigs from db.", zap.Error(err), zap.Uint("tenantID", tenantID))
		return
	}
	log.Info("CreateInvoiceWorkerByTenant get managing billingConfigs ", zap.Uint("tenantID", tenantID), zap.Int("managingBillingConfigs_len", len(managingBillingConfigs)))

	for _, billingConfig := range append(billingConfigs, managingBillingConfigs...) {
		paymentMethod, err := paymentModel.GetActivePaymentMethod(tenantID, billingConfig.ID)
		if err != nil {
			log.Info("failed to get active payment", zap.Error(err), zap.Uint("tenantID", tenantID), zap.Uint("billingConfigID", billingConfig.ID))
			continue
		}
		if !paymentMethod.Payable {
			log.Info("tenant active payment is not payable, stop to generate invoice")
			continue
		}

		createInvoice(paymentMethod.TenantID, tenant.IsMsp || tenant.IsCPPOReseller, paymentMethod.PaymentType, billingConfig.ID, permanentFree)
		if !permanentFree {
			sendInvoice(paymentMethod.TenantID, billingConfig.ID)
		}

	}

}

func CreateInvoiceWorkerFunc() {
	if !config.Get().GetBool(config.EnableInvoiceWorker) {
		return
	}
	paymentModel := &paymentsub.PaymentModel{}
	payableTenantIDs, err := paymentModel.GetAllPayableTenantIDs()
	if err != nil {
		log.Error("failed to get payableTenantIDs in worker.", zap.Error(err))
		return
	}
	log.Info("CreateInvoiceWorkerFunc ", zap.Int("payableTenantIDs", len(payableTenantIDs)))
	tenantIDs := make([]uint, len(payableTenantIDs))
	for i, tenant := range payableTenantIDs {
		tenantIDs[i] = tenant.TenantID
	}

	err = concurrency.RunInPoolWithIDs(
		config.Get().GetUint(config.InvoiceWorkerConcurrency),
		CreateInvoiceWorkerByTenant,
		tenantIDs,
	)
	if err != nil {
		log.Error("failed to run CreateInvoiceWorkerFunc in worker.", zap.Error(err))
	}
}

var CreateInvoiceWorker = CreateInvoiceWorkerFunc

func SyncTenantPayableWorker() error {
	if !config.Get().GetBool(config.EnableSyncPayableWorker) {
		return nil
	}
	return concurrency.RunInPoolWithAllTenants(
		config.Get().GetUint(config.SyncPayableWorkerConcurrency),
		func(tenantID uint) {
			paymentModel := &paymentsub.PaymentModel{}
			err := paymentModel.SyncPayable(tenantID)
			if err != nil {
				log.Error("failed to sync payable in worker", zap.Uint("TenantID", tenantID), zap.Error(err))
			}
		},
	)
}
