package auth

import (
	"errors"
	"fmt"
	"net/http"
	"path"
	"strings"
	"time"

	"github.com/go-chi/render"
	validation "github.com/go-ozzo/ozzo-validation"
	"github.com/go-ozzo/ozzo-validation/is"
	"github.com/mssola/user_agent"
	uuid "github.com/satori/go.uuid"

	"github.com/dhax/go-base/email"
)

// The list of error types presented to the end user as error message.
var (
	ErrInvalidLogin  = errors.New("invalid email address")
	ErrUnknownLogin  = errors.New("email not registered")
	ErrLoginDisabled = errors.New("login for account disabled")
	ErrLoginToken    = errors.New("invalid or expired login token")
)

type loginRequest struct {
	Email string
}

func (body *loginRequest) Bind(r *http.Request) error {
	body.Email = strings.TrimSpace(body.Email)
	body.Email = strings.ToLower(body.Email)

	if err := validation.ValidateStruct(body,
		validation.Field(&body.Email, validation.Required, is.Email),
	); err != nil {
		return err
	}
	return nil
}

func (rs *Resource) login(w http.ResponseWriter, r *http.Request) {
	body := &loginRequest{}
	if err := render.Bind(r, body); err != nil {
		log(r).WithField("email", body.Email).Warn(err)
		render.Render(w, r, ErrUnauthorized(ErrInvalidLogin))
		return
	}

	acc, err := rs.store.GetByEmail(body.Email)
	if err != nil {
		log(r).WithField("email", body.Email).Warn(err)
		render.Render(w, r, ErrUnauthorized(ErrUnknownLogin))
		return
	}

	if !acc.CanLogin() {
		render.Render(w, r, ErrUnauthorized(ErrLoginDisabled))
		return
	}

	lt := rs.Login.CreateToken(acc.ID)

	go func() {
		content := email.ContentLoginToken{
			Email:  acc.Email,
			Name:   acc.Name,
			URL:    path.Join(rs.Login.loginURL, lt.Token),
			Token:  lt.Token,
			Expiry: lt.Expiry,
		}
		if err := rs.mailer.LoginToken(acc.Name, acc.Email, content); err != nil {
			log(r).WithField("module", "email").Error(err)
		}
	}()

	render.Respond(w, r, http.NoBody)
}

type tokenRequest struct {
	Token string `json:"token"`
}

type tokenResponse struct {
	Access  string `json:"access_token"`
	Refresh string `json:"refresh_token"`
}

func (body *tokenRequest) Bind(r *http.Request) error {
	body.Token = strings.TrimSpace(body.Token)

	if err := validation.ValidateStruct(body,
		validation.Field(&body.Token, validation.Required, is.Alphanumeric),
	); err != nil {
		return err
	}
	return nil
}

func (rs *Resource) token(w http.ResponseWriter, r *http.Request) {
	body := &tokenRequest{}
	if err := render.Bind(r, body); err != nil {
		log(r).Warn(err)
		render.Render(w, r, ErrUnauthorized(ErrLoginToken))
		return
	}

	id, err := rs.Login.GetAccountID(body.Token)
	if err != nil {
		render.Render(w, r, ErrUnauthorized(ErrLoginToken))
		return
	}

	acc, err := rs.store.GetByID(id)
	if err != nil {
		// account deleted before login token expired
		render.Render(w, r, ErrUnauthorized(ErrUnknownLogin))
		return
	}

	if !acc.CanLogin() {
		render.Render(w, r, ErrUnauthorized(ErrLoginDisabled))
		return
	}

	ua := user_agent.New(r.UserAgent())
	browser, _ := ua.Browser()

	token := &Token{
		Token:      uuid.NewV4().String(),
		Expiry:     time.Now().Add(time.Minute * rs.Token.jwtRefreshExpiry),
		UpdatedAt:  time.Now(),
		AccountID:  acc.ID,
		Mobile:     ua.Mobile(),
		Identifier: fmt.Sprintf("%s on %s", browser, ua.OS()),
	}

	if err := rs.store.SaveRefreshToken(token); err != nil {
		log(r).Error(err)
		render.Render(w, r, ErrInternalServerError)
		return
	}

	access, refresh, err := rs.Token.GenTokenPair(acc.Claims(), token.Claims())
	if err != nil {
		log(r).Error(err)
		render.Render(w, r, ErrInternalServerError)
		return
	}

	acc.LastLogin = time.Now()
	if err := rs.store.UpdateAccount(acc); err != nil {
		log(r).Error(err)
		render.Render(w, r, ErrInternalServerError)
		return
	}

	render.Respond(w, r, &tokenResponse{
		Access:  access,
		Refresh: refresh,
	})
}

func (rs *Resource) refresh(w http.ResponseWriter, r *http.Request) {
	rt := RefreshTokenFromCtx(r.Context())

	acc, token, err := rs.store.GetByRefreshToken(rt)
	if err != nil {
		render.Render(w, r, ErrUnauthorized(errTokenExpired))
		return
	}

	if time.Now().After(token.Expiry) {
		rs.store.DeleteRefreshToken(token)
		render.Render(w, r, ErrUnauthorized(errTokenExpired))
		return
	}

	if !acc.CanLogin() {
		render.Render(w, r, ErrUnauthorized(ErrLoginDisabled))
		return
	}

	token.Token = uuid.NewV4().String()
	token.Expiry = time.Now().Add(time.Minute * rs.Token.jwtRefreshExpiry)
	token.UpdatedAt = time.Now()

	access, refresh, err := rs.Token.GenTokenPair(acc.Claims(), token.Claims())
	if err != nil {
		log(r).Error(err)
		render.Render(w, r, ErrInternalServerError)
		return
	}

	if err := rs.store.SaveRefreshToken(token); err != nil {
		log(r).Error(err)
		render.Render(w, r, ErrInternalServerError)
		return
	}

	acc.LastLogin = time.Now()
	if err := rs.store.UpdateAccount(acc); err != nil {
		log(r).Error(err)
		render.Render(w, r, ErrInternalServerError)
		return
	}

	render.Respond(w, r, &tokenResponse{
		Access:  access,
		Refresh: refresh,
	})
}

func (rs *Resource) logout(w http.ResponseWriter, r *http.Request) {
	rt := RefreshTokenFromCtx(r.Context())
	_, token, err := rs.store.GetByRefreshToken(rt)
	if err != nil {
		render.Render(w, r, ErrUnauthorized(errTokenExpired))
		return
	}
	rs.store.DeleteRefreshToken(token)

	render.Respond(w, r, http.NoBody)
}
