package emailverification

import (
	"context"
	"crypto/subtle"
	"errors"
	"fmt"
	"time"

	"github.com/italypaleale/francis/actor"

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

// ActorType is the actor type for email verification state
const ActorType = "EmailVerification"

const (
	// MethodIssue replaces the outstanding verification state for a user
	MethodIssue   = "issue"
	methodConsume = "consume"
	methodDiscard = "discard"
	methodRestore = "restore"
)

type consumeStatus string

const (
	consumeOK       consumeStatus = "ok"
	consumeNotFound consumeStatus = "not_found"
)

// State is the persisted verification state for one user
type State struct {
	TokenHash string
	Email     string
	ExpiresAt time.Time
}

type tokenRequest struct {
	TokenHash string
}

type consumeResponse struct {
	Status consumeStatus
	State  State
}

type emailVerificationActor struct {
	client actor.Client[State]
}

// NewActor allocates the email verification actor for a user
func NewActor(actorID string, service *actor.Service) actor.Actor {
	return &emailVerificationActor{
		client: actor.NewActorClient[State](ActorType, actorID, service),
	}
}

// Invoke implements actor.ActorInvoke
func (a *emailVerificationActor) Invoke(ctx context.Context, method string, data actor.Envelope) (any, error) {
	switch method {
	case MethodIssue:
		return nil, a.issue(ctx, data)
	case methodConsume:
		return a.consume(ctx, data)
	case methodDiscard:
		return nil, a.discard(ctx, data)
	case methodRestore:
		return nil, a.restore(ctx, data)
	default:
		return nil, common.ErrUnsupportedActorMethod{Method: method}
	}
}

func (a *emailVerificationActor) issue(ctx context.Context, data actor.Envelope) error {
	state, err := decodeState(data, MethodIssue)
	if err != nil {
		return err
	}

	return a.setState(ctx, state)
}

func (a *emailVerificationActor) consume(ctx context.Context, data actor.Envelope) (consumeResponse, error) {
	request, err := decodeTokenRequest(data, methodConsume)
	if err != nil {
		return consumeResponse{}, err
	}

	state, err := a.client.GetState(ctx)
	if err != nil {
		return consumeResponse{}, fmt.Errorf("error retrieving actor state: %w", err)
	}

	// Compare if the hash matches
	if state.TokenHash == "" || state.ExpiresAt.Before(time.Now()) ||
		subtle.ConstantTimeCompare([]byte(state.TokenHash), []byte(request.TokenHash)) != 1 {
		return consumeResponse{Status: consumeNotFound}, nil
	}

	err = a.client.DeleteState(ctx)
	if err != nil {
		return consumeResponse{}, fmt.Errorf("error deleting actor state: %w", err)
	}

	return consumeResponse{
		Status: consumeOK,
		State:  state,
	}, nil
}

func (a *emailVerificationActor) discard(ctx context.Context, data actor.Envelope) error {
	request, err := decodeTokenRequest(data, methodDiscard)
	if err != nil {
		return err
	}

	state, err := a.client.GetState(ctx)
	if err != nil {
		return fmt.Errorf("error retrieving actor state: %w", err)
	}

	// Only discard if the token hash matches, to avoid discarding a newer token that may have been issued after the one being discarded
	if state.TokenHash == "" || subtle.ConstantTimeCompare([]byte(state.TokenHash), []byte(request.TokenHash)) != 1 {
		return nil
	}

	err = a.client.DeleteState(ctx)
	if err != nil && !errors.Is(err, actor.ErrStateNotFound) {
		return fmt.Errorf("error deleting actor state: %w", err)
	}

	return nil
}

func (a *emailVerificationActor) restore(ctx context.Context, data actor.Envelope) error {
	state, err := decodeState(data, methodRestore)
	if err != nil {
		return err
	}

	current, err := a.client.GetState(ctx)
	if err != nil {
		return fmt.Errorf("error retrieving actor state: %w", err)
	}

	// Preserve a newer verification request that may have been issued after consumption
	if current.TokenHash != "" {
		return nil
	}

	return a.setState(ctx, state)
}

func (a *emailVerificationActor) setState(ctx context.Context, state State) error {
	ttl := time.Until(state.ExpiresAt)
	if ttl <= 0 {
		return nil
	}

	err := a.client.SetState(ctx, state, &actor.SetStateOpts{TTL: ttl})
	if err != nil {
		return fmt.Errorf("error saving actor state: %w", err)
	}

	return nil
}

func decodeState(data actor.Envelope, method string) (State, error) {
	if data == nil {
		return State{}, fmt.Errorf("request body is empty for method '%s'", method)
	}

	var state State
	err := data.Decode(&state)
	if err != nil {
		return State{}, fmt.Errorf("request body is not valid for method '%s': %w", method, err)
	}

	return state, nil
}

func decodeTokenRequest(data actor.Envelope, method string) (tokenRequest, error) {
	if data == nil {
		return tokenRequest{}, fmt.Errorf("request body is empty for method '%s'", method)
	}

	var request tokenRequest
	err := data.Decode(&request)
	if err != nil {
		return tokenRequest{}, fmt.Errorf("request body is not valid for method '%s': %w", method, err)
	}

	return request, nil
}
