package subscriptions

import (
	"log"

	"code.secondbit.org/uuid.hg"

	"github.com/lib/pq"
	"github.com/secondbit/pan"
)

// GetSQLTableName fulfills the pan.SQLTableNamer interface, allowing
// us to manipulate Subscriptions with pan.
func (s Subscription) GetSQLTableName() string {
	return "subscriptions"
}

func (p Postgres) resetSQL() *pan.Query {
	var subscription Subscription
	query := pan.New(pan.POSTGRES, "TRUNCATE "+pan.GetTableName(subscription))
	return query.FlushExpressions(" ")
}

// Reset returns the database to its initialised state. It is meant to
// be used during testing to reset the databases to a known state between
// tests. It should never be used in production.
func (p Postgres) Reset() error {
	query := p.resetSQL()
	_, err := p.Exec(query.String(), query.Args...)
	if err != nil {
		return err
	}
	return nil
}

func (p Postgres) createSubscriptionSQL(subscription Subscription) *pan.Query {
	fields, values := pan.GetFields(subscription)
	query := pan.New(pan.POSTGRES, "INSERT INTO "+pan.GetTableName(subscription))
	query.Include("(" + pan.QueryList(fields) + ")")
	query.Include("VALUES")
	query.Include("("+pan.VariableList(len(values))+")", values...)
	return query.FlushExpressions(" ")
}

// CreateSubscription stores the passed Subscription in the database. If
// a Subscription sharing the same UserID already exists, an
// ErrSubscriptionAlreadyExists error will be returned. If a Subscription
// sharing the same StripeSubscription already exists, an
// ErrStripeSubscriptionAlreadyExists error will be returned.
func (p Postgres) CreateSubscription(sub Subscription) error {
	query := p.createSubscriptionSQL(sub)
	_, err := p.Exec(query.String(), query.Args...)
	if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_pkey" {
		err = ErrSubscriptionAlreadyExists
	} else if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_subscription_key" {
		err = ErrStripeSubscriptionAlreadyExists
	}
	return err
}

func (p Postgres) updateSubscriptionSQL(id uuid.ID, change SubscriptionChange) *pan.Query {
	var subscription Subscription
	query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(subscription)+" SET")
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "StripeSubscription")+" = ?", change.StripeSubscription)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "Plan")+" = ?", change.Plan)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "Status")+" = ?", change.Status)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "Canceling")+" = ?", change.Canceling)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "TrialStart")+" = ?", change.TrialStart)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "TrialEnd")+" = ?", change.TrialEnd)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "PeriodStart")+" = ?", change.PeriodStart)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "PeriodEnd")+" = ?", change.PeriodEnd)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "CanceledAt")+" = ?", change.CanceledAt)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "FailedChargeAttempts")+" = ?", change.FailedChargeAttempts)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "LastFailedCharge")+" = ?", change.LastFailedCharge)
	query.IncludeIfNotNil(pan.GetUnquotedColumn(subscription, "LastNotified")+" = ?", change.LastNotified)
	query.FlushExpressions(", ")
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(subscription, "UserID")+" = ?", id)
	return query.FlushExpressions(" ")
}

// UpdateSubscription applies the SubscriptionChange passed to the Subscription
// stored in the database associated with the passed ID. If change is empty,
// an ErrSubscriptionChangeEmpty error is returned. If no Subscription is found
// in the database with the passed ID, an ErrSubscriptionNotFound error is returned.
// If change is updating the StripeSubscription and a Subscription in the database
// already has that value set for StripeSubscription, an
// ErrStripeSubscriptionAlreadyExists error is returned.
func (p Postgres) UpdateSubscription(id uuid.ID, change SubscriptionChange) error {
	if change.IsEmpty() {
		return ErrSubscriptionChangeEmpty
	}

	query := p.updateSubscriptionSQL(id, change)
	res, err := p.Exec(query.String(), query.Args...)
	if e, ok := err.(*pq.Error); ok && e.Constraint == "subscriptions_stripe_subscription_key" {
		return ErrStripeSubscriptionAlreadyExists
	} else if err != nil {
		return err
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if rows < 1 {
		return ErrSubscriptionNotFound
	}
	return nil
}

func (p Postgres) deleteSubscriptionSQL(id uuid.ID) *pan.Query {
	var subscription Subscription
	query := pan.New(pan.POSTGRES, "DELETE FROM "+pan.GetTableName(subscription))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(subscription, "UserID")+" = ?", id)
	return query.FlushExpressions(" ")
}

// DeleteSubscription removes the Subscription stored in the database associated
// with the passed ID from the database. If no Subscription is found
// in the database with the passed ID, an ErrSubscriptionNotFound error is returned.
func (p Postgres) DeleteSubscription(id uuid.ID) error {
	query := p.deleteSubscriptionSQL(id)
	res, err := p.Exec(query.String(), query.Args...)
	if err != nil {
		return err
	}
	rows, err := res.RowsAffected()
	if err != nil {
		return err
	}
	if rows < 1 {
		return ErrSubscriptionNotFound
	}
	return nil
}

func (p Postgres) getSubscriptionsSQL(ids []uuid.ID) *pan.Query {
	var subscription Subscription
	fields, _ := pan.GetFields(subscription)
	intIDs := make([]interface{}, len(ids))
	for pos, id := range ids {
		intIDs[pos] = id
	}
	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM "+pan.GetTableName(subscription))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(subscription, "UserID") + " IN")
	query.Include("("+pan.VariableList(len(intIDs))+")", intIDs...)
	return query.FlushExpressions(" ")
}

// GetSubscriptions retrieves the Subscriptions stored in the database associated
// with the passed IDs. If no IDs are passed, an ErrNoSubscriptionID error is
// returned. No matter how many of the IDs are found (including none), a map is
// returned, with the key being a String()ed version of the ID for the Subscription in
// the value. If no error is returned, the map will represent all of the Subscriptions
// matching the passed IDs that exist in the database, even if it's empty.
func (p Postgres) GetSubscriptions(ids []uuid.ID) (map[string]Subscription, error) {
	results := map[string]Subscription{}
	if len(ids) < 1 {
		return results, ErrNoSubscriptionID
	}
	query := p.getSubscriptionsSQL(ids)
	rows, err := p.Query(query.String(), query.Args...)
	if err != nil {
		return results, err
	}
	for rows.Next() {
		var subscription Subscription
		err := pan.Unmarshal(rows, &subscription)
		if err != nil {
			return results, err
		}
		results[subscription.UserID.String()] = subscription
	}
	if err := rows.Err(); err != nil {
		return results, err
	}
	return results, nil
}

func (p Postgres) getSubscriptionStatsCountSQL() *pan.Query {
	var subscription Subscription
	query := pan.New(pan.POSTGRES, "SELECT COUNT(*) FROM")
	query.Include(pan.GetTableName(subscription))
	return query.FlushExpressions(" ")
}

func (p Postgres) getSubscriptionStatsCancelingSQL() *pan.Query {
	var subscription Subscription
	query := pan.New(pan.POSTGRES, "SELECT COUNT(*) FROM")
	query.Include(pan.GetTableName(subscription))
	query.IncludeWhere()
	query.Include(pan.GetUnquotedColumn(subscription, "Canceling")+" = ?", true)
	return query.FlushExpressions(" ")
}

func (p Postgres) getSubscriptionStatsFailingSQL() *pan.Query {
	var subscription Subscription
	query := pan.New(pan.POSTGRES, "SELECT COUNT(*) FROM")
	query.Include(pan.GetTableName(subscription))
	query.IncludeWhere()
	statuses := []interface{}{"past_due", "unpaid"}
	query.Include(pan.GetUnquotedColumn(subscription, "Status")+" IN ("+pan.VariableList(len(statuses))+")", statuses...)
	return query.FlushExpressions(" ")
}

func (p Postgres) getSubscriptionStatsPlansSQL() *pan.Query {
	var subscription Subscription
	fields := []interface{}{pan.GetUnquotedColumn(subscription, "Plan"), "COUNT(*)"}
	query := pan.New(pan.POSTGRES, "SELECT "+pan.QueryList(fields)+" FROM")
	query.Include(pan.GetTableName(subscription))
	query.Include("GROUP BY " + pan.GetUnquotedColumn(subscription, "Plan"))
	return query.FlushExpressions(" ")
}

// GetSubscriptionStats returns statistics about the subscription data stored in the
// database as a SubscriptionStats variable. The number of Subscriptions, the
// breakdown of how many Subscriptions belong to each plan, the number of
// Subscriptions that are canceling, and the number of Subscriptions whose payment
// information is failing are all tracked.
func (p Postgres) GetSubscriptionStats() (SubscriptionStats, error) {
	stats := SubscriptionStats{
		Plans: map[string]int64{},
	}
	query := p.getSubscriptionStatsCountSQL()
	err := p.QueryRow(query.String(), query.Args...).Scan(&stats.Number)
	if err != nil {
		log.Printf("Error querying for total subscriptions: %+v\n", err)
		return stats, err
	}
	query = p.getSubscriptionStatsCancelingSQL()
	err = p.QueryRow(query.String(), query.Args...).Scan(&stats.Canceling)
	if err != nil {
		log.Printf("Error querying for canceling subscriptions: %+v\n", err)
		return stats, err
	}
	query = p.getSubscriptionStatsFailingSQL()
	err = p.QueryRow(query.String(), query.Args...).Scan(&stats.Failing)
	if err != nil {
		log.Printf("Error querying for failing subscriptions: %+v\n", err)
		return stats, err
	}
	query = p.getSubscriptionStatsPlansSQL()
	rows, err := p.Query(query.String(), query.Args...)
	if err != nil {
		log.Printf("Error querying for plans: %+v\n", err)
		return stats, err
	}
	for rows.Next() {
		var plan string
		var count int64
		err := rows.Scan(&plan, &count)
		if err != nil {
			log.Printf("Error scanning database row for plans: %+v\n", err)
			continue
		}
		stats.Plans[plan] = count
	}
	if err := rows.Err(); err != nil {
		log.Printf("Error querying for plans: %+v\n", err)
		return stats, err
	}
	return stats, nil
}
