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")

	planOptions = map[string]bool{
		"basic_monthly":     false,
		"basic_yearly":      false,
		"supporter_monthly": false,
		"supporter_yearly":  false,
		"free":              true,
		PendingPlan:         true,
	}

	// Version tracks the build ID of the binary, set using
	// ldflags.
	Version string
)

// 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   `json:"user_id"`
	StripeSubscription   string    `json:"stripe_subscription"`
	Plan                 string    `json:"plan"`
	Status               string    `json:"status"`
	Canceling            bool      `json:"canceling"`
	Created              time.Time `json:"created"`
	TrialStart           time.Time `json:"trial_start,omitempty"`
	TrialEnd             time.Time `json:"trial_end,omitempty"`
	PeriodStart          time.Time `json:"period_start,omitempty"`
	PeriodEnd            time.Time `json:"period_end,omitempty"`
	CanceledAt           time.Time `json:"canceled_at,omitempty"`
	FailedChargeAttempts int       `json:"failed_charge_attempts"`
	LastFailedCharge     time.Time `json:"last_failed_charge,omitempty"`
	LastNotified         time.Time `json:"last_notified,omitempty"`
}

// SubscriptionChange represents desired changes to a Subscription
// object. A nil value means that property should remain unchanged.
type SubscriptionChange struct {
	UserID uuid.ID `json:"user_id"`

	// User-controlled and not stored in DB (helper properties for the API)
	StripeSource *string `json:"stripe_source,omitempty"`
	Email        *string `json:"email,omitempty"`

	// User-controlled and stored in DB
	Plan      *string `json:"plan,omitempty"`
	Canceling *bool   `json:"cenceling,omitempty"`

	// System-controlled
	StripeSubscription   *string    `json:"stripe_subscription,omitempty"`
	Status               *string    `json:"status,omitempty"`
	TrialStart           *time.Time `json:"trial_start,omitempty"`
	TrialEnd             *time.Time `json:"trial_end,omitempty"`
	PeriodStart          *time.Time `json:"period_start,omitempty"`
	PeriodEnd            *time.Time `json:"period_end,omitempty"`
	CanceledAt           *time.Time `json:"canceled_at,omitempty"`
	FailedChargeAttempts *int       `json:"failed_charge_attempts,omitempty"`
	LastFailedCharge     *time.Time `json:"last_failed_charge,omitempty"`
	LastNotified         *time.Time `json:"last_notified,omitempty"`
}

// IsEmpty returns true if the SubscriptionChange doesn't request
// a change to any property of the Subscription.
func (change SubscriptionChange) IsEmpty() bool {
	if change.Plan != nil {
		return false
	}
	if change.Canceling != nil {
		return false
	}
	if change.StripeSubscription != nil {
		return false
	}
	if change.Status != 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
	}
}

// IsAcceptablePlan returns true if the user can select the specified
// plan, taking into account their admin status. If a plan exists, and
// is not designated as an admin-only plan, any user selecting it will
// return true. If a plan exists, but is designated as admin-only,
// IsAcceptablePlan will only return true if admin is true. If the plan
// doesn't exist, IsAcceptablePlan always returns false.
func IsAcceptablePlan(plan string, admin bool) bool {
	for p, adminOnly := range planOptions {
		if plan == p {
			return admin || adminOnly == false
		}
	}
	return false
}

// 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."
}

// SubscriptionStore is an interface describing datastore interactions for
// Subscriptions.
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)
}
