package auth

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"sync"

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

	"bountyboard/internal/config"
)

// OIDCClient implements the §7 SSO code flow with PKCE. The provider is
// discovered lazily on first use (and retried on failure) so a temporarily
// unreachable IdP cannot prevent the app from booting.
type OIDCClient struct {
	cfg *config.Config
	log *slog.Logger

	mu       sync.Mutex
	provider *oidc.Provider
}

// OIDCClaims is what the rest of the app needs from a verified ID token.
type OIDCClaims struct {
	Issuer        string
	Subject       string
	Email         string
	EmailVerified bool
	Name          string
}

func NewOIDCClient(cfg *config.Config, log *slog.Logger) *OIDCClient {
	return &OIDCClient{cfg: cfg, log: log}
}

func (o *OIDCClient) Enabled() bool { return o.cfg.OIDC.Enabled() }

func (o *OIDCClient) getProvider(ctx context.Context) (*oidc.Provider, error) {
	o.mu.Lock()
	defer o.mu.Unlock()
	if o.provider != nil {
		return o.provider, nil
	}
	p, err := oidc.NewProvider(ctx, o.cfg.OIDC.IssuerURL)
	if err != nil {
		return nil, fmt.Errorf("oidc discovery for %s: %w", o.cfg.OIDC.IssuerURL, err)
	}
	o.provider = p
	return p, nil
}

func (o *OIDCClient) oauthConfig(p *oidc.Provider) *oauth2.Config {
	return &oauth2.Config{
		ClientID:     o.cfg.OIDC.ClientID,
		ClientSecret: o.cfg.OIDC.ClientSecret,
		Endpoint:     p.Endpoint(),
		RedirectURL:  o.cfg.AppBaseURL + "/api/v1/auth/oidc/callback",
		Scopes:       []string{oidc.ScopeOpenID, "email", "profile"},
	}
}

// AuthURL starts the flow: returns the IdP redirect plus the state and PKCE
// verifier the caller must persist (short-lived cookies).
func (o *OIDCClient) AuthURL(ctx context.Context) (url, state, verifier string, err error) {
	if !o.Enabled() {
		return "", "", "", errors.New("oidc is not configured")
	}
	p, err := o.getProvider(ctx)
	if err != nil {
		return "", "", "", err
	}
	state = NewToken()
	verifier = oauth2.GenerateVerifier()
	url = o.oauthConfig(p).AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
	return url, state, verifier, nil
}

// Exchange swaps the code (with PKCE verifier) for tokens and returns the
// verified ID-token claims.
func (o *OIDCClient) Exchange(ctx context.Context, code, verifier string) (*OIDCClaims, error) {
	p, err := o.getProvider(ctx)
	if err != nil {
		return nil, err
	}
	tok, err := o.oauthConfig(p).Exchange(ctx, code, oauth2.VerifierOption(verifier))
	if err != nil {
		return nil, fmt.Errorf("oidc code exchange: %w", err)
	}
	rawID, ok := tok.Extra("id_token").(string)
	if !ok {
		return nil, errors.New("token response missing id_token")
	}
	idToken, err := p.Verifier(&oidc.Config{ClientID: o.cfg.OIDC.ClientID}).Verify(ctx, rawID)
	if err != nil {
		return nil, fmt.Errorf("verify id_token: %w", err)
	}
	var claims struct {
		Email         string `json:"email"`
		EmailVerified bool   `json:"email_verified"`
		Name          string `json:"name"`
	}
	if err := idToken.Claims(&claims); err != nil {
		return nil, fmt.Errorf("decode id_token claims: %w", err)
	}
	return &OIDCClaims{
		Issuer:        idToken.Issuer,
		Subject:       idToken.Subject,
		Email:         claims.Email,
		EmailVerified: claims.EmailVerified,
		Name:          claims.Name,
	}, nil
}
