package oidc

import (
	"context"
	"fmt"
	"net/url"
	"time"

	jwxjwt "github.com/lestrrat-go/jwx/v3/jwt"
	"github.com/ory/fosite"

	"github.com/pocket-id/pocket-id/backend/internal/model"
	"github.com/pocket-id/pocket-id/backend/internal/utils"
)

type ClientPreview struct {
	IDToken     map[string]any
	AccessToken map[string]any
	UserInfo    map[string]any
}

type ClientPreviewBuilder struct {
	claimsService *ClaimsService
	strategies    tokenStrategies
}

func newClientPreviewBuilder(claimsService *ClaimsService, strategies tokenStrategies) *ClientPreviewBuilder {
	return &ClientPreviewBuilder{
		claimsService: claimsService,
		strategies:    strategies,
	}
}

func (b *ClientPreviewBuilder) BuildClientPreview(ctx context.Context, client model.OidcClient, userID string, scopes []string, authenticationMethod string) (*ClientPreview, error) {
	scopeArgs, err := b.validatedScopes(ctx, client, scopes)
	if err != nil {
		return nil, err
	}

	userInfo, err := b.claimsService.GetUserClaims(ctx, userID, scopeArgs)
	if err != nil {
		return nil, err
	}

	request := b.newPreviewRequest(ctx, client, userID, scopeArgs, authenticationMethod)
	session := request.GetSession().(*Session)
	applyUserClaimsToIDToken(session, userID, userInfo)

	idToken, err := b.strategies.idToken.GenerateIDToken(ctx, b.strategies.config.GetIDTokenLifespan(ctx), request)
	if err != nil {
		return nil, fmt.Errorf("failed to generate preview ID token: %w", err)
	}

	accessToken, _, err := b.strategies.accessToken.GenerateAccessToken(ctx, request)
	if err != nil {
		return nil, fmt.Errorf("failed to generate preview access token: %w", err)
	}

	idTokenPayload, err := claimsFromJWTString(idToken)
	if err != nil {
		return nil, fmt.Errorf("failed to decode preview ID token: %w", err)
	}

	accessTokenPayload, err := claimsFromJWTString(accessToken)
	if err != nil {
		return nil, fmt.Errorf("failed to decode preview access token: %w", err)
	}

	return &ClientPreview{
		IDToken:     idTokenPayload,
		AccessToken: accessTokenPayload,
		UserInfo:    userInfo,
	}, nil
}

func (b *ClientPreviewBuilder) validatedScopes(ctx context.Context, client model.OidcClient, scopes []string) (fosite.Arguments, error) {
	// Mirror the authorize endpoint's scope handling so the preview matches the tokens a real
	// authorization would produce, including dropping unknown scopes when configured.
	requested := fosite.Arguments(fosite.RemoveEmpty(scopes))
	validated, err := fosite.FilterRequestedScopes(b.strategies.config.GetScopeStrategy(ctx), Client{OidcClient: client}, requested, b.strategies.config.GetIgnoreUnknownScopes(ctx))
	if err != nil {
		return nil, err
	}

	scopeArgs := make(fosite.Arguments, 0, len(validated))
	for _, scope := range validated {
		if !scopeArgs.Has(scope) {
			scopeArgs = append(scopeArgs, scope)
		}
	}

	return scopeArgs, nil
}

func (b *ClientPreviewBuilder) newPreviewRequest(ctx context.Context, client model.OidcClient, userID string, scopes fosite.Arguments, authenticationMethod string) *fosite.Request {
	now := time.Now().UTC()
	runtimeClient := Client{OidcClient: client}
	session := NewAuthenticatedSession(userID, authenticationMethod, now, now)
	accessTokenLifespan := fosite.GetEffectiveLifespan(runtimeClient, fosite.GrantTypeAuthorizationCode, fosite.AccessToken, b.strategies.config.GetAccessTokenLifespan(ctx))
	session.SetExpiresAt(fosite.AccessToken, now.Add(accessTokenLifespan))

	request := fosite.NewRequest()
	request.RequestedAt = now
	request.Client = runtimeClient
	request.RequestedScope = scopes
	request.GrantedScope = scopes
	request.RequestedAudience = fosite.Arguments{client.ID}
	request.GrantedAudience = fosite.Arguments{client.ID}
	request.Form = url.Values{}
	request.Session = session

	return request
}

func claimsFromJWTString(tokenString string) (map[string]any, error) {
	token, err := jwxjwt.ParseString(
		tokenString,
		jwxjwt.WithValidate(false),
		jwxjwt.WithVerify(false),
	)
	if err != nil {
		return nil, err
	}

	return utils.GetClaimsFromToken(token)
}
