package asiimov

import (
	"encoding/json"
	"errors"
	"net/http"

	"golang.org/x/oauth2"
)

// Error is an error returned by an asiimov handler that should be
// returned to the user
type Error interface {
	// actual error message. use Message() to retrieve the error message to
	// return to the user
	Error() string

	// http status code recommended for this error
	StatusCode() int

	// error message ready to be returned to the user
	Message() string
}

type asiimovError struct {
	err        error
	statusCode int
	msg        string
}

func (a asiimovError) Error() string { return a.err.Error() }

func (a asiimovError) StatusCode() int { return a.statusCode }

func (a asiimovError) Message() string {
	if a.msg != "" {
		return a.msg
	}
	return http.StatusText(a.statusCode)
}

const oauthStateMismatch = "oauth2 state mismatch"

var (
	// ErrOAuthStateMismatch is returned when the state parameter returned in
	// the redirect does not match the original value
	ErrOAuthStateMismatch = asiimovError{
		err:        errors.New(oauthStateMismatch),
		statusCode: http.StatusBadRequest,
		msg:        oauthStateMismatch,
	}
)

func (a *Asiimov) handleCallback(w http.ResponseWriter, r *http.Request) (token *oauth2.Token, err Error) {
	nonce := r.FormValue("state")
	validNonce, checkErr := a.NonceStore.Check(nonce)
	if checkErr != nil {
		err = asiimovError{
			err:        checkErr,
			statusCode: http.StatusInternalServerError,
		}
		return
	}
	if !validNonce {
		err = ErrOAuthStateMismatch
		return
	}

	authCode := r.FormValue("code")

	var exchangeErr error
	token, exchangeErr = a.oauthConfig.Exchange(oauth2.NoContext, authCode)
	if exchangeErr != nil {
		err = asiimovError{
			err:        exchangeErr,
			statusCode: http.StatusUnauthorized,
		}
		return
	}
	return
}

// ErrorHandler handles errors from the oauth flow
type ErrorHandler func(err Error, w http.ResponseWriter, r *http.Request)

// TokenHandler is called when the oauth flow is complete
type TokenHandler func(token *oauth2.Token, w http.ResponseWriter, r *http.Request)

// OAuth2CallbackHandler creates a context-enabled http.Handler
func (a *Asiimov) OAuth2CallbackHandler(errorHandler ErrorHandler, tokenHandler TokenHandler) (handler http.Handler) {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token, err := a.handleCallback(w, r)
		if err != nil {
			errorHandler(err, w, r)
			return
		}
		tokenHandler(token, w, r)
	})
}

// OAuth2RedirectHandler creates an http.Handler for the oauth2 redirect
func (a *Asiimov) OAuth2RedirectHandler() (handler http.Handler) {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		nonce, err := a.NonceStore.Generate()
		if err != nil {
			a.logger.Printf("could not generate a nonce for guardian: %s", err.Error())
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
		http.Redirect(w, r, a.oauthConfig.AuthCodeURL(nonce, oauth2.AccessTypeOnline), http.StatusFound)
		return
	})
}

// JSONTokenHandler json marshals the token and writes it in the response
func JSONTokenHandler(token *oauth2.Token, w http.ResponseWriter, r *http.Request) {
	err := json.NewEncoder(w).Encode(token)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	return
}

// JSONErrorHandler json marhals the error and writes it in the response
func JSONErrorHandler(err Error, w http.ResponseWriter, r *http.Request) {
	http.Error(w, err.Message(), err.StatusCode())
	return
}
