package service

import (
	"context"
	"crypto/tls"
	"net/http"
	"time"

	"github.com/tinyauthapp/tinyauth/internal/model"
	"golang.org/x/oauth2"
)

type MapClaims func(claims map[string]any) model.Claims
type OAuthUserinfoExtractor func(client *http.Client, ctx context.Context, url string, mapClaims MapClaims) (*model.Claims, error)

type OAuthService struct {
	serviceCfg        model.OAuthServiceConfig
	config            *oauth2.Config
	ctx               context.Context
	userinfoExtractor OAuthUserinfoExtractor
	id                string
}

func NewOAuthService(config model.OAuthServiceConfig, id string, ctx context.Context) *OAuthService {
	httpClient := &http.Client{
		Timeout: 30 * time.Second,
		Transport: &http.Transport{
			Proxy: http.ProxyFromEnvironment,
			TLSClientConfig: &tls.Config{
				InsecureSkipVerify: config.Insecure,
				MinVersion:         tls.VersionTLS12,
			},
		},
	}
	vctx := context.WithValue(ctx, oauth2.HTTPClient, httpClient)

	return &OAuthService{
		serviceCfg: config,
		config: &oauth2.Config{
			ClientID:     config.ClientID,
			ClientSecret: config.ClientSecret,
			RedirectURL:  config.RedirectURL,
			Scopes:       config.Scopes,
			Endpoint: oauth2.Endpoint{
				AuthURL:  config.AuthURL,
				TokenURL: config.TokenURL,
			},
		},
		ctx:               vctx,
		userinfoExtractor: defaultExtractor,
		id:                id,
	}
}

func (s *OAuthService) WithUserinfoExtractor(extractor OAuthUserinfoExtractor) *OAuthService {
	s.userinfoExtractor = extractor
	return s
}

func (s *OAuthService) Name() string {
	return s.serviceCfg.Name
}

func (s *OAuthService) ID() string {
	return s.id
}

func (s *OAuthService) NewRandom() string {
	// The generate verifier function just creates a random string,
	// so we can use it to generate a random state as well
	random := oauth2.GenerateVerifier()
	return random
}

func (s *OAuthService) GetAuthURL(state, verifier string) string {
	return s.config.AuthCodeURL(state, oauth2.AccessTypeOnline, oauth2.S256ChallengeOption(verifier))
}

func (s *OAuthService) GetToken(code string, verifier string) (*oauth2.Token, error) {
	return s.config.Exchange(s.ctx, code, oauth2.VerifierOption(verifier))
}

func (s *OAuthService) GetUserinfo(token *oauth2.Token) (*model.Claims, error) {
	client := oauth2.NewClient(s.ctx, oauth2.StaticTokenSource(token))
	return s.userinfoExtractor(client, s.ctx, s.serviceCfg.UserinfoURL, s.mapClaims)
}

func (s *OAuthService) GetConfig() model.OAuthServiceConfig {
	return s.serviceCfg
}

func (s *OAuthService) UpdateConfig(config model.OAuthServiceConfig) {
	s.serviceCfg = config
	s.config.ClientID = config.ClientID
	s.config.ClientSecret = config.ClientSecret
	s.config.Scopes = config.Scopes
	s.config.Endpoint.AuthURL = config.AuthURL
	s.config.Endpoint.TokenURL = config.TokenURL
	s.config.RedirectURL = config.RedirectURL
}

func (s *OAuthService) mapClaims(claims map[string]any) model.Claims {
	return model.Claims{
		Sub:               mapClaim[string]("sub", "", claims),
		Name:              mapClaim[string]("name", s.serviceCfg.Claims.Name, claims),
		PreferredUsername: mapClaim[string]("preferred_username", s.serviceCfg.Claims.Username, claims),
		Email:             mapClaim[string]("email", s.serviceCfg.Claims.Email, claims),
		Groups:            mapClaim[any]("groups", s.serviceCfg.Claims.Groups, claims),
	}
}

func mapClaim[T any](fallback, override string, kv map[string]any) T {
	key := fallback
	if override != "" {
		key = override
	}
	v, ok := kv[key].(T)
	if !ok {
		var zero T
		return zero
	}
	return v
}
