package asiimov

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/derekdowling/go-stdlogger"

	"golang.org/x/net/context"
	"golang.org/x/oauth2"

	"code.justin.tv/systems/guardian/guardian"
)

// NonceStore describes the api for generating and checking nonces
type NonceStore interface {
	// generate and store a new nonce with expiry
	Generate() (nonce string, err error)

	// checks store to see if nonce is exists, hasn't been used and has not
	// expired
	Check(nonce string) (valid bool, err error)
}

// Asiimov implements middleware for handling requests that include GuardiaN
// oauth access tokens
type Asiimov struct {
	logger           std.Logger
	oauthConfig      *oauth2.Config
	allowedGroups    map[string]struct{} //for existence check
	disallowedGroups map[string]struct{} //for existence check
	VerifyAccess     func(user *guardian.User) error
	CheckTokenURL    string
	NonceStore       NonceStore
}

// guardian endpoints
const (
	AuthURL       = "https://guardian.internal.justin.tv/authorize"
	TokenURL      = "https://guardian.internal.justin.tv/oauth2/token"
	CheckTokenURL = "https://guardian.internal.justin.tv/oauth2/check_token"
)

// Endpoint is oauth2.Endpoint with guardian endpoints
var Endpoint = oauth2.Endpoint{
	AuthURL:  AuthURL,
	TokenURL: TokenURL,
}

// New returns a new asiimov instance
func New(logger std.Logger, oauthConfig *oauth2.Config) (a *Asiimov) {
	a = new(Asiimov)
	if logger != nil {
		a.logger = logger
	} else {
		// default stdlib logger
		a.logger = log.New(os.Stderr, "", log.LstdFlags)
	}

	a.oauthConfig = oauthConfig

	if a.oauthConfig.Endpoint.AuthURL == "" {
		a.oauthConfig.Endpoint.AuthURL = AuthURL
	}

	if a.oauthConfig.Endpoint.TokenURL == "" {
		a.oauthConfig.Endpoint.TokenURL = TokenURL
	}

	a.CheckTokenURL = CheckTokenURL

	a.VerifyAccess = a.DefaultVerifyAccess

	return
}

// ErrNilToken is returned when a nil token is provided to CheckToken
var ErrNilToken = errors.New("asiimov: nil token passed to CheckToken")

// CheckToken makes a request to the /check_token endpoint and returns a
// TokenCheck containing token and user information. If the token is invalid,
// TokenCheck is nil.
func (a *Asiimov) CheckToken(token *oauth2.Token) (tc *guardian.TokenCheck, err error) {
	if token == nil {
		err = ErrNilToken
		return
	}

	client := a.oauthConfig.Client(context.Background(), token)
	resp, err := client.Get(a.CheckTokenURL)
	if err != nil {
		err = fmt.Errorf("asiimov: error checking token: %s", err.Error())
		return
	}
	if resp != nil {
		defer func() {
			closeErr := resp.Body.Close()
			if err == nil {
				err = closeErr
			}
		}()
	}

	switch resp.StatusCode {
	case http.StatusOK:
	case http.StatusNotFound:
		return
	default:
		err = fmt.Errorf("asiimov: error checking token: received %v status code from %v instead of %v", resp.StatusCode, a.CheckTokenURL, http.StatusOK)
		return
	}

	tc = new(guardian.TokenCheck)
	err = json.NewDecoder(resp.Body).Decode(tc)
	if err != nil {
		err = fmt.Errorf("asiimov: error decoding token: %s", err.Error())
		return
	}
	return
}

// HandleGuardianRequest parses oauth token from request and fetches user
// information associated with the token.
func (a *Asiimov) HandleGuardianRequest(r *http.Request) (user *guardian.User, err error) {
	token := GetToken(r)
	if token == "" {
		return nil, nil
	}

	oauth2Token := &oauth2.Token{AccessToken: token}
	tc, err := a.CheckToken(oauth2Token)
	if err != nil {
		return nil, err
	}

	if tc == nil || tc.User == nil {
		return
	}
	user = tc.User
	if a.VerifyAccess == nil {
		return nil, fmt.Errorf("asiimov: Required value (a *Asiimov).VerifyAccess is nil")
	}
	err = a.VerifyAccess(user)
	if err != nil {
		return nil, err
	}
	return user, err
}

// AskForAuth sets the WWW-Authenticate header to the provided realm. If realm
// is empty, 'guardian' is used. Also sets http.StatusUnauthorized.
func AskForAuth(w http.ResponseWriter, r *http.Request, realm string) {
	const guardianRealm = "guardian"
	if realm == "" {
		realm = guardianRealm
	}
	w.Header().Set("WWW-Authenticate", fmt.Sprintf("OAuth realm='%s'", realm))
	w.WriteHeader(http.StatusUnauthorized)
	return
}

// GetToken parses oauth token from Authorization http header
func GetToken(r *http.Request) (token string) {
	_, token, _ = r.BasicAuth()
	if token != "" {
		return token
	}

	authHeader := strings.Split(r.Header.Get("Authorization"), " ")
	if len(authHeader) == 2 && strings.EqualFold(authHeader[0], "bearer") {
		if authHeader[1] != "" {
			token = authHeader[1]
			return token
		}
	}

	return ""
}
