package oauth2

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const (
	googleAuthURL     = "https://accounts.google.com/o/oauth2/v2/auth"
	googleTokenURL    = "https://oauth2.googleapis.com/token"
	googleUserInfoURL = "https://openidconnect.googleapis.com/v1/userinfo"
	googleScopes      = "openid email profile"
)

var ErrNotConfigured = errors.New("oauth2: google client not configured")

type Google struct {
	ClientID     string
	ClientSecret string
	RedirectURL  string
	HTTP         *http.Client

	authURL     string
	tokenURL    string
	userInfoURL string
}

func NewGoogle(clientID, clientSecret, redirectURL string) *Google {
	return &Google{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		RedirectURL:  redirectURL,
		HTTP:         &http.Client{Timeout: 15 * time.Second},
		authURL:      googleAuthURL,
		tokenURL:     googleTokenURL,
		userInfoURL:  googleUserInfoURL,
	}
}

func (g *Google) Configured() bool {
	return g.ClientID != "" && g.ClientSecret != "" && g.RedirectURL != ""
}

func (g *Google) AuthCodeURL(state string) (string, error) {
	if !g.Configured() {
		return "", ErrNotConfigured
	}
	q := url.Values{
		"client_id":     {g.ClientID},
		"redirect_uri":  {g.RedirectURL},
		"response_type": {"code"},
		"scope":         {googleScopes},
		"state":         {state},
		"access_type":   {"online"},
		"prompt":        {"select_account"},
	}
	return g.authURL + "?" + q.Encode(), nil
}

type UserInfo struct {
	Sub     string `json:"sub"`
	Email   string `json:"email"`
	Name    string `json:"name"`
	Picture string `json:"picture"`
}

func (g *Google) Exchange(ctx context.Context, code string) (*UserInfo, error) {
	if !g.Configured() {
		return nil, ErrNotConfigured
	}

	form := url.Values{
		"grant_type":    {"authorization_code"},
		"code":          {code},
		"client_id":     {g.ClientID},
		"client_secret": {g.ClientSecret},
		"redirect_uri":  {g.RedirectURL},
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, g.tokenURL, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	resp, err := g.HTTP.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("oauth2: token exchange failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
	}

	var token struct {
		AccessToken string `json:"access_token"`
	}
	if err := json.Unmarshal(body, &token); err != nil {
		return nil, err
	}
	if token.AccessToken == "" {
		return nil, errors.New("oauth2: empty access token")
	}

	return g.userInfo(ctx, token.AccessToken)
}

func (g *Google) userInfo(ctx context.Context, accessToken string) (*UserInfo, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, g.userInfoURL, nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := g.HTTP.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() { _ = resp.Body.Close() }()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("oauth2: userinfo failed: %s", resp.Status)
	}

	info := &UserInfo{}
	if err := json.Unmarshal(body, info); err != nil {
		return nil, err
	}
	if info.Email == "" {
		return nil, errors.New("oauth2: missing email in userinfo")
	}
	return info, nil
}
