package auth

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

	"strings"
	"secondbit.org/uuid"
)

// AuthorizeRequestType is the type for OAuth param `response_type`
type AuthorizeRequestType string

const (
	CodeAuthRT  AuthorizeRequestType = "code"
	TokenAuthRT                      = "token"
)

type AuthRequest struct {
	Client      Client
	Scope       string
	RedirectURI string
	State       string
}

// Authorization data
type AuthorizeData struct {
	// Authorization code
	Code string

	// Token expiration in seconds
	ExpiresIn int32

	// Date created
	CreatedAt time.Time

	ProfileID uuid.ID

	AuthRequest
}

// IsExpired is true if authorization expired
func (d *AuthorizeData) IsExpired() bool {
	return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now())
}

// ExpireAt returns the expiration date
func (d *AuthorizeData) ExpireAt() time.Time {
	return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second)
}

// HandleAuthorizeRequest is the main http.HandlerFunc for handling
// authorization requests
func HandleAuthorizeRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
	r.ParseForm()
	// create the authorization request
	redirectURI := r.Form.Get("redirect_uri")
	var err error
	if redirectURI != "" {
		redirectURI, err = url.QueryUnescape(redirectURI)
		if err != nil {
			ctx.RenderError(w, URIFormatError(redirectURI))
			return
		}
	}

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

	// must have a valid client
	id, err := uuid.Parse(r.Form.Get("client_id"))
	if err != nil {
		ctx.RenderError(w, InvalidClientIDError(r.Form.Get("client_id")))
		return
	}
	client, err := GetClient(id, ctx)
	if err != nil {
		if err == ClientNotFoundError {
			ctx.RenderError(w, ClientNotFoundError)
			return
		}
		if redirectURI == "" {
			ctx.RenderError(w, URIMissingError)
			return
		}
		req := AuthRequest{
			RedirectURI: redirectURI,
			Scope:       scope,
			State:       state,
		}
		redir, err := req.GetErrorRedirect(ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		if err != nil {
			ctx.RenderError(w, URIFormatError(redirectURI))
			return
		}
		http.Redirect(w, r, redir, http.StatusFound)
		return
	}
	if client.RedirectURI == "" {
		ctx.RenderError(w, URIMissingError)
		return
	}

	// check redirect uri
	if redirectURI == "" {
		redirectURI = client.RedirectURI
	}
	if err = validateURI(client.RedirectURI, redirectURI); err != nil {
		ctx.RenderError(w, NewURIMismatchError(client.RedirectURI, redirectURI))
		return
	}

	req := AuthRequest{
		Client:      client,
		RedirectURI: redirectURI,
		Scope:       scope,
		State:       state,
	}

	requestType := AuthorizeRequestType(r.Form.Get("response_type"))
	if ctx.Config.AllowedAuthorizeTypes.Exists(requestType) {
		switch requestType {
		case CodeAuthRT:
			req.handleCodeRequest(w, r, ctx)
			return
		case TokenAuthRT:
			req.handleTokenRequest(w, r, ctx)
			return
		}
	}
	redir, err := req.GetErrorRedirect(ErrorInvalidRequest, "Invalid response type.", ctx.Config.DocumentationDomain)
	if err != nil {
		ctx.RenderError(w, URIFormatError(req.RedirectURI))
		return
	}
	http.Redirect(w, r, redir, http.StatusFound)
}

func (req AuthRequest) handleCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) {

	if r.Method == "GET" {
		ctx.RenderConfirmation(w, r, req)
		return
	} else if r.Method != "POST" {
		ctx.RenderError(w, InvalidMethodError)
		return
	}

	if err := validateSession(r, ctx); err == ErrorNotAuthenticated {
		// TODO: redirect to login
		return
	} else if err != nil {
		ctx.RenderError(w, err)
		return
	}

	if r.FormValue("approved") != "true" {
		redir, err := req.GetErrorRedirect(ErrorAccessDenied, "Request was not authorized.", ctx.Config.DocumentationDomain)
		if err != nil {
			ctx.RenderError(w, URIFormatError(req.RedirectURI))
			return
		}
		http.Redirect(w, r, redir, http.StatusFound)
		return
	}

	data := AuthorizeData{AuthRequest: req}

	data.ExpiresIn = ctx.Config.AuthorizationExpiration
	data.Code = newToken()
	data.CreatedAt = time.Now()

	err := ctx.Tokens.SaveAuthorization(data)
	if err != nil {
		redir, err := req.GetErrorRedirect(ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
		if err != nil {
			ctx.RenderError(w, URIFormatError(req.RedirectURI))
			return
		}
		http.Redirect(w, r, redir, http.StatusFound)
		return
	}

	redir, err := data.GetRedirect()
	if err != nil {
		ctx.RenderError(w, URIFormatError(req.RedirectURI))
		return
	}
	http.Redirect(w, r, redir, http.StatusFound)
}

func (req AuthRequest) handleTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {

	if r.Method == "GET" {
		ctx.RenderConfirmation(w, r, req)
		return
	} else if r.Method != "POST" {
		ctx.RenderError(w, InvalidMethodError)
		return
	}

	if err := validateSession(r, ctx); err == ErrorNotAuthenticated {
		// TODO: redirect to login
		return
	} else if err != nil {
		ctx.RenderError(w, err)
		return
	}

	if r.FormValue("approved") != "true" {
		redir, err := req.GetErrorRedirect(ErrorAccessDenied, "Request was not authorized.", ctx.Config.DocumentationDomain)
		if err != nil {
			ctx.RenderError(w, URIFormatError(req.RedirectURI))
			return
		}
		http.Redirect(w, r, redir, http.StatusFound)
		return
	}

	data := AccessData{AuthRequest: req}

	err := fillTokens(&data, false, ctx)
	if err != nil {
		ctx.RenderError(w, InternalServerError)
		return
	}

	redir, err := data.GetRedirect(true)
	if err != nil {
		ctx.RenderError(w, URIFormatError(req.RedirectURI))
		return
	}
	http.Redirect(w, r, redir, http.StatusFound)
}

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

	// add parameters
	q := u.Query()
	q.Set("code", data.Code)
	q.Set("state", data.State)
	u.RawQuery = q.Encode()

	return u.String(), nil
}

func (req AuthRequest) GetErrorRedirect(code, description, uriBase string) (string, error) {
	u, err := url.Parse(req.RedirectURI)
	if err != nil {
		return "", err
	}

	// add parameters
	q := u.Query()
	q.Set("error", code)
	q.Set("error_description", description)
	q.Set("error_uri", strings.Join([]string{
		strings.TrimRight(uriBase, "/"),
		strings.TrimLeft(code, "/"),
	}, "/"))
	q.Set("state", req.State)
	u.RawQuery = q.Encode()

	return u.String(), nil
}
