package paymenthandler

import (
	"errors"
	"fmt"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/log"
	"github.com/shopspring/decimal"
	"github.com/spf13/viper"
	"github.com/stripe/stripe-go"
	scclient "github.com/stripe/stripe-go/client"
	"go.uber.org/zap"
)

type StripePaymentHandler interface {
	SetupIntent(userCode string) (*forms.SetupIntent, error)
	ListPaymentMethods(userCode string) ([]forms.StripePaymentMethod, error)
	UpdatePaymentMethod(userCode, methodId string, method *forms.StripePaymentMethod) (*forms.StripePaymentMethod, error)
	DeletePaymentMethod(userCode, methodId string) error
	SetPaymentMethodDefault(userCode, methodId string) error
	GetCustomerInfo(userCode string) (*forms.StripeCustomerInfo, error)
	ListStripeInvoices(userCode string) ([]forms.StripeInvoice, error)
}

type StripePaymentHandlerImpl struct {
}

func getStripeClient() *scclient.API {
	sc := &scclient.API{}
	sc.Init(viper.GetString(config.StripeSecretKey), nil)
	return sc
}

const (
	//StripeDBaaSIDLabel is the dbaas tenant id label name in stripe
	StripeDBaaSIDLabel = "dbaas-id"
)

func stripeEnabled() bool {
	return viper.GetBool(config.StripePaymentEnable)
}

func (*StripePaymentHandlerImpl) GetUserCode(tenant forms.Tenant) (string, error) {
	if !stripeEnabled() {
		return fmt.Sprintf("mock_stripe_%v", tenant.TenantID), nil
	}
	client := getStripeClient()
	tenantID := tenant.TenantID
	customer, err := client.Customers.New(&stripe.CustomerParams{
		Params: stripe.Params{
			IdempotencyKey: stripe.String(fmt.Sprintf("%s-%d", viper.Get(config.Project), tenantID)),
			Metadata:       map[string]string{StripeDBaaSIDLabel: fmt.Sprintf("%d", tenantID)},
		},
		Name:  stripe.String(tenant.Name),
		Email: stripe.String(tenant.Email),
	})
	if err != nil {
		log.Error("failed to create customer", zap.Uint("tenant id", tenantID), zap.Error(err))
		return "", billing_errors.NewSystemError(err)
	}
	log.Info("created customer on stripe", zap.String("customer id", customer.ID), zap.Uint("tenant id", tenantID))
	return customer.ID, nil
}

func getIdempotencyKey(keyType string, userCode string, invoiceID uint) string {
	return fmt.Sprintf("%s-%s-%d", keyType, userCode, invoiceID)
}

const (
	InvoiceIdempotencyKey = "invoice"
	StripeCurrencyFactor  = 100
	Start                 = "invoice_start"
	End                   = "invoice_end"
)

func (s *StripePaymentHandlerImpl) Payable(userCode string) (bool, error) {
	if !stripeEnabled() {
		return true, nil
	}
	cards, err := s.ListPaymentMethods(userCode)
	if err != nil {
		return false, err
	}
	log.Info("stripe payable result", zap.Int("cards num", len(cards)))
	return len(cards) > 0, nil
}

func (*StripePaymentHandlerImpl) Pay(userCode string, invoice *forms.Invoice) error {
	if !stripeEnabled() {
		return nil
	}
	client := getStripeClient()

	for category, categoryPrice := range invoice.CategoryUnitsCosts {
		rawIntPrice := categoryPrice.RawPrice.Mul(decimal.NewFromInt(StripeCurrencyFactor)).IntPart()
		actualIntPrice := categoryPrice.CostPrice.Mul(decimal.NewFromInt(StripeCurrencyFactor)).IntPart()
		discountIntPrice := rawIntPrice - actualIntPrice
		invoiceItem := &stripe.InvoiceItemParams{
			Params:      stripe.Params{IdempotencyKey: stripe.String(getIdempotencyKey(category, userCode, invoice.InvoiceID))},
			Amount:      stripe.Int64(rawIntPrice),
			Currency:    stripe.String(string(stripe.CurrencyUSD)),
			Customer:    stripe.String(userCode),
			Description: stripe.String(category),
		}
		_, err := client.InvoiceItems.New(invoiceItem)
		if err != nil {
			return billing_errors.NewSystemError(err)
		}

		if discountIntPrice != 0 {
			invoiceItem = &stripe.InvoiceItemParams{
				Params:      stripe.Params{IdempotencyKey: stripe.String(getIdempotencyKey(fmt.Sprintf("%s Discount", category), userCode, invoice.InvoiceID))},
				Amount:      stripe.Int64(-discountIntPrice),
				Currency:    stripe.String(string(stripe.CurrencyUSD)),
				Customer:    stripe.String(userCode),
				Description: stripe.String(fmt.Sprintf("%s Discount", category)),
			}
			_, err = client.InvoiceItems.New(invoiceItem)
			if err != nil {
				return billing_errors.NewSystemError(err)
			}
		}
	}

	params := &stripe.InvoiceParams{
		Customer: &userCode,
		Params: stripe.Params{
			IdempotencyKey: stripe.String(getIdempotencyKey(InvoiceIdempotencyKey, userCode, invoice.InvoiceID)),
			Metadata: map[string]string{
				Start: fmt.Sprintf("%d", invoice.StartDate.Unix()),
				End:   fmt.Sprintf("%d", invoice.EndDate.Unix()),
			},
		},
		CustomFields: []*stripe.InvoiceCustomFieldParams{
			{
				Name:  stripe.String("Bill start"),
				Value: stripe.String(invoice.StartDate.Format("2006-01-02 15:04:05")),
			},
			{
				Name:  stripe.String("Bill end"),
				Value: stripe.String(invoice.EndDate.Format("2006-01-02 15:04:05")),
			},
		},
	}

	_, err := client.Invoices.New(params)
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	return nil
}

func (*StripePaymentHandlerImpl) SetupIntent(userCode string) (*forms.SetupIntent, error) {
	if !stripeEnabled() {
		return &forms.SetupIntent{}, nil
	}
	params := &stripe.SetupIntentParams{
		Customer: stripe.String(userCode),
	}
	client := getStripeClient()
	intent, err := client.SetupIntents.New(params)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return &forms.SetupIntent{
		ClientSecret: intent.ClientSecret,
		PublicKey:    viper.GetString(config.StripePublicKey),
	}, nil
}

func newPaymentMethod(method *stripe.PaymentMethod) *forms.StripePaymentMethod {
	var card *forms.Card
	if method.Card != nil {
		card = &forms.Card{
			Brand:       string(method.Card.Brand),
			Country:     method.Card.Country,
			ExpMonth:    method.Card.ExpMonth,
			ExpYear:     method.Card.ExpYear,
			Fingerprint: method.Card.Fingerprint,
			Funding:     string(method.Card.Funding),
			Last4:       method.Card.Last4,
		}
	}
	var detail *forms.BillingDetails
	if method.BillingDetails != nil {
		var addr *forms.Address
		if method.BillingDetails.Address != nil {
			addr = &forms.Address{
				City:       method.BillingDetails.Address.City,
				Country:    method.BillingDetails.Address.Country,
				Line1:      method.BillingDetails.Address.Line1,
				Line2:      method.BillingDetails.Address.Line2,
				PostalCode: method.BillingDetails.Address.PostalCode,
				State:      method.BillingDetails.Address.State,
			}
		}
		detail = &forms.BillingDetails{
			Address: addr,
			Email:   &method.BillingDetails.Email,
			Name:    method.BillingDetails.Name,
			Phone:   &method.BillingDetails.Phone,
		}
	}
	return &forms.StripePaymentMethod{
		ID:             method.ID,
		Card:           card,
		BillingDetails: detail,
	}
}

func (*StripePaymentHandlerImpl) ListPaymentMethods(userCode string) ([]forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return []forms.StripePaymentMethod{}, nil
	}
	params := &stripe.PaymentMethodListParams{
		Customer: stripe.String(userCode),
		Type:     stripe.String("card"),
	}
	it := getStripeClient().PaymentMethods.List(params)
	var items = make([]forms.StripePaymentMethod, 0)
	for it.Next() {
		items = append(items, *newPaymentMethod(it.PaymentMethod()))
	}
	err := it.Err()
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return items, nil
}

func newBillingDetailParams(method *forms.StripePaymentMethod) *stripe.BillingDetailsParams {
	var detail *stripe.BillingDetailsParams
	if method.BillingDetails != nil {
		var addr *stripe.AddressParams
		if method.BillingDetails.Address != nil {
			addr = &stripe.AddressParams{
				City:       stripe.String(method.BillingDetails.Address.City),
				Country:    stripe.String(method.BillingDetails.Address.Country),
				Line1:      stripe.String(method.BillingDetails.Address.Line1),
				Line2:      stripe.String(method.BillingDetails.Address.Line2),
				PostalCode: stripe.String(method.BillingDetails.Address.PostalCode),
				State:      stripe.String(method.BillingDetails.Address.State),
			}
		}
		detail = &stripe.BillingDetailsParams{
			Address: addr,
			Email:   method.BillingDetails.Email,
			Name:    stripe.String(method.BillingDetails.Name),
			Phone:   method.BillingDetails.Phone,
		}
	}
	return detail
}

func (s *StripePaymentHandlerImpl) UpdatePaymentMethod(userCode, methodId string, method *forms.StripePaymentMethod) (*forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return nil, nil
	}

	_, err := s.GetPaymentMethod(userCode, methodId)
	if err != nil {
		return nil, err
	}
	params := &stripe.PaymentMethodParams{
		BillingDetails: newBillingDetailParams(method),
	}
	paymentMethod, err := getStripeClient().PaymentMethods.Update(methodId, params)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	log.Info("update a payment method from customer", zap.String("method id", methodId))
	return newPaymentMethod(paymentMethod), nil
}

func (s *StripePaymentHandlerImpl) DeletePaymentMethod(userCode, methodId string) error {
	if !stripeEnabled() {
		return nil
	}

	_, err := s.GetPaymentMethod(userCode, methodId)
	if err != nil {
		return err
	}

	cards, err := s.ListPaymentMethods(userCode)
	if err != nil {
		return err
	}
	if len(cards) <= 1 {
		return billing_errors.NewInvalidInputError(errors.New("you must keep at least one credit card"))
	}

	pm, err := getStripeClient().PaymentMethods.Detach(methodId, nil)
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	log.Info("delete a payment method from customer", zap.String("method id", pm.ID))
	return nil
}

func (s *StripePaymentHandlerImpl) GetPaymentMethod(userCode, methodId string) (*forms.StripePaymentMethod, error) {
	if !stripeEnabled() {
		return &forms.StripePaymentMethod{}, nil
	}
	paymentMethod, err := getStripeClient().PaymentMethods.Get(methodId, nil)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	if paymentMethod.Customer.ID != userCode {
		return nil, fmt.Errorf("payment method %s is not belongs to customer %s", methodId, userCode)
	}
	return newPaymentMethod(paymentMethod), nil

}

func (s *StripePaymentHandlerImpl) SetPaymentMethodDefault(userCode, methodId string) error {
	if !stripeEnabled() {
		return nil
	}

	_, err := s.GetPaymentMethod(userCode, methodId)
	if err != nil {
		return err
	}
	_, err = getStripeClient().Customers.Update(userCode, &stripe.CustomerParams{InvoiceSettings: &stripe.CustomerInvoiceSettingsParams{DefaultPaymentMethod: &methodId}})
	if err != nil {
		return billing_errors.NewSystemError(err)
	}
	return nil
}

func newStripeCustomerInfo(c *stripe.Customer) *forms.StripeCustomerInfo {
	var sourceID string
	invoiceSetting := c.InvoiceSettings
	if invoiceSetting != nil && invoiceSetting.DefaultPaymentMethod != nil {
		sourceID = invoiceSetting.DefaultPaymentMethod.ID
	}
	return &forms.StripeCustomerInfo{
		ID:            c.ID,
		TenantID:      c.Metadata[StripeDBaaSIDLabel],
		Email:         c.Email,
		Name:          c.Name,
		Description:   c.Description,
		DefaultSource: sourceID,
		Address: &forms.Address{
			City:       c.Address.City,
			Country:    c.Address.Country,
			Line1:      c.Address.Line1,
			Line2:      c.Address.Line2,
			PostalCode: c.Address.PostalCode,
			State:      c.Address.State,
		},
	}
}

func (s *StripePaymentHandlerImpl) GetCustomerInfo(userCode string) (*forms.StripeCustomerInfo, error) {
	if !stripeEnabled() {
		return nil, nil
	}
	sc, err := getStripeClient().Customers.Get(userCode, nil)

	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	return newStripeCustomerInfo(sc), nil
}

func newStripeInvoice(invoice *stripe.Invoice) *forms.StripeInvoice {
	dueDate := invoice.DueDate
	if dueDate == 0 {
		dueDate = invoice.StatusTransitions.PaidAt
	}
	return &forms.StripeInvoice{
		AmountDue:  invoice.AmountDue,
		AmountPaid: invoice.AmountPaid,
		Created:    invoice.Created,
		Currency:   string(invoice.Currency),
		DueDate:    dueDate,
		CreditUsed: invoice.EndingBalance - invoice.StartingBalance,
		ID:         invoice.ID,
		Number:     invoice.Number,
		InvoicePDF: invoice.InvoicePDF,
		Total:      invoice.Total,
		Paid:       invoice.Paid,
		Status:     string(invoice.Status),
	}
}

func (s *StripePaymentHandlerImpl) ListStripeInvoices(userCode string) ([]forms.StripeInvoice, error) {
	params := &stripe.InvoiceListParams{Customer: stripe.String(userCode)}
	it := getStripeClient().Invoices.List(params)
	var items = make([]forms.StripeInvoice, 0)
	for it.Next() {
		invoice := it.Invoice()
		if invoice.Status != stripe.InvoiceStatusDraft {
			items = append(items, *newStripeInvoice(invoice))
		}
	}
	err := it.Err()
	if err != nil {
		return nil, err
	}
	return items, nil
}
