package oidc

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/italypaleale/francis/host/local"
	"github.com/lestrrat-go/jwx/v3/jwa"
	"github.com/pocket-id/pocket-id/backend/internal/model"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"gorm.io/gorm"
)

type Config struct {
	BaseURL                   string
	TokenBaseURL              string
	Secret                    []byte
	AllowInsecureCallbackURLs bool
}

type TokenSigner interface {
	GetPrivateKey() any
	GetKeyAlg() (jwa.KeyAlgorithm, error)
	GetKeyID() (string, bool)
}

type CustomClaimSource interface {
	GetCustomClaimsForUserWithUserGroups(ctx context.Context, userID string, tx *gorm.DB) ([]model.CustomClaim, error)
}

type ReauthenticationTokenConsumer interface {
	ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error)
}

type AuditLogger interface {
	Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool)
}

type Dependencies struct {
	DB         *gorm.DB
	Actors     *local.Host
	Config     Config
	HTTPClient *http.Client

	GetCIMDURLAllowlist func() []string

	Signer       TokenSigner
	CustomClaims CustomClaimSource
	Reauth       ReauthenticationTokenConsumer
	AuditLog     AuditLogger
	APIAccess    APIAccessProvider

	// CleanupDisabled skips registering the cron jobs that delete expired rows from the database, for example in tests
	CleanupDisabled bool
}

type Module struct {
	Preview *ClientPreviewBuilder

	config       Config
	store        *Store
	cimdResolver *cimdClientResolver

	authorizationHandler *authorizationHandler
	tokenHandler         *tokenHandler
	userInfoHandler      *userInfoHandler
	parHandler           *parHandler
	introspectionHandler *introspectionHandler
	endSessionHandler    *endSessionHandler
	deviceHandler        *deviceHandler
}

func New(ctx context.Context, deps Dependencies) (*Module, error) {
	store := NewStore(deps.DB, deps.APIAccess).WithIssuer(deps.Config.BaseURL)
	cimdResolver := newCIMDClientResolver(store, cimdResolverConfig{
		getURLAllowlist: deps.GetCIMDURLAllowlist,
		transportDecorator: func(transport http.RoundTripper) http.RoundTripper {
			return otelhttp.NewTransport(transport)
		},
	})
	store.clientResolver = cimdResolver

	authenticator, err := newFederatedClientAuthenticator(ctx, store, deps.HTTPClient, deps.Config.BaseURL)
	if err != nil {
		return nil, fmt.Errorf("failed to create federated client authenticator: %w", err)
	}
	provider, err := newProvider(store, authenticator, deps.Signer, deps.Config, cimdResolver)
	if err != nil {
		return nil, fmt.Errorf("failed to create OAuth2 provider: %w", err)
	}

	claimsService := newClaimsService(deps.DB, deps.CustomClaims, deps.Config.BaseURL, deps.Signer)
	previewBuilder := newClientPreviewBuilder(claimsService, provider.tokenStrategies)
	interactionSessionService := newInteractionSessionService(deps.DB)
	authorizationService := newAuthorizationService(deps.DB, interactionSessionService, claimsService, deps.Reauth, deps.AuditLog, deps.APIAccess)
	deviceService := newDeviceService(provider, store, provider.deviceStrategy, authorizationService, claimsService, deps.AuditLog, deps.DB)
	endSessionService := newEndSessionService(deps.DB, store, deps.Signer, deps.Config.BaseURL)

	// Register the cleanup jobs for expired OIDC rows
	if !deps.CleanupDisabled {
		if deps.Actors == nil {
			return nil, errors.New("actor host is required for the OIDC cleanup cron jobs")
		}

		jobs, err := newCleanupJobs(deps.DB)
		if err != nil {
			return nil, err
		}

		for _, cj := range jobs {
			err = deps.Actors.RegisterBuiltInActor(cj)
			if err != nil {
				return nil, fmt.Errorf("error registering OIDC cleanup cron actor %q: %w", cj.ActorType(), err)
			}
		}
	}

	return &Module{
		Preview: previewBuilder,

		config:       deps.Config,
		store:        store,
		cimdResolver: cimdResolver,

		authorizationHandler: newAuthorizationHandler(provider, authorizationService),
		tokenHandler:         newTokenHandler(provider, claimsService, deps.APIAccess),
		userInfoHandler:      newUserInfoHandler(provider, claimsService, deps.Config.BaseURL),
		parHandler:           newPARHandler(provider),
		introspectionHandler: newIntrospectionHandler(provider, authenticator, deps.Config.BaseURL),
		endSessionHandler:    newEndSessionHandler(endSessionService, deps.Config.BaseURL),
		deviceHandler:        newDeviceHandler(provider, deviceService),
	}, nil
}

// RefreshClientMetadata forces a re-fetch of the OAuth Client ID Metadata Document.
func (m *Module) RefreshClientMetadata(ctx context.Context, clientID string) (model.OidcClient, error) {
	return m.cimdResolver.RefreshMetadataClient(ctx, clientID)
}

func (m *Module) RegisterRoutes(rootGroup *gin.RouterGroup, apiGroup *gin.RouterGroup, optionalBrowserAuth gin.HandlerFunc, browserAuth gin.HandlerFunc) {
	rootGroup.GET("/authorize", optionalBrowserAuth, m.authorizationHandler.authorize)
	rootGroup.POST("/authorize", optionalBrowserAuth, m.authorizationHandler.authorize)

	apiGroup.GET("/oidc/interactions/:id", m.authorizationHandler.getInteractionSession)
	apiGroup.POST("/oidc/interactions/:id/complete", browserAuth, m.authorizationHandler.completeInteraction)

	apiGroup.POST("/oidc/par", m.parHandler.pushedAuthorizationRequest)

	apiGroup.POST("/oidc/token", m.tokenHandler.token)

	apiGroup.GET("/oidc/userinfo", m.userInfoHandler.userInfo)
	apiGroup.POST("/oidc/userinfo", m.userInfoHandler.userInfo)

	apiGroup.POST("/oidc/introspect", m.introspectionHandler.introspectToken)

	apiGroup.GET("/oidc/end-session", optionalBrowserAuth, m.endSessionHandler.endSession)
	apiGroup.POST("/oidc/end-session", optionalBrowserAuth, m.endSessionHandler.endSession)

	apiGroup.POST("/oidc/device/authorize", m.deviceHandler.authorizeDevice)
	apiGroup.POST("/oidc/device/verify", browserAuth, m.deviceHandler.verifyDeviceCode)
	apiGroup.GET("/oidc/device/info", browserAuth, m.deviceHandler.deviceCodeInfo)
}
