package main

import (
	"bytes"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"html/template"
	"io"
	"log"
	"math"
	"net/http"
	"net/http/httputil"
	"net/mail"
	"net/url"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"
	// pdf "github.com/SebastiaanKlippert/go-wkhtmltopdf"
	"github.com/brianvoe/gofakeit/v6"
	"github.com/disintegration/gift"
	"github.com/dustin/go-humanize"
	"github.com/golang-jwt/jwt/v4"
	"github.com/stripe/stripe-go/v76"
	"github.com/stripe/stripe-go/v76/customer"
	"github.com/stripe/stripe-go/v76/subscription"
	// "github.com/stripe/stripe-go/v76/invoice"
	// "github.com/stripe/stripe-go/v76/paymentintent"
	"github.com/stripe/stripe-go/v76/webhook"
	"image"
	_ "image/jpeg"
	"image/png"
)

type Config struct {
	DBName     string
	DBUsername string
	DBPassword string
}

type Address struct {
	Id      int    `json:"id"`
	Full    string `json:"full"`
	Street  string `json:"street"`
	City    string `json:"city"`
	Region  string `json:"region"`
	Country string `json:"country"`
	Zip     string `json:"zip"`
}

type Branch struct {
	Id         int     `json:"id"`
	Name       string  `json:"name"`
	Type       string  `json:"type"`
	Letterhead []byte  `json:"letterhead"`
	Num        string  `json:"num"`
	Phone      string  `json:"phone"`
	Address    Address `json:"address"`
}

type Subscription struct {
	Id	int     `json:"id"`
	UserId	int     `json:"userId"`
	StripeId	string  `json:"stripeId"`
	CustomerId	string  `json:"customerId"`
	PriceId	string  `json:"priceId"`
	Start	int     `json:"start"`
	End	int     `json:"end"`
	ClientSecret	string  `json:"clientSecret,omitempty"`
	PaymentStatus	string  `json:"paymentStatus"`
	Status	string  `json:"status"`
}

type User struct {
	Id        int     `json:"id"`
	Email     string  `json:"email"`
	FirstName string  `json:"firstName"`
	LastName  string  `json:"lastName"`
	Phone     string  `json:"phone"`
	Address   Address `json:"address"`
	Branch    Branch  `json:"branch"`
	License   License `json:"license"`
	Sub   Subscription `json:"sub"`
	Status    string  `json:"status"`
	Country   string  `json:"country"`
	Title     string  `json:"title"`
	Verified  bool    `json:"verified"`
	Role      string  `json:"role"`
	Password  string  `json:"password,omitempty"`
	CustomerId  string  `json:"customerId"`
}

type License struct {
	Id     int    `json:"id"`
	UserId int    `json:"userId"`
	Type   string `json:"type"`
	Num    string `json:"num"`
}

type UserClaims struct {
	Id   int    `json:"id"`
	Role string `json:"role"`
	Exp  string `json:"exp"`
}

type Page struct {
	tpl   *template.Template
	Title string
	Name  string
}

type Borrower struct {
	Id     int `json:"id"`
	Credit int `json:"credit"`
	Income int `json:"income"`
	Num    int `json:"num"`
}

type FeeTemplate struct {
	Id       int     `json:"id"`
	User     int     `json:"user"`
	Branch   int     `json:"branch"`
	Amount   int     `json:"amount"`
	Perc     float32 `json:"perc"`
	Type     string  `json:"type"`
	Notes    string  `json:"notes"`
	Name     string  `json:"name"`
	Category string  `json:"category"`
	Auto     bool    `json:"auto"`
}

type Fee struct {
	Id       int     `json:"id"`
	LoanId   int     `json:"loan_id"`
	Amount   int     `json:"amount"`
	Perc     float32 `json:"perc"`
	Type     string  `json:"type"`
	Notes    string  `json:"notes"`
	Name     string  `json:"name"`
	Category string  `json:"category"`
}

type LoanType struct {
	Id     int    `json:"id"`
	User   int    `json:"user"`
	Branch int    `json:"branch"`
	Name   string `json:"name"`
}

type Loan struct {
	Id           int      `json:"id"`
	EstimateId   int      `json:"estimateId"`
	Type         LoanType `json:"type"`
	Amount       int      `json:"amount"`
	Amortization string   `json:"amortization"`
	Term         int      `json:"term"`
	Ltv          float32  `json:"ltv"`
	Dti          float32  `json:"dti"`
	Hoi          int      `json:"hoi"`
	Hazard       int      `json:"hazard"`
	Tax          int      `json:"tax"`
	Interest     float32  `json:"interest"`
	Mi           MI       `json:"mi"`
	Fees         []Fee    `json:"fees"`
	Credits      []Fee    // Fees with negative amounts for internal use
	Name         string   `json:"title"`
	Result       Result   `json:"result"`
}

type MI struct {
	Type                string  `json:"user"`
	Label               string  `json:"label"`
	Lender              string  `json:"lender"`
	Rate                float32 `json:"rate"`
	Premium             int     `json:"premium"`
	Upfront             int     `json:"upfront"`
	Monthly             bool    `json:"monthly"`
	FiveYearTotal       float32 `json:"fiveYearTotal"`
	InitialAllInPremium float32 `json:"initialAllInPremium"`
	InitialAllInRate    float32 `json:"initialAllInRate"`
	InitialAmount       float32 `json:"initialAmount"`
}

type Result struct {
	Id           int `json:"id"`
	LoanId       int `json:"loanId"`
	LoanPayment  int `json:"loanPayment"`
	TotalMonthly int `json:"totalMonthly"`
	TotalFees    int `json:"totalFees"`
	TotalCredits int `json:"totalCredits"`
	CashToClose  int `json:"cashToClose"`
}

type Estimate struct {
	Id          int      `json:"id"`
	User        int      `json:"user"`
	Borrower    Borrower `json:"borrower"`
	Transaction string   `json:"transaction"`
	Price       int      `json:"price"`
	Property    string   `json:"property"`
	Occupancy   string   `json:"occupancy"`
	Zip         string   `json:"zip"`
	Pud         bool     `json:"pud"`
	Loans       []Loan   `json:"loans"`
}

type ETemplate struct {
	Id       int      `json:"id"`
	Estimate Estimate `json:"estimate"`
	UserId   int      `json:"userId"`
	BranchId int      `json:"branchId"`
}

type Report struct {
	Title      string
	Name       string
	Avatar     string
	Letterhead string
	User       User
	Estimate   Estimate
}

type Password struct {
	Old string `json:"old"`
	New string `json:"new"`
}

type Endpoint func(http.ResponseWriter, *sql.DB, *http.Request)

type HookKeys struct {
	InvoicePaid	string
	InvoiceFailed	string
	SubCreated	string
	SubUpdated	string
	SubDeleted	string
}

var (
	regexen     = make(map[string]*regexp.Regexp)
	relock      sync.Mutex
	address     = "localhost:8001"
	mainAddress = "localhost:8002"
)

var paths = map[string]string{
	"home":       "views/home.tpl",
	"terms":      "views/terms.tpl",
	"app":        "views/app.tpl",
	"comparison": "views/report/comparison.tpl",
}

var pages = map[string]Page{
	"home":   cache("home", "Home"),
	"terms":  cache("terms", "Terms and Conditions"),
	"report": cachePdf("comparison"),
	"app":    cache("app", "App"),
}

var roles = map[string]int{
	"User":    1,
	"Manager": 2,
	"Admin":   3,
}

var statuses = map[string]int{
	"Unsubscribed":    1,
	"Trial":    2,
	"Free":    3,
	"Subscriber": 4,
	"Branch":   5,
	"Admin":   6,
}

var propertyTypes = []string{
	"Single Detached",
	"Single Attached",
	"Condo Lo-rise",
	"Condo Hi-rise",
}

var feeTypes = []string{
	"Government",
	"Title",
	"Required",
	"Lender",
	"Other",
}

var hookKeys = HookKeys{
	InvoicePaid: "",
	InvoiceFailed: "",
	SubCreated: "",
	SubUpdated: "",
	SubDeleted: "",
}

var standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs"

// Used to validate claim in JWT token body. Checks if user id is greater than
// zero and time format is valid
func (c UserClaims) Valid() error {
	if c.Id < 1 {
		return errors.New("Invalid id")
	}
	t, err := time.Parse(time.UnixDate, c.Exp)
	if err != nil {
		return err
	}
	if t.Before(time.Now()) {
		return errors.New("Token expired.")
	}
	return err
}

func cache(name string, title string) Page {
	var p = []string{"views/master.tpl", paths[name]}
	tpl := template.Must(template.ParseFiles(p...))
	return Page{tpl: tpl, Title: title, Name: name}
}

func cachePdf(name string) Page {
	// Money is stored in cents, so it must be converted to dollars in reports
	dollars := func(cents int) string {
		return humanize.Commaf(float64(cents) / 100)
	}

	// For calculating down payments
	diff := func(a, b int) string {
		return humanize.Commaf(float64(b-a) / 100)
	}

	sortFees := func(ftype string, fees []Fee) []Fee {
		result := make([]Fee, 0)
		for i := range fees {
			if fees[i].Type != ftype {
				continue
			}
			result = append(result, fees[i])
		}
		return result
	}

	fm := template.FuncMap{
		"dollars":  dollars,
		"diff":     diff,
		"sortFees": sortFees}

	var p = []string{"views/report/master.tpl",
		"views/report/header.tpl",
		"views/report/summary.tpl",
		"views/report/comparison.tpl"}

	tpl := template.Must(template.New("master.tpl").Funcs(fm).ParseFiles(p...))
	return Page{tpl: tpl, Title: "", Name: name}
}

func (page Page) Render(w http.ResponseWriter) {
	err := page.tpl.Execute(w, page)
	if err != nil {
		log.Print(err)
	}
}

func match(path, pattern string, args *[]string) bool {
	relock.Lock()
	defer relock.Unlock()
	regex := regexen[pattern]

	if regex == nil {
		regex = regexp.MustCompile("^" + pattern + "$")
		regexen[pattern] = regex
	}

	matches := regex.FindStringSubmatch(path)
	if len(matches) <= 0 {
		return false
	}

	*args = matches[1:]
	return true
}

func (estimate *Estimate) makeResults() []Result {
	var results []Result
	amortize := func(principle float64, rate float64, periods float64) int {
		exp := math.Pow(1+rate, periods)
		return int(principle * rate * exp / (exp - 1))
	}

	for i := range estimate.Loans {
		var loan = &estimate.Loans[i]
		var result Result = Result{}
		// Monthly payments use amortized loan payment formula plus monthly MI,
		// plus all other recurring fees
		result.LoanPayment = amortize(float64(loan.Amount),
			float64(loan.Interest/100/12),
			float64(loan.Term*12))
		result.TotalMonthly = result.LoanPayment + loan.Hoi + loan.Tax + loan.Hazard
		if loan.Mi.Monthly {
			result.TotalMonthly = result.TotalMonthly +
				int(loan.Mi.Rate/100/12*float32(loan.Amount))
		} else {
			loan.Mi.Upfront = int(loan.Mi.Rate / 100 * float32(loan.Amount))
		}

		for i := range loan.Fees {
			if loan.Fees[i].Amount > 0 {
				result.TotalFees = result.TotalFees + loan.Fees[i].Amount
			} else {
				result.TotalCredits = result.TotalCredits + loan.Fees[i].Amount
			}
		}

		result.CashToClose =
			result.TotalFees + result.TotalCredits + (estimate.Price - loan.Amount)
		result.LoanId = loan.Id

		loan.Result = result
		results = append(results, result)
	}

	return results
}

func summarize(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var estimate Estimate
	err := json.NewDecoder(r.Body).Decode(&estimate)
	if err != nil {
		http.Error(w, "Invalid estimate.", 422)
		return
	}
	estimate.makeResults()

	json.NewEncoder(w).Encode(estimate)
}

func getLoanType(db *sql.DB, id int) (LoanType, error) {
	types, err := getLoanTypes(db, id, 0, 0)
	if err != nil {
		return LoanType{Id: id}, err
	}
	if len(types) == 0 {
		return LoanType{Id: id}, errors.New("No type with that id")
	}

	return types[0], nil
}

func getLoanTypes(db *sql.DB, id int, user int, branch int) (
	[]LoanType, error) {
	var loans []LoanType
	var query = `SELECT
	id,
	coalesce(user_id, 0),
	coalesce(branch_id, 0),
	name
	FROM loan_type WHERE loan_type.id = CASE @e := ? WHEN 0 THEN id ELSE @e END
	OR
	loan_type.user_id = CASE @e := ? WHEN 0 THEN id ELSE @e END
	OR
	loan_type.branch_id = CASE @e := ? WHEN 0 THEN id ELSE @e END`

	// Should be changed to specify user
	rows, err := db.Query(query, id, user, branch)

	if err != nil {
		return nil, fmt.Errorf("loan_type error: %v", err)
	}

	defer rows.Close()

	for rows.Next() {
		var loan LoanType

		if err := rows.Scan(
			&loan.Id,
			&loan.User,
			&loan.Branch,
			&loan.Name); err != nil {
			log.Printf("Error occured fetching loan: %v", err)
			return nil, fmt.Errorf("Error occured fetching loan: %v", err)
		}

		loans = append(loans, loan)
	}

	return loans, nil
}

func getFees(db *sql.DB, loan int) ([]Fee, error) {
	var fees []Fee

	query := `SELECT id, loan_id, amount, perc, type, notes, name, category
	FROM fee
	WHERE loan_id = ?`

	rows, err := db.Query(query, loan)

	if err != nil {
		return nil, fmt.Errorf("Fee query error %v", err)
	}

	defer rows.Close()

	for rows.Next() {
		var fee Fee

		if err := rows.Scan(
			&fee.Id,
			&fee.LoanId,
			&fee.Amount,
			&fee.Perc,
			&fee.Type,
			&fee.Notes,
			&fee.Name,
			&fee.Category,
		); err != nil {
			return nil, fmt.Errorf("Fees scanning error: %v", err)
		}

		fees = append(fees, fee)
	}

	return fees, nil
}

func fetchFeesTemp(db *sql.DB, user int, branch int) ([]FeeTemplate, error) {
	var fees []FeeTemplate
	query := `SELECT
	id, user_id, COALESCE(branch_id, 0), amount, perc, type, notes, name,
	category, auto
	FROM fee_template
	WHERE user_id = ? OR branch_id = ?
	`

	rows, err := db.Query(query, user, branch)

	if err != nil {
		return nil, fmt.Errorf("Fee template query error %v", err)
	}

	defer rows.Close()

	for rows.Next() {
		var fee FeeTemplate

		if err := rows.Scan(
			&fee.Id,
			&fee.User,
			&fee.Branch,
			&fee.Amount,
			&fee.Perc,
			&fee.Type,
			&fee.Notes,
			&fee.Name,
			&fee.Category,
			&fee.Auto); err != nil {
			return nil, fmt.Errorf("FeesTemplate scanning error: %v", err)
		}

		fees = append(fees, fee)
	}

	return fees, nil
}

// Fetch fees from the database
func getFeesTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var fees []FeeTemplate
	claims, err := getClaims(r)
	if err != nil {
		w.WriteHeader(500)
		return
	}
	user, err := queryUser(db, claims.Id)
	if err != nil {
		w.WriteHeader(422)
		return
	}

	fees, err = fetchFeesTemp(db, claims.Id, user.Branch.Id)
	json.NewEncoder(w).Encode(fees)
}

// Fetch fees from the database
func createFeesTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var fee FeeTemplate
	var query string
	var row *sql.Row
	var err error

	claims, err := getClaims(r)
	// var id int // Inserted estimate's id
	err = json.NewDecoder(r.Body).Decode(&fee)
	if err != nil {
		w.WriteHeader(422)
		return
	}

	query = `INSERT INTO fee_template
	(
		user_id,
		branch_id,
		amount,
		perc,
		type,
		notes,
		name,
		auto
	)
	VALUES (?, NULL, ?, ?, ?, ?, ?, ?)
	RETURNING id
	`
	row = db.QueryRow(query,
		claims.Id,
		fee.Amount,
		fee.Perc,
		fee.Type,
		fee.Notes,
		fee.Name,
		fee.Auto,
	)

	err = row.Scan(&fee.Id)
	if err != nil {
		w.WriteHeader(500)
		return
	}

	json.NewEncoder(w).Encode(fee)
}

// Fetch fees from the database
func deleteFeeTemp(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var fee FeeTemplate
	var query string
	var err error

	// claims, err := getClaims(r)
	// var id int // Inserted estimate's id
	err = json.NewDecoder(r.Body).Decode(&fee)
	if err != nil {
		w.WriteHeader(422)
		return
	}

	query = `DELETE FROM fee_template WHERE id = ?`
	_, err = db.Exec(query, fee.Id)
	if err != nil {
		w.WriteHeader(500)
		return
	}
}

func deleteEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var estimate Estimate
	var err error

	err = json.NewDecoder(r.Body).Decode(&estimate)
	if err != nil {
		w.WriteHeader(422)
		return
	}

	claims, err := getClaims(r)
	err = estimate.del(db, claims.Id)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
}

func deleteET(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var et ETemplate
	var err error

	err = json.NewDecoder(r.Body).Decode(&et)
	if err != nil {
		w.WriteHeader(422)
		return
	}

	claims, err := getClaims(r)
	err = et.del(db, claims.Id)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
}

func getMi(db *sql.DB, loan int) (MI, error) {
	var mi MI

	query := `SELECT
	type, label, lender, rate, premium, upfront, five_year_total,
	initial_premium, initial_rate, initial_amount
	FROM mi WHERE loan_id = ?`

	rows, err := db.Query(query, loan)
	if err != nil {
		return mi, err
	}

	defer rows.Close()

	if !rows.Next() {
		return mi, nil
	}

	if err := rows.Scan(
		&mi.Type,
		&mi.Label,
		&mi.Lender,
		&mi.Rate,
		&mi.Premium,
		&mi.Upfront,
		&mi.FiveYearTotal,
		&mi.InitialAllInPremium,
		&mi.InitialAllInRate,
		&mi.InitialAmount,
	); err != nil {
		return mi, err
	}

	return mi, nil
}

func (estimate *Estimate) getBorrower(db *sql.DB) error {
	query := `SELECT id, credit_score, monthly_income, num FROM borrower
		WHERE estimate_id = ? LIMIT 1`

	row := db.QueryRow(query, estimate.Id)

	if err := row.Scan(
		&estimate.Borrower.Id,
		&estimate.Borrower.Credit,
		&estimate.Borrower.Income,
		&estimate.Borrower.Num,
	); err != nil {
		return fmt.Errorf("Borrower scanning error: %v", err)
	}

	return nil
}

// Query Lender APIs and parse responses into MI structs
func fetchMi(db *sql.DB, estimate *Estimate, pos int) []MI {
	var loan Loan = estimate.Loans[pos]

	var ltv = func(l float32) string {
		switch {
		case l > 95:
			return "LTV97"
		case l > 90:
			return "LTV95"
		case l > 85:
			return "LTV90"
		default:
			return "LTV85"
		}
	}

	var term = func(t int) string {
		switch {
		case t <= 10:
			return "A10"
		case t <= 15:
			return "A15"
		case t <= 20:
			return "A20"
		case t <= 25:
			return "A25"
		case t <= 30:
			return "A30"
		default:
			return "A40"
		}
	}

	var propertyCodes = map[string]string{
		"Single Attached": "SFO",
		"Single Detached": "SFO",
		"Condo Lo-rise":   "CON",
		"Condo Hi-rise":   "CON",
	}

	var purposeCodes = map[string]string{
		"Purchase":  "PUR",
		"Refinance": "RRT",
	}

	body, err := json.Marshal(map[string]any{
		"zipCode":                estimate.Zip,
		"stateCode":              "CA",
		"address":                "",
		"propertyTypeCode":       propertyCodes[estimate.Property],
		"occupancyTypeCode":      "PRS",
		"loanPurposeCode":        purposeCodes[estimate.Transaction],
		"loanAmount":             loan.Amount,
		"loanToValue":            ltv(loan.Ltv),
		"amortizationTerm":       term(loan.Term),
		"loanTypeCode":           "FXD",
		"duLpDecisionCode":       "DAE",
		"loanProgramCodes":       []any{},
		"debtToIncome":           loan.Dti,
		"wholesaleLoan":          0,
		"coveragePercentageCode": "L30",
		"productCode":            "BPM",
		"renewalTypeCode":        "CON",
		"numberOfBorrowers":      1,
		"coBorrowerCreditScores": []any{},
		"borrowerCreditScore":    strconv.Itoa(estimate.Borrower.Credit),
		"masterPolicy":           nil,
		"selfEmployedIndicator":  false,
		"armType":                "",
		"userId":                 44504,
	})
	/*
		if err != nil {
			log.Printf("Could not marshal NationalMI body: \n%v\n%v\n",
			bytes.NewBuffer(body), err)
			func queryAddress(db *sql.DB, id int) ( Address, error ) {
				var address Address = Address{Id: id}
				var err error

				row := db.QueryRow(
					`SELECT id, full_address, street, city, region, country, zip
					FROM address WHERE id = ?`, id)

				err = row.Scan(
					&address.Id,
					&address.Full,
					&address.Street,
					&address.City,
					&address.Region,
					&address.Country,
					&address.Zip,
					)

				return address, err
		}
		}
	*/

	req, err := http.NewRequest("POST",
		"https://rate-gps.nationalmi.com/rates/productRateQuote",
		bytes.NewBuffer(body))
	req.Header.Add("Content-Type", "application/json")

	req.AddCookie(&http.Cookie{
		Name:  "nmirategps_email",
		Value: os.Getenv("NationalMIEmail")})

	resp, err := http.DefaultClient.Do(req)
	var res map[string]interface{}
	var result []MI

	if resp.StatusCode != 200 {
		log.Printf("the status: %v\nthe resp: %v\n the req: %v\n the body: %v\n",
			resp.Status, resp, req.Body, bytes.NewBuffer(body))
	} else {
		json.NewDecoder(resp.Body).Decode(&res)
		// estimate.Loans[pos].Mi = res
		// Parse res into result here
	}

	log.Println(err)

	return result
}

// Make comparison PDF
func generatePDF(w http.ResponseWriter, db *sql.DB, r *http.Request) {
}

func login(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var id int
	var role string
	var err error
	var user User
	json.NewDecoder(r.Body).Decode(&user)

	row := db.QueryRow(
		`SELECT id, role FROM user WHERE email = ? AND password = sha2(?, 256)`,
		user.Email, user.Password,
	)

	err = row.Scan(&id, &role)
	if err != nil {
		http.Error(w, "Invalid Credentials.", http.StatusUnauthorized)
		return
	}

	err = setTokenCookie(id, role, w)
	if err != nil {
		http.Error(w, err.Error(), 500)
	}
}

func setTokenCookie(id int, role string, w http.ResponseWriter) error {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256,
		UserClaims{Id: id, Role: role,
			Exp: time.Now().Add(time.Minute * 30).Format(time.UnixDate)})

	tokenStr, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
	if err != nil {
		log.Println("Token could not be signed: ", err, tokenStr)
		return err
	}
	
	cookie := http.Cookie{Name: "skouter",
	Value:   tokenStr,
	Path:    "/",
	Expires: time.Now().Add(time.Hour * 1)}
	http.SetCookie(w, &cookie)
	
	return nil
}

func getToken(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claims, _ := getClaims(r)
	
	err := setTokenCookie(claims.Id, claims.Role, w)

	if err != nil {
		http.Error(w,
			"Token generation error",
			http.StatusInternalServerError)
	}
}

func getClaims(r *http.Request) (UserClaims, error) {
	claims := new(UserClaims)
	_, tokenStr, found := strings.Cut(r.Header.Get("Authorization"), " ")

	if !found {
		return *claims, errors.New("Token not found")
	}

	// Pull token payload into UserClaims
	_, err := jwt.ParseWithClaims(tokenStr, claims,
		func(token *jwt.Token) (any, error) {
			return []byte(os.Getenv("JWT_SECRET")), nil
		})

	if err != nil {
		return *claims, err
	}

	if err = claims.Valid(); err != nil {
		return *claims, err
	}

	return *claims, nil
}

func guard(r *http.Request, required int) bool {
	claims, err := getClaims(r)
	if err != nil {
		return false
	}
	if roles[claims.Role] < required {
		return false
	}
	

	return true
}

// Inserts an address and returns it's ID along with any errors.
func insertAddress(db *sql.DB, address Address) (int, error) {
	var query string
	var row *sql.Row
	var err error
	var id int // Inserted user's id

	query = `INSERT INTO address
	(
		full_address,
		street,
		city,
		region,
		country,
		zip
	)
	VALUES (?, ?, ?, ?, ?, ?)
	RETURNING id 
	`

	row = db.QueryRow(query,
		address.Full,
		address.Street,
		address.City,
		address.Region,
		address.Country,
		address.Zip,
	)

	err = row.Scan(&id)

	return id, err
}

// Inserts an address and returns it's ID along with any errors.
func insertBranch(db *sql.DB, branch Branch) (int, error) {
	var query string
	var row *sql.Row
	var err error
	var id int // Inserted user's id

	query = `INSERT INTO branch
	(
		name,
		type,
		letterhead,
		num,
		phone,
		address
	)
	VALUES (?, ?, ?, ?, ?, ?)
	RETURNING id 
	`

	row = db.QueryRow(query,
		branch.Name,
		branch.Type,
		branch.Letterhead,
		branch.Num,
		branch.Phone,
		branch.Address.Id,
	)

	err = row.Scan(&id)

	return id, err
}

// Inserts an address and returns it's ID along with any errors.
func insertLicense(db *sql.DB, license License) (int, error) {
	var query string
	var row *sql.Row
	var err error
	var id int // Inserted license's id

	query = `INSERT INTO license
	(
		user_id,
		type,
		num
	)
	VALUES (?, ?, ?)
	RETURNING id 
	`

	row = db.QueryRow(query,
		license.UserId,
		license.Type,
		license.Num,
	)

	err = row.Scan(&id)

	return id, err
}

func queryLicense(db *sql.DB, user int) (License, error) {
	var license License = License{UserId: user}
	var err error

	row := db.QueryRow(
		`SELECT id, type, num FROM license WHERE user_id = ?`,
		user)

	err = row.Scan(
		&license.Id,
		&license.Type,
		&license.Num,
	)

	return license, err
}

func queryAddress(db *sql.DB, id int) (Address, error) {
	var address Address = Address{Id: id}
	var err error

	row := db.QueryRow(
		`SELECT id, full_address, street, city, region, country, zip
		FROM address WHERE id = ?`, id)

	err = row.Scan(
		&address.Id,
		&address.Full,
		&address.Street,
		&address.City,
		&address.Region,
		&address.Country,
		&address.Zip,
	)

	return address, err
}

func queryBranch(db *sql.DB, id int) (Branch, error) {
	var branch Branch = Branch{Id: id}
	var err error

	row := db.QueryRow(
		`SELECT id, name, type, letterhead, num, phone, address
		FROM branch WHERE id = ?`, id)

	err = row.Scan(
		&branch.Id,
		&branch.Name,
		&branch.Type,
		&branch.Letterhead,
		&branch.Num,
		&branch.Phone,
		&branch.Address.Id,
	)

	return branch, err
}

func queryUser(db *sql.DB, id int) (User, error) {
	var user User
	var query string
	var err error

	query = `SELECT
	u.id,
	u.email,
	u.first_name,
	u.last_name,
	coalesce(u.branch_id, 0),
	u.country,
	u.title,
	coalesce(u.status, ''),
	coalesce(u.customer_id, ''),
	u.verified,
	u.role,
	u.address,
	u.phone
	FROM user u WHERE u.id = ?
	`
	row := db.QueryRow(query, id)

	if err != nil {
		return user, err
	}

	err = row.Scan(
		&user.Id,
		&user.Email,
		&user.FirstName,
		&user.LastName,
		&user.Branch.Id,
		&user.Country,
		&user.Title,
		&user.Status,
		&user.CustomerId,
		&user.Verified,
		&user.Role,
		&user.Address.Id,
		&user.Phone,
	)

	if err != nil {
		return user, err
	}

	user.Address, err = queryAddress(db, user.Address.Id)
	if err != nil {
		return user, err
	}

	if user.Branch.Id > 0 {
		user.Branch, err = queryBranch(db, user.Branch.Id)
		if err != nil {
			return user, err
		}
	}

	return user, nil
}

func queryCustomer(db *sql.DB, id string) (User, error) {
	var user User
	var query string
	var err error

	query = `SELECT
	u.id,
	u.email,
	u.first_name,
	u.last_name,
	coalesce(u.branch_id, 0),
	u.country,
	u.title,
	coalesce(u.status, ''),
	coalesce(u.customer_id, ''),
	u.verified,
	u.role,
	u.address,
	u.phone
	FROM user u WHERE u.customer_id = ?
	`
	row := db.QueryRow(query, id)

	if err != nil {
		return user, err
	}

	err = row.Scan(
		&user.Id,
		&user.Email,
		&user.FirstName,
		&user.LastName,
		&user.Branch.Id,
		&user.Country,
		&user.Title,
		&user.Status,
		&user.CustomerId,
		&user.Verified,
		&user.Role,
		&user.Address.Id,
		&user.Phone,
	)

	if err != nil {
		return user, err
	}

	user.Address, err = queryAddress(db, user.Address.Id)
	if err != nil {
		return user, err
	}

	user.Branch, err = queryBranch(db, user.Branch.Id)
	if err != nil {
		return user, err
	}

	return user, nil
}

// Can probably be deleted.
func queryUsers(db *sql.DB, id int) ([]User, error) {
	var users []User
	var query string
	var rows *sql.Rows
	var err error

	query = `SELECT
	u.id,
	u.email,
	u.first_name,
	u.last_name,
	coalesce(u.branch_id, 0),
	u.country,
	u.title,
	coalesce(u.status, ''),
	u.verified,
	u.role,
	u.address,
	u.phone
	FROM user u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END
	`
	rows, err = db.Query(query, id)

	if err != nil {
		return users, err
	}

	defer rows.Close()

	for rows.Next() {
		var user User

		if err := rows.Scan(
			&user.Id,
			&user.Email,
			&user.FirstName,
			&user.LastName,
			&user.Branch.Id,
			&user.Country,
			&user.Title,
			&user.Status,
			&user.Verified,
			&user.Role,
			&user.Address.Id,
			&user.Phone,
		); err != nil {
			return users, err
		}

		user.Address, err = queryAddress(db, user.Address.Id)
		if err != nil {
			return users, err
		}

		user.Branch, err = queryBranch(db, user.Branch.Id)
		if err != nil {
			return users, err
		}

		users = append(users, user)
	}

	// Prevents runtime panics
	if len(users) == 0 {
		return users, errors.New("User not found.")
	}

	return users, nil
}

func querySub(db *sql.DB, id int) (Subscription, error) {
	var query string
	var err error
	var s Subscription

	query = `SELECT
	id,
	stripe_id,
	user_id,
	customer_id,
	current_period_end,
	current_period_start,
	client_secret,
	payment_status
	FROM subscription WHERE id = ?
	`
	row := db.QueryRow(query, id)

	err = row.Scan(
		&s.Id,
		&s.StripeId,
		&s.CustomerId,
		&s.End,
		&s.Start,
		&s.ClientSecret,
		&s.PaymentStatus,
	)

	return s, err
}

func (user *User) querySub(db *sql.DB) error {
	var query string
	var err error

	query = `SELECT
	id,
	stripe_id,
	user_id,
	customer_id,
	price_id,
	current_period_end,
	current_period_start,
	client_secret,
	payment_status
	FROM subscription WHERE user_id = ?
	`
	row := db.QueryRow(query, user.Id)

	err = row.Scan(
		&user.Sub.Id,
		&user.Sub.StripeId,
		&user.Sub.UserId,
		&user.Sub.CustomerId,
		&user.Sub.PriceId,
		&user.Sub.End,
		&user.Sub.Start,
		&user.Sub.ClientSecret,
		&user.Sub.PaymentStatus,
	)

	return err
}

func (estimate *Estimate) insertResults(db *sql.DB) error {
	var query string
	var row *sql.Row
	var err error
	var id int

	query = `INSERT INTO estimate_result
	(
		loan_id,
		loan_payment,
		total_monthly,
		total_fees,
		total_credits,
		cash_to_close
	)
	VALUES (?, ?, ?, ?, ?, ?)
	RETURNING id
	`
	for i := range estimate.Loans {
		r := estimate.Loans[i].Result
		r.LoanId = estimate.Loans[i].Id

		row = db.QueryRow(query,
			r.LoanId,
			r.LoanPayment,
			r.TotalMonthly,
			r.TotalFees,
			r.TotalCredits,
			r.CashToClose,
		)
		err = row.Scan(&id)
		if err != nil {
			return err
		}
		r.Id = id
	}

	return nil
}

// Insert user returning it's ID or any error
func insertUser(db *sql.DB, user User) (int, error) {
	var query string
	var row *sql.Row
	var err error
	var id int // Inserted user's id

	user.Address.Id, err = insertAddress(db, user.Address)
	if err != nil {
		return 0, err
	}

	query = `INSERT INTO user
	(
		email,
		first_name,
		last_name,
		password,
		role,
		title,
		status,
		verified,
		address,
		country,
		branch_id,
		phone,
		created,
		last_login
	)
	VALUES (?, ?, ?, sha2(?, 256), ?, ?, ?, ?, ?, ?,
	CASE @b := ? WHEN 0 THEN NULL ELSE @b END,
	?, NOW(), NOW())
	RETURNING id 
	`

	row = db.QueryRow(query,
		user.Email,
		user.FirstName,
		user.LastName,
		user.Password,
		user.Role,
		user.Title,
		user.Status,
		user.Verified,
		user.Address.Id,
		user.Country,
		user.Branch.Id,
		user.Phone,
	)

	err = row.Scan(&id)
	if err != nil {
		return 0, err
	}

	user.Id = id

	return id, nil
}

// Insert user returning it's ID or any error
func (sub *Subscription) insertSub(db *sql.DB) (error) {
	var query string
	var row *sql.Row
	var err error

	query = `INSERT INTO subscription
	(
		stripe_id,
		user_id,
		customer_id,
		price_id,
		current_period_end,
		current_period_start,
		client_secret,
		payment_status,
		status
	)
	VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
	RETURNING id
	`
	
	row = db.QueryRow(query,
		sub.StripeId,
		sub.UserId,
		sub.CustomerId,
		sub.PriceId,
		sub.End,
		sub.Start,
		sub.ClientSecret,
		sub.PaymentStatus,
		sub.Status,
	)

	err = row.Scan(&sub.Id)
	return err
}

func (sub *Subscription) updateSub(db *sql.DB) error {
	var query string
	var err error

	query = `UPDATE subscription
	SET client_secret = CASE @a := ? WHEN '' THEN client_secret ELSE @a END,
		current_period_end = CASE
		@b := ? WHEN 0 THEN current_period_end ELSE @b END,
		current_period_start = CASE
		@c := ? WHEN 0 THEN current_period_start ELSE @c END,
		payment_status = CASE @d := ? WHEN '' THEN client_secret ELSE @d END,
		status = CASE @e := ? WHEN '' THEN client_secret ELSE @e END
	WHERE id = ?
	`
	
	_, err = db.Exec(query,
		sub.ClientSecret,
		sub.End,
		sub.Start,
		sub.PaymentStatus,
		sub.Status,
		sub.Id,
	)
	if err != nil { return err }

	return err
}


// Updates a user's stripe customer ID.
func (user *User) updateCustomerId(db *sql.DB, cid string) (error) {
	var query string
	var err error

	query = `UPDATE user SET
	customer_id = ?
	WHERE id = ?
	`
	_, err = db.Exec(query,
		cid,
		user.Id,
	)
	if err != nil { return err }
	
	user.CustomerId = cid

	return nil
}

func updateAddress(address Address, db *sql.DB) error {
	query := `
	UPDATE address
	SET
	full_address = CASE @e := ? WHEN '' THEN full_address ELSE @e END,
	street = CASE @fn := ? WHEN '' THEN street ELSE @fn END,
	city = CASE @ln := ? WHEN '' THEN city ELSE @ln END,
	region = CASE @r := ? WHEN '' THEN region ELSE @r END,
	country = CASE @r := ? WHEN '' THEN country ELSE @r END,
	zip = CASE @r := ? WHEN '' THEN zip ELSE @r END
	WHERE id = ?
	`

	_, err := db.Exec(query,
		address.Full,
		address.Street,
		address.City,
		address.Region,
		address.Country,
		address.Zip,
		address.Id,
	)

	return err
}

func (user *User) update(db *sql.DB) error {
	query := `
	UPDATE user
	SET
	email = CASE @e := ? WHEN '' THEN email ELSE @e END,
	first_name = CASE @fn := ? WHEN '' THEN first_name ELSE @fn END,
	last_name = CASE @ln := ? WHEN '' THEN last_name ELSE @ln END,
	role = CASE @r := ? WHEN '' THEN role ELSE @r END,
	password = CASE @p := ? WHEN '' THEN password ELSE sha2(@p, 256) END
	WHERE id = ?
	`

	_, err := db.Exec(query,
		user.Email,
		user.FirstName,
		user.LastName,
		user.Role,
		user.Password,
		user.Id,
	)

	return err
}

func getUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claims, err := getClaims(r)
	if err != nil {
		w.WriteHeader(500)
		return
	}
	
	user, err := queryUser(db, claims.Id)
	if err != nil {
		w.WriteHeader(422)
		log.Println(err)
		return
	}
	
	json.NewEncoder(w).Encode(user)
}

func getUsers(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	users, err := queryUsers(db, 0)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	json.NewEncoder(w).Encode(users)
}

// Updates a user using only specified values in the JSON body
func setUser(user User, db *sql.DB) error {
	_, err := mail.ParseAddress(user.Email)
	if err != nil {
		return err
	}

	if roles[user.Role] == 0 {
		return errors.New("Invalid role")
	}

	err = user.update(db)
	if err != nil {
		return err
	}
	err = updateAddress(user.Address, db)
	if err != nil {
		return err
	}

	return nil
}

func patchUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var user User
	err := json.NewDecoder(r.Body).Decode(&user)
	if err != nil {
		http.Error(w, "Invalid fields", 422)
		return
	}

	err = setUser(user, db)
	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}
}

// Update specified fields of the user specified in the claim
func patchSelf(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claim, err := getClaims(r)
	var user User
	json.NewDecoder(r.Body).Decode(&user)

	// First check that the target user to be updated is the same as the claim id, and
	// their role is unchanged.
	if err != nil || claim.Id != user.Id {
		http.Error(w, "Target user's id does not match claim.", 401)
		return
	}

	if claim.Role != user.Role && user.Role != "" {
		http.Error(w, "Administrator required to escalate role.", 401)
		return
	}

	err = setUser(user, db)
	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}
}

func deleteUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var user User
	err := json.NewDecoder(r.Body).Decode(&user)
	if err != nil {
		http.Error(w, "Invalid fields.", 422)
		return
	}

	query := `DELETE FROM user WHERE id = ?`
	_, err = db.Exec(query, user.Id)

	if err != nil {
		http.Error(w, "Could not delete.", 500)
	}
}

// Checks if a user's entries are reasonable before database insertion.
// This function is very important because it is the only thing preventing
// anyone from creating an admin user. These error messages are displayed to
// the user.
func (user *User) validate() error {
	_, err := mail.ParseAddress(user.Email)
	if err != nil {
		return errors.New("Invalid email.")
	}

	if roles[user.Role] == 0 {
		return errors.New("Invalid role.")
	}

	if roles[user.Role] == roles["Admin"] {
		return errors.New("New user cannot be an Admin.")
	}

	if user.FirstName == "" {
		return errors.New("Given name cannot be empty.")
	}

	if user.LastName == "" {
		return errors.New("Surname cannot be empty.")
	}

	if user.Password == "" {
		return errors.New("Empty password")
	}

	return nil
}

func createUser(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var user User
	err := json.NewDecoder(r.Body).Decode(&user)
	if err != nil {
		http.Error(w, "Invalid fields.", 422)
		return
	}

	user.Role = "User"
	user.Status = "Trial"
	err = user.validate()
	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}

	user.Id, err = insertUser(db, user)
	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}
	
	err = setTokenCookie(user.Id, user.Role, w)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	json.NewEncoder(w).Encode(user)
}

func checkPassword(db *sql.DB, id int, pass string) bool {
	var p string
	query := `SELECT
	password
	FROM user WHERE user.id = ? AND password = sha2(?, 256)
	`
	row := db.QueryRow(query, id, pass)
	err := row.Scan(&p)
	if err != nil {
		return false
	}

	return true
}

func setPassword(db *sql.DB, id int, pass string) error {
	query := `UPDATE user
	SET password = sha2(?, 256)
	WHERE user.id = ?
	`
	_, err := db.Exec(query, pass, id)
	if err != nil {
		return errors.New("Could not insert password.")
	}

	return nil
}

func changePassword(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var pass Password
	claim, err := getClaims(r)
	err = json.NewDecoder(r.Body).Decode(&pass)
	if err != nil {
		http.Error(w, "Bad fields.", 422)
		return
	}

	if checkPassword(db, claim.Id, pass.Old) {
		err = setPassword(db, claim.Id, pass.New)
	} else {
		http.Error(w, "Incorrect old password.", 401)
		return
	}

	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
}

func fetchAvatar(db *sql.DB, user int) ([]byte, error) {
	var img []byte
	var query string
	var err error

	query = `SELECT
	avatar
	FROM user WHERE user.id = ?
	`
	row := db.QueryRow(query, user)
	err = row.Scan(&img)

	if err != nil {
		return img, err
	}

	return img, nil
}

func insertAvatar(db *sql.DB, user int, img []byte) error {
	query := `UPDATE user
	SET avatar = ?
	WHERE id = ?
	`
	_, err := db.Exec(query, img, user)
	if err != nil {
		return err
	}

	return nil
}

func fetchLetterhead(db *sql.DB, user int) ([]byte, error) {
	var img []byte
	var query string
	var err error

	query = `SELECT
	letterhead
	FROM user WHERE user.id = ?
	`
	row := db.QueryRow(query, user)
	err = row.Scan(&img)

	if err != nil {
		return img, err
	}

	return img, nil
}

func insertLetterhead(db *sql.DB, user int, img []byte) error {
	query := `UPDATE user
	SET letterhead = ?
	WHERE id = ?
	`
	_, err := db.Exec(query, img, user)
	if err != nil {
		return err
	}

	return nil
}

func setAvatar(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var validTypes []string = []string{"image/png", "image/jpeg"}
	var isValidType bool

	claims, err := getClaims(r)
	if err != nil {
		http.Error(w, "Invalid token.", 422)
		return
	}
	img, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Invalid file.", 422)
		return
	}
	for _, v := range validTypes {
		if v == http.DetectContentType(img) {
			isValidType = true
		}
	}
	if !isValidType {
		http.Error(w, "Invalid file type.", 422)
		return
	}

	err = insertAvatar(db, claims.Id, img)
	if err != nil {
		http.Error(w, "Could not insert.", 500)
		return
	}
}

func getAvatar(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claims, err := getClaims(r)
	if err != nil {
		http.Error(w, "Invalid token.", 422)
		return
	}
	img, err := fetchAvatar(db, claims.Id)
	if err != nil {
		http.Error(w, "Could not retrieve.", 500)
		return
	}

	w.Header().Set("Content-Type", http.DetectContentType(img))
	w.Write(img)
}

func setLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var validTypes []string = []string{"image/png", "image/jpeg"}
	var isValidType bool

	claims, err := getClaims(r)
	if err != nil {
		http.Error(w, "Invalid token.", 422)
		return
	}
	img, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Invalid file.", 422)
		return
	}
	for _, v := range validTypes {
		if v == http.DetectContentType(img) {
			isValidType = true
		}
	}
	if !isValidType {
		http.Error(w, "Invalid file type.", 422)
		return
	}

	err = insertLetterhead(db, claims.Id, img)
	if err != nil {
		http.Error(w, "Could not insert.", 500)
		return
	}
}

func getLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claims, err := getClaims(r)
	if err != nil {
		http.Error(w, "Invalid token.", 422)
		return
	}
	img, err := fetchLetterhead(db, claims.Id)
	if err != nil {
		http.Error(w, "Could not retrieve.", 500)
		return
	}

	w.Header().Set("Content-Type", http.DetectContentType(img))
	w.Write(img)
}

func queryBorrower(db *sql.DB, id int) (Borrower, error) {
	var borrower Borrower
	var query string
	var err error

	query = `SELECT
	l.id,
	l.credit_score,
	l.num,
	l.monthly_income
	FROM borrower l WHERE l.id = ?
	`
	row := db.QueryRow(query, id)
	err = row.Scan(
		borrower.Id,
		borrower.Credit,
		borrower.Num,
		borrower.Income,
	)

	if err != nil {
		return borrower, err
	}

	return borrower, nil
}

// Must have an estimate ID 'e', but not necessarily a loan id 'id'
func getResults(db *sql.DB, e int, id int) ([]Result, error) {
	var results []Result
	var query string
	var rows *sql.Rows
	var err error

	query = `SELECT
	r.id,
	loan_id,
	loan_payment,
	total_monthly,
	total_fees,
	total_credits,
	cash_to_close
	FROM estimate_result r
	INNER JOIN loan
	ON r.loan_id = loan.id
	WHERE r.id = CASE @e := ? WHEN 0 THEN r.id ELSE @e END
	AND loan.estimate_id = ?
	`
	rows, err = db.Query(query, id, e)

	if err != nil {
		return results, err
	}

	defer rows.Close()

	for rows.Next() {
		var result Result

		if err := rows.Scan(
			&result.Id,
			&result.LoanId,
			&result.LoanPayment,
			&result.TotalMonthly,
			&result.TotalFees,
			&result.TotalCredits,
			&result.CashToClose,
		); err != nil {
			return results, err
		}

		results = append(results, result)
	}

	// Prevents runtime panics
	// if len(results) == 0 { return results, errors.New("Result not found.") }

	return results, nil
}

// Retrieve an estimate result with a specified loan id
func getResult(db *sql.DB, loan int) (Result, error) {
	var result Result
	var query string
	var err error

	query = `SELECT
	r.id,
	loan_id,
	loan_payment,
	total_monthly,
	total_fees,
	total_credits,
	cash_to_close
	FROM estimate_result r
	INNER JOIN loan
	ON r.loan_id = loan.id
	WHERE loan.Id = ?
	`
	row := db.QueryRow(query, loan)

	err = row.Scan(
		&result.Id,
		&result.LoanId,
		&result.LoanPayment,
		&result.TotalMonthly,
		&result.TotalFees,
		&result.TotalCredits,
		&result.CashToClose,
	)

	if err != nil {
		return result, err
	}

	return result, nil
}

// Must have an estimate ID 'e', but not necessarily a loan id 'id'
func getLoans(db *sql.DB, e int, id int) ([]Loan, error) {
	var loans []Loan
	var query string
	var rows *sql.Rows
	var err error

	query = `SELECT
	l.id,
	l.type_id,
	l.estimate_id,
	l.amount,
	l.term,
	l.interest,
	l.ltv,
	l.dti,
	l.hoi,
	l.tax,
	l.name
	FROM loan l WHERE l.id = CASE @e := ? WHEN 0 THEN l.id ELSE @e END AND
	l.estimate_id = ?
	`
	rows, err = db.Query(query, id, e)

	if err != nil {
		return loans, err
	}

	defer rows.Close()

	for rows.Next() {
		var loan Loan

		if err := rows.Scan(
			&loan.Id,
			&loan.Type.Id,
			&loan.EstimateId,
			&loan.Amount,
			&loan.Term,
			&loan.Interest,
			&loan.Ltv,
			&loan.Dti,
			&loan.Hoi,
			&loan.Tax,
			&loan.Name,
		); err != nil {
			return loans, err
		}

		mi, err := getMi(db, loan.Id)
		if err != nil {
			return loans, err
		}
		loan.Mi = mi
		fees, err := getFees(db, loan.Id)
		if err != nil {
			return loans, err
		}
		loan.Fees = fees

		loan.Result, err = getResult(db, loan.Id)
		if err != nil {
			return loans, err
		}

		loan.Type, err = getLoanType(db, loan.Type.Id)
		if err != nil {
			return loans, err
		}

		loans = append(loans, loan)
	}

	// Prevents runtime panics
	if len(loans) == 0 {
		return loans, errors.New("Loan not found.")
	}

	return loans, nil
}

func getEstimate(db *sql.DB, id int) (Estimate, error) {
	estimates, err := getEstimates(db, id, 0)

	if err != nil {
		return Estimate{}, err
	}

	return estimates[0], nil
}

func getEstimates(db *sql.DB, id int, user int) ([]Estimate, error) {
	var estimates []Estimate
	var query string
	var rows *sql.Rows
	var err error

	query = `SELECT
	id,
	user_id,
	transaction,
	price,
	property,
	occupancy,
	zip,
	pud
	FROM estimate WHERE id = CASE @e := ? WHEN 0 THEN id ELSE @e END AND
	user_id = CASE @e := ? WHEN 0 THEN user_id ELSE @e END
	`
	rows, err = db.Query(query, id, user)

	if err != nil {
		return estimates, err
	}

	defer rows.Close()

	for rows.Next() {
		var estimate Estimate

		if err := rows.Scan(
			&estimate.Id,
			&estimate.User,
			&estimate.Transaction,
			&estimate.Price,
			&estimate.Property,
			&estimate.Occupancy,
			&estimate.Zip,
			&estimate.Pud,
		); err != nil {
			return estimates, err
		}

		err := estimate.getBorrower(db)
		if err != nil {
			return estimates, err
		}
		estimates = append(estimates, estimate)
	}

	// Prevents runtime panics
	if len(estimates) == 0 {
		return estimates, errors.New("Estimate not found.")
	}

	for i := range estimates {
		estimates[i].Loans, err = getLoans(db, estimates[i].Id, 0)
		if err != nil {
			return estimates, err
		}
	}

	return estimates, nil
}

func queryETemplates(db *sql.DB, id int, user int) ([]ETemplate, error) {
	var eTemplates []ETemplate
	var query string
	var rows *sql.Rows
	var err error

	query = `SELECT
	id,
	estimate_id,
	user_id,
	branch_id
	FROM estimate_template WHERE id = CASE @e := ? WHEN 0 THEN id ELSE @e END AND
	user_id = CASE @e := ? WHEN 0 THEN user_id ELSE @e END
	`
	rows, err = db.Query(query, id, user)

	if err != nil {
		return eTemplates, err
	}

	defer rows.Close()

	for rows.Next() {
		var e ETemplate

		if err := rows.Scan(
			&e.Id,
			&e.Estimate.Id,
			&e.UserId,
			&e.BranchId,
		); err != nil {
			return eTemplates, err
		}

		e.Estimate, err = getEstimate(db, e.Estimate.Id)
		if err != nil {
			return eTemplates, err
		}

		eTemplates = append(eTemplates, e)
	}

	return eTemplates, nil
}

// Accepts a borrower struct and returns the id of the inserted borrower and
// any related error.
func (estimate *Estimate) insertETemplate(db *sql.DB, user int, branch int) error {
	var query string
	var err error

	query = `INSERT INTO estimate_template
	(
		estimate_id,
		user_id,
		branch_id
	)
	VALUES (?, ?, ?)
	`
	_, err = db.Exec(query,
		estimate.Id,
		user,
		branch,
	)
	if err != nil {
		return err
	}

	return nil
}

// Accepts a borrower struct and returns the id of the inserted borrower and
// any related error.
func (estimate *Estimate) insertBorrower(db *sql.DB) error {
	var query string
	var row *sql.Row
	var err error

	query = `INSERT INTO borrower
	(
		estimate_id,
		credit_score,
		monthly_income,
		num
	)
	VALUES (?, ?, ?, ?)
	RETURNING id
	`
	row = db.QueryRow(query,
		estimate.Id,
		estimate.Borrower.Credit,
		estimate.Borrower.Income,
		estimate.Borrower.Num,
	)

	err = row.Scan(&estimate.Borrower.Id)
	if err != nil {
		return err
	}

	return nil
}

func insertMi(db *sql.DB, mi MI) (int, error) {
	var id int

	query := `INSERT INTO mi 
		(
			type,
			label,
			lender,
			rate,
			premium,
			upfront,
			five_year_total,
			initial_premium,
			initial_rate,
			initial_amount
		)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		RETURNING id`

	row := db.QueryRow(query,
		&mi.Type,
		&mi.Label,
		&mi.Lender,
		&mi.Rate,
		&mi.Premium,
		&mi.Upfront,
		&mi.FiveYearTotal,
		&mi.InitialAllInPremium,
		&mi.InitialAllInRate,
		&mi.InitialAmount,
	)

	err := row.Scan(&id)
	if err != nil {
		return 0, err
	}

	return id, nil
}

func insertFee(db *sql.DB, fee Fee) (int, error) {
	var id int

	query := `INSERT INTO fee 
		(loan_id, amount, perc, type, notes, name, category)
		VALUES (?, ?, ?, ?, ?, ?, ?)
		RETURNING id`

	row := db.QueryRow(query,
		fee.LoanId,
		fee.Amount,
		fee.Perc,
		fee.Type,
		fee.Notes,
		fee.Name,
		fee.Category,
	)

	err := row.Scan(&id)
	if err != nil {
		return 0, err
	}

	return id, nil
}

func insertLoanType(db *sql.DB, lt LoanType) (int, error) {
	var id int

	query := `INSERT INTO loan_type (branch_id, user_id, name)
		VALUES (NULLIF(?, 0), NULLIF(?, 0), ?)
		RETURNING id`

	row := db.QueryRow(query,
		lt.Branch,
		lt.User,
		lt.Name,
	)

	err := row.Scan(&id)
	if err != nil {
		return 0, err
	}

	return id, nil
}

func (loan *Loan) insertLoan(db *sql.DB) error {
	var query string
	var row *sql.Row
	var err error

	query = `INSERT INTO loan
	(
		estimate_id,
		type_id,
		amount,
		term,
		interest,
		ltv,
		dti,
		hoi,
		tax,
		name
	)
	VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	RETURNING id
	`
	row = db.QueryRow(query,
		loan.EstimateId,
		loan.Type.Id,
		loan.Amount,
		loan.Term,
		loan.Interest,
		loan.Ltv,
		loan.Dti,
		loan.Hoi,
		loan.Tax,
		loan.Name,
	)

	err = row.Scan(&loan.Id)
	if err != nil {
		return err
	}
	_, err = insertMi(db, loan.Mi)
	if err != nil {
		return err
	}
	for i := range loan.Fees {
		loan.Fees[i].LoanId = loan.Id
		_, err := insertFee(db, loan.Fees[i])
		if err != nil {
			return err
		}
	}

	return nil
}

func (estimate *Estimate) insertEstimate(db *sql.DB) error {
	var query string
	var row *sql.Row
	var err error
	// var id int // Inserted estimate's id

	query = `INSERT INTO estimate
	(
		user_id,
		transaction,
		price,
		property,
		occupancy,
		zip,
		pud
	)
	VALUES (?, ?, ?, ?, ?, ?, ?)
	RETURNING id
	`
	row = db.QueryRow(query,
		estimate.User,
		estimate.Transaction,
		estimate.Price,
		estimate.Property,
		estimate.Occupancy,
		estimate.Zip,
		estimate.Pud,
	)

	err = row.Scan(&estimate.Id)
	if err != nil {
		return err
	}

	err = estimate.insertBorrower(db)
	if err != nil {
		return err
	}

	for i := range estimate.Loans {
		estimate.Loans[i].EstimateId = estimate.Id
		err = estimate.Loans[i].insertLoan(db)
		if err != nil {
			return err
		}
	}

	return nil
}

func (estimate *Estimate) del(db *sql.DB, user int) error {
	var query string
	var err error

	query = `DELETE FROM estimate WHERE id = ? AND user_id = ?`

	_, err = db.Exec(query, estimate.Id, user)
	if err != nil {
		return err
	}

	return nil
}

func (et *ETemplate) del(db *sql.DB, user int) error {
	var query string
	var err error

	query = `DELETE FROM estimate_template WHERE id = ? AND user_id = ?`

	_, err = db.Exec(query, et.Id, user)
	if err != nil {
		return err
	}

	return nil
}

func (eTemplate *ETemplate) insert(db *sql.DB) error {
	var query string
	var row *sql.Row
	var err error

	query = `INSERT INTO estimate_template
	(
		user_id,
		branch_id,
		estimate_id,
	)
	VALUES (?, ?, ?)
	RETURNING id
	`
	row = db.QueryRow(query,
		eTemplate.UserId,
		eTemplate.BranchId,
		eTemplate.Estimate.Id,
	)

	err = row.Scan(&eTemplate.Id)
	if err != nil {
		return err
	}

	return nil
}

func createEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var estimate Estimate
	err := json.NewDecoder(r.Body).Decode(&estimate)
	if err != nil {
		http.Error(w, "Invalid fields.", 422)
		return
	}
	claims, err := getClaims(r)
	estimate.User = claims.Id

	err = estimate.insertEstimate(db)
	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}
	estimate.makeResults()
	err = estimate.insertResults(db)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
	e, err := getEstimates(db, estimate.Id, 0)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	json.NewEncoder(w).Encode(e[0])
}

// Query all estimates that belong to the current user
func fetchEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var estimates []Estimate
	claims, err := getClaims(r)

	estimates, err = getEstimates(db, 0, claims.Id)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	json.NewEncoder(w).Encode(estimates)
}

func checkConventional(l Loan, b Borrower) error {
	if b.Credit < 620 {
		return errors.New("Credit score too low for conventional loan")
	}

	// Buyer needs to put down 20% to avoid mortgage insurance
	if l.Ltv > 80 && l.Mi.Rate == 0 {
		return errors.New(fmt.Sprintln(
			l.Name,
			"down payment must be 20% to avoid insurance",
		))
	}

	return nil
}

func checkFHA(l Loan, b Borrower) error {
	if b.Credit < 500 {
		return errors.New("Credit score too low for FHA loan")
	}

	if l.Ltv > 96.5 {
		return errors.New("FHA down payment must be at least 3.5%")
	}

	if b.Credit < 580 && l.Ltv > 90 {
		return errors.New("FHA down payment must be at least 10%")
	}

	// Debt-to-income must be below 45% if credit score is below 580.
	if b.Credit < 580 && l.Dti > 45 {
		return errors.New(fmt.Sprintln(
			l.Name, "debt to income is too high for credit score.",
		))
	}

	return nil
}

// Loan option for veterans with no set rules. Mainly placeholder.
func checkVA(l Loan, b Borrower) error {
	return nil
}

// Loan option for residents of rural areas with no set rules.
// Mainly placeholder.
func checkUSDA(l Loan, b Borrower) error {
	return nil
}

// Should also check loan amount limit maybe with an API.
func checkEstimate(e Estimate) error {
	if e.Property == "" {
		return errors.New("Empty property type")
	}
	if e.Price == 0 {
		return errors.New("Empty property price")
	}

	if e.Borrower.Num == 0 {
		return errors.New("Missing number of borrowers")
	}

	if e.Borrower.Credit == 0 {
		return errors.New("Missing borrower credit score")
	}

	if e.Borrower.Income == 0 {
		return errors.New("Missing borrower credit income")
	}

	for _, l := range e.Loans {
		if l.Amount == 0 {
			return errors.New(fmt.Sprintln(l.Name, "loan amount cannot be zero"))
		}
		if l.Term == 0 {
			return errors.New(fmt.Sprintln(l.Name, "loan term cannot be zero"))
		}
		if l.Interest == 0 {
			return errors.New(fmt.Sprintln(l.Name, "loan interest cannot be zero"))
		}

		// Can be used to check rules for specific loan types
		var err error
		switch l.Type.Id {
		case 1:
			err = checkConventional(l, e.Borrower)
		case 2:
			err = checkFHA(l, e.Borrower)
		case 3:
			err = checkVA(l, e.Borrower)
		case 4:
			err = checkUSDA(l, e.Borrower)
		default:
			err = errors.New("Invalid loan type")
		}

		if err != nil {
			return err
		}
	}

	return nil

}

func validateEstimate(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var estimate Estimate
	err := json.NewDecoder(r.Body).Decode(&estimate)

	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}

	err = checkEstimate(estimate)
	if err != nil {
		http.Error(w, err.Error(), 406)
		return
	}
}

func checkPdf(w http.ResponseWriter, r *http.Request) {
	db, err := sql.Open("mysql",
		fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/skouter_dev",
			os.Getenv("DBUser"),
			os.Getenv("DBPass")))

	// w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	err = db.Ping()
	if err != nil {
		fmt.Println("Bad database configuration: %v\n", err)
		panic(err)
		// maybe os.Exit(1) instead
	}

	estimates, err := getEstimates(db, 1, 0)
	if err != nil {
		w.WriteHeader(500)
		return
	}

	// claims, err := getClaims(r)
	if err != nil {
		w.WriteHeader(500)
		return
	}
	user, err := queryUser(db, 1)

	info := Report{
		Title:    "test PDF",
		Name:     "idk-random-name",
		User:     user,
		Estimate: estimates[0],
	}
	avatar, err := fetchAvatar(db, info.User.Id)
	letterhead, err := fetchLetterhead(db, info.User.Id)
	info.Avatar =
		base64.StdEncoding.EncodeToString(avatar)
	info.Letterhead =
		base64.StdEncoding.EncodeToString(letterhead)

	for l := range info.Estimate.Loans {
		loan := info.Estimate.Loans[l]
		for f := range info.Estimate.Loans[l].Fees {
			if info.Estimate.Loans[l].Fees[f].Amount < 0 {
				loan.Credits = append(loan.Credits, loan.Fees[f])
			}
		}
	}

	err = pages["report"].tpl.ExecuteTemplate(w, "master.tpl", info)
	if err != nil {
		fmt.Println(err)
	}

}

func getETemplates(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claims, err := getClaims(r)

	et, err := queryETemplates(db, 0, claims.Id)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	json.NewEncoder(w).Encode(et)
}

func createETemplate(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var estimate Estimate
	err := json.NewDecoder(r.Body).Decode(&estimate)

	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}
	claims, err := getClaims(r)
	if err != nil {
		http.Error(w, err.Error(), 422)
		return
	}

	err = estimate.insertETemplate(db, claims.Id, 0)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
}

func getPdf(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var estimate Estimate
	err := json.NewDecoder(r.Body).Decode(&estimate)
	cmd := exec.Command("wkhtmltopdf", "-", "-")

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		w.WriteHeader(500)
		log.Println(err)
		return
	}

	stdin, err := cmd.StdinPipe()
	if err != nil {
		w.WriteHeader(500)
		log.Println(err)
		return
	}

	if err := cmd.Start(); err != nil {
		log.Fatal(err)
	}

	claims, err := getClaims(r)
	if err != nil {
		w.WriteHeader(500)
		log.Println(err)
		return
	}
	user, err := queryUser(db, claims.Id)

	info := Report{
		Title:    "test PDF",
		Name:     "idk-random-name",
		User:     user,
		Estimate: estimate,
	}
	avatar, err := fetchAvatar(db, info.User.Id)
	letterhead, err := fetchLetterhead(db, info.User.Id)

	if len(avatar) > 1 {
		info.Avatar =
			base64.StdEncoding.EncodeToString(avatar)
	}

	if len(letterhead) > 1 {
		info.Letterhead =
			base64.StdEncoding.EncodeToString(letterhead)
	}

	err = pages["report"].tpl.ExecuteTemplate(stdin, "master.tpl", info)
	if err != nil {
		w.WriteHeader(500)
		log.Println(err)
		return
	}
	stdin.Close()

	buf, err := io.ReadAll(stdout)

	if _, err := w.Write(buf); err != nil {
		w.WriteHeader(500)
		log.Println(err)
		return
	}

	if err := cmd.Wait(); err != nil {
		log.Println(err)
		return
	}

}

func clipLetterhead(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var validTypes []string = []string{"image/png", "image/jpeg"}
	var isValidType bool
	var err error

	// claims, err := getClaims(r)
	if err != nil {
		http.Error(w, "Invalid token.", 422)
		return
	}
	img, t, err := image.Decode(r.Body)
	if err != nil {
		http.Error(w, "Invalid file, JPEG and PNG only.", 422)
		return
	}
	for _, v := range validTypes {
		if v == "image/"+t {
			isValidType = true
		}
	}
	if !isValidType {
		http.Error(w, "Invalid file type.", 422)
		return
	}

	g := gift.New(
		gift.ResizeToFit(400, 200, gift.LanczosResampling),
	)
	dst := image.NewRGBA(g.Bounds(img.Bounds()))
	g.Draw(dst, img)

	w.Header().Set("Content-Type", "image/png")
	err = png.Encode(w, dst)
	if err != nil {
		http.Error(w, "Error encoding.", 500)
		return
	}
}

func createCustomer(name string, email string, address Address) (
stripe.Customer, error) {

	params := &stripe.CustomerParams{
		Email: stripe.String(email),
		Name: stripe.String(name),
		Address: &stripe.AddressParams{
			City: stripe.String(address.City),
			Country: stripe.String(address.Country),
			Line1: stripe.String(address.Street),
			PostalCode: stripe.String(address.Zip),
			State: stripe.String(address.Region),
		},
	};
	
	result, err := customer.New(params)
	return *result, err
}

// Initiates a new standard subscription using a given customer ID
func createSubscription(cid string) (*stripe.Subscription, error) {

	// Automatically save the payment method to the subscription
    // when the first payment is successful.
    paymentSettings := &stripe.SubscriptionPaymentSettingsParams{
		    SaveDefaultPaymentMethod: stripe.String("on_subscription"),
  	}
  	
  	// Create the subscription. Note we're expanding the Subscription's
    // latest invoice and that invoice's payment_intent
    // so we can pass it to the front end to confirm the payment
    subscriptionParams := &stripe.SubscriptionParams{
        Customer: stripe.String(cid),
        Items: []*stripe.SubscriptionItemsParams{
            {
                Price: stripe.String(standardPriceId),
            },
        },
        PaymentSettings: paymentSettings,
        PaymentBehavior: stripe.String("default_incomplete"),
    }
    subscriptionParams.AddExpand("latest_invoice.payment_intent")
    s, err := subscription.New(subscriptionParams)
    
    return s, err
}

// Creates a new subscription instance for a new user or retrieves the
// existing instance if possible. It's main purpose is to supply a
// client secret used for sending billing information to stripe.
func subscribe(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claims, err := getClaims(r)
	user, err := queryUser(db, claims.Id)

	if err != nil {
		w.WriteHeader(422)
		return
	}
	
	user.querySub(db)
	
	var name string = user.FirstName + " " + user.LastName
	
	if user.CustomerId == "" {
		c, err := createCustomer(name, user.Email, user.Address)
		if err != nil {
			http.Error(w, err.Error(), 500)
			return
		}
	
		err = user.updateCustomerId(db, c.ID)
		if err != nil {
			http.Error(w, err.Error(), 500)
			return
		}
	}

	if user.Sub.Id == 0 {
		s, err := createSubscription(user.CustomerId)
		if err != nil {
			http.Error(w, err.Error(), 500)
			return
		}
		
		user.Sub.UserId = user.Id
		user.Sub.StripeId = s.ID
		user.Sub.CustomerId = user.CustomerId
		user.Sub.PriceId = standardPriceId
		user.Sub.End = int(s.CurrentPeriodEnd)
		user.Sub.Start = int(s.CurrentPeriodStart)
		user.Sub.ClientSecret = s.LatestInvoice.PaymentIntent.ClientSecret
		user.Sub.PaymentStatus = string(s.LatestInvoice.PaymentIntent.Status)
		
		// Inserting from here is probably unnecessary and confusing because
		// new subs are already handled by the stripe hook
		err = user.Sub.insertSub(db)
		if err != nil {
			http.Error(w, err.Error(), 500)
			return
		}
	} else {
		// This should handle creating a new subscription when the old one
		// has an incomplete_expired status and cannot be paid
	}
	
	json.NewEncoder(w).Encode(user.Sub)
	
}

// A successful subscription payment should be confirmed by Stripe and
// Updated through this hook.
func invoicePaid(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var invoice stripe.Invoice
	b, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("io.ReadAll: %v", err)
		return
	}
	
	event, err := webhook.ConstructEvent(b,
	r.Header.Get("Stripe-Signature"),
	hookKeys.InvoicePaid)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("webhook.ConstructEvent: %v", err)
		return
	}
	
	// OK should be sent before any processing to confirm with Stripe that
	// the hook was received
	w.WriteHeader(http.StatusOK)
	if event.Type != "invoice.paid" {
		log.Println("Invalid event type sent to invoice-paid.")
		return
	}
	
	json.Unmarshal(event.Data.Raw, &invoice)
	log.Println(event.Type, invoice.ID, invoice.Customer.ID)
	
	user, err := queryCustomer(db, invoice.Customer.ID)
	if err != nil {
		log.Printf("Could not query customer: %v", err)
		return
	}
	s, err := subscription.Get(invoice.Subscription.ID, nil)
	if err != nil {
		log.Printf("Could not fetch subscription: %v", err)
		return
	}
	
	log.Println(user.Id, s.ID)
}

func invoiceFailed(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	b, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("io.ReadAll: %v", err)
		return
	}
	
	event, err := webhook.ConstructEvent(b, r.Header.Get("Stripe-Signature"),
	os.Getenv("STRIPE_SECRET_KEY"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("webhook.ConstructEvent: %v", err)
		return
	}

	log.Println(event.Data)
}

// Important for catching subscription creation through Stripe dashboard
// although it already happens at subscribe(). It checks if the user already
// has a subscription and replaces those fields if necessary so a seperate
// subCreated() is not necessary.
func subUpdated(w http.ResponseWriter, db *sql.DB, r *http.Request) {

	var sub stripe.Subscription
	b, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("io.ReadAll: %v", err)
		return
	}
	
	event, err := webhook.ConstructEvent(b,
	r.Header.Get("Stripe-Signature"),
	hookKeys.SubUpdated)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("webhook.ConstructEvent: %v", err)
		return
	}
	
	// OK should be sent before any processing to confirm with Stripe that
	// the hook was received
	w.WriteHeader(http.StatusOK)
	if event.Type != "customer.subscription.created" {
		log.Println(
		"Invalid event type sent to customer.subscription.created.")
		return
	}
	
	json.Unmarshal(event.Data.Raw, &sub)
	log.Println(event.Type, sub.ID, sub.Customer.ID)
	
	user, err := queryCustomer(db, sub.Customer.ID)
	if err != nil {
		log.Printf("Could not query customer: %v", err)
		return
	}
	
	if statuses[user.Status] < 5 && sub.Status == "trialing" {
		user.Status = "Trial"
		user.update(db)
	} else if sub.Status != "active" {
		user.Status = "Unsubscribed"
		user.update(db)
	}
	
	user.Sub.UserId = user.Id
	user.Sub.StripeId = sub.ID
	user.Sub.CustomerId = user.CustomerId
	user.Sub.PriceId = standardPriceId
	user.Sub.End = int(sub.CurrentPeriodEnd)
	user.Sub.Start = int(sub.CurrentPeriodStart)
	user.Sub.ClientSecret = sub.LatestInvoice.PaymentIntent.ClientSecret
	user.Sub.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status)
	user.Sub.Status = string(sub.Status)
	
	if user.Sub.Id != 0 {
		err = user.Sub.insertSub(db)
	} else {
		user.Sub.updateSub(db)
	}
	
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
	
	log.Println("User subscription created:", user.Id, sub.ID)
}

// Handles changes to customer subscriptions sent by Stripe
func subDeleted(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	var sub stripe.Subscription
	b, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("io.ReadAll: %v", err)
		return
	}
	
	event, err := webhook.ConstructEvent(b,
	r.Header.Get("Stripe-Signature"),
	hookKeys.SubUpdated)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		log.Printf("webhook.ConstructEvent: %v", err)
		return
	}
	
	// OK should be sent before any processing to confirm with Stripe that
	// the hook was received
	w.WriteHeader(http.StatusOK)
	if event.Type != "customer.subscription.updated" {
		log.Println(
		"Invalid event type sent to customer.subscription.updated.")
		return
	}
	
	json.Unmarshal(event.Data.Raw, &sub)
	log.Println(event.Type, sub.ID, sub.Customer.ID)
	
	user, err := queryCustomer(db, sub.Customer.ID)
	if err != nil {
		log.Printf("Could not query customer: %v", err)
		return
	}
	
	if statuses[user.Status] < 5 && sub.Status == "trialing" {
		user.Status = "Trial"
		user.update(db)
	} else if sub.Status != "active" {
		user.Status = "Unsubscribed"
		user.update(db)
	}
	
	user.Sub.UserId = user.Id
	user.Sub.StripeId = sub.ID
	user.Sub.CustomerId = user.CustomerId
	user.Sub.PriceId = standardPriceId
	user.Sub.End = int(sub.CurrentPeriodEnd)
	user.Sub.Start = int(sub.CurrentPeriodStart)
	user.Sub.ClientSecret = sub.LatestInvoice.PaymentIntent.ClientSecret
	user.Sub.PaymentStatus = string(sub.LatestInvoice.PaymentIntent.Status)
	user.Sub.Status = string(sub.Status)
	
	if user.Sub.Id != 0 {
		err = user.Sub.insertSub(db)
	} else {
		user.Sub.updateSub(db)
	}
	
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
	
	log.Println("User subscription created:", user.Id, sub.ID)
}

func api(w http.ResponseWriter, r *http.Request) {
	var args []string

	p := r.URL.Path
	db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s",
		os.Getenv("DBUser"),
		os.Getenv("DBPass"),
		os.Getenv("DBName"),
	))

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	err = db.Ping()
	if err != nil {
		fmt.Println("Bad database configuration: %v\n", err)
		panic(err)
		// maybe os.Exit(1) instead
	}

	switch {
	case match(p, "/api/login", &args) &&
		r.Method == http.MethodPost:
		login(w, db, r)
	case match(p, "/api/token", &args) &&
		r.Method == http.MethodGet && guard(r, 1):
		getToken(w, db, r)
	case match(p, "/api/letterhead", &args) &&
		r.Method == http.MethodPost && guard(r, 1):
		clipLetterhead(w, db, r)
	case match(p, "/api/users", &args) && // Array of all users
		r.Method == http.MethodGet && guard(r, 3):
		getUsers(w, db, r)
	case match(p, "/api/user", &args) &&
		r.Method == http.MethodGet && guard(r, 1):
		getUser(w, db, r)
	case match(p, "/api/user", &args) &&
		r.Method == http.MethodPost:
		createUser(w, db, r)
	case match(p, "/api/user", &args) &&
		r.Method == http.MethodPatch &&
		guard(r, 3): // For admin to modify any user
		patchUser(w, db, r)
	case match(p, "/api/user", &args) &&
		r.Method == http.MethodPatch &&
		guard(r, 1): // For employees to modify own accounts
		patchSelf(w, db, r)
	case match(p, "/api/user", &args) &&
		r.Method == http.MethodDelete &&
		guard(r, 3):
		deleteUser(w, db, r)
	case match(p, "/api/user/avatar", &args) &&
		r.Method == http.MethodGet &&
		guard(r, 1):
		getAvatar(w, db, r)
	case match(p, "/api/user/avatar", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		setAvatar(w, db, r)
	case match(p, "/api/user/letterhead", &args) &&
		r.Method == http.MethodGet &&
		guard(r, 1):
		getLetterhead(w, db, r)
	case match(p, "/api/user/letterhead", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		setLetterhead(w, db, r)
	case match(p, "/api/user/password", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		changePassword(w, db, r)
	case match(p, "/api/user/subscribe", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		subscribe(w, db, r)
	case match(p, "/api/fees", &args) &&
		r.Method == http.MethodGet &&
		guard(r, 1):
		getFeesTemp(w, db, r)
	case match(p, "/api/fee", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		createFeesTemp(w, db, r)
	case match(p, "/api/fee", &args) &&
		r.Method == http.MethodDelete &&
		guard(r, 1):
		deleteFeeTemp(w, db, r)
	case match(p, "/api/estimates", &args) &&
		r.Method == http.MethodGet &&
		guard(r, 1):
		fetchEstimate(w, db, r)
	case match(p, "/api/estimate", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		createEstimate(w, db, r)
	case match(p, "/api/estimate", &args) &&
		r.Method == http.MethodDelete &&
		guard(r, 1):
		deleteEstimate(w, db, r)
	case match(p, "/api/estimate/validate", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		validateEstimate(w, db, r)
	case match(p, "/api/estimate/summarize", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		summarize(w, db, r)
	case match(p, "/api/templates", &args) &&
		r.Method == http.MethodGet &&
		guard(r, 1):
		getETemplates(w, db, r)
	case match(p, "/api/templates", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		createETemplate(w, db, r)
	case match(p, "/api/templates", &args) &&
		r.Method == http.MethodDelete &&
		guard(r, 1):
		deleteET(w, db, r)
	case match(p, "/api/pdf", &args) &&
		r.Method == http.MethodPost &&
		guard(r, 1):
		getPdf(w, db, r)
	case match(p, "/api/stripe/invoice-paid", &args) &&
		r.Method == http.MethodPost:
		invoicePaid(w, db, r)
	case match(p, "/api/stripe/invoice-payment-failed", &args) &&
		r.Method == http.MethodPost:
		invoiceFailed(w, db, r)
	case match(p, "/api/stripe/sub-created", &args) &&
		r.Method == http.MethodPost:
		subUpdated(w, db, r)
	case match(p, "/api/stripe/sub-updated", &args) &&
		r.Method == http.MethodPost:
		subUpdated(w, db, r)
	case match(p, "/api/stripe/sub-updated", &args) &&
		r.Method == http.MethodPost:
		subUpdated(w, db, r)
	default:
		http.Error(w, "Invalid route or token", 404)
	}

	db.Close()
}

func route(w http.ResponseWriter, r *http.Request) {
	var page Page
	var args []string
	p := r.URL.Path

	switch {
	case r.Method == "GET" && match(p, "/", &args):
		page = pages["home"]
	case match(p, "/terms", &args):
		page = pages["terms"]
	case match(p, "/app", &args):
		page = pages["app"]
	default:
		http.NotFound(w, r)
		return
	}

	page.Render(w)
}

func serve() {

	files := http.FileServer(http.Dir(""))

	proxy, err := url.Parse("http://localhost:8002")
	if err != nil {
		log.Fatal("invalid origin server URL")
	}

	http.Handle("/assets/", files)
	http.HandleFunc("/api/", api)
	http.HandleFunc("/app", route)
	http.Handle("/", httputil.NewSingleHostReverseProxy(proxy))
	log.Fatal(http.ListenAndServe(address, nil))
}

func dbReset(db *sql.DB) {
	b, err := os.ReadFile("migrations/reset.sql")
	if err != nil {
		log.Fatal(err)
	}

	_, err = db.Exec(string(b))
	if err != nil {
		log.Fatal(err)
	}

	b, err = os.ReadFile("migrations/0_29092022_setup_tables.sql")
	if err != nil {
		log.Fatal(err)
	}

	_, err = db.Exec(string(b))
	if err != nil {
		log.Fatal(err)
	}
}

func generateFees(loan Loan) []Fee {
	var fees []Fee
	var fee Fee
	p := gofakeit.Float32Range(0.5, 10)
	size := gofakeit.Number(1, 10)

	for f := 0; f < size; f++ {
		fee = Fee{
			Amount: int(float32(loan.Amount) * p / 100),
			Perc:   p,
			Name:   gofakeit.BuzzWord(),
			Type:   feeTypes[gofakeit.Number(0, len(feeTypes)-1)],
		}
		fees = append(fees, fee)
	}

	return fees
}

func generateCredits(loan Loan) []Fee {
	var fees []Fee
	var fee Fee
	p := gofakeit.Float32Range(-10, -0.5)
	size := gofakeit.Number(1, 10)

	for f := 0; f < size; f++ {
		fee = Fee{
			Amount: int(float32(loan.Amount) * p / 100),
			Perc:   p,
			Name:   gofakeit.BuzzWord(),
			Type:   feeTypes[gofakeit.Number(0, len(feeTypes)-1)],
		}
		fees = append(fees, fee)
	}

	return fees
}

func seedAddresses(db *sql.DB) []Address {
	addresses := make([]Address, 10)

	for i, a := range addresses {
		a.Street = gofakeit.Street()
		a.City = gofakeit.City()
		a.Region = gofakeit.State()
		a.Country = "Canada"
		a.Full = fmt.Sprintf("%s, %s %s", a.Street, a.City, a.Region)
		id, err := insertAddress(db, a)
		if err != nil {
			log.Println(err)
			break
		}
		addresses[i].Id = id
	}

	return addresses
}

func seedBranches(db *sql.DB, addresses []Address) []Branch {
	branches := make([]Branch, 4)

	for i := range branches {
		branches[i].Name = gofakeit.Company()
		branches[i].Type = "NMLS"
		branches[i].Letterhead = gofakeit.ImagePng(400, 200)
		branches[i].Num = gofakeit.HexUint8()
		branches[i].Phone = gofakeit.Phone()
		branches[i].Address.Id = gofakeit.Number(1, 5)
		id, err := insertBranch(db, branches[i])
		if err != nil {
			log.Println(err)
			break
		}
		branches[i].Id = id
	}

	return branches
}

func seedUsers(db *sql.DB, addresses []Address, branches []Branch) []User {
	users := make([]User, 10)

	for i := range users {
		p := gofakeit.Person()
		users[i].FirstName = p.FirstName
		users[i].LastName = p.LastName
		users[i].Email = p.Contact.Email
		users[i].Phone = p.Contact.Phone
		users[i].Branch = branches[gofakeit.Number(0, 3)]
		users[i].Address = addresses[gofakeit.Number(1, 9)]
		// users[i].Letterhead = gofakeit.ImagePng(400, 200)
		// users[i].Avatar = gofakeit.ImagePng(200, 200)
		users[i].Country = []string{"Canada", "USA"}[gofakeit.Number(0, 1)]
		users[i].Password = "test123"
		users[i].Verified = true
		users[i].Title = "Loan Officer"
		users[i].Status = "Subscriber"
		users[i].Role = "User"
	}

	users[0].Email = "test@example.com"
	users[0].Email = "test@example.com"

	users[1].Email = "test2@example.com"
	users[1].Status = "Branch"
	users[1].Role = "Manager"

	users[2].Email = "test3@example.com"
	users[2].Status = "Free"
	users[2].Role = "Admin"

	for i := range users {
		var err error
		users[i].Id, err = insertUser(db, users[i])
		if err != nil {
			log.Println(err)
			break
		}
	}

	return users
}

func seedLicenses(db *sql.DB, users []User) []License {
	licenses := make([]License, len(users))

	for i := range licenses {
		licenses[i].UserId = users[i].Id
		licenses[i].Type = []string{"NMLS", "FSRA"}[gofakeit.Number(0, 1)]
		licenses[i].Num = gofakeit.UUID()
		id, err := insertLicense(db, licenses[i])
		if err != nil {
			log.Println(err)
			break
		}
		licenses[i].Id = id
	}

	return licenses
}

func seedLoanTypes(db *sql.DB) []LoanType {
	var loantypes []LoanType
	var loantype LoanType
	var err error

	loantype = LoanType{Branch: 0, User: 0, Name: "Conventional"}
	loantype.Id, err = insertLoanType(db, loantype)
	if err != nil {
		panic(err)
	}
	loantypes = append(loantypes, loantype)

	loantype = LoanType{Branch: 0, User: 0, Name: "FHA"}
	loantype.Id, err = insertLoanType(db, loantype)
	if err != nil {
		panic(err)
	}
	loantypes = append(loantypes, loantype)

	loantype = LoanType{Branch: 0, User: 0, Name: "USDA"}
	loantype.Id, err = insertLoanType(db, loantype)
	if err != nil {
		panic(err)
	}
	loantypes = append(loantypes, loantype)

	loantype = LoanType{Branch: 0, User: 0, Name: "VA"}
	loantype.Id, err = insertLoanType(db, loantype)
	if err != nil {
		panic(err)
	}
	loantypes = append(loantypes, loantype)

	return loantypes
}

func seedEstimates(db *sql.DB, users []User, ltypes []LoanType) []Estimate {
	var estimates []Estimate
	var estimate Estimate
	var l Loan
	var err error

	for i := 0; i < 15; i++ {
		estimate = Estimate{}
		estimate.User = users[gofakeit.Number(0, len(users)-1)].Id
		estimate.Borrower = Borrower{
			Credit: gofakeit.Number(600, 800),
			Income: gofakeit.Number(1000000, 15000000),
			Num:    gofakeit.Number(1, 20),
		}
		estimate.Transaction = []string{"Purchase", "Refinance"}[gofakeit.Number(0, 1)]
		estimate.Price = gofakeit.Number(50000, 200000000)
		estimate.Property =
			propertyTypes[gofakeit.Number(0, len(propertyTypes)-1)]
		estimate.Occupancy =
			[]string{"Primary", "Secondary", "Investment"}[gofakeit.Number(0, 2)]
		estimate.Zip = gofakeit.Zip()

		lsize := gofakeit.Number(1, 6)
		for j := 0; j < lsize; j++ {
			l.Type = ltypes[gofakeit.Number(0, len(ltypes)-1)]
			l.Amount = gofakeit.Number(
				int(float32(estimate.Price)*0.5),
				int(float32(estimate.Price)*0.93))
			l.Term = gofakeit.Number(4, 30)
			l.Hoi = gofakeit.Number(50000, 700000)
			l.Hazard = gofakeit.Number(5000, 200000)
			l.Tax = gofakeit.Number(5000, 200000)
			l.Interest = gofakeit.Float32Range(0.5, 8)
			l.Fees = generateFees(l)
			l.Credits = generateCredits(l)
			l.Name = gofakeit.AdjectiveDescriptive()
			estimate.Loans = append(estimate.Loans, l)
		}

		estimates = append(estimates, estimate)
	}

	estimates[0].User = users[0].Id
	estimates[1].User = users[0].Id

	for i := range estimates {
		err = estimates[i].insertEstimate(db)
		if err != nil {
			log.Println(err)
			return estimates
		}
	}

	return estimates
}

func seedResults(db *sql.DB, estimates []Estimate) error {
	var err error

	for i := range estimates {
		estimates[i].makeResults()
		err = estimates[i].insertResults(db)
		if err != nil {
			log.Println(err)
			return err
		}
	}

	return nil
}

func dbSeed(db *sql.DB) {
	addresses := seedAddresses(db)
	branches := seedBranches(db, addresses)
	users := seedUsers(db, addresses, branches)
	_ = seedLicenses(db, users)
	loantypes := seedLoanTypes(db)
	estimates := seedEstimates(db, users, loantypes)
	_ = seedResults(db, estimates)
}

func dev(args []string) {
	os.Setenv("DBName", "skouter_dev")
	os.Setenv("DBUser", "tester")
	os.Setenv("DBPass", "test123")
	stripe.Key = os.Getenv("STRIPE_SECRET_KEY")
	standardPriceId = "price_1OZLK9BPMoXn2pf9kuTAf8rs"
	hookKeys = HookKeys{
		InvoicePaid: os.Getenv("DEV_WEBHOOK_KEY"),
		InvoiceFailed: os.Getenv("DEV_WEBHOOK_KEY"),
	}

	db, err := sql.Open("mysql",
		fmt.Sprintf("%s:%s@tcp(127.0.0.1:3306)/%s?multiStatements=true",
			os.Getenv("DBUser"),
			os.Getenv("DBPass"),
			os.Getenv("DBName"),
		))

	err = db.Ping()
	if err != nil {
		log.Println("Bad database configuration: %v", err)
		panic(err)
		// maybe os.Exit(1) instead
	}

	if len(args) == 0 {
		serve()
		return
	}

	switch args[0] {
	case "seed":
		dbSeed(db)
	case "reset":
		dbReset(db)
	default:
		return
	}

	db.Close()
}

func check(args []string) {
	os.Setenv("DBName", "skouter_dev")
	os.Setenv("DBUser", "tester")
	os.Setenv("DBPass", "test123")
	files := http.FileServer(http.Dir(""))

	http.Handle("/assets/", files)
	http.HandleFunc("/", checkPdf)
	log.Fatal(http.ListenAndServe(address, nil))
}

func main() {
	if len(os.Args) <= 1 {
		serve()
		return
	}

	switch os.Args[1] {
	case "dev":
		dev(os.Args[2:])
	case "checkpdf":
		check(os.Args[2:])
	default:
		return
	}
}