package subscriptions

import (
	"database/sql/driver"
	"errors"
	"time"

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

const (
	// MonthlyPeriod represents a period of once a month.
	MonthlyPeriod period = "monthly"
)

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")
	// ErrStripeCustomerAlreadyExists is returned when a Subscription
	// is created or updates its StripeCustomer property, but that
	// StripeCustomer is already associated with another Subscription.
	ErrStripeCustomerAlreadyExists = errors.New("Stripe customer 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")
	// ErrPeriodInvalid is returned when attempting to use a period that
	// is not a valid period.
	ErrPeriodInvalid = errors.New("invalid period")
)

type period string

func (p period) Value() (driver.Value, error) {
	return string(p), nil
}

func (p *period) Scan(src interface{}) error {
	if src == nil {
		*p = period("")
		return nil
	}
	switch src.(type) {
	case []byte:
		*p = period(string(src.([]byte)))
		return nil
	case string:
		*p = period(src.(string))
		return nil
	default:
		return ErrPeriodInvalid
	}
}

// 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
	StripeCustomer string
	Amount         int
	Period         period
	Created        time.Time
	BeginCharging  time.Time
	LastCharged    time.Time
	LastNotified   time.Time
	InLockout      bool
}

// SubscriptionChange represents desired changes to a Subscription
// object. A nil value means that property should remain unchanged.
type SubscriptionChange struct {
	StripeCustomer *string
	Amount         *int
	Period         *period
	BeginCharging  *time.Time
	LastCharged    *time.Time
	LastNotified   *time.Time
	InLockout      *bool
}

// IsEmpty returns true if the SubscriptionChange doesn't request
// a change to any property of the Subscription.
func (change SubscriptionChange) IsEmpty() bool {
	if change.StripeCustomer != nil {
		return false
	}
	if change.Amount != nil {
		return false
	}
	if change.Period != nil {
		return false
	}
	if change.BeginCharging != nil {
		return false
	}
	if change.LastCharged != nil {
		return false
	}
	if change.LastNotified != nil {
		return false
	}
	if change.InLockout != 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.StripeCustomer != nil {
		s.StripeCustomer = *change.StripeCustomer
	}
	if change.Amount != nil {
		s.Amount = *change.Amount
	}
	if change.Period != nil {
		s.Period = *change.Period
	}
	if change.BeginCharging != nil {
		s.BeginCharging = *change.BeginCharging
	}
	if change.LastCharged != nil {
		s.LastCharged = *change.LastCharged
	}
	if change.LastNotified != nil {
		s.LastNotified = *change.LastNotified
	}
	if change.InLockout != nil {
		s.InLockout = *change.InLockout
	}
}

// ByLastChargeDate allows us to sort a []Subscription by the LastCharged
// property, with the lowest LastCharged date first.
type ByLastChargeDate []Subscription

// Len returns the length the SubscriptionsByLastChargeDate. It fulfills
// the sort.Interface interface.
func (s ByLastChargeDate) Len() int {
	return len(s)
}

// Swap puts the item in position i in position j, and the item in position
// j in position i. It fulfills the sort.Interface interface.
func (s ByLastChargeDate) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

// Less returns true if the item in position i should be sorted before the
// item in position j.
func (s ByLastChargeDate) Less(i, j int) bool {
	return s[i].LastCharged.Before(s[j].LastCharged)
}

// SubscriptionStats represents a set of statistics about our Subscription
// data that will be exposed to the Prometheus scraper.
type SubscriptionStats struct {
	Number      int64
	TotalAmount int64
	MeanAmount  float64
	// 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
	listSubscriptionsLastChargedBefore(time.Time) ([]Subscription, error)
	getSubscriptions(ids []uuid.ID) (map[string]Subscription, error)
	getSubscriptionStats() (SubscriptionStats, error)
}
