package authentication

import (
	"errors"
	"net/http"

	"github.com/pushbits/server/internal/authentication/credentials"
	"github.com/pushbits/server/internal/model"

	"github.com/gin-gonic/gin"
)

const (
	headerName = "X-Gotify-Key"
)

// The Database interface for encapsulating database access.
type Database interface {
	GetApplicationByToken(token string) (*model.Application, error)
	GetUserByName(name string) (*model.User, error)
}

// Authenticator is the provider for authentication middleware.
type Authenticator struct {
	DB Database
}

type hasUserProperty func(user *model.User) bool

func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error) {
	if name, password, ok := ctx.Request.BasicAuth(); ok {
		if user, err := a.DB.GetUserByName(name); err != nil {
			return nil, err
		} else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) {
			return user, nil
		} else {
			return nil, errors.New("credentials were invalid")
		}
	}

	return nil, errors.New("no credentials were supplied")
}

func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		user, err := a.userFromBasicAuth(ctx)
		if err != nil {
			ctx.AbortWithError(http.StatusForbidden, err)
			return
		}

		if !has(user) {
			ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed"))
			return
		}

		ctx.Set("user", user)
	}
}

// RequireUser returns a Gin middleware which requires valid user credentials to be supplied with the request.
func (a *Authenticator) RequireUser() gin.HandlerFunc {
	return a.requireUserProperty(func(user *model.User) bool {
		return true
	})
}

// RequireAdmin returns a Gin middleware which requires valid admin credentials to be supplied with the request.
func (a *Authenticator) RequireAdmin() gin.HandlerFunc {
	return a.requireUserProperty(func(user *model.User) bool {
		return user.IsAdmin
	})
}

func (a *Authenticator) tokenFromQueryOrHeader(ctx *gin.Context) string {
	if token := a.tokenFromQuery(ctx); token != "" {
		return token
	} else if token := a.tokenFromHeader(ctx); token != "" {
		return token
	}

	return ""
}

func (a *Authenticator) tokenFromQuery(ctx *gin.Context) string {
	return ctx.Request.URL.Query().Get("token")
}

func (a *Authenticator) tokenFromHeader(ctx *gin.Context) string {
	return ctx.Request.Header.Get(headerName)
}

// RequireApplicationToken returns a Gin middleware which requires an application token to be supplied with the request.
func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		token := a.tokenFromQueryOrHeader(ctx)

		app, err := a.DB.GetApplicationByToken(token)
		if err != nil {
			ctx.AbortWithError(http.StatusForbidden, err)
			return
		}

		ctx.Set("app", app)
	}
}