package services

import (
	"context"
	"crypto/rand"
	"database/sql"
	"encoding/base32"
	"fmt"
	mathrand "math/rand"

	"github.com/google/uuid"

	"veza-backend-api/internal/database"
	"veza-backend-api/internal/models"

	"github.com/pquerna/otp/totp"
	"go.uber.org/zap"
)

// TwoFactorService handles 2FA operations
type TwoFactorService struct {
	db     *database.Database
	logger *zap.Logger
}

// NewTwoFactorService creates a new 2FA service
func NewTwoFactorService(db *database.Database, logger *zap.Logger) *TwoFactorService {
	return &TwoFactorService{
		db:     db,
		logger: logger,
	}
}

// TwoFactorSetup represents 2FA setup information
type TwoFactorSetup struct {
	Secret        string   `json:"secret"`
	QRCodeURL     string   `json:"qr_code_url"`
	RecoveryCodes []string `json:"recovery_codes"`
}

// TwoFactorVerification represents 2FA verification
type TwoFactorVerification struct {
	Code         string `json:"code" binding:"required"`
	RecoveryCode string `json:"recovery_code,omitempty"`
}

// GenerateSecret generates a new TOTP secret
func (s *TwoFactorService) GenerateSecret(user *models.User) (*TwoFactorSetup, error) {
	// Generate a random secret
	secret := make([]byte, 20)
	if _, err := rand.Read(secret); err != nil {
		return nil, fmt.Errorf("failed to generate secret: %w", err)
	}

	// Encode as base32
	secretBase32 := base32.StdEncoding.EncodeToString(secret)

	// Generate QR code URL
	qrCodeURL := fmt.Sprintf("otpauth://totp/Veza:%s?secret=%s&issuer=Veza&algorithm=SHA1&digits=6&period=30",
		user.Email, secretBase32)

	// Generate recovery codes
	recoveryCodes := s.generateRecoveryCodes()

	setup := &TwoFactorSetup{
		Secret:        secretBase32,
		QRCodeURL:     qrCodeURL,
		RecoveryCodes: recoveryCodes,
	}

	return setup, nil
}

// EnableTwoFactor enables 2FA for a user
func (s *TwoFactorService) EnableTwoFactor(ctx context.Context, userID uuid.UUID, secret string, recoveryCodes []string) error {
	// Hash the recovery codes before storing
	hashedCodes := make([]string, len(recoveryCodes))
	for i, code := range recoveryCodes {
		hashedCodes[i] = s.hashRecoveryCode(code)
	}

	// Update user with 2FA settings
	query := `
		UPDATE users 
		SET two_factor_enabled = true, 
			two_factor_secret = $1, 
			backup_codes = $2,
			updated_at = CURRENT_TIMESTAMP
		WHERE id = $3
	`

	_, err := s.db.ExecContext(ctx, query, secret, hashedCodes, userID)
	if err != nil {
		s.logger.Error("Failed to enable 2FA", zap.Error(err), zap.String("user_id", userID.String()))
		return fmt.Errorf("failed to enable 2FA: %w", err)
	}

	s.logger.Info("2FA enabled successfully", zap.String("user_id", userID.String()))
	return nil
}

// DisableTwoFactor disables 2FA for a user
func (s *TwoFactorService) DisableTwoFactor(ctx context.Context, userID uuid.UUID) error {
	query := `
		UPDATE users 
		SET two_factor_enabled = false, 
			two_factor_secret = '', 
			backup_codes = '{}',
			updated_at = CURRENT_TIMESTAMP
		WHERE id = $1
	`

	_, err := s.db.ExecContext(ctx, query, userID)
	if err != nil {
		s.logger.Error("Failed to disable 2FA", zap.Error(err), zap.String("user_id", userID.String()))
		return fmt.Errorf("failed to disable 2FA: %w", err)
	}

	s.logger.Info("2FA disabled successfully", zap.String("user_id", userID.String()))
	return nil
}

// VerifyTwoFactor verifies a 2FA code
func (s *TwoFactorService) VerifyTwoFactor(ctx context.Context, userID uuid.UUID, code string) (bool, error) {
	// Get user's 2FA secret
	var secret string
	var recoveryCodes []string
	query := `SELECT two_factor_secret, backup_codes FROM users WHERE id = $1 AND two_factor_enabled = true`

	err := s.db.QueryRowContext(ctx, query, userID).Scan(&secret, &recoveryCodes)
	if err != nil {
		if err == sql.ErrNoRows {
			return false, fmt.Errorf("2FA not enabled for user")
		}
		return false, fmt.Errorf("failed to get 2FA secret: %w", err)
	}

	// Check if it's a recovery code
	if s.isRecoveryCode(code, recoveryCodes) {
		// Remove the used recovery code
		s.removeRecoveryCode(ctx, userID, code)
		return true, nil
	}

	// Verify TOTP code
	valid := totp.Validate(code, secret)
	if !valid {
		s.logger.Warn("Invalid 2FA code", zap.String("user_id", userID.String()))
		return false, nil
	}

	return true, nil
}

// VerifyTOTPCode verifies a TOTP code against a secret
// BE-API-001: Helper method for 2FA verification
func (s *TwoFactorService) VerifyTOTPCode(secret, code string) bool {
	return totp.Validate(code, secret)
}

// GetTwoFactorStatus gets the 2FA status for a user
func (s *TwoFactorService) GetTwoFactorStatus(ctx context.Context, userID uuid.UUID) (bool, error) {
	var enabled bool
	query := `SELECT two_factor_enabled FROM users WHERE id = $1`

	err := s.db.QueryRowContext(ctx, query, userID).Scan(&enabled)
	if err != nil {
		return false, fmt.Errorf("failed to get 2FA status: %w", err)
	}

	return enabled, nil
}

// GenerateRecoveryCodes generates 8 recovery codes (public method)
// BE-API-001: Public method for generating recovery codes
func (s *TwoFactorService) GenerateRecoveryCodes() []string {
	return s.generateRecoveryCodes()
}

// generateRecoveryCodes generates 8 recovery codes (internal)
func (s *TwoFactorService) generateRecoveryCodes() []string {
	codes := make([]string, 8)
	for i := 0; i < 8; i++ {
		// Generate 8-character alphanumeric code
		code := make([]byte, 8)
		for j := 0; j < 8; j++ {
			code[j] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[mathrand.Intn(36)]
		}
		codes[i] = string(code)
	}
	return codes
}

// hashRecoveryCode hashes a recovery code for storage
func (s *TwoFactorService) hashRecoveryCode(code string) string {
	// In production, use proper hashing (bcrypt, argon2, etc.)
	// For now, using a simple hash for demonstration
	return fmt.Sprintf("hashed_%s", code)
}

// isRecoveryCode checks if a code is a valid recovery code
func (s *TwoFactorService) isRecoveryCode(code string, storedCodes []string) bool {
	for _, storedCode := range storedCodes {
		if s.hashRecoveryCode(code) == storedCode {
			return true
		}
	}
	return false
}

// removeRecoveryCode removes a used recovery code
func (s *TwoFactorService) removeRecoveryCode(ctx context.Context, userID uuid.UUID, usedCode string) {
	// Get current recovery codes
	var recoveryCodes []string
	query := `SELECT backup_codes FROM users WHERE id = $1`

	err := s.db.QueryRowContext(ctx, query, userID).Scan(&recoveryCodes)
	if err != nil {
		s.logger.Error("Failed to get recovery codes", zap.Error(err))
		return
	}

	// Remove the used code
	newCodes := make([]string, 0)
	hashedUsedCode := s.hashRecoveryCode(usedCode)
	for _, code := range recoveryCodes {
		if code != hashedUsedCode {
			newCodes = append(newCodes, code)
		}
	}

	// Update the user
	updateQuery := `UPDATE users SET backup_codes = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`
	_, err = s.db.ExecContext(ctx, updateQuery, newCodes, userID)
	if err != nil {
		s.logger.Error("Failed to remove recovery code", zap.Error(err))
	}
}
