package usersignup

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"time"

	"gorm.io/gorm"

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

// This file holds the one-time migration of the pre-actor signup tokens.
// The "actor tokens" migration freezes the signup_tokens table (and its user-group associations) into a JSON document stored in the "kv" table under the "signup_tokens_migrated" key.
// It's loaded here to create the per-token actors on first startup.

// signupTokensMigratedKey is the kv key under which the pre-actor signup tokens were frozen.
const signupTokensMigratedKey = "signup_tokens_migrated" // #nosec G101 -- database key name, not a credential

// migratedSignupToken is the JSON shape of a signup token frozen into the kv table by the migration.
// All timestamps are expressed as Unix seconds.
type migratedSignupToken struct {
	ID           string   `json:"id"`
	Token        string   `json:"token"`
	ExpiresAt    int64    `json:"expiresAt"`
	UsageLimit   int      `json:"usageLimit"`
	UsageCount   int      `json:"usageCount"`
	UserGroupIDs []string `json:"userGroupIds"`
	CreatedAt    int64    `json:"createdAt"`
}

// migrateSignupTokens creates an actor for every signup token frozen into the kv table by the migration.
// It requires the actor state store to be available, so it must run after the actor host is ready.
// It is idempotent: tokens that have already been migrated are left untouched, so a token that has been used since it was migrated is never reset.
func (s *Service) migrateSignupTokens(ctx context.Context) error {
	migrated, err := loadMigratedSignupTokens(ctx, s.db)
	if err != nil {
		return err
	}
	if len(migrated) == 0 {
		return nil
	}

	var count int
	for _, m := range migrated {
		// Skip tokens that have already expired, since there would be nothing left to store
		expiresAt := time.Unix(m.ExpiresAt, 0)
		if !expiresAt.After(time.Now()) {
			continue
		}

		state := SignupTokenState{
			ID:           m.ID,
			ExpiresAt:    expiresAt,
			UsageLimit:   m.UsageLimit,
			UsageCount:   m.UsageCount,
			UserGroupIDs: m.UserGroupIDs,
			CreatedAt:    time.Unix(m.CreatedAt, 0),
		}

		// The token's value is the actor's ID
		// The "migrate" method only writes the state if the actor doesn't have one already
		_, err = s.actorService.Invoke(ctx, SignupTokenActorType, m.Token, signupTokenMethodMigrate, state)
		if err != nil {
			return fmt.Errorf("error migrating signup token '%s': %w", m.ID, err)
		}
		count++
	}

	slog.InfoContext(ctx, "Migrated signup tokens to actors", slog.Int("count", count))

	return nil
}

// loadMigratedSignupTokens reads the signup tokens frozen into the kv table by the migration
// It returns nil if there's nothing to migrate
func loadMigratedSignupTokens(ctx context.Context, db *gorm.DB) ([]migratedSignupToken, error) {
	row := model.KV{
		Key: signupTokensMigratedKey,
	}
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()
	err := db.WithContext(ctx).First(&row).Error
	switch {
	case errors.Is(err, gorm.ErrRecordNotFound):
		// There are no migrated signup tokens in the database, nothing to do
		return nil, nil
	case err != nil:
		return nil, fmt.Errorf("failed to load migrated signup tokens from the database: %w", err)
	case row.Value == nil || len(*row.Value) == 0:
		// Also no migrated signup tokens, nothing to do
		return nil, nil
	}

	var migrated []migratedSignupToken
	err = json.Unmarshal([]byte(*row.Value), &migrated)
	if err != nil {
		return nil, fmt.Errorf("error parsing migrated signup tokens: %w", err)
	}

	return migrated, nil
}
