package services

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

	"veza-backend-api/internal/database"

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

// TOTPService gère l'authentification à deux facteurs
type TOTPService struct {
	db     *database.Database
	logger *zap.Logger
}

// TOTPSecret représente un secret TOTP pour un utilisateur
type TOTPSecret struct {
	ID        uuid.UUID `json:"id" db:"id"`
	UserID    uuid.UUID `json:"user_id" db:"user_id"`
	Secret    string    `json:"-" db:"secret"`
	CreatedAt time.Time `json:"created_at" db:"created_at"`
	Enabled   bool      `json:"enabled" db:"enabled"`
}

// TOTPSetupResponse réponse pour la configuration 2FA
type TOTPSetupResponse struct {
	Secret      string   `json:"secret"`
	QRCodeURL   string   `json:"qr_code_url"`
	BackupCodes []string `json:"backup_codes"`
}

// TOTPVerificationRequest requête de vérification 2FA
type TOTPVerificationRequest struct {
	UserID     uuid.UUID `json:"user_id"`
	Code       string    `json:"code"`
	BackupCode string    `json:"backup_code,omitempty"`
}

// BackupCode représente un code de sauvegarde
type BackupCode struct {
	ID        uuid.UUID  `json:"id" db:"id"`
	UserID    uuid.UUID  `json:"user_id" db:"user_id"`
	Code      string     `json:"code" db:"code"`
	Used      bool       `json:"used" db:"used"`
	CreatedAt time.Time  `json:"created_at" db:"created_at"`
	UsedAt    *time.Time `json:"used_at" db:"used_at"`
}

// NewTOTPService crée un nouveau service TOTP
func NewTOTPService(db *database.Database, logger *zap.Logger) *TOTPService {
	return &TOTPService{
		db:     db,
		logger: logger,
	}
}

// SetupTOTP configure le 2FA pour un utilisateur
func (ts *TOTPService) SetupTOTP(ctx context.Context, userID uuid.UUID, email string) (*TOTPSetupResponse, error) {
	// Vérifier si l'utilisateur a déjà un secret TOTP
	var existingSecret TOTPSecret
	err := ts.db.QueryRowContext(ctx, `
		SELECT id, user_id, secret, created_at, enabled
		FROM totp_secrets
		WHERE user_id = $1
	`, userID).Scan(
		&existingSecret.ID,
		&existingSecret.UserID,
		&existingSecret.Secret,
		&existingSecret.CreatedAt,
		&existingSecret.Enabled,
	)

	if err != nil && err != sql.ErrNoRows {
		ts.logger.Error("Failed to check existing TOTP secret",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return nil, fmt.Errorf("failed to check existing TOTP secret: %w", err)
	}

	var secret string
	var secretID uuid.UUID

	if err == sql.ErrNoRows {
		// Créer un nouveau secret
		secret = ts.generateSecret()
		secretID = uuid.New()

		_, err = ts.db.ExecContext(ctx, `
			INSERT INTO totp_secrets (id, user_id, secret, created_at, enabled)
			VALUES ($1, $2, $3, $4, $5)
		`, secretID, userID, secret, time.Now(), false)

		if err != nil {
			ts.logger.Error("Failed to create TOTP secret",
				zap.Error(err),
				zap.String("user_id", userID.String()),
			)
			return nil, fmt.Errorf("failed to create TOTP secret: %w", err)
		}
	} else {
		// Utiliser le secret existant
		secret = existingSecret.Secret
		secretID = existingSecret.ID
	}

	// Générer les codes de sauvegarde
	backupCodes, err := ts.generateBackupCodes(ctx, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to generate backup codes: %w", err)
	}

	// Générer l'URL QR Code
	issuer := "Veza Platform"
	accountName := email
	qrCodeURL := ts.generateQRCodeURL(issuer, accountName, secret)

	ts.logger.Info("TOTP setup initiated",
		zap.String("user_id", userID.String()),
		zap.String("secret_id", secretID.String()),
	)

	return &TOTPSetupResponse{
		Secret:      secret,
		QRCodeURL:   qrCodeURL,
		BackupCodes: backupCodes,
	}, nil
}

// VerifyTOTP vérifie un code TOTP
func (ts *TOTPService) VerifyTOTP(ctx context.Context, req *TOTPVerificationRequest) (bool, error) {
	// Récupérer le secret TOTP de l'utilisateur
	var secret string
	var enabled bool
	err := ts.db.QueryRowContext(ctx, `
		SELECT secret, enabled
		FROM totp_secrets
		WHERE user_id = $1
	`, req.UserID).Scan(&secret, &enabled)

	if err != nil {
		if err == sql.ErrNoRows {
			return false, fmt.Errorf("TOTP not configured for user")
		}
		ts.logger.Error("Failed to get TOTP secret",
			zap.Error(err),
			zap.String("user_id", req.UserID.String()),
		)
		return false, fmt.Errorf("failed to get TOTP secret: %w", err)
	}

	// Vérifier le code TOTP
	valid := totp.Validate(req.Code, secret)
	if valid {
		ts.logger.Info("TOTP verification successful",
			zap.String("user_id", req.UserID.String()),
		)
		return true, nil
	}

	// Si le code TOTP n'est pas valide, vérifier les codes de sauvegarde
	if req.BackupCode != "" {
		valid, err := ts.verifyBackupCode(ctx, req.UserID, req.BackupCode)
		if err != nil {
			return false, fmt.Errorf("failed to verify backup code: %w", err)
		}
		if valid {
			ts.logger.Info("Backup code verification successful",
				zap.String("user_id", req.UserID.String()),
			)
			return true, nil
		}
	}

	ts.logger.Warn("TOTP verification failed",
		zap.String("user_id", req.UserID.String()),
	)

	return false, nil
}

// EnableTOTP active le 2FA pour un utilisateur
func (ts *TOTPService) EnableTOTP(ctx context.Context, userID uuid.UUID, code string) error {
	// Vérifier le code avant d'activer
	valid, err := ts.VerifyTOTP(ctx, &TOTPVerificationRequest{
		UserID: userID,
		Code:   code,
	})
	if err != nil {
		return fmt.Errorf("failed to verify TOTP code: %w", err)
	}

	if !valid {
		return fmt.Errorf("invalid TOTP code")
	}

	// Activer le 2FA
	_, err = ts.db.ExecContext(ctx, `
		UPDATE totp_secrets
		SET enabled = true
		WHERE user_id = $1
	`, userID)

	if err != nil {
		ts.logger.Error("Failed to enable TOTP",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return fmt.Errorf("failed to enable TOTP: %w", err)
	}

	ts.logger.Info("TOTP enabled",
		zap.String("user_id", userID.String()),
	)

	return nil
}

// DisableTOTP désactive le 2FA pour un utilisateur
func (ts *TOTPService) DisableTOTP(ctx context.Context, userID uuid.UUID, code string) error {
	// Vérifier le code avant de désactiver
	valid, err := ts.VerifyTOTP(ctx, &TOTPVerificationRequest{
		UserID: userID,
		Code:   code,
	})
	if err != nil {
		return fmt.Errorf("failed to verify TOTP code: %w", err)
	}

	if !valid {
		return fmt.Errorf("invalid TOTP code")
	}

	// Désactiver le 2FA
	_, err = ts.db.ExecContext(ctx, `
		UPDATE totp_secrets
		SET enabled = false
		WHERE user_id = $1
	`, userID)

	if err != nil {
		ts.logger.Error("Failed to disable TOTP",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return fmt.Errorf("failed to disable TOTP: %w", err)
	}

	// Supprimer les codes de sauvegarde
	_, err = ts.db.ExecContext(ctx, `
		DELETE FROM backup_codes
		WHERE user_id = $1
	`, userID)

	if err != nil {
		ts.logger.Warn("Failed to delete backup codes",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
	}

	ts.logger.Info("TOTP disabled",
		zap.String("user_id", userID.String()),
	)

	return nil
}

// IsTOTPEnabled vérifie si le 2FA est activé pour un utilisateur
func (ts *TOTPService) IsTOTPEnabled(ctx context.Context, userID uuid.UUID) (bool, error) {
	var enabled bool
	err := ts.db.QueryRowContext(ctx, `
		SELECT enabled
		FROM totp_secrets
		WHERE user_id = $1
	`, userID).Scan(&enabled)

	if err != nil {
		if err == sql.ErrNoRows {
			return false, nil
		}
		ts.logger.Error("Failed to check TOTP status",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return false, fmt.Errorf("failed to check TOTP status: %w", err)
	}

	return enabled, nil
}

// generateSecret génère un secret TOTP
func (ts *TOTPService) generateSecret() string {
	// Générer 20 bytes aléatoires
	secret := make([]byte, 20)
	rand.Read(secret)

	// Encoder en base32
	return base32.StdEncoding.EncodeToString(secret)
}

// generateQRCodeURL génère l'URL du QR Code
func (ts *TOTPService) generateQRCodeURL(issuer, accountName, secret string) string {
	key, err := totp.Generate(totp.GenerateOpts{
		Issuer:      issuer,
		AccountName: accountName,
		Secret:      []byte(secret),
	})
	if err != nil {
		ts.logger.Error("Failed to generate TOTP key",
			zap.Error(err),
		)
		return ""
	}

	return key.URL()
}

// generateBackupCodes génère des codes de sauvegarde
func (ts *TOTPService) generateBackupCodes(ctx context.Context, userID uuid.UUID) ([]string, error) {
	// Supprimer les anciens codes
	_, err := ts.db.ExecContext(ctx, `
		DELETE FROM backup_codes
		WHERE user_id = $1
	`, userID)
	if err != nil {
		return nil, fmt.Errorf("failed to delete old backup codes: %w", err)
	}

	// Générer 10 nouveaux codes
	codes := make([]string, 10)
	for i := 0; i < 10; i++ {
		code := ts.generateBackupCode()
		codes[i] = code

		// Insérer en base
		_, err = ts.db.ExecContext(ctx, `
			INSERT INTO backup_codes (id, user_id, code, created_at, used)
			VALUES ($1, $2, $3, $4, $5)
		`, uuid.New(), userID, code, time.Now(), false)

		if err != nil {
			ts.logger.Error("Failed to insert backup code",
				zap.Error(err),
				zap.String("user_id", userID.String()),
				zap.Int("code_index", i),
			)
			return nil, fmt.Errorf("failed to insert backup code: %w", err)
		}
	}

	return codes, nil
}

// generateBackupCode génère un code de sauvegarde
func (ts *TOTPService) generateBackupCode() string {
	// Générer 8 bytes aléatoires
	code := make([]byte, 8)
	rand.Read(code)

	// Encoder en base32 et prendre les 8 premiers caractères
	encoded := base32.StdEncoding.EncodeToString(code)
	return encoded[:8]
}

// verifyBackupCode vérifie un code de sauvegarde
func (ts *TOTPService) verifyBackupCode(ctx context.Context, userID uuid.UUID, code string) (bool, error) {
	var backupCode BackupCode
	err := ts.db.QueryRowContext(ctx, `
		SELECT id, user_id, code, used, created_at, used_at
		FROM backup_codes
		WHERE user_id = $1 AND code = $2 AND used = false
	`, userID, code).Scan(
		&backupCode.ID,
		&backupCode.UserID,
		&backupCode.Code,
		&backupCode.Used,
		&backupCode.CreatedAt,
		&backupCode.UsedAt,
	)

	if err != nil {
		if err == sql.ErrNoRows {
			return false, nil
		}
		ts.logger.Error("Failed to verify backup code",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return false, fmt.Errorf("failed to verify backup code: %w", err)
	}

	// Marquer le code comme utilisé
	_, err = ts.db.ExecContext(ctx, `
		UPDATE backup_codes
		SET used = true, used_at = NOW()
		WHERE id = $1
	`, backupCode.ID)

	if err != nil {
		ts.logger.Error("Failed to mark backup code as used",
			zap.Error(err),
			zap.String("backup_code_id", backupCode.ID.String()),
		)
		return false, fmt.Errorf("failed to mark backup code as used: %w", err)
	}

	ts.logger.Info("Backup code used",
		zap.String("user_id", userID.String()),
		zap.String("backup_code_id", backupCode.ID.String()),
	)

	return true, nil
}

// GetBackupCodes récupère les codes de sauvegarde d'un utilisateur
func (ts *TOTPService) GetBackupCodes(ctx context.Context, userID uuid.UUID) ([]string, error) {
	rows, err := ts.db.QueryContext(ctx, `
		SELECT code
		FROM backup_codes
		WHERE user_id = $1 AND used = false
		ORDER BY created_at ASC
	`, userID)

	if err != nil {
		ts.logger.Error("Failed to get backup codes",
			zap.Error(err),
			zap.String("user_id", userID.String()),
		)
		return nil, fmt.Errorf("failed to get backup codes: %w", err)
	}
	defer rows.Close()

	var codes []string
	for rows.Next() {
		var code string
		err := rows.Scan(&code)
		if err != nil {
			ts.logger.Error("Failed to scan backup code",
				zap.Error(err),
			)
			continue
		}
		codes = append(codes, code)
	}

	return codes, nil
}
