package debug

import (
	"context"
	"encoding/json"
	"io/ioutil"
	"time"

	"github.com/dchest/uniuri"
	"github.com/form3tech-oss/jwt-go"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"golang.org/x/oauth2"
)

const oAuthRequestTimeout = time.Second * 30

type OAuthClient struct {
	config           oauth2.Config
	userInfoURL      string
	oauthStateSecret []byte
}

func NewOAuthClient(oauthConfig oauth2.Config, oauthUserInfoUrl string) *OAuthClient {
	return &OAuthClient{
		config:           oauthConfig,
		userInfoURL:      oauthUserInfoUrl,
		oauthStateSecret: []byte(oauthConfig.ClientSecret),
	}
}

func (client *OAuthClient) WithRedirectURL(redirectURL string) *OAuthClient {
	newClient := *client
	newClient.config.RedirectURL = redirectURL
	return &newClient
}

func (client *OAuthClient) GenerateAuthURL() (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, &jwt.StandardClaims{
		ExpiresAt: time.Now().Add(time.Minute * 5).Unix(),
	})
	tokenString, err := token.SignedString(client.oauthStateSecret)
	if err != nil {
		return "", errcode.NewInternalErr(err)
	}
	return client.config.AuthCodeURL(tokenString), nil
}

func (client *OAuthClient) Exchange(ctx context.Context, state string, code string) (*Session, error) {
	// Verify whether state is issued by us and is not expired.
	stateToken, err := jwt.ParseWithClaims(state, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
		return client.oauthStateSecret, nil
	})
	if err != nil {
		return nil, errcode.NewInvalidInputErr(errors.Annotate(err, "invalid oauth state"))
	}
	_, ok := stateToken.Claims.(*jwt.StandardClaims)
	if !ok || !stateToken.Valid {
		return nil, errcode.NewInvalidInputErr(errors.Annotate(err, "oauth state is expired"))
	}

	ctx2, cancel := context.WithTimeout(ctx, oAuthRequestTimeout)
	defer cancel()

	oAuthToken, err := client.config.Exchange(ctx2, code)
	if err != nil {
		return nil, errcode.NewInvalidInputErr(errors.Annotate(err, "exchange oauth code failed"))
	}
	httpClient := client.config.Client(ctx2, oAuthToken)
	response, err := httpClient.Get(client.userInfoURL)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.Annotate(err, "oauth read user info failed"))
	}
	defer response.Body.Close() //nolint:errcheck
	contents, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.Annotate(err, "oauth read user info failed"))
	}

	// UserInfo is the struct returned by remote OAuth endpoint.
	type UserInfo struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	userInfo := UserInfo{}
	err = json.Unmarshal(contents, &userInfo)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.Annotate(err, "oauth unmarshal user info failed"))
	}

	return &Session{
		Email:     userInfo.Email,
		Username:  userInfo.Name,
		CSRFToken: uniuri.New(),
	}, nil
}
