package auth

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

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

	"github.com/gorilla/mux"
)

const (
	authCookieName         = "auth"
	defaultGrantExpiration = 600 // default to ten minute grant expirations
	getGrantTemplateName   = "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")
)

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"`
}

func renderJSONError(enc *json.Encoder, errorType string) {
	err := enc.Encode(errorResponse{
		Error: errorType,
	})
	if err != nil {
		// TODO(paddy): log this or something
	}
}

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, GetGrantHandler))
	r.Handle("/token", wrap(context, GetTokenHandler))
}

// GetGrantHandler presents and processes the page for asking a user to grant access
// to their data. See RFC 6749, Section 4.1.
func GetGrantHandler(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, getGrantTemplateName, 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, getGrantTemplateName, map[string]interface{}{
			"internal_error": template.HTML(err.Error()),
		})
		return
	}
	if r.URL.Query().Get("client_id") == "" {
		w.WriteHeader(http.StatusBadRequest)
		context.Render(w, getGrantTemplateName, 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, getGrantTemplateName, 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, getGrantTemplateName, 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, getGrantTemplateName, 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, getGrantTemplateName, map[string]interface{}{
				"internal_error": template.HTML(err.Error()),
			})
		}
		return
	}
	// 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, getGrantTemplateName, 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, getGrantTemplateName, 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, getGrantTemplateName, 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, getGrantTemplateName, 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()
			grant := Grant{
				Code:        code,
				Created:     time.Now(),
				ExpiresIn:   defaultGrantExpiration,
				ClientID:    clientID,
				Scope:       scope,
				RedirectURI: r.URL.Query().Get("redirect_uri"),
				State:       state,
				ProfileID:   session.ProfileID,
			}
			err := context.SaveGrant(grant)
			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
	}
	w.WriteHeader(http.StatusOK)
	context.Render(w, getGrantTemplateName, map[string]interface{}{
		"client": client,
	})
}

// 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) {
	// BUG(paddy): this function is an absolute mess. Honestly, it should be more general purpose, with each grant mode being called based on the grant_type POST form value. Basically, each grant type could have its own function, accepting the Request and ResponseWriter, and returning a boolean if the request should continue being processed or not. The function is in charge of validating the grant, which offers more flexible extensibiliy when adding grant types and easier testing, while also making the token distribution code easier to reuse in an elegant way. There is a minor problem that the token distribution code has some dependencies on the grant type being used (some grant types don't issue refresh tokens, for example) but that's a minor issue. Something like a map of string -> custom grantType struct would fix that. The struct could hold the function to call to validate the grant type and booleans that impact the token issuance. Then you do a map lookup based on the POST form value, and call the function or read the booleans as needed. If we use the same "register" pattern found in database/sql drivers, allowing grant types to register themselves, it'll be possible to add a grant type without even touching this function.
	enc := json.NewEncoder(w)
	grantType := r.PostFormValue("grant_type")
	if grantType != "authorization_code" {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_request")
		return
	}
	code := r.PostFormValue("code")
	if code == "" {
		w.WriteHeader(http.StatusBadRequest)
		renderJSONError(enc, "invalid_request")
		return
	}
	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
	}
	token := Token{
		AccessToken:  uuid.NewID().String(),
		RefreshToken: uuid.NewID().String(),
		Created:      time.Now(),
		ExpiresIn:    defaultTokenExpiration,
		TokenType:    "bearer",
		Scope:        grant.Scope,
		ProfileID:    grant.ProfileID,
	}
	err = context.SaveToken(token)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		renderJSONError(enc, "server_error")
		return
	}
	resp := tokenResponse{
		AccessToken:  token.AccessToken,
		RefreshToken: token.RefreshToken,
		ExpiresIn:    token.ExpiresIn,
		TokenType:    token.TokenType,
	}
	err = enc.Encode(resp)
	if err != nil {
		// TODO(paddy): log this or something
		return
	}
	// BUG(paddy): we need to invalidate the grant for future requests
}

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