package scsusers

import (
	"bytes"
	"database/sql"
	"fmt"
	"github.com/jmoiron/sqlx"
	"golang.org/x/crypto/bcrypt"
	"html/template"
	"log"
	"math/rand"
	"net/smtp"
	"strings"
)


type templates struct {
	Registration *template.Template
	Alert        *template.Template
	Recovery     *template.Template
}

type config struct {
	SiteName    string
	FromEmail   string
	Templates   templates
	db          *sqlx.DB
	TablePrefix string
}

var c config

func Init(dbin *sqlx.DB, tp, sitename, fromaddr string) {
	c.db = dbin
	c.TablePrefix = tp
	c.SiteName = sitename
	c.FromEmail = fromaddr

	SetRegistrationTemplate("")
	SetAlertTemplate("")
	SetRecoveryTemplate("")
}

func UsernameAvailable(username string) bool {
	var u string
	q := fmt.Sprintf("select username from %s_auth where username=$1", c.TablePrefix)
	err := c.db.Get(&u, q, username)
	if err == sql.ErrNoRows {
		return true
	}
	if err != nil {
		log.Printf("UsernameAvailable returned error: " + err.Error() + " Query was " + q)
		return false
	}
	return false
}

/* Check for username availability, add to database, send email */

func Register(username, email, ip string) bool {
	if !UsernameAvailable(username) {
		return false
	}
	log.Println("getting random bytes")
	pass := randBytes(16)
	log.Println("Generating hash")
	crypt, err := bcrypt.GenerateFromPassword(pass, 10)
	if err != nil {
		log.Printf("Bcrypt GenerateFromPassword failed? Pass is %s and error is %s\n", pass, err.Error())
		return false
	}
	fmt.Println("db insert")
	q := fmt.Sprintf("insert into %s_auth (username, email, password, registration_date, registration_ip) values ($1, $2, $3, CURRENT_TIMESTAMP, $4)", c.TablePrefix)
	_, err = c.db.Exec(q, username, email, crypt, ip)
	if err != nil {
		log.Println("Register: insert failed: " + err.Error())
		return false
	}
	if sendRegistrationEmail(email, username, string(pass)) {
		return true
	}
	log.Println("Failed to send registration email, deleting user.")
	q=fmt.Sprintf("delete from %s_auth where username=$1 AND password=$2", c.TablePrefix)
	_,err = c.db.Exec(q, username, string(crypt))
	if err != nil {
		log.Println("Failed to delete new user " + username + " : " + err.Error())
	}
	return false
}

func Login(username, password string) bool {
	q:=fmt.Sprintf("select password from %s_auth where username=$1",c.TablePrefix)
	var crypt string
	err:=c.db.Get(&crypt, q, username)
	if err != nil {
		log.Println("Failed login attempt for unknown username: " + username)
		return false
	}
	if bcrypt.CompareHashAndPassword([]byte(crypt), []byte(password)) != nil {
		log.Println("Failed password for " + username)
		return false
	}
	bump(username)
	return true
}

func bump(username string) {
	q:=fmt.Sprintf("update %s_auth set lastseen=CURRENT_TIMESTAMP where username=$1", c.TablePrefix)
	_, err :=c.db.Exec(q, username)
	if err != nil {
		log.Println("Error on user bump: " + err.Error())
	}
}
func RecoverByUsername(u string) {
	var username, email string
	q:=fmt.Sprintf("select username, email from %s_auth where username=$1", c.TablePrefix)
	row:=c.db.QueryRow(q, u)
	err:=row.Scan(&username, &email)
	if err!=sql.ErrNoRows {
		recoverycode:=randBytes(10)
		qq:=fmt.Sprintf("update %s_auth set recoverycode=$1, recoverytime=NOW() where username=$2", c.TablePrefix)
		_,err:=c.db.Exec(qq, recoverycode, username)
		if err==nil {
			sendRecoveryEmail(email, username, string(recoverycode))
		}
	}
}

func RecoverByEmail(e string) {
	var username, email string
	q:=fmt.Sprintf("select username, email from %s_auth where email=$1", c.TablePrefix)
	row:=c.db.QueryRow(q, e)
	err:=row.Scan(&username, &email)
	if err!=sql.ErrNoRows {
		recoverycode:=randBytes(10)
		qq:=fmt.Sprintf("update %s_auth set recoverycode=$1, recoverytime=NOW() where username=$2", c.TablePrefix)
		_,err:=c.db.Exec(qq, recoverycode, username)
		if err==nil {
			sendRecoveryEmail(email, username, string(recoverycode))
		}
	}
}


func randBytes(n int) []byte {
	const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
	b := make([]byte, n)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return b
}

func sendRegistrationEmail(recipient, username, password string) bool {
	data := struct {
		SiteName  string
		FromEmail string
		UserName  string
		Pass      string
	}{
		SiteName:  c.SiteName,
		FromEmail: c.FromEmail,
		UserName:  username,
		Pass:      password,
	}
	var body bytes.Buffer
	err := c.Templates.Registration.Execute(&body, data)
	if err != nil {
		log.Printf("Registration template failed to execute: %v returned %s\n", data, err.Error())
		return false
	}
	subject := fmt.Sprintf("Welcome to %s", c.SiteName)
	err = SendMail("localhost:25", c.FromEmail, subject, body.String(), recipient)
	if err != nil {
		log.Printf("Error sending mail to %s: %s\n", recipient, err.Error())
		return false
	}
	return true
}


func sendAlertEmail(username, recipient, message string) bool {
	data := struct {
		SiteName  string
		FromEmail string
		UserName  string
		Activity      string
	}{
		SiteName:  c.SiteName,
		FromEmail: c.FromEmail,
		UserName:  username,
		Activity:  message,
	}
	var body bytes.Buffer
	err := c.Templates.Registration.Execute(&body, data)
	if err != nil {
		log.Printf("Alert template failed to execute: %v returned %s\n", data, err.Error())
		return false
	}
	
	subject := fmt.Sprintf("New activity on %s", c.SiteName)
	err = SendMail("localhost:25", c.FromEmail, subject, body.String(), recipient)
	if err != nil {
		log.Printf("Error sending mail to %s: %s\n", recipient, err.Error())
		return false
	}
	return true
}

func sendRecoveryEmail(recipient, username, code string) bool {
	data := struct {
		SiteName  string
		FromEmail string
		UserName  string
		RecoveryCode      string
	}{
		SiteName:  c.SiteName,
		FromEmail: c.FromEmail,
		UserName:  username,
		RecoveryCode:      code,
	}
	var body bytes.Buffer
	err := c.Templates.Registration.Execute(&body, data)
	if err != nil {
		log.Printf("Registration template failed to execute: %v returned %s\n", data, err.Error())
		return false
	}
	subject := fmt.Sprintf("Welcome to %s", c.SiteName)
	err = SendMail("localhost:25", c.FromEmail, subject, body.String(), recipient)
	if err != nil {
		log.Printf("Error sending mail to %s: %s\n", recipient, err.Error())
		return false
	}
	return true
}


func SetRegistrationTemplate(t string) bool {
	if len(t) != 0 {
		r, err := template.New("reg").Parse(t)
		if err != nil {
			c.Templates.Registration = r
			return true
		}
	}
	df := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p>Hello {{.UserName}}! Welcome to {{.SiteName}}! We've created your account with the username you selected and the following password: {{.Pass}}<br>You can change your password to whatever you want once you log in.</p></body></html>`

	r, err := template.New("reg").Parse(df)
	if err != nil {
		log.Fatal("Default registration template MUST compile. Error: " + err.Error())
	}
	c.Templates.Registration = r

	return false
}

func SetAlertTemplate(t string) bool {
	if len(t) != 0 {
		r, err := template.New("alert").Parse(t)
		if err != nil {
			c.Templates.Alert = r
			return true
		}
	}
	df := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p>    Hey {{.UserName}}! Just letting you know that {{.Activity}}.<br>    You can disable future notifications in your user settings.</p></body></html>`
	r, err := template.New("alert").Parse(df)
	if err != nil {
		log.Fatal("Default alert template MUST compile. Error: " + err.Error())
	}
	c.Templates.Alert = r
	return false
}

func SetRecoveryTemplate(t string) bool {
	if len(t) != 0 {
		r, err := template.New("recovery").Parse(t)
		if err != nil {
			c.Templates.Recovery = r
			return true
		}
	}
	df := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p>Hello {{.UserName}}! Someone (hopefully you) has attempted an account recovery agt {{.SiteName}}. If this was you, enter the following code to regain access: {{.RecoveryCode}}<br>    If this was not you, you can ignore this email.</p></body></html>`
	r, err := template.New("recovery").Parse(df)
	if err != nil {
		log.Fatal("Default alert template MUST compile. Error: " + err.Error())
	}
	c.Templates.Recovery = r

	return false
}

func SendMail(addr, from, subject, body string, to string) error {
	r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")

	c, err := smtp.Dial(addr)
	if err != nil {
		return err
	}
	defer c.Close()
	if err = c.Mail(r.Replace(from)); err != nil {
		return err
	}
	to = r.Replace(to)
	if err = c.Rcpt(to); err != nil {
		return err
	}

	w, err := c.Data()
	if err != nil {
		return err
	}

	msg := "To: " + to + "\r\n" +
		"From: " + from + "\r\n" +
		"Subject: " + subject + "\r\n" +
		"Content-Type: text/html; charset=\"UTF-8\"\r\n" +
		"\r\n" + body

	_, err = w.Write([]byte(msg))
	if err != nil {
		return err
	}
	err = w.Close()
	if err != nil {
		return err
	}
	return c.Quit()
}
