package auth

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

	"code.secondbit.org/uuid"
)

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

var (
	// ErrNoGrantStore is returned when a Context tries to act on a grantStore without setting one first.
	ErrNoGrantStore = errors.New("no grantStore was specified for the Context")
	// ErrGrantNotFound is returned when a Grant is requested but not found in the grantStore.
	ErrGrantNotFound = errors.New("grant not found in grantStore")
	// ErrGrantAlreadyExists is returned when a Grant is added to a grantStore, but another Grant with the
	// same Code already exists in the grantStore.
	ErrGrantAlreadyExists = errors.New("grant already exists in grantStore")
)

// Grant 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 Grant struct {
	Code        string
	Created     time.Time
	ExpiresIn   int32
	ClientID    uuid.ID
	Scope       string
	RedirectURI string
	State       string
	ProfileID   uuid.ID
}

type grantStore interface {
	getGrant(code string) (Grant, error)
	saveGrant(grant Grant) error
	deleteGrant(code string) error
}

func (m *memstore) getGrant(code string) (Grant, error) {
	m.grantLock.RLock()
	defer m.grantLock.RUnlock()
	grant, ok := m.grants[code]
	if !ok {
		return Grant{}, ErrGrantNotFound
	}
	return grant, nil
}

func (m *memstore) saveGrant(grant Grant) error {
	m.grantLock.Lock()
	defer m.grantLock.Unlock()
	_, ok := m.grants[grant.Code]
	if ok {
		return ErrGrantAlreadyExists
	}
	m.grants[grant.Code] = grant
	return nil
}

func (m *memstore) deleteGrant(code string) error {
	m.grantLock.Lock()
	defer m.grantLock.Unlock()
	_, ok := m.grants[code]
	if !ok {
		return ErrGrantNotFound
	}
	delete(m.grants, code)
	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
	}
	// BUG(paddy): We really ought to break client verification out into its own helper functions, but I think it may depend on which grant_type is used...
	redirectURI := r.PostFormValue("redirect_uri")
	clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
	if !fromAuthHeader {
		clientIDStr = r.PostFormValue("client_id")
	}
	clientID, err := uuid.Parse(clientIDStr)
	if err != nil {
		w.WriteHeader(http.StatusUnauthorized)
		if fromAuthHeader {
			w.Header().Set("WWW-Authenticate", "Basic")
		}
		renderJSONError(enc, "invalid_client")
		return
	}
	client, err := context.GetClient(clientID)
	if err != nil {
		if err == ErrClientNotFound {
			w.WriteHeader(http.StatusUnauthorized)
			renderJSONError(enc, "invalid_client")
		} else {
			w.WriteHeader(http.StatusInternalServerError)
			renderJSONError(enc, "server_error")
		}
		return
	}
	if client.Secret != clientSecret {
		w.WriteHeader(http.StatusUnauthorized)
		if fromAuthHeader {
			w.Header().Set("WWW-Authenticate", "Basic")
		}
		renderJSONError(enc, "invalid_client")
		return
	}
	grant, err := context.GetGrant(code)
	if err != nil {
		if err == ErrGrantNotFound {
			w.WriteHeader(http.StatusBadRequest)
			renderJSONError(enc, "invalid_grant")
			return
		}
		w.WriteHeader(http.StatusInternalServerError)
		renderJSONError(enc, "server_error")
		return
	}
	if grant.RedirectURI != redirectURI {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_grant")
		return
	}
	if !grant.ClientID.Equal(clientID) {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_grant")
		return
	}
	return grant.Scope, grant.ProfileID, true
}
