package oidc

import (
	"errors"
	"fmt"
	"time"

	"github.com/jonboulle/clockwork"

	"github.com/coreos/go-oidc/jose"
	"github.com/coreos/go-oidc/key"
)

func VerifySignature(jwt jose.JWT, keys []key.PublicKey) (bool, error) {
	jwtBytes := []byte(jwt.Data())
	for _, k := range keys {
		v, err := k.Verifier()
		if err != nil {
			return false, err
		}
		if v.Verify(jwt.Signature, jwtBytes) == nil {
			return true, nil
		}
	}
	return false, nil
}

// Verify claims in accordance with OIDC spec
// http://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
func VerifyClaims(jwt jose.JWT, issuer, clientID string) error {
	now := time.Now().UTC()

	claims, err := jwt.Claims()
	if err != nil {
		return err
	}

	ident, err := IdentityFromClaims(claims)
	if err != nil {
		return err
	}

	if ident.ExpiresAt.Before(now) {
		return errors.New("token is expired")
	}

	// iss REQUIRED. Issuer Identifier for the Issuer of the response.
	// The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components.
	if iss, exists := claims["iss"].(string); exists {
		if !urlEqual(iss, issuer) {
			return fmt.Errorf("invalid claim value: 'iss'. expected=%s, found=%s.", issuer, iss)
		}
	} else {
		return errors.New("missing claim: 'iss'")
	}

	// iat REQUIRED. Time at which the JWT was issued.
	// Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.
	if _, exists := claims["iat"].(float64); !exists {
		return errors.New("missing claim: 'iat'")
	}

	// aud REQUIRED. Audience(s) that this ID Token is intended for.
	// It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings. In the common special case when there is one audience, the aud value MAY be a single case sensitive string.
	if aud, exists := claims["aud"].(string); exists {
		if aud != clientID {
			return errors.New("invalid claim value: 'aud'")
		}
	} else {
		return errors.New("missing claim: 'aud'")
	}

	return nil
}

// VerifyClientClaims verifies all the required claims are valid for a "client credentials" JWT.
// Returns the client ID if valid, or an error if invalid.
func VerifyClientClaims(jwt jose.JWT, issuer string) (string, error) {
	claims, err := jwt.Claims()
	if err != nil {
		return "", fmt.Errorf("failed to parse JWT claims: %v", err)
	}

	iss, ok, err := claims.StringClaim("iss")
	if err != nil {
		return "", fmt.Errorf("failed to parse 'iss' claim: %v", err)
	} else if !ok {
		return "", errors.New("missing required 'iss' claim")
	} else if !urlEqual(iss, issuer) {
		return "", fmt.Errorf("'iss' claim does not match expected issuer, iss=%s", iss)
	}

	sub, ok, err := claims.StringClaim("sub")
	if err != nil {
		return "", fmt.Errorf("failed to parse 'sub' claim: %v", err)
	} else if !ok {
		return "", errors.New("missing required 'sub' claim")
	}

	aud, ok, err := claims.StringClaim("aud")
	if err != nil {
		return "", fmt.Errorf("failed to parse 'aud' claim: %v", err)
	} else if !ok {
		return "", errors.New("missing required 'aud' claim")
	}

	if sub != aud {
		return "", fmt.Errorf("invalid claims, 'aud' claim and 'sub' claim do not match, aud=%s, sub=%s", aud, sub)
	}

	now := time.Now().UTC()
	exp, ok, err := claims.TimeClaim("exp")
	if err != nil {
		return "", fmt.Errorf("failed to parse 'exp' claim: %v", err)
	} else if !ok {
		return "", errors.New("missing required 'exp' claim")
	} else if exp.Before(now) {
		return "", fmt.Errorf("token already expired at: %v", exp)
	}

	return sub, nil
}

type JWTVerifier struct {
	issuer   string
	clientID string
	syncFunc func() error
	keysFunc func() []key.PublicKey
	clock    clockwork.Clock
}

func NewJWTVerifier(issuer, clientID string, syncFunc func() error, keysFunc func() []key.PublicKey) JWTVerifier {
	return JWTVerifier{
		issuer:   issuer,
		clientID: clientID,
		syncFunc: syncFunc,
		keysFunc: keysFunc,
		clock:    clockwork.NewRealClock(),
	}
}

func (v *JWTVerifier) Verify(jwt jose.JWT) error {
	ok, err := VerifySignature(jwt, v.keysFunc())
	if ok {
		goto SignatureVerified
	} else if err != nil {
		return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
	}

	if err = v.syncFunc(); err != nil {
		return fmt.Errorf("oidc: failed syncing KeySet: %v", err)
	}

	ok, err = VerifySignature(jwt, v.keysFunc())
	if err != nil {
		return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
	} else if !ok {
		return errors.New("oidc: unable to verify JWT signature: no matching keys")
	}

SignatureVerified:
	if err := VerifyClaims(jwt, v.issuer, v.clientID); err != nil {
		return fmt.Errorf("oidc: JWT claims invalid: %v", err)
	}

	return nil
}
