package models

import (
	"errors"
	"fmt"
	"strings"

	"git.kirsle.net/apps/gophertype/pkg/console"
	"git.kirsle.net/apps/gophertype/pkg/constants"
	"golang.org/x/crypto/bcrypt"
)

type userMan struct{}

// Users is a singleton helper to deal with user models.
var Users = userMan{}

// User account for the site.
type User struct {
	BaseModel

	Email          string `gorm:"unique_index"`
	Name           string
	HashedPassword string `json:"-"`
	IsAdmin        bool   `gorm:"index"`

	// Relationships
	Posts []Post `gorm:"foreignkey:AuthorID"`
}

// Validate the User object has everything filled in. Fixes what it can,
// returns an error if something is wrong. Ensures the HashedPassword is hashed.
func (u *User) Validate() error {
	u.Email = strings.TrimSpace(strings.ToLower(u.Email))
	u.Name = strings.TrimSpace(u.Name)

	if len(u.Email) == 0 {
		return errors.New("Email is required")
	}
	return nil
}

// AuthenticateUser checks a login for an email and password.
func (m userMan) AuthenticateUser(email string, password string) (User, error) {
	user, err := m.GetUserByEmail(email)
	if err != nil {
		console.Error("AuthenticateUser: email %s not found: %s", email, err)
		return User{}, errors.New("incorrect email or password")
	}

	if user.VerifyPassword(password) {
		return user, nil
	}

	return User{}, errors.New("incorrect email or password")
}

// GetUserByID looks up a user by their ID.
func (m userMan) GetUserByID(id int) (User, error) {
	var user User
	r := DB.First(&user, id)
	return user, r.Error
}

// GetUserByEmail looks up a user by their email address.
func (m userMan) GetUserByEmail(email string) (User, error) {
	var user User
	r := DB.Where("email = ?", strings.ToLower(email)).First(&user)
	return user, r.Error
}

// ListAdminEmails returns the array of email addresses of all admin users.
func (m userMan) ListAdminEmails() ([]string, error) {
	var (
		users  []User
		emails []string
	)
	r := DB.Where("is_admin=true AND email IS NOT NULL").Find(&users)
	for _, user := range users {
		if len(user.Email) > 0 {
			emails = append(emails, user.Email)
		}
	}
	return emails, r.Error
}

// SetPassword stores the hashed password for a user.
func (u *User) SetPassword(password string) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), constants.BcryptCost)
	if err != nil {
		return fmt.Errorf("SetPassword: %s", err)
	}

	u.HashedPassword = string(hash)
	return nil
}

// VerifyPassword checks if the password matches the user's hashed password.
func (u *User) VerifyPassword(password string) bool {
	if u.HashedPassword == "" {
		console.Error("ERROR: VerifyPassword: user %s has no HashedPassword", u.Email)
		return false
	}

	err := bcrypt.CompareHashAndPassword([]byte(u.HashedPassword), []byte(password))
	if err == nil {
		return true
	}
	console.Error("VerifyPassword: %s", err)
	return false
}

// FirstAdmin returns the admin user with the lowest ID number.
func (m userMan) FirstAdmin() (User, error) {
	var user User
	r := DB.First(&user, "is_admin = ?", true)
	return user, r.Error
}

// CreateUser adds a new user to the database.
func (m userMan) CreateUser(u User) error {
	if err := u.Validate(); err != nil {
		return err
	}

	r := DB.Create(&u)
	return r.Error
}
