package auth

import (
	"net/http"
	"net/url"
	"time"

	"strconv"
	"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"
)

// AccessData represents an access grant (tokens, expiration, client, etc)
type AccessData struct {
	PreviousAuthorizeData *AuthorizeData
	PreviousAccessData    *AccessData // previous access data, when refreshing
	AccessToken           string
	RefreshToken          string
	ExpiresIn             int32
	CreatedAt             time.Time
	TokenType             string
	ProfileID             uuid.ID
	AuthRequest
}

// 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)
}

// 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 {
			ctx.RenderJSONError(w, ErrorInvalidRequest, "Invalid request method.", ctx.Config.DocumentationDomain)
			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)
		default:
			ctx.RenderJSONError(w, ErrorUnsupportedGrantType, "Unsupported grant type.", ctx.Config.DocumentationDomain)
			return
		}
	}
}

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

	code := r.Form.Get("code")
	// "code" is required
	if code == "" {
		ctx.RenderJSONError(w, ErrorInvalidRequest, "Code must be supplied.", ctx.Config.DocumentationDomain)
		return
	}

	// must have a valid client
	client, err := getClient(auth, ctx)
	if err != nil {
		if err == ClientNotFoundError || err == InvalidClientError {
			ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
		} else {
			ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		}
		return
	}

	// must be a valid authorization code
	authData, err := ctx.Tokens.GetAuthorization(code)
	if err != nil {
		// TODO: return error
		return
	}
	if authData.RedirectURI == "" {
		ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain)
		return
	}
	if authData.IsExpired() {
		ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain)
		return
	}

	// code must be from the client
	if !authData.Client.ID.Equal(client.ID) {
		ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain)
		return
	}

	// check redirect uri
	redirectURI := r.Form.Get("redirect_uri")
	if redirectURI == "" {
		redirectURI = client.RedirectURI
	}
	if err = validateURI(client.RedirectURI, redirectURI); err != nil {
		ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain)
		return
	}
	if authData.RedirectURI != redirectURI {
		ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain)
		return
	}

	data := AccessData{
		AuthRequest: AuthRequest{
			Client:      client,
			RedirectURI: redirectURI,
			Scope:       authData.Scope,
		},
		PreviousAuthorizeData: &authData,
	}

	err = fillTokens(&data, true, ctx)
	if err != nil {
		ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		return
	}
	ctx.RenderJSONToken(w, data)
}

func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	// get client authentication
	auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)

	if err != nil {
		ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
		return
	}

	code := r.Form.Get("refresh_token")

	// "refresh_token" is required
	if code == "" {
		ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain)
		return
	}

	// must have a valid client
	client, err := getClient(auth, ctx)
	if err != nil {
		if err == ClientNotFoundError || err == InvalidClientError {
			ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
		} else {
			ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		}
		return
	}

	// must be a valid refresh code
	refreshData, err := ctx.Tokens.GetRefresh(code)
	if err != nil {
		// TODO: return error
		return
	}

	// client must be the same as the previous token
	if !refreshData.Client.ID.Equal(client.ID) {
		ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain)
		return
	}

	scope := r.Form.Get("scope")
	if scope == "" {
		scope = refreshData.Scope
	}

	data := AccessData{
		AuthRequest: AuthRequest{
			Client: client,
			Scope:  scope,
		},
		PreviousAccessData: &refreshData,
	}
	err = fillTokens(&data, true, ctx)
	if err != nil {
		ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		return
	}
	ctx.RenderJSONToken(w, data)
}

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

	username := r.Form.Get("username")
	password := r.Form.Get("password")
	scope := r.Form.Get("scope")

	// "username" and "password" is required
	if username == "" || password == "" {
		ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain)
		return
	}

	// must have a valid client
	client, err := getClient(auth, ctx)
	if err != nil {
		if err == ClientNotFoundError || err == InvalidClientError {
			ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
		} else {
			ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		}
		return
	}

	_, err = ctx.Profiles.GetProfile(username, password)
	if err != nil {
		// TODO: return error
		return
	}

	data := AccessData{
		AuthRequest: AuthRequest{
			Client: client,
			Scope:  scope,
		},
	}

	err = fillTokens(&data, true, ctx)
	if err != nil {
		ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		return
	}
	ctx.RenderJSONToken(w, data)
}

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

	scope := r.Form.Get("scope")

	// must have a valid client
	client, err := getClient(auth, ctx)
	if err != nil {
		if err == ClientNotFoundError || err == InvalidClientError {
			ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
		} else {
			ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		}
		return
	}

	data := AccessData{
		AuthRequest: AuthRequest{
			Client: client,
			Scope:  scope,
		},
	}

	err = fillTokens(&data, true, ctx)
	if err != nil {
		ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		return
	}
	ctx.RenderJSONToken(w, data)
}

func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
	var err error

	// generate access token
	data.AccessToken = newToken()
	if includeRefresh {
		data.RefreshToken = newToken()
	}

	// save access token
	err = ctx.Tokens.SaveAccess(*data)
	if err != nil {
		// TODO: log error
		return InternalServerError
	}

	// remove authorization token
	if data.PreviousAuthorizeData != nil {
		err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
		if err != nil {
			// TODO: log error
		}
	}

	// remove previous access token
	if data.PreviousAccessData != nil {
		if data.PreviousAccessData.RefreshToken != "" {
			err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
			if err != nil {
				// TODO: log error
			}
		}
		err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
		if err != nil {
			// TODO: log error
		}
	}

	data.TokenType = ctx.Config.TokenType
	data.ExpiresIn = ctx.Config.AccessExpiration
	data.CreatedAt = time.Now()
	return nil
}

func (data AccessData) GetRedirect(fragment bool) (string, error) {
	u, err := url.Parse(data.RedirectURI)
	if err != nil {
		return "", err
	}

	// add parameters
	q := u.Query()
	q.Set("access_token", data.AccessToken)
	q.Set("token_type", data.TokenType)
	q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
	if data.RefreshToken != "" {
		q.Set("refresh_token", data.RefreshToken)
	}
	if data.Scope != "" {
		q.Set("scope", data.Scope)
	}
	if len(data.ProfileID) > 0 {
		q.Set("profile", data.ProfileID.String())
	}
	if fragment {
		u.RawQuery = ""
		u.Fragment = q.Encode()
	} else {
		u.RawQuery = q.Encode()
	}

	return u.String(), nil
}

// 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 := ctx.Clients.GetClient(id)
	if err != nil {
		if err == ClientNotFoundError {
			return Client{}, err
		}
		// TODO: log error
		return Client{}, InternalServerError
	}
	if client.Secret != auth.Password {
		return Client{}, InvalidClientError
	}
	if client.RedirectURI == "" {
		return Client{}, InvalidClientError
	}
	return client, nil
}
