package main

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

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 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"`
	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"`
}

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

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

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,
}

// 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 strconv.FormatFloat(float64(cents)/100, 'f', 2, 32)
	}
	
	// For calculating down payments
	diff := func(a, b int) string {
		return strconv.FormatFloat(float64(b - a)/100, 'f', 2, 32)
	}
	
	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 makeResults(estimate Estimate) ([]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 _, loan := range estimate.Loans {
		var 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*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
		
		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 }
	results := makeResults(estimate)
	
	json.NewEncoder(w).Encode(results)
}

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,
	user_id,
	branch_id,
	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 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 getBorrower(db *sql.DB, id int) (Borrower, error) {
	var borrower Borrower

	row := db.QueryRow(
		"SELECT * FROM borrower " +
		"WHERE id = ? LIMIT 1",
	id)

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

	return borrower, 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
	}

	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)
		http.Error(w, "Token generation error.", http.StatusInternalServerError)
		return
	}

	cookie := http.Cookie{Name: "skouter",
	Value: tokenStr,
	Path: "/",
	Expires: time.Now().Add(time.Hour * 24)}
	http.SetCookie(w, &cookie)
	_, err = w.Write([]byte(tokenStr))
	if err != nil {
		http.Error(w,
		"Could not complete token write.",
		http.StatusInternalServerError)}
}

func getToken(w http.ResponseWriter, db *sql.DB, r *http.Request) {
	claims, err := getClaims(r)
	// Will verify existing signature and expiry time
	token := jwt.NewWithClaims(jwt.SigningMethodHS256,
	UserClaims{ Id: claims.Id, Role: claims.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)
		http.Error(w, "Token generation error.", http.StatusInternalServerError)
		return
	}

	cookie := http.Cookie{Name: "skouter",
	Value: tokenStr,
	Path: "/",
	Expires: time.Now().Add(time.Hour * 24)}
	http.SetCookie(w, &cookie)
	_, err = w.Write([]byte(tokenStr))
	if err != nil {
		http.Error(w,
		"Could not complete token write.",
		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, ''),
	u.verified,
	u.role,
	u.address,
	u.phone
	FROM user u WHERE u.id = CASE @e := ? WHEN 0 THEN u.id ELSE @e END
	`
	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.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 insertResults(db *sql.DB, estimate Estimate) (error){
	var query string
	var row *sql.Row
	var err error
	var results []Result
	
	for i := range estimate.Loans {
		results = append(results, estimate.Loans[i].Result)
	}

	query = `INSERT INTO estimate_result
	(
		loan_id,
		loan_payment,
		total_monthly,
		total_fees,
		total_credits,
		cash_to_close
	)
	VALUES (?, ?, ?, ?, ?, ?, ?)
	RETURNING id
	`
	for i := range results {
		db.QueryRow(query,
			results[i].LoanId,
			results[i].LoanPayment,
			results[i].TotalMonthly,
			results[i].TotalFees,
			results[i].TotalCredits,
			results[i].CashToClose,
		)
		err = row.Scan(&results[i].Id)
		if err != nil { return err }
	}
	
	return nil
}


func insertUser(db *sql.DB, user User) (User, 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 user, 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 User{}, err }

	user, err = queryUser(db, id)
	if err != nil { return User{}, err }

	return user, 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 updateUser(user User, 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 = updateUser(user, 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)
	}
}

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 }

	_, err = mail.ParseAddress(user.Email)
	if err != nil { http.Error(w, "Invalid email.", 422); return }

	if roles[user.Role] == 0 {
		http.Error(w, "Invalid role.", 422)
	}

	user, err = insertUser(db, user)
	if err != nil { http.Error(w, "Error creating user.", 422); 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
}

// 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.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 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,
	borrower_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.Borrower.Id,
			&estimate.Transaction,
			&estimate.Price,
			&estimate.Property,
			&estimate.Occupancy,
			&estimate.Zip,
			&estimate.Pud,
			)
		err != nil {
			return estimates, err
        }
        
		borrower, err := getBorrower(db, estimate.Borrower.Id)
		if err != nil {
			return estimates, err
        }
        estimate.Borrower = borrower
		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
}

// Accepts a borrower struct and returns the id of the inserted borrower and
// any related error.
func insertBorrower(db *sql.DB, borrower Borrower) (int, error) {
	var query string
	var row *sql.Row
	var err error
	var id int // Inserted loan's id

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

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

	return id, 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 insertLoan(db *sql.DB, loan Loan) (Loan, error){
	var query string
	var row *sql.Row
	var err error
	var id int // Inserted loan's id

	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(&id)
	if err != nil { return loan, err }
	_, err = insertMi(db, loan.Mi)
	if err != nil { return loan, err }
	for i := range loan.Fees {
		_, err := insertFee(db, loan.Fees[i])
		if err != nil { return loan, err }
	}

	loans, err := getLoans(db, id, 0)
	if err != nil { return Loan{}, err }

	return loans[0], nil
}


func insertEstimate(db *sql.DB, estimate Estimate) (Estimate, error){
	var query string
	var row *sql.Row
	var err error
	// var id int // Inserted estimate's id
	
	estimate.Borrower.Id, err = insertBorrower(db, estimate.Borrower)
	if err != nil { return Estimate{}, err }

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

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

	for _, l := range estimate.Loans {
		l.EstimateId = estimate.Id
		_, err = insertLoan(db, l)
		if err != nil { return estimate, err }
	}
	
	estimates, err := getEstimates(db, estimate.Id, 0)
	if err != nil { return Estimate{}, err }

	return estimates[0], 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

	estimate, err = insertEstimate(db, estimate)
	if err != nil { http.Error(w, err.Error(), 422); return }
	makeResults(estimate)
	err = insertResults(db, estimate)
	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 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)
	info.Avatar =
	base64.StdEncoding.EncodeToString(avatar)
	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.", 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 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 &&
	guard(r, 3):
		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/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/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/pdf", &args) &&
	r.Method == http.MethodPost &&
	guard(r, 1):
		getPdf(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" ]
    case match(p, "/test", &args):
		checkPdf(w, r)
		return
    default:
        http.NotFound(w, r)
        return
    }

    page.Render(w)
}

func serve() {
	files := http.FileServer(http.Dir(""))

	http.Handle("/assets/", files)
	http.HandleFunc("/api/", api)
	http.HandleFunc("/", route)
	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 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 = "Subscribed"
		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 {
		u, err := insertUser(db, users[i])
		if err != nil {log.Println(err); break}
		users[i].Id = u.Id
	}
	
	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 dbSeed(db *sql.DB) {
	addresses := seedAddresses(db)
	branches := seedBranches(db, addresses)
	users := seedUsers(db, addresses, branches)
	_ = seedLicenses(db, users)
}

func dev(args []string) {
	os.Setenv("DBName", "skouter_dev")
	os.Setenv("DBUser", "tester")
	os.Setenv("DBPass", "test123")
	
	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
    }
}

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 "check":
		check(os.Args[2:])
    default:
        return
    }
}