package auth

import (
	"context"
	"fmt"

	"github.com/coreos/go-oidc/v3/oidc"
	"golang.org/x/oauth2"
)

// OIDCProvider wraps an OIDC provider and OAuth2 configuration.
type OIDCProvider struct {
	provider     *oidc.Provider
	oauth2Config oauth2.Config
	verifier     *oidc.IDTokenVerifier
}

// OIDCConfig holds the configuration needed to set up an OIDC provider.
type OIDCConfig struct {
	IssuerURL    string
	ClientID     string
	ClientSecret string
	RedirectURL  string
}

// OIDCUserInfo represents the user information extracted from an OIDC ID token.
type OIDCUserInfo struct {
	Subject  string `json:"sub"`
	Email    string `json:"email"`
	Username string `json:"preferred_username"`
	Name     string `json:"name"`
}

// NewOIDCProvider initializes an OIDC provider using the discovery URL.
func NewOIDCProvider(ctx context.Context, cfg OIDCConfig) (*OIDCProvider, error) {
	provider, err := oidc.NewProvider(ctx, cfg.IssuerURL)
	if err != nil {
		return nil, fmt.Errorf("create oidc provider: %w", err)
	}

	oauth2Config := oauth2.Config{
		ClientID:     cfg.ClientID,
		ClientSecret: cfg.ClientSecret,
		RedirectURL:  cfg.RedirectURL,
		Endpoint:     provider.Endpoint(),
		Scopes:       []string{oidc.ScopeOpenID, "profile", "email"},
	}

	verifier := provider.Verifier(&oidc.Config{ClientID: cfg.ClientID})

	return &OIDCProvider{
		provider:     provider,
		oauth2Config: oauth2Config,
		verifier:     verifier,
	}, nil
}

// AuthCodeURL returns the URL to redirect the user to for OIDC authentication.
func (op *OIDCProvider) AuthCodeURL(state string) string {
	return op.oauth2Config.AuthCodeURL(state)
}

// Exchange trades an authorization code for tokens and returns the user info
// extracted from the ID token.
func (op *OIDCProvider) Exchange(ctx context.Context, code string) (OIDCUserInfo, error) {
	token, err := op.oauth2Config.Exchange(ctx, code)
	if err != nil {
		return OIDCUserInfo{}, fmt.Errorf("exchange code: %w", err)
	}

	rawIDToken, ok := token.Extra("id_token").(string)
	if !ok {
		return OIDCUserInfo{}, fmt.Errorf("no id_token in response")
	}

	idToken, err := op.verifier.Verify(ctx, rawIDToken)
	if err != nil {
		return OIDCUserInfo{}, fmt.Errorf("verify id_token: %w", err)
	}

	var userInfo OIDCUserInfo
	if err := idToken.Claims(&userInfo); err != nil {
		return OIDCUserInfo{}, fmt.Errorf("parse id_token claims: %w", err)
	}

	return userInfo, nil
}
