package oauth2

import (
	"net/http"
	"time"

	"secondbit.org/uuid"
)

// GrantType is the type for OAuth param `grant_type`
type GrantType string

const (
	AuthorizationCodeGrant GrantType = "authorization_code"
	RefreshTokenGrant                = "refresh_token"
	PasswordGrant                    = "password"
	ClientCredentialsGrant           = "client_credentials"
	AssertionGrant                   = "assertion"
	ImplicitGrant                    = "__implicit"
)

// AccessRequest is a request for access tokens
type AccessRequest struct {
	Code          string
	Client        Client
	AuthorizeData AuthorizeData
	AccessData    AccessData
	RedirectURI   string
	Scope         string
	Username      string
	Password      string
	AssertionType string
	Assertion     string

	// Token expiration in seconds. Change if different from default
	Expiration int32

	// Set if a refresh token should be generated
	GenerateRefresh bool
}

// AccessData represents an access grant (tokens, expiration, client, etc)
type AccessData struct {
	// Client information
	Client Client

	// Authorize data, for authorization code
	AuthorizeData *AuthorizeData

	// Previous access data, for refresh token
	AccessData *AccessData

	// Access token
	AccessToken string

	// Refresh Token. Can be blank
	RefreshToken string

	// Token expiration in seconds
	ExpiresIn int32

	// Requested scope
	Scope string

	// Redirect URI from request
	RedirectURI string

	// Date created
	CreatedAt time.Time
}

// IsExpired returns true if access expired
func (d *AccessData) IsExpired() bool {
	return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now())
}

// ExpireAt returns the expiration date
func (d *AccessData) ExpireAt() time.Time {
	return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second)
}

// AccessTokenGen generates access tokens
type AccessTokenGen interface {
	GenerateAccessToken(data *AccessData, generaterefresh bool) (accesstoken string, refreshtoken string, err error)
}

// HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests.
func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	// Only allow GET or POST
	if r.Method != "POST" {
		if r.Method == "GET" && !ctx.Config.AllowGetAccessRequest {
			// TODO: return error
			return
		}
	}

	err := r.ParseForm()
	if err != nil {
		// TODO: return error
		return
	}

	grantType := GrantType(r.Form.Get("grant_type"))
	if ctx.Config.AllowedAccessTypes.Exists(grantType) {
		switch grantType {
		case AuthorizationCodeGrant:
			handleAuthorizationCodeRequest(w, r, ctx)
		case RefreshTokenGrant:
			handleRefreshTokenRequest(w, r, ctx)
		case PasswordGrant:
			handlePasswordRequest(w, r, ctx)
		case ClientCredentialsGrant:
			handleClientCredentialsRequest(w, r, ctx)
		case AssertionGrant:
			handleAssertionRequest(w, r, ctx)
		default:
			// TODO: return error
			return
		}
	}
}

func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	// get client authentication
	auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
	if err != nil {
		// TODO: return error
		return
	}

	// generate access token
	ret := AccessRequest{
		Code:            r.Form.Get("code"),
		RedirectURI:     r.Form.Get("redirect_uri"),
		GenerateRefresh: true,
		Expiration:      ctx.Config.AccessExpiration,
	}

	// "code" is required
	if ret.Code == "" {
		// TODO: return error
		return
	}

	// must have a valid client
	ret.Client, err = getClient(auth, ctx)
	if err != nil {
		// TODO: return error
		return
	}

	// must be a valid authorization code
	ret.AuthorizeData, err = loadAuthorize(ret.Code, ctx)
	if err != nil {
		// TODO: return error
		return
	}
	if ret.AuthorizeData.Client.RedirectURI == "" {
		// TODO: return error
		return
	}
	if ret.AuthorizeData.IsExpired() {
		return // TODO: return error
	}

	// code must be from the client
	if !ret.AuthorizeData.Client.ID.Equal(ret.Client.ID) {
		// TODO: return error
		return
	}

	// check redirect uri
	if ret.RedirectURI == "" {
		ret.RedirectURI = ret.Client.RedirectURI
	}
	if err = ValidateURI(ret.Client.RedirectURI, ret.RedirectURI); err != nil {
		// TODO: return error
		return
	}
	if ret.AuthorizeData.RedirectURI != ret.RedirectURI {
		// TODO: return error
		return
	}

	// set rest of data
	ret.Scope = ret.AuthorizeData.Scope
	// TODO: write ret
}

func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	// get client authentication
	auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
	if err != nil {
		// TODO: return error
		return
	}

	// generate access token
	ret := AccessRequest{
		Code:            r.Form.Get("refresh_token"),
		Scope:           r.Form.Get("scope"),
		GenerateRefresh: true,
		Expiration:      ctx.Config.AccessExpiration,
	}

	// "refresh_token" is required
	if ret.Code == "" {
		// TODO: return error
		return
	}

	// must have a valid client
	ret.Client, err = getClient(auth, ctx)
	if err != nil {
		// TODO: return error
		return
	}

	// must be a valid refresh code
	ret.AccessData, err = loadRefresh(ret.Code, ctx)
	if err != nil {
		// TODO: return error
		return
	}
	if ret.AccessData.Client.RedirectURI == "" {
		// TODO: return error
		return
	}

	// client must be the same as the previous token
	if !ret.AccessData.Client.ID.Equal(ret.Client.ID) {
		// TODO: return error
		return

	}

	// set rest of data
	ret.RedirectURI = ret.AccessData.RedirectURI
	if ret.Scope == "" {
		ret.Scope = ret.AccessData.Scope
	}

	// TODO: write ret
}

func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	// get client authentication
	auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
	if err != nil {
		// TODO: return error
		return
	}

	// generate access token
	ret := AccessRequest{
		Username:        r.Form.Get("username"),
		Password:        r.Form.Get("password"),
		Scope:           r.Form.Get("scope"),
		GenerateRefresh: true,
		Expiration:      ctx.Config.AccessExpiration,
	}

	// "username" and "password" is required
	if ret.Username == "" || ret.Password == "" {
		// TODO: return error
		return
	}

	// must have a valid client
	ret.Client, err = getClient(auth, ctx)
	if err != nil {
		// TODO: return error
		return
	}

	// set redirect uri
	ret.RedirectURI = ret.Client.RedirectURI

	// TODO: write ret
}

func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	// get client authentication
	auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
	if err != nil {
		// TODO: return error
		return
	}

	// generate access token
	ret := AccessRequest{
		Scope:           r.Form.Get("scope"),
		GenerateRefresh: true,
		Expiration:      ctx.Config.AccessExpiration,
	}

	// must have a valid client
	ret.Client, err = getClient(auth, ctx)
	if err != nil {
		// TODO: return error
		return
	}

	// set redirect uri
	ret.RedirectURI = ret.Client.RedirectURI

	// TODO: write ret
}

func handleAssertionRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	// get client authentication
	auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
	if err != nil {
		// TODO: return error
		return
	}

	// generate access token
	ret := &AccessRequest{
		Scope:           r.Form.Get("scope"),
		AssertionType:   r.Form.Get("assertion_type"),
		Assertion:       r.Form.Get("assertion"),
		GenerateRefresh: false, // assertion should NOT generate a refresh token, per the RFC
		Expiration:      ctx.Config.AccessExpiration,
	}

	// "assertion_type" and "assertion" is required
	if ret.AssertionType == "" || ret.Assertion == "" {
		// TODO: return error
		return
	}

	// must have a valid client
	ret.Client, err = getClient(auth, ctx)
	if err != nil {
		//TODO: return error
		return
	}

	// set redirect uri
	ret.RedirectURI = ret.Client.RedirectURI

	// TODO: write ret
}

func FinishAccessRequest(w http.ResponseWriter, r *http.Request, ar AccessRequest, ctx Context) {
	// TODO: check if authorized?
	redirectURI := r.Form.Get("redirect_uri")
	// Get redirect uri from AccessRequest if it's there (e.g., refresh token request)
	if ar.RedirectURI != "" {
		redirectURI = ar.RedirectURI
	}
	ret := AccessData{
		Client:        ar.Client,
		AuthorizeData: &ar.AuthorizeData,
		AccessData:    &ar.AccessData,
		RedirectURI:   redirectURI,
		CreatedAt:     time.Now(),
		ExpiresIn:     ar.Expiration,
		Scope:         ar.Scope,
	}

	var err error

	// generate access token
	ret.AccessToken = newToken()
	if ar.GenerateRefresh {
		ret.RefreshToken = newToken()
	}

	// save access token
	err = saveAccess(ret, ctx)
	if err != nil {
		// TODO: return error
		return
	}

	// remove authorization token
	if ret.AuthorizeData != nil {
		err = removeAuthorize(ret.AuthorizeData.Code, ctx)
		if err != nil {
			// TODO: log error
		}
	}

	// remove previous access token
	if ret.AccessData != nil {
		if ret.AccessData.RefreshToken != "" {
			err = removeRefresh(ret.AccessData.RefreshToken, ctx)
			if err != nil {
				// TODO: log error
			}
		}
		err = removeAccess(ret.AccessData.AccessToken, ctx)
		if err != nil {
			// TODO: log error
		}
	}

	// output data
	//w.Output["access_token"] = ret.AccessToken
	//w.Output["token_type"] = ctx.Config.TokenType
	//w.Output["expires_in"] = ret.ExpiresIn
	//if ret.RefreshToken != "" {
	//	w.Output["refresh_token"] = ret.RefreshToken
	//}
	//if ar.Scope != "" {
	//	w.Output["scope"] = ar.Scope
	//}
	// TODO: write ret
}

// Helper Functions

// getClient looks up and authenticates the basic auth using the given
// storage. Sets an error on the response if auth fails or a server error occurs.
func getClient(auth BasicAuth, ctx Context) (Client, error) {
	id, err := uuid.Parse(auth.Username)
	if err != nil {
		return Client{}, err
	}
	client, err := GetClient(id, ctx)
	if err != nil {
		// TODO: abstract out errors
		return Client{}, err
	}
	if client.Secret != auth.Password {
		// TODO: return E_UNAUTHORIZED_CLIENT error
		return Client{}, nil
	}
	if client.RedirectURI == "" {
		// TODO: return E_UNAUTHORIZED_CLIENT error
		return Client{}, nil
	}
	return client, nil
}

func loadRefresh(code string, ctx Context) (AccessData, error) {
	return AccessData{}, nil
}

func loadAccess(code string, ctx Context) (AccessData, error) {
	return AccessData{}, nil
}

func saveAccess(data AccessData, ctx Context) error {
	return nil
}

func removeAccess(token string, ctx Context) error {
	return nil
}

func removeRefresh(token string, ctx Context) error {
	return nil
}
