// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth/recovery_verify.go
package auth

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"time"

	"github.com/awnumar/memguard"
	"go.uber.org/zap"

	uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/storage/cache/cassandracache"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/transaction"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
)

type RecoveryVerifyRequestDTO struct {
	SessionID          string `json:"session_id"`
	DecryptedChallenge string `json:"decrypted_challenge"`
}

type RecoveryVerifyResponseDTO struct {
	Message                           string `json:"message"`
	RecoveryToken                     string `json:"recovery_token"`
	CanResetCredentials               bool   `json:"can_reset_credentials"`
	MasterKeyEncryptedWithRecoveryKey string `json:"master_key_encrypted_with_recovery_key"`
}

type RecoveryVerifyService interface {
	Execute(ctx context.Context, req *RecoveryVerifyRequestDTO) (*RecoveryVerifyResponseDTO, error)
}

type recoveryVerifyServiceImpl struct {
	logger           *zap.Logger
	cache            cassandracache.CassandraCacher
	userGetByEmailUC uc_user.UserGetByEmailUseCase
}

func NewRecoveryVerifyService(
	logger *zap.Logger,
	cache cassandracache.CassandraCacher,
	userGetByEmailUC uc_user.UserGetByEmailUseCase,
) RecoveryVerifyService {
	return &recoveryVerifyServiceImpl{
		logger:           logger.Named("RecoveryVerifyService"),
		cache:            cache,
		userGetByEmailUC: userGetByEmailUC,
	}
}

func (s *recoveryVerifyServiceImpl) Execute(ctx context.Context, req *RecoveryVerifyRequestDTO) (*RecoveryVerifyResponseDTO, error) {
	// Create SAGA for recovery verify workflow
	saga := transaction.NewSaga("recovery-verify", s.logger)

	s.logger.Info("starting recovery verification")

	// Step 1: Get the original challenge from cache
	challengeKey := fmt.Sprintf("recovery_challenge:%s", req.SessionID)
	originalChallenge, err := s.cache.Get(ctx, challengeKey)
	if err != nil || originalChallenge == nil {
		s.logger.Warn("Recovery challenge not found or expired", zap.String("session_id", req.SessionID))
		return nil, fmt.Errorf("invalid or expired recovery session")
	}
	defer memguard.WipeBytes(originalChallenge) // SECURITY: Wipe challenge from memory

	// Step 2: Decode the decrypted challenge from base64
	decryptedChallenge, err := base64.StdEncoding.DecodeString(req.DecryptedChallenge)
	if err != nil {
		s.logger.Warn("Failed to decode decrypted challenge", zap.Error(err))
		return nil, fmt.Errorf("invalid decrypted challenge format")
	}
	defer memguard.WipeBytes(decryptedChallenge) // SECURITY: Wipe decrypted challenge from memory

	// Step 3: Verify that decrypted challenge matches original
	if !bytes.Equal(decryptedChallenge, originalChallenge) {
		s.logger.Warn("Recovery challenge verification failed", zap.String("session_id", req.SessionID))
		return nil, fmt.Errorf("challenge verification failed")
	}

	// Step 4: Generate recovery token (random secure token)
	tokenBytes := make([]byte, 32)
	if _, err := rand.Read(tokenBytes); err != nil {
		s.logger.Error("Failed to generate recovery token", zap.Error(err))
		return nil, fmt.Errorf("failed to generate recovery token")
	}
	defer memguard.WipeBytes(tokenBytes) // SECURITY: Wipe token bytes from memory
	recoveryToken := base64.URLEncoding.EncodeToString(tokenBytes)

	// Step 5: Get email associated with recovery session (read-only, no compensation)
	emailKey := fmt.Sprintf("recovery_email:%s", req.SessionID)
	email, err := s.cache.Get(ctx, emailKey)
	if err != nil || email == nil {
		s.logger.Error("Recovery email not found", zap.String("session_id", req.SessionID))
		return nil, fmt.Errorf("recovery session invalid")
	}

	// Step 5b: Fetch user to get their encrypted master key with recovery key
	user, err := s.userGetByEmailUC.Execute(ctx, string(email))
	if err != nil || user == nil {
		s.logger.Error("User not found for recovery", zap.String("email", validation.MaskEmail(string(email))))
		return nil, fmt.Errorf("user not found")
	}

	// Validate user has the required key data
	if user.SecurityData == nil ||
		user.SecurityData.MasterKeyEncryptedWithRecoveryKey.Ciphertext == nil ||
		user.SecurityData.MasterKeyEncryptedWithRecoveryKey.Nonce == nil {
		s.logger.Error("User missing master key encrypted with recovery key",
			zap.String("email", validation.MaskEmail(string(email))))
		return nil, fmt.Errorf("account recovery data not available")
	}

	// Combine nonce + ciphertext for transmission (matches frontend expectation)
	// Format: nonce (24 bytes) || ciphertext (variable length)
	nonce := user.SecurityData.MasterKeyEncryptedWithRecoveryKey.Nonce
	ciphertext := user.SecurityData.MasterKeyEncryptedWithRecoveryKey.Ciphertext
	combined := make([]byte, len(nonce)+len(ciphertext))
	copy(combined[:len(nonce)], nonce)
	copy(combined[len(nonce):], ciphertext)
	defer memguard.WipeBytes(combined) // SECURITY: Wipe combined key data from memory

	// Encode the combined data to base64 for transmission
	masterKeyEncryptedWithRecoveryKeyBase64 := base64.StdEncoding.EncodeToString(combined)

	// Step 6: Store recovery token FIRST (compensate: delete recovery token)
	// CRITICAL: Store recovery token before deleting challenge to prevent flow interruption
	tokenKey := fmt.Sprintf("recovery_token:%s", recoveryToken)
	if err := s.cache.SetWithExpiry(ctx, tokenKey, email, 15*time.Minute); err != nil {
		s.logger.Error("Failed to store recovery token", zap.Error(err))
		return nil, fmt.Errorf("failed to complete recovery verification")
	}

	// Register compensation: delete recovery token if challenge deletion fails
	tokenKeyCaptured := tokenKey
	saga.AddCompensation(func(ctx context.Context) error {
		s.logger.Info("compensating: deleting recovery token",
			zap.String("token_key", tokenKeyCaptured))
		return s.cache.Delete(ctx, tokenKeyCaptured)
	})

	// Step 7: Clear recovery challenge (one-time use) (compensate: restore challenge)
	challengeKeyCaptured := challengeKey
	originalChallengeCaptured := originalChallenge
	if err := s.cache.Delete(ctx, challengeKey); err != nil {
		s.logger.Error("Failed to delete recovery challenge",
			zap.String("challenge_key", challengeKey),
			zap.Error(err))

		// Trigger compensation: Delete recovery token
		saga.Rollback(ctx)
		return nil, fmt.Errorf("failed to delete recovery challenge: %w", err)
	}

	// Register compensation: restore challenge with reduced TTL (15 minutes for retry)
	saga.AddCompensation(func(ctx context.Context) error {
		s.logger.Info("compensating: restoring recovery challenge",
			zap.String("challenge_key", challengeKeyCaptured))
		// Restore with same TTL (15 minutes) to allow user retry
		return s.cache.SetWithExpiry(ctx, challengeKeyCaptured, originalChallengeCaptured, 15*time.Minute)
	})

	s.logger.Info("Recovery verification successful",
		zap.String("session_id", req.SessionID),
		zap.String("email", validation.MaskEmail(string(email))),
		zap.String("recovery_token", recoveryToken[:16]+"...")) // Log prefix for security

	return &RecoveryVerifyResponseDTO{
		Message:                           "Recovery challenge verified successfully. You can now reset your credentials.",
		RecoveryToken:                     recoveryToken,
		CanResetCredentials:               true,
		MasterKeyEncryptedWithRecoveryKey: masterKeyEncryptedWithRecoveryKeyBase64,
	}, nil
}
