package auth

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
	"golang.org/x/oauth2"
)

// githubAPIBase is a variable rather than a constant so tests can point it at
// an httptest server. Nothing in production reassigns it.
var githubAPIBase = "https://api.github.com"

const (
	githubAuthURL  = "https://github.com/login/oauth/authorize"
	githubTokenURL = "https://github.com/login/oauth/access_token"
)

type githubEmail struct {
	Email    string `json:"email"`
	Primary  bool   `json:"primary"`
	Verified bool   `json:"verified"`
}

// githubOAuthConfig builds the OAuth2 config for a GitHub provider. GitHub has
// no discovery document, so the endpoints are constants rather than fetched.
func githubOAuthConfig(p *models.AuthProvider, secret, redirectURL string) *oauth2.Config {
	return &oauth2.Config{
		ClientID:     p.ClientID,
		ClientSecret: secret,
		RedirectURL:  redirectURL,
		Scopes:       p.Scopes,
		Endpoint: oauth2.Endpoint{
			AuthURL:  githubAuthURL,
			TokenURL: githubTokenURL,
		},
	}
}

// selectGitHubEmail requires an address that is both primary and verified.
//
// Verified alone is not enough: a non-primary address is one the person happens
// to have proved, not the one they present as themselves. Primary alone is far
// worse — an unverified address is not proof of control at all, and accepting
// one would let anyone with a GitHub account claim any address in the instance.
func selectGitHubEmail(emails []githubEmail) (string, error) {
	for _, e := range emails {
		if e.Primary && e.Verified && e.Email != "" {
			return e.Email, nil
		}
	}
	return "", errors.New("no primary verified email address on the GitHub account")
}

func githubGetJSON(ctx context.Context, client *http.Client, url string, out any) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	req.Header.Set("Accept", "application/vnd.github+json")
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return errors.New("github api returned " + resp.Status)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

// githubIdentity resolves the signed-in GitHub account to an email and a name.
//
// The name is best-effort: it is cosmetic, and a failing /user must not fail a
// sign-in whose identity is already established.
func githubIdentity(ctx context.Context, cfg *oauth2.Config, token *oauth2.Token) (string, string, error) {
	client := cfg.Client(ctx, token)

	var emails []githubEmail
	if err := githubGetJSON(ctx, client, githubAPIBase+"/user/emails", &emails); err != nil {
		return "", "", err
	}
	email, err := selectGitHubEmail(emails)
	if err != nil {
		return "", "", err
	}

	var user struct {
		Name string `json:"name"`
	}
	if err := githubGetJSON(ctx, client, githubAPIBase+"/user", &user); err != nil {
		return email, "", nil
	}
	return email, user.Name, nil
}
