package paymentsub

import (
	"errors"
	"fmt"
	"strings"

	"github.com/asaskevich/govalidator"
	"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/centralhandler"
	"github.com/pingcap/dbaas/billing2/handler/paymenthandler"
	"github.com/pingcap/dbaas/billing2/model/billingconfig"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type BillingInformationModel struct{}

func companyKeyFromCountryCode(countryCode string) (string, error) {
	countryCodeMapEntity := &entity.CountryCodeCompanyMap{}
	err := errorutils.WrapSQLError(entity.NewCountryCodeCompanyMapQuerySet(db.GetDB()).CountryCodeEq(countryCode).One(countryCodeMapEntity), fmt.Sprintf("country code company map %s", countryCode))
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return config.Get().GetString(config.DefaultBillingCompanyKey), nil
		}
		return "", err
	}
	return countryCodeMapEntity.CompanyKey, nil
}

func billingInfoFromEntity(billingInfoEntity *entity.BillingInformation) *forms.BillingInformation {
	ret := &forms.BillingInformation{
		TenantID: billingInfoEntity.TenantID,
		BillingAddress: forms.BillingAddress{
			CompanyName:            billingInfoEntity.CompanyName,
			BillingEmail:           billingInfoEntity.BillingEmail,
			CountryCode:            billingInfoEntity.CountryCode,
			State:                  billingInfoEntity.State,
			City:                   billingInfoEntity.City,
			Line1:                  billingInfoEntity.Line1,
			Line2:                  billingInfoEntity.Line2,
			PostalCode:             billingInfoEntity.PostalCode,
			CustomInputCountryName: billingInfoEntity.CustomInputCountryName,
		},
		TaxInfo: forms.TaxInfo{
			TaxIDCountryCode: billingInfoEntity.TaxIDCountryCode,
			TaxIDType:        billingInfoEntity.TaxIDType,
			TaxID:            billingInfoEntity.TaxID,
			TaxRateID:        billingInfoEntity.TaxRateID,
		},
	}
	return ret
}

func BillingInfoFromEntityWithTaxRate(billingInfoEntity *entity.BillingInformation) (*forms.BillingInformation, error) {
	info := billingInfoFromEntity(billingInfoEntity)
	if billingInfoEntity.TaxRateID != "" && config.Get().GetBool(config.EnableTaxCalculation) {
		taxRate, err := (&TaxModel{}).GetTaxRate(billingInfoEntity.TaxRateID)
		if err != nil {
			return nil, err
		}
		info.TaxRate = taxRate
	}
	return info, nil
}

func (m *BillingInformationModel) CreateOrUpdateBillingInfo(tenantID uint, info *forms.BillingInformation) (*forms.BillingInformation, error) {
	return m.createBillingInfo(tenantID, info, true)
}

func (m *BillingInformationModel) createBillingInfo(tenantID uint, info *forms.BillingInformation, updateExists bool) (*forms.BillingInformation, error) {
	var err error
	info.TenantID = tenantID
	enableTaxCalculation := config.Get().GetBool(config.EnableTaxCalculation)
	if info.TaxID == "" {
		info.TaxIDType = ""
	}
	if enableTaxCalculation && (info.TaxIDType == "" && info.TaxID != "") {
		return nil, billing_errors.NewInvalidBillingProfileInputError(errors.New("invalid Tax ID Type or Tax ID"))
	}
	if info.BillingEmail != "" && !govalidator.IsEmail(info.BillingEmail) {
		return nil, billing_errors.NewInvalidBillingProfileInputError(errors.New("invalid Billing Email"))
	}
	countryCode := info.CountryCode
	info.CustomInputCountryName = strings.TrimSpace(info.CustomInputCountryName)
	if info.CountryCode == constant.OtherCountryCode {
		for _, b := range config.Get().GetStringSlice(config.TradingProhibitedCountryNameList) {
			if strings.EqualFold(b, info.CustomInputCountryName) {
				return nil, billing_errors.NewSystemBusyError(errors.New("please try later again"))
			}
		}
	}
	creatSession := db.GetDB()
	if updateExists {
		creatSession = creatSession.Clauses(clause.Insert{Modifier: "IGNORE"})
	}
	billingInfo := entity.BillingInformation{
		TenantID: tenantID,
	}
	err = billingInfo.Create(creatSession)
	if err != nil {
		return nil, err
	}
	companyKey, err := companyKeyFromCountryCode(countryCode)
	if err != nil {
		return nil, err
	}
	company, err := m.GetBillingCompany(companyKey)
	if err != nil {
		return nil, err
	}
	err = db.GetDB().Transaction(func(tx *gorm.DB) error {
		err = entity.NewBillingInformationQuerySet(tx.Clauses(clause.Locking{Strength: "UPDATE"})).TenantIDEq(tenantID).One(&billingInfo)
		if err != nil {
			return err
		}
		query := entity.NewBillingInformationQuerySet(tx).TenantIDEq(tenantID).GetUpdater().
			SetCompanyName(info.CompanyName).SetBillingEmail(info.BillingEmail).SetCountryCode(info.CountryCode).SetState(info.State).SetCity(info.City).
			SetLine1(info.Line1).SetLine2(info.Line2).SetPostalCode(info.PostalCode).SetTaxID(info.TaxID).SetTaxIDType(info.TaxIDType).SetCustomInputCountryName(info.CustomInputCountryName).SetTaxIDCountryCode(info.TaxIDCountryCode)
		if enableTaxCalculation {
			taxModel := TaxModel{}
			taxRate, err := taxModel.ValidateBillingInfoForTax(info, company.PaymentSubCode)
			if err != nil {
				return err
			}
			needCreateTaxRate := true
			needInactiveOldTaxRate := false
			if billingInfo.TaxRateID != "" {
				oldTaxRate, err := taxModel.GetTaxRate(billingInfo.TaxRateID)
				if err != nil {
					log.Error("failed to get tax rate", zap.Error(err))
				} else if taxModel.MatchTaxRate(oldTaxRate, taxRate) {
					log.Info("same tax rate, skip create new tax rate")
					needCreateTaxRate = false
				} else {
					log.Info("unmatched tax rate, need new tax rate", zap.Any("old", oldTaxRate), zap.Any("new", taxRate))
					needCreateTaxRate = true
					needInactiveOldTaxRate = true
				}
			}
			if needCreateTaxRate {
				taxRate, err = taxModel.CreateTenantTaxRate(tenantID, taxRate)
				if err != nil {
					return err
				}
				query = query.SetTaxRateID(taxRate.StripeTaxRateID)
			}
			if needInactiveOldTaxRate {
				err = taxModel.InactiveTaxRate(billingInfo.TaxRateID)
				if err != nil {
					log.Error("failed to inactive old tax rate", zap.Error(err))
					// do not return because of no effect to the main process
				}
			}
		}
		return query.Update()
	})
	if err != nil {
		log.Info("failed to update tenant billing info", zap.Error(err), zap.Uint("TenantID", tenantID))
		return nil, err
	}
	log.Info("success to update tenant billing info", zap.Uint("TenantID", tenantID))

	if enableTaxCalculation {
		err := m.UpdateStripeBillingInformation(tenantID, info)
		if err != nil {
			log.Error("failed to update tenant billing info to stripe", zap.Error(err))
			// do not return because of db has been updated
		}
	}
	_, err = m.UpdateTenantBillingCompany(tenantID, &forms.TenantBillingCompany{
		CompanyKey: companyKey,
	})
	if err != nil {
		log.Error("failed to update tenant billing company in update billing info", zap.Uint("TenantID", tenantID), zap.String("CompanyKey", companyKey), zap.Error(err))
		// do not return because of db has been updated
	}
	return m.getBillingInfo(tenantID)
}

func (m *BillingInformationModel) UpdateStripeBillingInformation(tenantID uint, info *forms.BillingInformation) error {
	payment, err := (&PaymentModel{}).GetPaymentMethodWithPaymentTypeCurrentBillingConfig(tenantID, entity.PaymentType_STRIPE)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			payment, err = (&PaymentModel{}).CreateDefaultStripePaymentForBillingInformation(tenantID)
			if err != nil {
				return err
			}
		} else {
			log.Error("failed to get stripe payment on update billing info", zap.Error(err), zap.Uint("TenantID", tenantID))
			return err
		}
	}
	sh, err := paymenthandler.GetStripeProvider()
	if err != nil {
		log.Error("failed to get stripe provider")
		return err
	}
	tenantInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return err
	}
	info.TenantID = tenantID
	if info.BillingEmail == "" {
		info.BillingEmail = tenantInfo.Email
	}
	_, err = sh.UpdateCustomerBillingInfo(payment.PaymentCode, info)
	return err
}

func (m *BillingInformationModel) CreateDefaultBillingInfo(tenantID uint, info *forms.BillingInformation) (*forms.BillingInformation, error) {
	billingInfo, err := m.getBillingInfo(tenantID)
	if err == nil {
		return billingInfo, nil
	}
	if !errors.Is(err, billing_errors.ObjectNotExistsError) {
		return nil, err
	}
	return m.createBillingInfo(tenantID, info, true)
}

func (*BillingInformationModel) getBillingInfo(tenantID uint) (*forms.BillingInformation, error) {
	billingInfoEntity := &entity.BillingInformation{}
	err := errorutils.WrapSQLError(entity.NewBillingInformationQuerySet(db.GetDB()).TenantIDEq(tenantID).One(billingInfoEntity), fmt.Sprintf("tenant %d billing info", tenantID))
	if err != nil {
		return nil, err
	}
	return BillingInfoFromEntityWithTaxRate(billingInfoEntity)
}

func (m *BillingInformationModel) GetBillingInfo(tenantID uint) (*forms.BillingInformation, error) {
	info, err := m.getBillingInfo(tenantID)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return &forms.BillingInformation{
				TenantID: tenantID,
				BillingAddress: forms.BillingAddress{
					CountryCode: "",
					State:       "",
					City:        "",
					Line1:       "",
					Line2:       "",
					PostalCode:  "",
				},
			}, nil
		}
		return nil, err
	}
	return info, nil
}

func (m *BillingInformationModel) BillingInfoIsNotEmpty(tenantID uint) (bool, error) {
	cnt, err := entity.NewBillingInformationQuerySet(db.GetDB()).TenantIDEq(tenantID).Count()
	if err != nil {
		return false, err
	}
	return cnt != 0, nil
}

func (m *BillingInformationModel) UpdateTaxRate(tenantID uint, taxRateID string) error {
	return entity.NewBillingInformationQuerySet(db.GetDB()).TenantIDEq(tenantID).GetUpdater().SetTaxRateID(taxRateID).Update()
}

func billingCompanyFromEntity(billingCompanyEntity entity.BillingCompany) *forms.BillingCompany {
	return &forms.BillingCompany{
		CompanyKey:     billingCompanyEntity.CompanyKey,
		Name:           billingCompanyEntity.Name,
		PaymentSubCode: billingCompanyEntity.PaymentSubCode,
		StripeTaxID:    billingCompanyEntity.StripeTaxID,

		UsingDesignatedTaxAddressToCalculateTaxRate: billingCompanyEntity.UsingDesignatedTaxAddressToCalculateTaxRate,
		DesignatedTaxAddress:                        (*forms.BillingAddress)(billingCompanyEntity.DesignatedTaxAddress),

		LocalCurrency:                        billingCompanyEntity.LocalCurrency,
		DisplayLocalCurrencyCommentOnInvoice: billingCompanyEntity.DisplayLocalCurrencyCommentOnInvoice,
	}
}

func (m *BillingInformationModel) ListBillingCompany() ([]forms.BillingCompany, error) {
	entities := []entity.BillingCompany{}
	err := entity.NewBillingCompanyQuerySet(db.GetDB()).All(&entities)
	if err != nil {
		return nil, err
	}

	result := []forms.BillingCompany{}

	for _, billingCompanyEntity := range entities {
		result = append(result, *billingCompanyFromEntity(billingCompanyEntity))
	}
	return result, nil
}

func (m *BillingInformationModel) GetBillingCompany(companyKey string) (*forms.BillingCompany, error) {
	companyEntity := &entity.BillingCompany{}
	err := errorutils.WrapSQLError(entity.NewBillingCompanyQuerySet(db.GetDB()).CompanyKeyEq(companyKey).One(companyEntity), fmt.Sprintf("company %s", companyKey))
	if err != nil {
		return nil, err
	}
	return billingCompanyFromEntity(*companyEntity), nil
}

func tenantBillingCompanyFromEntity(tenantBillingCompanyEntity *entity.TenantBillingCompany) *forms.TenantBillingCompany {
	return &forms.TenantBillingCompany{
		TenantID:   tenantBillingCompanyEntity.TenantID,
		CompanyKey: tenantBillingCompanyEntity.CompanyKey,
	}
}

func (m *BillingInformationModel) GetTenantBillingCompany(tenantID uint) (*forms.TenantBillingCompany, error) {
	tenantBillingCompany := &entity.TenantBillingCompany{}
	err := errorutils.WrapSQLError(
		entity.NewTenantBillingCompanyQuerySet(db.GetDB()).TenantIDEq(tenantID).One(tenantBillingCompany),
		fmt.Sprintf("tenant %d billing company", tenantID),
	)
	if err != nil {
		if errors.Is(err, billing_errors.ObjectNotExistsError) {
			return &forms.TenantBillingCompany{
				TenantID:   tenantID,
				CompanyKey: config.Get().GetString(config.DefaultBillingCompanyKey),
			}, nil
		}
		return nil, err
	}
	return tenantBillingCompanyFromEntity(tenantBillingCompany), nil
}

func (m *BillingInformationModel) UpdateTenantBillingCompany(tenantID uint, company *forms.TenantBillingCompany) (*forms.TenantBillingCompany, error) {
	companyKey := company.CompanyKey
	_, err := m.GetBillingCompany(companyKey)
	if err != nil {
		return nil, err
	}

	tenantBillingCompanyEntity := &entity.TenantBillingCompany{
		TenantID:   tenantID,
		CompanyKey: company.CompanyKey,
	}
	err = tenantBillingCompanyEntity.Create(db.GetDB().Clauses(clause.OnConflict{
		UpdateAll: true,
	}))
	if err != nil {
		log.Error("failed to update tenant billing company", zap.Uint("TenantID", tenantID), zap.String("CompanyKey", companyKey))
		return nil, err
	}
	log.Info("updated tenant billing company", zap.Uint("TenantID", tenantID), zap.String("CompanyKey", companyKey))
	return tenantBillingCompanyFromEntity(tenantBillingCompanyEntity), nil
}

func (m *BillingInformationModel) GetTenantPaymentSubCode(tenantID uint) (string, error) {
	tenantBillingCompany, err := m.GetTenantBillingCompany(tenantID)
	if err != nil {
		return "", err
	}

	billingCompany, err := m.GetBillingCompany(tenantBillingCompany.CompanyKey)
	if err != nil {
		return "", err
	}

	return billingCompany.PaymentSubCode, nil
}

func (m *BillingInformationModel) CreateBillingCompany(company *forms.BillingCompany) error {
	companyEntity := entity.BillingCompany{
		CompanyKey:     company.CompanyKey,
		Name:           company.Name,
		PaymentSubCode: company.PaymentSubCode,
	}
	err := companyEntity.Create(db.GetDB())
	if err != nil {
		return err
	}
	return nil
}

func (m *PaymentModel) CreateDefaultStripePaymentForBillingInformation(tenantID uint) (*forms.PaymentMethod, error) {
	tenantInfo, err := centralhandler.GetCentralHandler().GetTenant(tenantID)
	if err != nil {
		return nil, err
	}
	var payment *forms.PaymentMethod
	needActive := tenantInfo.ThirdPartyAccountProvider == "" && !tenantInfo.IsMspCustomer && !tenantInfo.IsMsp
	billingConfigID, err := (&billingconfig.BillingConfigModel{}).GetActiveBillingConfigID(tenantID)
	if err != nil {
		return nil, err
	}
	if needActive {
		payment, err = m.CreatePaymentMethodAndActive(tenantID, &forms.PaymentMethodCreateQuery{
			PaymentType:           entity.PaymentType_STRIPE,
			BindedBillingConfigID: &billingConfigID})
	} else {
		payment, err = m.createPaymentMethod(tenantID, &forms.PaymentMethodCreateQuery{
			PaymentType:           entity.PaymentType_STRIPE,
			BindedBillingConfigID: &billingConfigID})
	}
	if err != nil {
		return nil, err
	}
	log.Info("success to create default stripe handler", zap.Uint("tenantID", tenantID))
	return payment, err
}
