package subscriptions

import (
	"errors"
	"time"

	"code.secondbit.org/uuid.hg"

	"github.com/stripe/stripe-go"
	"github.com/stripe/stripe-go/customer"
	"github.com/stripe/stripe-go/sub"
)

const (
	// PendingPlan holds the name for the plan users that haven't chosen a plan are subscribed to.
	PendingPlan = "pending"
)

var (
	// ErrNilCustomer is returned when a *Customer is expected, but nil is passed.
	ErrNilCustomer = errors.New("nil customer passed")
	// ErrNilCustomerSubs is returned when *Customer.Subs is expected to be a slice, but is nil.
	ErrNilCustomerSubs = errors.New("customer with nil subscriptions list passed")
	// ErrWrongNumberOfCustomerSubs is returned when a *Customer.Subs slice has more or fewer elements than expected.
	ErrWrongNumberOfCustomerSubs = errors.New("customer with wrong number of subscriptions passed")
	// ErrNilSubscription is returned when a *Subscription is expected, but nil is passed.
	ErrNilSubscription = errors.New("nil subscription passed")
)

// Stripe is a wrapper around the clients for Stripe that we're using. It's responsible for
// all the interaction with the Stripe API that we're doing. It is a level higher than the
// raw Stripe clients, and can be thought of as Stripe-from-the-perspective-of-subscriptions.
type Stripe struct {
	apiKey        string
	customers     customer.Client
	subscriptions sub.Client
}

// NewStripe creates a new Stripe instance using the passed parameters. The stripe.Backend
// parameter can be used to create a stub instead of something that actually hits the Stripe
// API. See the github.com/stripe/stripe-go documentation for more information about Backends.
func NewStripe(apiKey string, backend stripe.Backend) Stripe {
	return Stripe{
		apiKey: apiKey,
		customers: customer.Client{
			B:   backend,
			Key: apiKey,
		},
		subscriptions: sub.Client{
			B:   backend,
			Key: apiKey,
		},
	}
}

// CreateStripeCustomer sets up a customer using the passed Stripe client, ensuring it follows all
// the conventions that subscriptions expects (the description being set properly, the UserID meta,
// etc.) Creating a customer automatically creates a subscription for that customer.
func CreateStripeCustomer(plan, email string, userID uuid.ID, s Stripe) (*stripe.Customer, error) {
	customerParams := &stripe.CustomerParams{
		Desc:  "Customer for user " + userID.String(),
		Email: email,
		Plan:  plan,
	}
	customerParams.AddMeta("UserID", userID.String())
	c, err := s.customers.New(customerParams)
	if err != nil {
		return nil, err
	}
	return c, nil
}

// UpdateStripeSubscription modifies a subscription in Stripe, updating the plan and/or token.
// A nil plan or token indicates no change to the parameter.
func UpdateStripeSubscription(customerID string, plan, token *string, s Stripe) (*stripe.Sub, error) {
	params := &stripe.SubParams{}
	if plan != nil {
		params.Plan = *plan
	}
	if token != nil {
		params.Token = *token
	}
	subscription, err := s.subscriptions.Update(customerID, params)
	if err != nil {
		return nil, err
	}
	return subscription, nil
}

// New should be called when a user's profile is created. At this point, we know nothing about the subscription
// they actually _want_. We just sign them up for the dedicated "pending" plan. This is to make their free trial begin
// immediately and not have to worry about automatically locking them out until they actually create a subscription.
// Basically, we want everyone to have a subscription at all times, but some users will have placeholders until they
// actually update their subscription with a desired plan and payment method.
func New(req SubscriptionChange, s Stripe, store SubscriptionStore) (Subscription, error) {
	subscription := Subscription{}
	subscription.ApplyChange(req)
	// BUG(paddy): need to validate the change

	// create the customer in Stripe, storing the token for reuse
	customer, err := CreateStripeCustomer(PendingPlan, *req.Email, req.UserID, s)
	if err != nil {
		return subscription, err
	}
	if customer == nil {
		return subscription, ErrNilCustomer
	}
	if customer.Subs == nil {
		return subscription, ErrNilCustomerSubs
	}
	if len(customer.Subs.Values) != 1 {
		return subscription, ErrWrongNumberOfCustomerSubs
	}
	if customer.Subs.Values[0] == nil {
		return subscription, ErrNilSubscription
	}

	change := StripeSubscriptionChange(subscription, *customer.Subs.Values[0])
	subscription.ApplyChange(change)

	err = store.CreateSubscription(subscription)
	if err != nil {
		return subscription, err
	}

	return subscription, nil
}

// StripeSubscriptionChange takes a Subscription and a stripe.Sub, and returns a SubscriptionChange
// that will change only the properties necessary to make the Subscription match the stripe.Sub.
func StripeSubscriptionChange(orig Subscription, subscription stripe.Sub) SubscriptionChange {
	var change SubscriptionChange
	if subscription.ID != orig.StripeSubscription {
		change.StripeSubscription = &subscription.ID
	}
	if subscription.Plan != nil && orig.Plan != subscription.Plan.ID {
		change.Plan = &subscription.Plan.ID
	}
	if string(subscription.Status) != orig.Status {
		status := string(subscription.Status)
		change.Status = &status
	}
	if subscription.EndCancel != orig.Canceling {
		change.Canceling = &subscription.EndCancel
	}
	if !time.Unix(subscription.TrialStart, 0).Equal(orig.TrialStart) && !(subscription.TrialStart == 0 && orig.TrialStart.IsZero()) {
		trialStart := time.Unix(subscription.TrialStart, 0)
		change.TrialStart = &trialStart
	}
	if !time.Unix(subscription.TrialEnd, 0).Equal(orig.TrialEnd) && !(subscription.TrialEnd == 0 && orig.TrialEnd.IsZero()) {
		trialEnd := time.Unix(subscription.TrialEnd, 0)
		change.TrialEnd = &trialEnd
	}
	if !time.Unix(subscription.PeriodStart, 0).Equal(orig.PeriodStart) && !(subscription.PeriodStart == 0 && orig.PeriodStart.IsZero()) {
		periodStart := time.Unix(subscription.PeriodStart, 0)
		change.PeriodStart = &periodStart
	}
	if !time.Unix(subscription.PeriodEnd, 0).Equal(orig.PeriodEnd) && !(subscription.PeriodEnd == 0 && orig.PeriodEnd.IsZero()) {
		periodEnd := time.Unix(subscription.PeriodEnd, 0)
		change.PeriodEnd = &periodEnd
	}
	if !time.Unix(subscription.Canceled, 0).Equal(orig.CanceledAt) && !(subscription.Canceled == 0 && orig.CanceledAt.IsZero()) {
		canceledAt := time.Unix(subscription.Canceled, 0)
		change.CanceledAt = &canceledAt
	}
	return change
}
