package webauthn

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/italypaleale/francis/host/local"
	"github.com/lestrrat-go/jwx/v3/jwt"
	"gorm.io/gorm"

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

type TokenService interface {
	GenerateAccessToken(user model.User, authenticationMethod string, sessionDuration time.Duration) (string, error)
	VerifyAccessToken(tokenString string) (jwt.Token, error)
	GetAuthenticationMethod(token jwt.Token) (string, error)
}

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

type Dependencies struct {
	DB     *gorm.DB
	Actors *local.Host
	AppURL string

	Signer    TokenService
	AuditLog  AuditLogger
	AppConfig appconfig.AppConfigResolver

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

type Module struct {
	service *Service
	handler *handler
}

func New(deps Dependencies) (*Module, error) {
	service, err := newService(deps)
	if err != nil {
		return nil, err
	}

	// Register the cleanup jobs for expired WebAuthn rows
	if !deps.CleanupDisabled {
		if deps.Actors == nil {
			return nil, errors.New("actor host is required for the WebAuthn 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 WebAuthn cleanup cron actor %q: %w", cj.ActorType(), err)
			}
		}
	}

	return &Module{
		service: service,
		handler: newHandler(service, deps.AppConfig),
	}, nil
}

// RegisterRoutes mounts the WebAuthn registration, login and reauthentication endpoints
func (m *Module) RegisterRoutes(apiGroup *gin.RouterGroup, userAuth, browserAuth, loginRateLimit, reauthRateLimit gin.HandlerFunc) {
	apiGroup.GET("/webauthn/register/start", browserAuth, httpserver.Handle(m.handler.beginRegistration))
	apiGroup.POST("/webauthn/register/finish", browserAuth, httpserver.Handle(m.handler.verifyRegistration))

	apiGroup.GET("/webauthn/login/start", httpserver.Handle(m.handler.beginLogin))
	apiGroup.POST("/webauthn/login/finish", loginRateLimit, httpserver.Handle(m.handler.verifyLogin))

	apiGroup.POST("/webauthn/logout", userAuth, httpserver.Handle(m.handler.logout))

	apiGroup.POST("/webauthn/reauthenticate", browserAuth, reauthRateLimit, httpserver.Handle(m.handler.reauthenticate))

	apiGroup.GET("/webauthn/credentials", userAuth, httpserver.Handle(m.handler.listCredentials))
	apiGroup.PATCH("/webauthn/credentials/:id", userAuth, httpserver.Handle(m.handler.updateCredential))
	apiGroup.DELETE("/webauthn/credentials/:id", userAuth, httpserver.Handle(m.handler.deleteCredential))
}

// ConsumeReauthenticationToken implements the OIDC module's ReauthenticationTokenConsumer interface
func (m *Module) ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error) {
	return m.service.ConsumeReauthenticationToken(ctx, tx, token, userID)
}

// ListCredentials returns the passkeys registered for the given user
// It is consumed by the user controller for the admin "manage passkeys" view
func (m *Module) ListCredentials(ctx context.Context, userID string) ([]model.WebauthnCredential, error) {
	return m.service.ListCredentials(ctx, userID)
}

// DeleteCredential removes a passkey, optionally on behalf of an admin acting for another user
// It is consumed by the user controller for the admin "manage passkeys" view
func (m *Module) DeleteCredential(ctx context.Context, userID, credentialID, ipAddress, userAgent, actorUserID string) error {
	return m.service.DeleteCredential(ctx, userID, credentialID, ipAddress, userAgent, actorUserID)
}
