package subscriptions

import (
	"errors"
	"time"

	"code.secondbit.org/uuid.hg"
)

var (
	// ErrSubscriptionAlreadyExists is returned when a Subscription
	// with an identical ID already exists in the subscriptionStore.
	ErrSubscriptionAlreadyExists = errors.New("Subscription already exists")
	// ErrSubscriptionNotFound is returned when a single Subscription
	// is acted upon or requested, but cannot be found.
	ErrSubscriptionNotFound = errors.New("Subscription not found")
	// ErrStripeSubscriptionAlreadyExists is returned when a Subscription
	// is created or updates its StripeSubscription property, but that
	// StripeSubscription is already associated with another Subscription.
	ErrStripeSubscriptionAlreadyExists = errors.New("Stripe subscription already assigned to another Subscription")
	// ErrSubscriptionChangeEmpty is returned when a SubscriptionChange
	// is empty but is passed to subscriptionStore.UpdateSubscription
	// anyways.
	ErrSubscriptionChangeEmpty = errors.New("SubscriptionChange is empty")
	// ErrNoSubscriptionID is returned when one or more Subscription IDs
	// are required, but none are provided.
	ErrNoSubscriptionID = errors.New("no Subscription ID provided")
)

// Subscription represents the state of a user's payments. It holds
// metadata about the last time a user was charged, how much a user
// should be charged, how to charge a user and how much to charge
// the user.
type Subscription struct {
	UserID               uuid.ID
	StripeSubscription   string
	Plan                 string
	Status               string
	Canceling            bool
	Created              time.Time
	TrialStart           time.Time
	TrialEnd             time.Time
	PeriodStart          time.Time
	PeriodEnd            time.Time
	CanceledAt           time.Time
	FailedChargeAttempts int
	LastFailedCharge     time.Time
	LastNotified         time.Time
}

// SubscriptionChange represents desired changes to a Subscription
// object. A nil value means that property should remain unchanged.
type SubscriptionChange struct {
	StripeSubscription   *string
	Plan                 *string
	Status               *string
	Canceling            *bool
	TrialStart           *time.Time
	TrialEnd             *time.Time
	PeriodStart          *time.Time
	PeriodEnd            *time.Time
	CanceledAt           *time.Time
	FailedChargeAttempts *int
	LastFailedCharge     *time.Time
	LastNotified         *time.Time
}

// IsEmpty returns true if the SubscriptionChange doesn't request
// a change to any property of the Subscription.
func (change SubscriptionChange) IsEmpty() bool {
	if change.StripeSubscription != nil {
		return false
	}
	if change.Plan != nil {
		return false
	}
	if change.Status != nil {
		return false
	}
	if change.Canceling != nil {
		return false
	}
	if change.TrialStart != nil {
		return false
	}
	if change.TrialEnd != nil {
		return false
	}
	if change.PeriodStart != nil {
		return false
	}
	if change.PeriodEnd != nil {
		return false
	}
	if change.CanceledAt != nil {
		return false
	}
	if change.LastNotified != nil {
		return false
	}
	if change.LastFailedCharge != nil {
		return false
	}
	if change.FailedChargeAttempts != nil {
		return false
	}
	return true
}

// ApplyChange updates a Subscription based on the changes requested
// by a SubscriptionChange.
func (s *Subscription) ApplyChange(change SubscriptionChange) {
	if change.StripeSubscription != nil {
		s.StripeSubscription = *change.StripeSubscription
	}
	if change.Plan != nil {
		s.Plan = *change.Plan
	}
	if change.Status != nil {
		s.Status = *change.Status
	}
	if change.Canceling != nil {
		s.Canceling = *change.Canceling
	}
	if change.TrialStart != nil {
		s.TrialStart = *change.TrialStart
	}
	if change.TrialEnd != nil {
		s.TrialEnd = *change.TrialEnd
	}
	if change.PeriodStart != nil {
		s.PeriodStart = *change.PeriodStart
	}
	if change.PeriodEnd != nil {
		s.PeriodEnd = *change.PeriodEnd
	}
	if change.CanceledAt != nil {
		s.CanceledAt = *change.CanceledAt
	}
	if change.LastFailedCharge != nil {
		s.LastFailedCharge = *change.LastFailedCharge
	}
	if change.LastNotified != nil {
		s.LastNotified = *change.LastNotified
	}
	if change.FailedChargeAttempts != nil {
		s.FailedChargeAttempts = *change.FailedChargeAttempts
	}
}

// SubscriptionStats represents a set of statistics about our Subscription
// data that will be exposed to the Prometheus scraper.
type SubscriptionStats struct {
	Number    int64
	Canceling int64
	Failing   int64
	Plans     map[string]int64
	// BUG(paddy): Currently, Kubernetes doesn't offer any way to contact _all_ nodes in a service.
	// Because of this, we can only report stats that will be identical across nodes, e.g. stats
	// that come from the database. More info here: https://github.com/GoogleCloudPlatform/kubernetes/issues/6666
	// In the future, we'll need per-node metrics. For now, we'll make do.
	//
	// Actually, as of https://github.com/GoogleCloudPlatform/kubernetes/pull/9073, we may be all set:
	// "SRV Records are created for named ports that are part of normal or Headless Services. For each named port,
	// the SRV record would have the form _my-port-name._my-port-protocol.my-svc.my-namespace.svc.cluster.local.
	// For a regular service, this resolves to the port number and the CNAME: my-svc.my-namespace.svc.cluster.local.
	// For a headless service, this resolves to multiple answers, one for each pod that is backing the service, and
	// contains the port number and a CNAME of the pod with the format auto-generated-name.my-svc.my-namespace.svc.cluster.local
	// SRV records always contain the 'svc' segment in them and are not supported for old-style CNAMEs where the 'svc' segment
	// was omitted."
}

type subscriptionStore interface {
	reset() error
	createSubscription(sub Subscription) error
	updateSubscription(id uuid.ID, change SubscriptionChange) error
	deleteSubscription(id uuid.ID) error
	getSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
	getSubscriptionStats() (SubscriptionStats, error)
}
