package auth

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

	"code.secondbit.org/uuid"
)

func init() {
	RegisterGrantType("authorization_code", GrantType{
		Validate:      authCodeGrantValidate,
		Invalidate:    authCodeGrantInvalidate,
		IssuesRefresh: true,
		ReturnToken:   RenderJSONToken,
	})
}

var (
	// ErrNoAuthorizationCodeStore is returned when a Context tries to act on a authorizationCodeStore without setting one first.
	ErrNoAuthorizationCodeStore = errors.New("no authorizationCodeStore was specified for the Context")
	// ErrAuthorizationCodeNotFound is returned when an AuthorizationCode is requested but not found in the authorizationCodeStore.
	ErrAuthorizationCodeNotFound = errors.New("authorization code not found in authorizationCodeStore")
	// ErrAuthorizationCodeAlreadyExists is returned when an AuthorizationCode is added to a authorizationCodeStore, but another AuthorizationCode with the
	// same Code already exists in the authorizationCodeStore.
	ErrAuthorizationCodeAlreadyExists = errors.New("authorization code already exists in authorizationCodeStore")
)

// AuthorizationCode represents an authorization grant made by a user to a Client, to
// access user data within a defined Scope for a limited amount of time.
type AuthorizationCode struct {
	Code        string
	Created     time.Time
	ExpiresIn   int32
	ClientID    uuid.ID
	Scope       string
	RedirectURI string
	State       string
	ProfileID   uuid.ID
	Used        bool
}

type authorizationCodeStore interface {
	getAuthorizationCode(code string) (AuthorizationCode, error)
	saveAuthorizationCode(authCode AuthorizationCode) error
	deleteAuthorizationCode(code string) error
	useAuthorizationCode(code string) error
}

func (m *memstore) getAuthorizationCode(code string) (AuthorizationCode, error) {
	m.authCodeLock.RLock()
	defer m.authCodeLock.RUnlock()
	authCode, ok := m.authCodes[code]
	if !ok {
		return AuthorizationCode{}, ErrAuthorizationCodeNotFound
	}
	return authCode, nil
}

func (m *memstore) saveAuthorizationCode(authCode AuthorizationCode) error {
	m.authCodeLock.Lock()
	defer m.authCodeLock.Unlock()
	_, ok := m.authCodes[authCode.Code]
	if ok {
		return ErrAuthorizationCodeAlreadyExists
	}
	m.authCodes[authCode.Code] = authCode
	return nil
}

func (m *memstore) deleteAuthorizationCode(code string) error {
	m.authCodeLock.Lock()
	defer m.authCodeLock.Unlock()
	_, ok := m.authCodes[code]
	if !ok {
		return ErrAuthorizationCodeNotFound
	}
	delete(m.authCodes, code)
	return nil
}

func (m *memstore) useAuthorizationCode(code string) error {
	m.authCodeLock.Lock()
	defer m.authCodeLock.Unlock()
	a, ok := m.authCodes[code]
	if !ok {
		return ErrAuthorizationCodeNotFound
	}
	a.Used = true
	m.authCodes[code] = a
	return nil
}

func authCodeGrantValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) {
	enc := json.NewEncoder(w)
	code := r.PostFormValue("code")
	if code == "" {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_request")
		return
	}
	clientID, success := verifyClient(w, r, true, context)
	if !success {
		return
	}
	authCode, err := context.GetAuthorizationCode(code)
	if err != nil {
		if err == ErrAuthorizationCodeNotFound {
			w.WriteHeader(http.StatusBadRequest)
			renderJSONError(enc, "invalid_grant")
			return
		}
		w.WriteHeader(http.StatusInternalServerError)
		renderJSONError(enc, "server_error")
		return
	}
	redirectURI := r.PostFormValue("redirect_uri")
	if authCode.RedirectURI != redirectURI {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_grant")
		return
	}
	if !authCode.ClientID.Equal(clientID) {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_grant")
		return
	}
	return authCode.Scope, authCode.ProfileID, true
}

func authCodeGrantInvalidate(r *http.Request, context Context) error {
	code := r.PostFormValue("code")
	if code == "" {
		return ErrAuthorizationCodeNotFound
	}
	return context.UseAuthorizationCode(code)
}
