package auth

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"html/template"
	"log"
	"net/http"
	"net/url"
	"sync"
	"time"

	"code.secondbit.org/pass"
	"code.secondbit.org/uuid"

	"github.com/gorilla/mux"
)

const (
	authCookieName                     = "auth"
	defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
	getAuthorizationCodeTemplateName   = "get_grant"
)

var (
	// ErrNoAuth is returned when an Authorization header is not present or is empty.
	ErrNoAuth = errors.New("no authorization header supplied")
	// ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
	ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
	// ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
	ErrIncorrectAuth = errors.New("invalid authentication")
	// ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
	ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
	// ErrNoSession is returned when no session ID is passed with a request.
	ErrNoSession = errors.New("no session ID found")

	grantTypesMap = grantTypes{types: map[string]GrantType{}}
)

type grantTypes struct {
	types map[string]GrantType
	sync.RWMutex
}

// GrantType defines a set of functions and metadata around a specific authorization grant strategy.
//
// The Validate function will be called when requests are made that match the GrantType, and should write any
// errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
// It must return the scope the grant was for and the ID of the Profile that issued the grant, as well as if the grant
// is valid or not. It must not be nil.
//
// The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
// been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
// care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
// can be nil.
//
// IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
// will be issued a refresh token.
//
// The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
// was successfully returned and the Invalidate function will be called asynchronously.
type GrantType struct {
	Validate      func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
	Invalidate    func(r *http.Request, context Context) error
	ReturnToken   func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
	IssuesRefresh bool
}

type tokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type,omitempty"`
	ExpiresIn    int32  `json:"expires_in,omitempty"`
	RefreshToken string `json:"refresh_token,omitempty"`
}

type errorResponse struct {
	Error       string `json:"error"`
	Description string `json:"error_description,omitempty"`
	URI         string `json:"error_uri,omitempty"`
}

// RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
// an access token, the associated GrantType's properties will be used.
//
// RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
// if a GrantType tries to register under a string that already has a GrantType registered for it.
func RegisterGrantType(name string, g GrantType) {
	grantTypesMap.Lock()
	defer grantTypesMap.Unlock()
	if _, ok := grantTypesMap.types[name]; ok {
		panic("Duplicate registration of grant_type " + name)
	}
	grantTypesMap.types[name] = g
}

func findGrantType(name string) (GrantType, bool) {
	grantTypesMap.RLock()
	defer grantTypesMap.RUnlock()
	t, ok := grantTypesMap.types[name]
	return t, ok
}

func renderJSONError(enc *json.Encoder, errorType string) {
	err := enc.Encode(errorResponse{
		Error: errorType,
	})
	if err != nil {
		log.Println(err)
	}
}

// RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
// according to the spec. See RFC 6479, Section 4.1.4.
func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
	enc := json.NewEncoder(w)
	resp := tokenResponse{
		AccessToken:  token.AccessToken,
		RefreshToken: token.RefreshToken,
		ExpiresIn:    token.ExpiresIn,
		TokenType:    token.TokenType,
	}
	err := enc.Encode(resp)
	if err != nil {
		log.Println(err)
		return false
	}
	return true
}

func checkCookie(r *http.Request, context Context) (Session, error) {
	cookie, err := r.Cookie(authCookieName)
	if err == http.ErrNoCookie {
		return Session{}, ErrNoSession
	} else if err != nil {
		log.Println(err)
		return Session{}, err
	}
	sess, err := context.GetSession(cookie.Value)
	if err == ErrSessionNotFound {
		return Session{}, ErrInvalidSession
	} else if err != nil {
		return Session{}, err
	}
	if !sess.Active {
		return Session{}, ErrInvalidSession
	}
	return sess, nil
}

func buildLoginRedirect(r *http.Request, context Context) string {
	if context.loginURI == nil {
		return ""
	}
	uri := *context.loginURI
	q := uri.Query()
	q.Set("from", r.URL.String())
	uri.RawQuery = q.Encode()
	return uri.String()
}

func authenticate(user, passphrase string, context Context) (Profile, error) {
	profile, err := context.GetProfileByLogin(user)
	if err != nil {
		if err == ErrProfileNotFound || err == ErrLoginNotFound {
			return Profile{}, ErrIncorrectAuth
		}
		return Profile{}, err
	}
	switch profile.PassphraseScheme {
	case 1:
		realPass, err := hex.DecodeString(profile.Passphrase)
		if err != nil {
			return Profile{}, err
		}
		candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
		if !pass.Compare(candidate, realPass) {
			return Profile{}, ErrIncorrectAuth
		}
	default:
		return Profile{}, ErrInvalidPassphraseScheme
	}
	return profile, nil
}

func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		f(w, r, context)
	})
}

// RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
func RegisterOAuth2(r *mux.Router, context Context) {
	r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
	r.Handle("/token", wrap(context, GetTokenHandler))
}

// GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
// to their data. See RFC 6749, Section 4.1.
func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
	session, err := checkCookie(r, context)
	if err != nil {
		if err == ErrNoSession || err == ErrInvalidSession {
			redir := buildLoginRedirect(r, context)
			if redir == "" {
				log.Println("No login URL configured.")
				w.WriteHeader(http.StatusInternalServerError)
				context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
					"internal_error": template.HTML("Missing login URL."),
				})
				return
			}
			http.Redirect(w, r, redir, http.StatusFound)
			return
		}
		log.Println(err.Error())
		w.WriteHeader(http.StatusInternalServerError)
		context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
			"internal_error": template.HTML(err.Error()),
		})
		return
	}
	if r.URL.Query().Get("client_id") == "" {
		w.WriteHeader(http.StatusBadRequest)
		context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
			"error": template.HTML("Client ID must be specified in the request."),
		})
		return
	}
	clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
			"error": template.HTML("client_id is not a valid Client ID."),
		})
		return
	}
	redirectURI := r.URL.Query().Get("redirect_uri")
	redirectURL, err := url.Parse(redirectURI)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
			"error": template.HTML("The redirect_uri specified is not valid."),
		})
		return
	}
	client, err := context.GetClient(clientID)
	if err != nil {
		if err == ErrClientNotFound {
			w.WriteHeader(http.StatusBadRequest)
			context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
				"error": template.HTML("The specified Client couldn&rsquo;t be found."),
			})
		} else {
			log.Println(err.Error())
			w.WriteHeader(http.StatusInternalServerError)
			context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
				"internal_error": template.HTML(err.Error()),
			})
		}
		return
	}
	// TODO(paddy): checking if the redirect URI is valid should be a helper function
	// whether a redirect URI is valid or not depends on the number of endpoints
	// the client has registered
	numEndpoints, err := context.CountEndpoints(clientID)
	if err != nil {
		log.Println(err.Error())
		w.WriteHeader(http.StatusInternalServerError)
		context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
			"internal_error": template.HTML(err.Error()),
		})
		return
	}
	var validURI bool
	if redirectURI != "" {
		// BUG(paddy): We really should normalize URIs before trying to compare them.
		validURI, err = context.CheckEndpoint(clientID, redirectURI)
		if err != nil {
			log.Println(err.Error())
			w.WriteHeader(http.StatusInternalServerError)
			context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
				"internal_error": template.HTML(err.Error()),
			})
			return
		}
	} else if redirectURI == "" && numEndpoints == 1 {
		// if we don't specify the endpoint and there's only one endpoint, the
		// request is valid, and we're redirecting to that one endpoint
		validURI = true
		endpoints, err := context.ListEndpoints(clientID, 1, 0)
		if err != nil {
			log.Println(err.Error())
			w.WriteHeader(http.StatusInternalServerError)
			context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
				"internal_error": template.HTML(err.Error()),
			})
			return
		}
		if len(endpoints) != 1 {
			validURI = false
		} else {
			u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
			redirectURI = u.String()
			redirectURL = &u
		}
	} else {
		validURI = false
	}
	if !validURI {
		w.WriteHeader(http.StatusBadRequest)
		context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
			"error": template.HTML("The redirect_uri specified is not valid."),
		})
		return
	}
	scope := r.URL.Query().Get("scope")
	state := r.URL.Query().Get("state")
	if r.URL.Query().Get("response_type") != "code" {
		q := redirectURL.Query()
		q.Add("error", "invalid_request")
		q.Add("state", state)
		redirectURL.RawQuery = q.Encode()
		http.Redirect(w, r, redirectURL.String(), http.StatusFound)
		return
	}
	if r.Method == "POST" {
		// BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
		if r.PostFormValue("grant") == "approved" {
			code := uuid.NewID().String()
			authCode := AuthorizationCode{
				Code:        code,
				Created:     time.Now(),
				ExpiresIn:   defaultAuthorizationCodeExpiration,
				ClientID:    clientID,
				Scope:       scope,
				RedirectURI: r.URL.Query().Get("redirect_uri"),
				State:       state,
				ProfileID:   session.ProfileID,
			}
			err := context.SaveAuthorizationCode(authCode)
			if err != nil {
				q := redirectURL.Query()
				q.Add("error", "server_error")
				q.Add("state", state)
				redirectURL.RawQuery = q.Encode()
				http.Redirect(w, r, redirectURL.String(), http.StatusFound)
				return
			}
			q := redirectURL.Query()
			q.Add("code", code)
			q.Add("state", state)
			redirectURL.RawQuery = q.Encode()
			http.Redirect(w, r, redirectURL.String(), http.StatusFound)
			return
		}
		q := redirectURL.Query()
		q.Add("error", "access_denied")
		q.Add("state", state)
		redirectURL.RawQuery = q.Encode()
		http.Redirect(w, r, redirectURL.String(), http.StatusFound)
		return
	}
	profile, err := context.GetProfileByID(session.ProfileID)
	if err != nil {
		q := redirectURL.Query()
		q.Add("error", "server_error")
		q.Add("state", state)
		redirectURL.RawQuery = q.Encode()
		http.Redirect(w, r, redirectURL.String(), http.StatusFound)
		return
	}
	w.WriteHeader(http.StatusOK)
	context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
		"client":      client,
		"redirectURL": redirectURL,
		"scope":       scope,
		"profile":     profile,
	})
}

// GetTokenHandler allows a client to exchange an authorization grant for an
// access token. See RFC 6749 Section 4.1.3.
func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
	enc := json.NewEncoder(w)
	grantType := r.PostFormValue("grant_type")
	gt, ok := findGrantType(grantType)
	if !ok {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_request")
		return
	}
	scope, profileID, valid := gt.Validate(w, r, context)
	if !valid {
		return
	}
	refresh := ""
	if gt.IssuesRefresh {
		refresh = uuid.NewID().String()
	}
	token := Token{
		AccessToken:      uuid.NewID().String(),
		RefreshToken:     refresh,
		Created:          time.Now(),
		ExpiresIn:        defaultTokenExpiration,
		RefreshExpiresIn: defaultRefreshTokenExpiration,
		TokenType:        "bearer",
		Scope:            scope,
		ProfileID:        profileID,
	}
	err := context.SaveToken(token)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		renderJSONError(enc, "server_error")
		return
	}
	if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
		go gt.Invalidate(r, context)
	}
}

// TODO(paddy): exchange user credentials for access token
// TODO(paddy): exchange client credentials for access token
// TODO(paddy): implicit grant for access token
// TODO(paddy): exchange refresh token for access token
