package services

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

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"go.uber.org/zap"
	"golang.org/x/crypto/bcrypt"

	"veza-backend-api/internal/database"
	"veza-backend-api/internal/validators"
)

const bcryptCost = 12

// PasswordService handles password operations
type PasswordService struct {
	db                *database.Database
	logger            *zap.Logger
	passwordValidator *validators.PasswordValidator
}

// PasswordResetToken represents a password reset token
type PasswordResetToken struct {
	ID        int64     `db:"id"`
	UserID    uuid.UUID `db:"user_id"`
	Token     string    `db:"token"`
	ExpiresAt time.Time `db:"expires_at"`
	Used      bool      `db:"used"`
	CreatedAt time.Time `db:"created_at"`
}

// UserInfo represents a user from the database
type UserInfo struct {
	ID       uuid.UUID `db:"id"`
	Email    string    `db:"email"`
	Username string    `db:"username"`
}

// NewPasswordService creates a new password service
func NewPasswordService(db *database.Database, logger *zap.Logger) *PasswordService {
	return &PasswordService{
		db:                db,
		logger:            logger,
		passwordValidator: validators.NewPasswordValidator(),
	}
}

// GetUserByEmail retrieves a user by email
func (ps *PasswordService) GetUserByEmail(email string) (*UserInfo, error) {
	ctx := context.Background()

	var user UserInfo
	err := ps.db.QueryRowContext(ctx, `
		SELECT id, email, username
		FROM users
		WHERE email = $1
	`, email).Scan(&user.ID, &user.Email, &user.Username)

	if err != nil {
		if err == sql.ErrNoRows {
			return nil, fmt.Errorf("user not found")
		}
		return nil, err
	}

	return &user, nil
}

// GeneratePasswordResetToken generates a secure password reset token
func (ps *PasswordService) GeneratePasswordResetToken(userID uuid.UUID) (string, time.Time, error) {
	// Generate random token
	tokenBytes := make([]byte, 32)
	_, err := rand.Read(tokenBytes)
	if err != nil {
		return "", time.Time{}, err
	}
	token := base64.URLEncoding.EncodeToString(tokenBytes)

	// Set expiration (1 hour)
	expiresAt := time.Now().Add(1 * time.Hour)

	// Store in database
	ctx := context.Background()
	_, err = ps.db.ExecContext(ctx, `
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used)
		VALUES ($1, $2, $3, FALSE)
	`, userID, token, expiresAt)

	if err != nil {
		return "", time.Time{}, err
	}

	ps.logger.Info("Password reset token generated",
		zap.String("user_id", userID.String()),
	)

	return token, expiresAt, nil
}

// ResetPassword validates and processes password reset
func (ps *PasswordService) ResetPassword(token, newPassword string) error {
	ctx := context.Background()

	// Get token info
	var resetToken PasswordResetToken
	err := ps.db.QueryRowContext(ctx, `
		SELECT id, user_id, token, expires_at, used, created_at
		FROM password_reset_tokens
		WHERE token = $1 AND used = FALSE
	`, token).Scan(
		&resetToken.ID,
		&resetToken.UserID,
		&resetToken.Token,
		&resetToken.ExpiresAt,
		&resetToken.Used,
		&resetToken.CreatedAt,
	)

	if err != nil {
		if err == sql.ErrNoRows {
			return fmt.Errorf("invalid or expired reset token")
		}
		return err
	}

	// Check if expired
	if time.Now().After(resetToken.ExpiresAt) {
		return fmt.Errorf("reset token has expired")
	}

	// Validate password strength
	// BE-SEC-006: Use comprehensive password validator
	strength, err := ps.passwordValidator.Validate(newPassword)
	if err != nil || !strength.Valid {
		if err != nil {
			return err
		}
		return fmt.Errorf("weak password: %s", strings.Join(strength.Details, ", "))
	}

	// Hash new password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), 12)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Update user password
	_, err = ps.db.ExecContext(ctx, `
		UPDATE users
		SET password_hash = $1, updated_at = NOW()
		WHERE id = $2
	`, string(hashedPassword), resetToken.UserID)
	if err != nil {
		return fmt.Errorf("failed to update password: %w", err)
	}

	// Mark token as used
	_, err = ps.db.ExecContext(ctx, `
		UPDATE password_reset_tokens
		SET used = TRUE
		WHERE id = $1
	`, resetToken.ID)
	if err != nil {
		ps.logger.Warn("Failed to mark reset token as used",
			zap.Error(err),
			zap.Int64("token_id", resetToken.ID),
		)
	}

	ps.logger.Info("Password reset successful",
		zap.String("user_id", resetToken.UserID.String()),
	)

	return nil
}

// ValidatePassword validates password strength
// BE-SEC-006: Uses comprehensive password validator
func (ps *PasswordService) ValidatePassword(password string) error {
	strength, err := ps.passwordValidator.Validate(password)
	if err != nil {
		return err
	}
	if !strength.Valid {
		return fmt.Errorf("weak password: %s", strings.Join(strength.Details, ", "))
	}
	return nil
}

// ChangePassword changes user's password (for authenticated users)
func (ps *PasswordService) ChangePassword(userID uuid.UUID, oldPassword, newPassword string) error {
	ctx := context.Background()

	// Get current password hash
	var currentHash string
	err := ps.db.QueryRowContext(ctx, `
		SELECT password_hash
		FROM users
		WHERE id = $1
	`, userID).Scan(&currentHash)
	if err != nil {
		return fmt.Errorf("user not found")
	}

	// Verify old password
	err = bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(oldPassword))
	if err != nil {
		return fmt.Errorf("incorrect old password")
	}

	// Validate new password
	if err := ps.ValidatePassword(newPassword); err != nil {
		return err
	}

	// Hash new password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcryptCost)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Update password
	_, err = ps.db.ExecContext(ctx, `
		UPDATE users
		SET password_hash = $1, updated_at = NOW()
		WHERE id = $2
	`, string(hashedPassword), userID)

	if err != nil {
		return fmt.Errorf("failed to update password: %w", err)
	}

	ps.logger.Info("Password changed successfully",
		zap.String("user_id", userID.String()),
	)

	return nil
}

// GenerateJWT generates a JWT token for the user (used internally)
func (ps *PasswordService) GenerateJWT(userID uuid.UUID, secret []byte) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"user_id": userID.String(), // Convert UUID to string for JWT claims
		"exp":     time.Now().Add(time.Hour * 24).Unix(),
	})

	return token.SignedString(secret)
}

// UpdatePassword updates a user's password by user ID
// T0194: Updates password with bcrypt hash
func (ps *PasswordService) UpdatePassword(userID uuid.UUID, newPassword string) error {
	ctx := context.Background()

	// Validate password strength
	// BE-SEC-006: Use comprehensive password validator
	strength, err := ps.passwordValidator.Validate(newPassword)
	if err != nil || !strength.Valid {
		if err != nil {
			return err
		}
		return fmt.Errorf("weak password: %s", strings.Join(strength.Details, ", "))
	}

	// Hash new password
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcryptCost)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Update user password
	_, err = ps.db.ExecContext(ctx, `
		UPDATE users
		SET password_hash = $1, updated_at = NOW()
		WHERE id = $2
	`, string(hashedPassword), userID)
	if err != nil {
		return fmt.Errorf("failed to update password: %w", err)
	}

	ps.logger.Info("Password updated successfully",
		zap.String("user_id", userID.String()),
	)

	return nil
}

// Hash hashes a password using bcrypt with cost 12
// This is a standalone method for T0154 that can be used independently
func (s *PasswordService) Hash(password string) (string, error) {
	// Bcrypt has a limit of 72 bytes. Truncate longer passwords to avoid errors.
	// This matches the behavior expected by tests and is a reasonable security practice.
	if len(password) > 72 {
		password = password[:72]
	}
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
	if err != nil {
		return "", err
	}
	return string(bytes), nil
}

// Compare compares a password with a hashed password
// Returns true if the password matches the hash
func (s *PasswordService) Compare(hashedPassword, password string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
	return err == nil
}
