// +build ignore

package midway

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"strings"

	"github.com/aws/aws-lambda-go/events"
	jwt "github.com/dgrijalva/jwt-go"
	"github.com/lestrrat/go-jwx/jwk"
)

type Request struct {
	NonceCookie          *http.Cookie
	TokenCookie          string
	Hostname             string
	MidwayRedirectTarget string
	TokenQuery           string
}

type Response struct {
	RedirectLocation string
	SetCookie        *http.Cookie

	VerifiedClaims *Claims
}

type Claims struct {
	Nonce string `json:"nonce"`
	jwt.StandardClaims
}

func NewRequest(req *events.APIGatewayProxyRequest) *Request {
	var (
		host       = req.Headers["Host"]
		cookies    = req.Headers["Cookie"]
		path       = req.Path
		tokenQuery = req.QueryStringParameters[tokenParam]
	)

	q := make(url.Values)
	for k, v := range req.QueryStringParameters {
		q.Set(k, v)
	}
	// avoid redirect loops
	q.Del(tokenParam)

	redirectTarget := &url.URL{
		Scheme:   "https",
		Host:     host,
		Path:     path,
		RawQuery: q.Encode(),
	}

	authReq := &Request{
		Hostname:             host,
		MidwayRedirectTarget: redirectTarget.String(),
		TokenQuery:           tokenQuery,
	}

	cookieReq := (&http.Request{Header: http.Header{"Cookie": []string{cookies}}})
	// TODO: check for and delete duplicate cookies
	if cookie, err := cookieReq.Cookie(nonceCookie); err == nil {
		authReq.NonceCookie = cookie
	}
	if cookie, err := cookieReq.Cookie(tokenCookie); err == nil {
		authReq.TokenCookie = cookie.Value
	}

	return authReq
}

type authN struct{}

func Check(ctx context.Context, req *Request) (*Response, error) {
	_, err := VerifyToken(ctx, req.TokenQuery, req.NonceCookie, req.Hostname)
	if err == nil {
		resp := &Response{
			RedirectLocation: req.MidwayRedirectTarget,
			SetCookie: &http.Cookie{
				Name:     tokenCookie,
				Value:    req.TokenQuery,
				Secure:   true,
				HttpOnly: true,
			},
		}
		return resp, nil
	}

	claims, err := VerifyToken(ctx, req.TokenCookie, req.NonceCookie, req.Hostname)
	if err != nil {
		log.Printf("bad token: %v", err)
		return an.requestAuthentication(ctx, req)
	}

	return &Response{VerifiedClaims: claims}, nil
}

func VerifyToken(ctx context.Context, tokenString string, nonceCookie *http.Cookie, hostname string) (*Claims, error) {
	keyFunc := func(token *jwt.Token) (interface{}, error) { return jwtKeyFunc(ctx, token) }

	var claims Claims
	token, err := (&jwt.Parser{}).ParseWithClaims(tokenString, &claims, keyFunc)
	if err != nil {
		return nil, err
	}

	if alg, _ := token.Header["alg"]; alg != "RS256" {
		return nil, fmt.Errorf("unknown algorithm %q", alg)
	}

	if nonceCookie == nil {
		return nil, fmt.Errorf("nonce cookie missing")
	}
	if subtle.ConstantTimeCompare([]byte(claims.Nonce), []byte(an.getHexNonce(nonceCookie))) != 1 {
		return nil, fmt.Errorf("nonce mismatch")
	}
	if subtle.ConstantTimeCompare([]byte(claims.Issuer), []byte("https://midway-auth.amazon.com")) != 1 {
		return nil, fmt.Errorf("issuer mismatch")
	}
	if subtle.ConstantTimeCompare([]byte(claims.Audience), []byte(hostname)) != 1 {
		return nil, fmt.Errorf("audience mismatch")
	}

	return &claims, nil
}

func (an *authN) requestAuthentication(ctx context.Context, req *Request) (*Response, error) {
	var resp Response

	hexNonce := an.getHexNonce(req.NonceCookie)
	if hexNonce == "" {
		log.Printf("replacing nonce")
		var err error
		req, err = an.replaceNonce(req)
		if err != nil {
			return nil, err
		}
		hexNonce = an.getHexNonce(req.NonceCookie)
		resp.SetCookie = req.NonceCookie
	}

	resp.RedirectLocation = an.midwayRedirect(req).String()
	return &resp, nil
}

func (an *authN) getHexNonce(cookie *http.Cookie) string {
	if cookie == nil {
		return ""
	}
	rfp, err := hex.DecodeString(cookie.Value)
	if err != nil {
		return ""
	}
	nonce := sha256.Sum256(rfp)
	return hex.EncodeToString(nonce[:])
}

func (an *authN) replaceNonce(req *Request) (*Request, error) {
	rfp := make([]byte, 32)
	_, err := io.ReadFull(rand.Reader, rfp)
	if err != nil {
		return nil, err
	}
	cookie := &http.Cookie{
		Name:     nonceCookie,
		Value:    hex.EncodeToString(rfp),
		Secure:   true,
		HttpOnly: true,
	}
	withCookie := *req
	withCookie.NonceCookie = cookie
	return &withCookie, nil
}

func (an *authN) midwayRedirect(req *Request) *url.URL {
	q := make(url.Values)
	q.Set("redirect_uri", req.MidwayRedirectTarget)
	q.Set("client_id", req.Hostname)
	q.Set("scope", "openid")
	q.Set("response_type", "id_token")
	q.Set("nonce", an.getHexNonce(req.NonceCookie))

	return &url.URL{
		Scheme:   "https",
		Host:     "midway-auth.amazon.com",
		Path:     "/SSO/redirect",
		RawQuery: q.Encode(),
	}
}

type OpenIDConfig struct {
	JWKS string `json:"jwks_uri"`
}

func getOpenIDConfig(ctx context.Context, host string) (*OpenIDConfig, error) {
	req, err := http.NewRequest("GET", (&url.URL{
		Scheme: "https",
		Host:   host,
		Path:   "/.well-known/openid-configuration",
	}).String(), nil)
	if err != nil {
		return nil, err
	}
	req = req.WithContext(ctx)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var config OpenIDConfig
	err = json.Unmarshal(body, &config)
	if err != nil {
		return nil, err
	}

	return &config, nil
}

func jwtKeyFunc(ctx context.Context, token *jwt.Token) (interface{}, error) {
	if token.Method.Alg() != "RS256" {
		return nil, fmt.Errorf("unknown alg: %q", token.Method.Alg())
	}

	config, err := getOpenIDConfig(ctx, "midway-auth.amazon.com")
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("GET", config.JWKS, nil)
	if err != nil {
		return nil, err
	}
	req = req.WithContext(ctx)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	{
		// Midway's keys don't conform to RFC 7515's requirement of
		// stripping the base64 padding.
		var jwks struct {
			Keys []struct {
				Alg string   `json:"alg"`
				E   string   `json:"e"`
				Kid string   `json:"kid"`
				Kty string   `json:"kty"`
				N   string   `json:"n"`
				X5c []string `json:"x5c"`
			} `json:"keys"`
		}
		err = json.Unmarshal(body, &jwks)
		if err != nil {
			return nil, err
		}
		for i := range jwks.Keys {
			jwks.Keys[i].N = strings.TrimRight(jwks.Keys[i].N, "=")
			jwks.Keys[i].E = strings.TrimRight(jwks.Keys[i].E, "=")
			for j := range jwks.Keys[i].X5c {
				jwks.Keys[i].X5c[j] = strings.TrimRight(jwks.Keys[i].X5c[j], "=")
			}
		}
		body, err = json.Marshal(&jwks)
		if err != nil {
			return nil, err
		}
	}

	keys, err := jwk.Parse(body)
	if err != nil {
		return nil, err
	}

	kid, ok := token.Header["kid"].(string)
	if !ok {
		return nil, fmt.Errorf("missing kid")
	}

	for _, key := range keys.LookupKeyID(kid) {
		k, err := key.Materialize()
		if err == nil {
			return k, nil
		}
	}

	return nil, fmt.Errorf("key not available")
}
