package main

import (
	"crypto/rand"
	"fmt"
	"github.com/gomodule/redigo/redis"
	"golang.org/x/crypto/bcrypt"
	"log"
	"strconv"
	"strings"
	"time"
)

var globalPool *redis.Pool

const userPrefix string = "user:"
const devPrefix string = "dev:"
const confirmPrefix string = "confirm:"

func newPool() *redis.Pool {
	return &redis.Pool{
		// Maximum number of idle connections in the pool.
		MaxIdle: 80,
		// max number of connections
		MaxActive: 12000,
		// Dial is an application supplied function for creating and
		// configuring a connection.
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", getenv("REDIS_CONNECTION_STRING", ":6379"))
			if err != nil {
				panic(err.Error())
			}
			return c, err
		},
	}
}

// ping tests connectivity for redis (PONG should be returned)
func ping(c redis.Conn) error {
	// Send PING command to Redis
	// PING command returns a Redis "Simple String"
	// Use redis.String to convert the interface type to string
	s, err := redis.String(c.Do("PING"))
	if err != nil {
		return err
	}

	logit("PING Response = " + s)
	// Output: PONG

	return nil
}

type Dev struct {
	Deveui           string
	Alias            string
	SmsAlarmactive   string
	Smsnumber        string
	EmailAlarmactive string
	Email            string
	Greenzone        string
	ActiveUntil      string // Abo bezahlt bis TT.MM.YYYY
}

func initDB() {

	// newPool returns a pointer to a redis.Pool
	pool := newPool()
	// get a connection from the globalPool (redis.Conn)
	conn := pool.Get()
	defer conn.Close()

	globalPool = pool

	// wir machen einen Connection Test
	ping(conn)

	// Wir legen einen initialen Admin User an, falls es diesen noch nicht gibt
	if checkUserAvailable("joerg.lehmann@nbit.ch") {
		insertUser("joerg.lehmann@nbit.ch", "changeme123", "Y")
	}

}

func closeDB() {
	globalPool.Close()
}

func getUsers() []string {
	res := []string{}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getUsers")
	users, err := redis.Strings(conn.Do("KEYS", userPrefix+"*"))
	if err == nil {
		logit("getUsers successful!")
		res = users
	} else {
		log.Print(err)
	}

	return res
}

func updateTrackerSettings(trackerSettings Dev) error {
	conn := globalPool.Get()
	defer conn.Close()

	// SET object
	_, err := conn.Do("HMSET", devPrefix+trackerSettings.Deveui, "alias", trackerSettings.Alias,
		"smsalarmactive", trackerSettings.SmsAlarmactive,
		"smsnumber", trackerSettings.Smsnumber,
		"emailalarmactive", trackerSettings.EmailAlarmactive,
		"email", trackerSettings.Email,
		"greenzone", trackerSettings.Greenzone)

	if err != nil {
		return err
	}

	return nil
}

func updateTrackerGreenzone(deveui string, greenzone string) error {
	conn := globalPool.Get()
	defer conn.Close()

	// SET object
	_, err := conn.Do("HMSET", devPrefix+deveui,
		"greenzone", greenzone)

	if err != nil {
		return err
	}

	return nil
}

func checkUserAvailable(username string) bool {
	logit("checkUserAvailable: User: " + username)
	conn := globalPool.Get()
	defer conn.Close()

	res, err := redis.Int(conn.Do("EXISTS", userPrefix+username))
	if err == nil {
		logit("Result of EXISTS: " + strconv.Itoa(res))
		return res == 0
	} else {
		logit("checkUserAvailable: Error to query Key Value Store")
		return false
	}
}

func getMyDevs(username string) []string {
	res := []string{}

	if username == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getMyDevs: User: " + username)
	mydevs, err := redis.String(conn.Do("HGET", userPrefix+username, "my_devs"))
	if err == nil {
		logit("getMyDevs: mydevs: " + mydevs)
		res = strings.Split(mydevs, ",")
	} else {
		log.Print(err)
	}

	return res
}

func getDevAlias(deveui string) string {
	res := deveui

	if deveui == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getDevAlias: Deveui: " + deveui)
	alias, err := redis.String(conn.Do("HGET", devPrefix+deveui, "alias"))
	if err == nil {
		logit("getDevAlias: alias: " + alias)
		res = alias
	} else {
		log.Print(err)
	}

	return res
}

func getDevSmsalarmactive(deveui string) string {
	res := "0"

	if deveui == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getDevSmslarmactive: Deveui: " + deveui)
	smsalarmactive, err := redis.String(conn.Do("HGET", devPrefix+deveui, "smsalarmactive"))
	if err == nil {
		logit("getDevSmsalarmactive: smsalarmactive: " + smsalarmactive)
		res = smsalarmactive
	} else {
		log.Print(err)
	}

	return res
}

func getDevSmsnumber(deveui string) string {
	res := "+4179XXXXXXX"

	if deveui == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getDevSmsnumber: Deveui: " + deveui)
	smsnumber, err := redis.String(conn.Do("HGET", devPrefix+deveui, "smsnumber"))
	if err == nil {
		logit("getDevSmsnumber: smsnumber: " + smsnumber)
		res = smsnumber
	} else {
		log.Print(err)
	}

	return res
}

func getDevEmailalarmactive(deveui string) string {
	res := "0"

	if deveui == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getDevEmailarmactive: Deveui: " + deveui)
	emailalarmactive, err := redis.String(conn.Do("HGET", devPrefix+deveui, "emailalarmactive"))
	if err == nil {
		logit("getDevEmailarmactive: emailalarmactive: " + emailalarmactive)
		res = emailalarmactive
	} else {
		log.Print(err)
	}

	return res
}

func getDevEmail(deveui string) string {
	res := ""

	if deveui == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getDevEmail: Deveui: " + deveui)
	email, err := redis.String(conn.Do("HGET", devPrefix+deveui, "email"))
	if err == nil {
		logit("getDevEmail: email: " + email)
		res = email
	} else {
		log.Print(err)
	}

	return res
}

func getDevGreenzone(deveui string) string {
	res := ""

	if deveui == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getDevGreenzone: Deveui: " + deveui)
	greenzone, err := redis.String(conn.Do("HGET", devPrefix+deveui, "greenzone"))
	if err == nil {
		logit("getDevGreenzone: greenzone: " + greenzone)
		res = greenzone
	} else {
		log.Print(err)
	}

	return res
}

func getActiveUntil(deveui string) string {
	res := ""

	if deveui == "" {
		return res
	}

	conn := globalPool.Get()
	defer conn.Close()

	logit("getActiveUntil: Deveui: " + deveui)
	activeuntil, err := redis.String(conn.Do("HGET", devPrefix+deveui, "active_until"))
	if err == nil {
		logit("getActiveUntil: activeuntil: " + activeuntil)
		res = activeuntil
	} else {
		log.Print(err)
	}

	return res
}

func getYearlyAboCost(deveui string) int {
	res := 0

	logit("getYearlyAboCost: Deveui: " + deveui)

	conn := globalPool.Get()
	defer conn.Close()

	// first we get the default
	yearlyAboCostDefault, err := redis.Int(conn.Do("HGET", "settings", "default_yearly_abo_cost"))
	if err == nil {
		yearlyAboCost, err := redis.Int(conn.Do("HGET", devPrefix+deveui, "yearly_abo_cost"))
		if err == nil {
			res = yearlyAboCost
		} else {
			res = yearlyAboCostDefault
		}
	} else {
		log.Print(err)

	}

	return res
}

func AboExpired(deveui string) bool {
	active_until := getActiveUntil(deveui)

	layout := "02.01.2006"
	t, _ := time.Parse(layout, active_until)

	return t.Before(time.Now())
}

func prolongActivation(deveui string, years int) (string, error) {
	conn := globalPool.Get()
	defer conn.Close()

	active_until_old, err := redis.String(conn.Do("HGET", devPrefix+deveui, "active_until"))
	if err == nil {
		logit("prolongActivation: active_until: " + active_until_old)
	} else {
		log.Print(err)
	}

	layout := "02.01.2006"
	t, err := time.Parse(layout, active_until_old)

	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(t.Unix())

	var t_new time.Time
	t_new = t.AddDate(years, 0, 0)
	active_until_new := t_new.Format(layout)

	// SET object
	_, err1 := conn.Do("HMSET", devPrefix+deveui, "active_until", active_until_new)
	if err1 != nil {
		return "", err1
	}

	return active_until_new, nil
}

func randString(n int) string {
	const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
	var bytes = make([]byte, n)
	rand.Read(bytes)
	for i, b := range bytes {
		bytes[i] = alphanum[b%byte(len(alphanum))]
	}
	return string(bytes)
}

func insertUser(username, password, is_admin string) {
	conn := globalPool.Get()
	defer conn.Close()
	logit("insertUser: " + username)
	pwd := []byte(password)
	hashedPassword, err := bcrypt.GenerateFromPassword(pwd, bcrypt.DefaultCost)
	if err != nil {
		logit("insertUser: Error with bcrypt.GenerateFromPassword, User: " + username)
		return
	}
	confirm_id := ""

	_, err = conn.Do("HMSET", userPrefix+username, "password", string(hashedPassword), "new_password", string(hashedPassword), "confirm_id", confirm_id, "last_login", "", "my_devs", "")

	if err != nil {
		logit("insertUser: Error inserting User: " + username)
		return
	}
}

func updateUser(username, password string) {
	conn := globalPool.Get()
	defer conn.Close()
	logit("updateUser: " + username)
	pwd := []byte(password)
	hashedPassword, err := bcrypt.GenerateFromPassword(pwd, bcrypt.DefaultCost)
	if err != nil {
		logit("updateUser: Error with bcrypt.GenerateFromPassword, User: " + username)
		return
	}
	confirm_id := randString(30)

	_, err = conn.Do("HMSET", userPrefix+username, "new_password", string(hashedPassword), "confirm_id", confirm_id)
	if err != nil {
		logit("updateUser: Error updateing User: " + username)
		return
	}
	_, err = conn.Do("SET", confirmPrefix+confirm_id, username)
	if err != nil {
		logit("updateUser: Error inserting confirm_id: " + confirm_id + ": " + username)
		return
	}

	sendEmailConfirm(username, confirm_id)
}

func checkLoginCredentials(username, password string) bool {
	conn := globalPool.Get()
	defer conn.Close()

	logit("checkLoginCredentials: called with username,password: " + username + "," + password)
	pwd, err := redis.String(conn.Do("HGET", userPrefix+username, "password"))
	if err == nil {
		logit("checkLoginCredentials: pwd: " + pwd + " CMD: HGET " + userPrefix + username + userPrefix + username + " confirm_id")
		cid, err := redis.String(conn.Do("HGET", userPrefix+username, "confirm_id"))
		if err == nil {
			logit("checkLoginCredentials: cid: " + cid)
			if !(err != nil && cid != "") {
				logit("checkLoginCredentials: pwd: " + pwd)
				if err != nil {
					return false
				}
			}
		}
	} else {
		log.Print(err)
		return false
	}

	hashedPassword := []byte(pwd)
	err = bcrypt.CompareHashAndPassword(hashedPassword, []byte(password))
	return err == nil
}

func updateLoginTime(username string) {
	conn := globalPool.Get()
	defer conn.Close()

	_, err := conn.Do("HSET", userPrefix+username, "last_login", time.Now().UTC().Format("2006-01-02 15:04:05"))
	if err != nil {
		logit("updateUser: Error updateing User: " + username)
		return
	}
}

func confirmUser(confirm_id string) bool {
	conn := globalPool.Get()
	defer conn.Close()

	u, err := redis.String(conn.Do("GET", confirmPrefix+confirm_id))
	if err != nil {
		logit("confirmUser: Error with searching confirm_id: " + confirm_id)
		return false
	}
	new_password, err := redis.String(conn.Do("HGET", userPrefix+u, "new_password"))
	if err != nil {
		logit("confirmUser: Error with getting new_password: " + u)
		return false
	}
	_, err = conn.Do("HMSET", userPrefix+u, "confirm_id", "", "password", new_password)
	if err != nil {
		logit("confirmUser: Error updateing User: " + u)
		return false
	}
	_, err = conn.Do("DEL", confirmPrefix+confirm_id)
	if err != nil {
		logit("confirmUser: Error deleting confirm_id: " + confirm_id)
		return false
	}

	return true
}

func InsertAlert(prefix string, deveui string, email string, threshold int) {
	conn := globalPool.Get()
	defer conn.Close()

	_, err := conn.Do("SET", prefix+deveui+":"+email, threshold)
	if err != nil {
		logit("InsertAlert: Error inserting: " + prefix + deveui + ":" + email)
	}
}

func DeleteAlert(prefix string, deveui string, email string) {
	conn := globalPool.Get()
	defer conn.Close()

	exists, _ := redis.Bool(conn.Do("EXISTS", prefix+deveui+":"+email))

	if exists {
		_, err := conn.Do("DEL", prefix+deveui+":"+email)
		if err != nil {
			logit("DeleteAlert: Error deleting: " + prefix + deveui + ":" + email)
		}
	}
}

func AlarmNotAlreadySent(prefix string, deveui string, email string, threshold int) bool {
	conn := globalPool.Get()
	defer conn.Close()

	exists, _ := redis.Bool(conn.Do("EXISTS", prefix+deveui+":"+email))
	if !exists {
		return true
	}
	alarm_threshold, _ := redis.Int(conn.Do("GET", prefix+deveui+":"+email))
	return threshold != alarm_threshold
}
