//Root is used to show the login page of the app
//when a user browses to the page (usually just the domain minus any path), the user is checked for a session
//if a session exists, the app attempts to auto-login the user
//otherwise a user is shown the log in prompt
//this also handles the "first run" of the app in which no users exist yet...it forces creation of the "super admin"
func Root(w http.ResponseWriter, r *http.Request) {
	//check that session store was initialized correctly
	if err := sessionutils.CheckSession(); err != nil {
		notificationPage(w, "panel-danger", sessionInitError, err, "btn-default", "/", "Go Back")
		return
	}

	//check that stripe private key and statement desecriptor were read correctly
	if err := card.CheckStripe(); err != nil {
		notificationPage(w, "panel-danger", sessionInitError, err, "btn-default", "/", "Go Back")
		return
	}

	//check if the admin user exists
	//redirect user to create admin if it does not exist
	err := users.DoesAdminExist(r)
	if err == users.ErrAdminDoesNotExist {
		http.Redirect(w, r, "/setup/", http.StatusFound)
		return
	} else if err != nil {
		notificationPage(w, "panel-danger", adminInitError, err, "btn-default", "/", "Go Back")
		return
	}

	//check if user is already signed in
	//if user is already logged in, redirect to /main/ page
	session := sessionutils.Get(r)
	if session.IsNew == false {
		uId := session.Values["user_id"].(int64)
		c := appengine.NewContext(r)
		u, err := users.Find(c, uId)
		if err != nil {
			sessionutils.Destroy(w, r)
			notificationPage(w, "panel-danger", "Autologin Error", "There was an issue looking up your user account. Please go back and try logging in.", "btn-default", "/", "Go Back")
			return
		}

		//user data was found
		//check if user is allowed access
		if users.AllowedAccess(u) == false {
			sessionutils.Destroy(w, r)
			notificationPage(w, "panel-danger", "Autologin Error", "You are not allowed access. Please contact an administrator.", "btn-default", "/", "Go Back")
		}

		//user account is found an allowed access
		//redirect user
		http.Redirect(w, r, "/main/", http.StatusFound)
		return
	}

	//load the login page
	templates.Load(w, "root", nil)
	return
}
//Auth checks if a user is logged in and is allowed access to the app
//this is done on every page load and every endpoint
func Auth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		//get user data from session
		session := sessionutils.Get(r)

		//session data does not exist yet
		//this is a new session
		//redirect user to log in page
		if session.IsNew {
			http.Redirect(w, r, "/", http.StatusFound)
			return
		}

		//check if user id is in session
		//it *should* be!
		//otherwise show user a notice and force user to log in again
		userId, ok := session.Values["user_id"].(int64)
		if ok == false {
			sessionutils.Destroy(w, r)
			notificationPage(w, "panel-danger", "Session Expired", "Your session has expired. Please log back in or contact an administrator if this problem persists.", "btn-default", "/", "Log In")
			return
		}

		//look up user in memcache and/or datastore
		c := appengine.NewContext(r)
		data, err := users.Find(c, userId)
		if err != nil {
			sessionutils.Destroy(w, r)
			notificationPage(w, "panel-danger", "Application Error", "The app encountered an error in the middleware while trying to authenticate you as a legitimate user. Please try logging in again or contact an administrator.", "btn-default", "/", "Log In")
			return
		}

		//check if user is allowed access to the app
		//this is a setting the app's administrators can toggle for each user
		if users.AllowedAccess(data) == false {
			sessionutils.Destroy(w, r)
			http.Redirect(w, r, "/", http.StatusFound)
			return
		}

		//user is allowed access
		//extend expiration of session cookie to allow user to stay "logged in"
		sessionutils.ExtendExpiration(session, w, r)

		//move to next middleware or handler
		next.ServeHTTP(w, r)
	})
}
//AddCards checks if the user is allowed to add credit cards to the app
func AddCards(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		//get session data
		session := sessionutils.Get(r)

		//look up user data
		c := appengine.NewContext(r)
		userId := session.Values["user_id"].(int64)
		data, err := users.Find(c, userId)
		if err != nil {
			output.Error(err, "An error occurred in the middleware.", w, r)
			return
		}

		//check if user can add cards
		if data.AddCards == false {
			output.Error(ErrNotAuthorized, "You do not have permission to add new cards.", w, r)
			return
		}

		//move to next middleware or handler
		next.ServeHTTP(w, r)
	})
}
//Main loads the main UI of the app
//this is the page the user sees once they are logged in
//this ui is a single page app and holds almost all the functionality of the app
//the user only sees the parts of the ui they have access to...the rest is removed via go's contemplating
//we also check if this page was loaded with a bunch of extra data in the url...this would be used to perform the api-like semi-automated charging of the card
//  if a link to the page has a "customer_id" form value, this will automatically find the customer's card data and show it in the panel
//  if "amount", "invoice", and/or "po" form values are given, these will also automatically be filled into the charge panel's form
//  if "customer_id" is not given, no auto filling will occur of any fields
//  "amount" must be in cents
//  card is not automatically charged, user still has to click "charge" button
func Main(w http.ResponseWriter, r *http.Request) {
	//placeholder for sending data back to template
	var tempData autoloader

	//get logged in user data
	//catch instances where session is not working and redirect user to log in page
	//use the user's data to show/hide certain parts of the ui per the users access rights
	session := sessionutils.Get(r)
	if session.IsNew == true {
		notificationPage(w, "panel-danger", "Cannot Load Page", "Your session has expired or there is an error.  Please try logging in again or contact an administrator.", "btn-default", "/", "Log In")
		return
	}

	userId := session.Values["user_id"].(int64)

	c := appengine.NewContext(r)
	user, err := users.Find(c, userId)
	if err != nil {
		notificationPage(w, "panel-danger", "Cannot Load Page", err, "btn-default", "/", "Try Again")
		return
	}

	//check for url form values for autofilling charge panel
	//if data in url does not exist, just load the page with user data only
	custId := r.FormValue("customer_id")
	if len(custId) == 0 {
		tempData.UserData = user
		templates.Load(w, "main", tempData)
		return
	}

	//data in url does exist
	//look up card data by customer id
	//get the card data to show in the panel so user can visually confirm they are charging the correct card
	//if an error occurs, just load the page normally
	custData, err := card.FindByCustId(c, custId)
	if err != nil {
		tempData.Error = "The form could not be autofilled because the customer ID you provided could not be found.  The ID is either incorrect or the customer's credit card has not been added yet."
		tempData.UserData = user
		templates.Load(w, "main", tempData)
		return
	}

	tempData.CardData = custData
	tempData.UserData = user

	//if amount was given, it is in cents
	//display it in html input as dollars
	amountUrl := r.FormValue("amount")
	amountFloat, _ := strconv.ParseFloat(amountUrl, 64)
	amountDollars := amountFloat / 100
	tempData.Amount = amountDollars

	//check for other form values and build template
	tempData.Invoice = r.FormValue("invoice")
	tempData.Po = r.FormValue("po")

	//load the page with the card data
	templates.Load(w, "main", tempData)
	return
}
//Report gets the data for charges and refunds by the defined filters (date range and customer) and builds the reports page
//the reports show up in a different page so they are easily printable and more easily inspected
//date range is inclusive of start and end days
func Report(w http.ResponseWriter, r *http.Request) {
	//get form valuess
	datastoreId := r.FormValue("customer-id")
	startString := r.FormValue("start-date")
	endString := r.FormValue("end-date")
	hoursToUTC := r.FormValue("timezone")

	//get report data form stripe
	//make sure inputs are given
	if len(startString) == 0 {
		output.Error(ErrMissingInput, "You must supply a 'start-date'.", w, r)
		return
	}
	if len(endString) == 0 {
		output.Error(ErrMissingInput, "You must supply a 'end-date'.", w, r)
		return
	}
	if len(hoursToUTC) == 0 {
		output.Error(ErrMissingInput, "You must supply a 'timezone'.", w, r)
		return
	}

	//get timezone offset
	//adjust for the local timezone the user is in so that the date range is correct
	//hoursToUTC is a number generated by JS (-4 for EST)
	tzOffset := calcTzOffset(hoursToUTC)

	//get datetimes from provided strings
	startDt, err := time.Parse("2006-01-02 -0700", startString+" "+tzOffset)
	if err != nil {
		output.Error(err, "Could not convert start date to a time.Time datetime.", w, r)
		return
	}
	endDt, err := time.Parse("2006-01-02 -0700", endString+" "+tzOffset)
	if err != nil {
		output.Error(err, "Could not convert end date to a time.Time datetime.", w, r)
		return
	}

	//get end of day datetime
	//need to get 23:59:59 so we include the whole day
	endDt = endDt.Add((24*60-1)*time.Minute + (59 * time.Second))

	//get unix timestamps
	//stripe only accepts timestamps for filtering charges
	startUnix := startDt.Unix()
	endUnix := endDt.Unix()

	//init stripe
	c := appengine.NewContext(r)
	sc := createAppengineStripeClient(c)

	//retrieve data from stripe
	//date is a range inclusive of the days the user chose
	//limit of 100 is the max per stripe
	params := &stripe.ChargeListParams{}
	params.Filters.AddFilter("created", "gte", strconv.FormatInt(startUnix, 10))
	params.Filters.AddFilter("created", "lte", strconv.FormatInt(endUnix, 10))
	params.Filters.AddFilter("limit", "", "100")

	//check if we need to filter by a specific customer
	//look up stripe customer id by the datastore id
	if len(datastoreId) != 0 {
		datastoreIdInt, _ := strconv.ParseInt(datastoreId, 10, 64)
		custData, err := findByDatastoreId(c, datastoreIdInt)
		if err != nil {
			output.Error(err, "An error occured and this report could not be generated.", w, r)
			return
		}

		params.Filters.AddFilter("customer", "", custData.StripeCustomerToken)
	}

	//get results
	//loop through each charge and extract charge data
	//add up total amount of all charges
	charges := sc.Charges.List(params)
	data := make([]chargeutils.Data, 0, 10)
	var amountTotal uint64 = 0
	var numCharges uint16 = 0
	for charges.Next() {
		//get each charges data
		chg := charges.Charge()
		d := chargeutils.ExtractData(chg)

		//make sure this charge was captured
		//do not count charges that failed
		if d.Captured == false {
			continue
		}

		data = append(data, d)

		//increment totals
		amountTotal += d.AmountCents
		numCharges++
	}

	//convert total amount to dollars
	amountTotalDollars := strconv.FormatFloat((float64(amountTotal) / 100), 'f', 2, 64)

	//retrieve refunds
	eventParams := &stripe.EventListParams{}
	eventParams.Filters.AddFilter("created", "gte", strconv.FormatInt(startUnix, 10))
	eventParams.Filters.AddFilter("created", "lte", strconv.FormatInt(endUnix, 10))
	eventParams.Filters.AddFilter("limit", "", "100")
	eventParams.Filters.AddFilter("type", "", "charge.refunded")

	events := sc.Events.List(eventParams)
	refunds := chargeutils.ExtractRefunds(events)

	//get logged in user's data
	//for determining if receipt/refund buttons need to be hidden or shown based on user's access rights
	session := sessionutils.Get(r)
	userId := session.Values["user_id"].(int64)
	userdata, _ := users.Find(c, userId)

	//store data for building template
	result := reportData{
		UserData:    userdata,
		StartDate:   startDt,
		EndDate:     endDt,
		Charges:     data,
		Refunds:     refunds,
		TotalAmount: amountTotalDollars,
		NumCharges:  numCharges,
	}

	//build template to display report
	//separate page in gui
	templates.Load(w, "report", result)
	return
}