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

import (
	"context"
	"crypto/rand"
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"strings"
	"time"

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

	uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/httperror"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/crypto"
	"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 VerifyOTTRequestDTO struct {
	Email string `json:"email"`
	OTT   string `json:"ott"`
}

type VerifyOTTResponseDTO struct {
	Message             string `json:"message"`
	ChallengeID         string `json:"challengeId"`
	EncryptedChallenge  string `json:"encryptedChallenge"`
	Salt                string `json:"salt"`
	EncryptedMasterKey  string `json:"encryptedMasterKey"`
	EncryptedPrivateKey string `json:"encryptedPrivateKey"`
	PublicKey           string `json:"publicKey"`
	// KDFAlgorithm specifies which key derivation algorithm to use.
	// Values: "PBKDF2-SHA256" (web frontend) or "argon2id" (native app legacy)
	KDFAlgorithm string `json:"kdfAlgorithm"`
}

type VerifyOTTService interface {
	Execute(ctx context.Context, req *VerifyOTTRequestDTO) (*VerifyOTTResponseDTO, error)
}

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

func NewVerifyOTTService(
	logger *zap.Logger,
	userGetByEmailUC uc_user.UserGetByEmailUseCase,
	cache cassandracache.CassandraCacher,
) VerifyOTTService {
	return &verifyOTTServiceImpl{
		logger:           logger.Named("VerifyOTTService"),
		userGetByEmailUC: userGetByEmailUC,
		cache:            cache,
	}
}

func (s *verifyOTTServiceImpl) Execute(ctx context.Context, req *VerifyOTTRequestDTO) (*VerifyOTTResponseDTO, error) {
	// Validate request
	if err := s.validateVerifyOTTRequest(req); err != nil {
		return nil, err // Returns RFC 9457 ProblemDetail
	}

	// Create SAGA for OTT verification workflow
	saga := transaction.NewSaga("verify-ott", s.logger)

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

	// Step 1: Normalize email
	email := strings.ToLower(strings.TrimSpace(req.Email))

	// Step 2: Get OTT from cache
	cacheKey := fmt.Sprintf("ott:%s", email)
	cachedOTT, err := s.cache.Get(ctx, cacheKey)
	if err != nil || cachedOTT == nil {
		s.logger.Warn("OTT not found in cache", zap.String("email", validation.MaskEmail(email)))
		return nil, httperror.NewUnauthorizedError("Invalid or expired verification code. Please request a new code.")
	}
	defer memguard.WipeBytes(cachedOTT) // SECURITY: Wipe OTT from memory after use

	// Step 3: Verify OTT matches using constant-time comparison
	// CWE-208: Prevents timing attacks by ensuring comparison takes same time regardless of match
	if subtle.ConstantTimeCompare(cachedOTT, []byte(req.OTT)) != 1 {
		s.logger.Warn("OTT mismatch", zap.String("email", validation.MaskEmail(email)))
		return nil, httperror.NewUnauthorizedError("Incorrect verification code. Please check the code and try again.")
	}

	// Step 4: Get user to retrieve encrypted keys (read-only, no compensation)
	user, err := s.userGetByEmailUC.Execute(ctx, email)
	if err != nil || user == nil {
		s.logger.Error("User not found after OTT verification", zap.String("email", validation.MaskEmail(email)), zap.Error(err))
		return nil, httperror.NewUnauthorizedError("User account not found. Please contact support.")
	}

	// Step 5: Generate random challenge (32 bytes)
	challenge := make([]byte, 32)
	if _, err := rand.Read(challenge); err != nil {
		s.logger.Error("Failed to generate challenge", zap.Error(err))
		return nil, httperror.NewInternalServerError("Failed to generate security challenge. Please try again.")
	}
	defer memguard.WipeBytes(challenge) // SECURITY: Wipe challenge from memory after use

	// Step 6: Generate challenge ID
	challengeID := gocql.TimeUUID().String()

	// Step 7: Store challenge in cache FIRST (compensate: delete challenge)
	// CRITICAL: Store challenge before deleting OTT to prevent lockout
	challengeKey := fmt.Sprintf("challenge:%s", challengeID)
	if err := s.cache.SetWithExpiry(ctx, challengeKey, challenge, 5*time.Minute); err != nil {
		s.logger.Error("Failed to store challenge", zap.Error(err))
		return nil, httperror.NewInternalServerError("Failed to store security challenge. Please try again.")
	}

	// Register compensation: delete challenge if OTT deletion fails
	challengeKeyCaptured := challengeKey
	saga.AddCompensation(func(ctx context.Context) error {
		s.logger.Info("compensating: deleting challenge",
			zap.String("challenge_key", challengeKeyCaptured))
		return s.cache.Delete(ctx, challengeKeyCaptured)
	})

	// Step 8: Delete OTT from cache (one-time use) (compensate: restore OTT)
	cacheKeyCaptured := cacheKey
	cachedOTTCaptured := cachedOTT
	if err := s.cache.Delete(ctx, cacheKey); err != nil {
		s.logger.Error("Failed to delete OTT",
			zap.String("cache_key", cacheKey),
			zap.Error(err))

		// Trigger compensation: Delete challenge
		saga.Rollback(ctx)
		return nil, httperror.NewInternalServerError("Verification failed. Please try again.")
	}

	// Register compensation: restore OTT with reduced TTL (5 minutes for retry)
	saga.AddCompensation(func(ctx context.Context) error {
		s.logger.Info("compensating: restoring OTT",
			zap.String("cache_key", cacheKeyCaptured))
		// Restore with reduced TTL (5 minutes) to allow user retry
		return s.cache.SetWithExpiry(ctx, cacheKeyCaptured, cachedOTTCaptured, 5*time.Minute)
	})

	// Encrypt the challenge with the user's public key using NaCl sealed box
	encryptedChallengeBytes, err := crypto.EncryptWithPublicKey(challenge, user.SecurityData.PublicKey.Key)
	if err != nil {
		s.logger.Error("Failed to encrypt challenge", zap.Error(err))
		return nil, httperror.NewInternalServerError("Failed to encrypt security challenge. Please try again.")
	}
	defer memguard.WipeBytes(encryptedChallengeBytes) // SECURITY: Wipe encrypted challenge after encoding
	encryptedChallenge := base64.StdEncoding.EncodeToString(encryptedChallengeBytes)

	s.logger.Info("OTT verified successfully",
		zap.String("email", validation.MaskEmail(email)),
		zap.String("challenge_id", challengeID),
		zap.String("challenge_key", challengeKey[:16]+"...")) // Log prefix for security

	// Prepare user's encrypted keys for frontend
	salt := base64.StdEncoding.EncodeToString(user.SecurityData.PasswordSalt)
	encryptedMasterKey := base64.StdEncoding.EncodeToString(append(user.SecurityData.EncryptedMasterKey.Nonce, user.SecurityData.EncryptedMasterKey.Ciphertext...))
	encryptedPrivateKey := base64.StdEncoding.EncodeToString(append(user.SecurityData.EncryptedPrivateKey.Nonce, user.SecurityData.EncryptedPrivateKey.Ciphertext...))
	publicKey := base64.StdEncoding.EncodeToString(user.SecurityData.PublicKey.Key)

	// Get KDF algorithm from user's security data
	kdfAlgorithm := user.SecurityData.KDFParams.Algorithm
	if kdfAlgorithm == "" {
		// Default to argon2id for backward compatibility with old accounts
		kdfAlgorithm = "argon2id"
	}

	return &VerifyOTTResponseDTO{
		Message:             "OTT verified. Please decrypt the challenge with your master key.",
		ChallengeID:         challengeID,
		EncryptedChallenge:  encryptedChallenge,
		Salt:                salt,
		EncryptedMasterKey:  encryptedMasterKey,
		EncryptedPrivateKey: encryptedPrivateKey,
		PublicKey:           publicKey,
		KDFAlgorithm:        kdfAlgorithm,
	}, nil
}

// validateVerifyOTTRequest validates the verify OTT request.
// Returns RFC 9457 ProblemDetail error with field-specific errors.
func (s *verifyOTTServiceImpl) validateVerifyOTTRequest(req *VerifyOTTRequestDTO) error {
	errors := make(map[string]string)

	// Validate email using shared validation utility
	if errMsg := validation.ValidateEmail(req.Email); errMsg != "" {
		errors["email"] = errMsg
	}

	// Validate OTT code
	ott := strings.TrimSpace(req.OTT)
	if ott == "" {
		errors["ott"] = "Verification code is required"
	} else if len(ott) != 8 {
		errors["ott"] = "Verification code must be 8 digits"
	} else {
		// Check if all characters are digits
		for _, c := range ott {
			if c < '0' || c > '9' {
				errors["ott"] = "Verification code must contain only numbers"
				break
			}
		}
	}

	// If there are validation errors, return RFC 9457 error
	if len(errors) > 0 {
		return httperror.NewValidationError(errors)
	}

	return nil
}
