// Package service provides user service business logic.
package service

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

	"git.dcentral.systems/toolz/goplt/internal/ent"
	"git.dcentral.systems/toolz/goplt/internal/ent/user"
	"git.dcentral.systems/toolz/goplt/pkg/logger"
	passwordpkg "git.dcentral.systems/toolz/goplt/services/identity/internal/password"
	"go.uber.org/zap"
)

// UserService provides user management functionality.
type UserService struct {
	client *ent.Client
	logger logger.Logger
}

// NewUserService creates a new user service.
func NewUserService(client *ent.Client, log logger.Logger) *UserService {
	return &UserService{
		client: client,
		logger: log,
	}
}

// generateToken generates a random token for email verification or password reset.
func generateToken() (string, error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("failed to generate token: %w", err)
	}
	return base64.URLEncoding.EncodeToString(b), nil
}

// CreateUser creates a new user.
func (s *UserService) CreateUser(ctx context.Context, email, username, password, firstName, lastName string) (*ent.User, error) {
	// Check if user with email already exists
	exists, err := s.client.User.Query().
		Where(user.Email(email)).
		Exist(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to check email existence: %w", err)
	}
	if exists {
		return nil, fmt.Errorf("user with email %s already exists", email)
	}

	// Hash password
	passwordHash, err := passwordpkg.Hash(password)
	if err != nil {
		return nil, fmt.Errorf("failed to hash password: %w", err)
	}

	// Generate email verification token
	verificationToken, err := generateToken()
	if err != nil {
		return nil, fmt.Errorf("failed to generate verification token: %w", err)
	}

	// Create user
	create := s.client.User.Create().
		SetID(fmt.Sprintf("%d", time.Now().UnixNano())).
		SetEmail(email).
		SetPasswordHash(passwordHash).
		SetVerified(false).
		SetEmailVerificationToken(verificationToken)

	if username != "" {
		create = create.SetUsername(username)
	}
	if firstName != "" {
		create = create.SetFirstName(firstName)
	}
	if lastName != "" {
		create = create.SetLastName(lastName)
	}

	u, err := create.Save(ctx)
	if err != nil {
		s.logger.Error("Failed to create user",
			zap.Error(err),
			zap.String("email", email),
		)
		return nil, fmt.Errorf("failed to create user: %w", err)
	}

	s.logger.Info("User created",
		zap.String("user_id", u.ID),
		zap.String("email", email),
	)

	return u, nil
}

// GetUser retrieves a user by ID.
func (s *UserService) GetUser(ctx context.Context, id string) (*ent.User, error) {
	u, err := s.client.User.Get(ctx, id)
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, fmt.Errorf("user not found: %w", err)
		}
		return nil, fmt.Errorf("failed to get user: %w", err)
	}
	return u, nil
}

// GetUserByEmail retrieves a user by email.
func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*ent.User, error) {
	u, err := s.client.User.Query().
		Where(user.Email(email)).
		Only(ctx)
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, fmt.Errorf("user not found: %w", err)
		}
		return nil, fmt.Errorf("failed to get user by email: %w", err)
	}
	return u, nil
}

// UpdateUser updates a user's profile.
func (s *UserService) UpdateUser(ctx context.Context, id string, email, username, firstName, lastName *string) (*ent.User, error) {
	update := s.client.User.UpdateOneID(id)

	if email != nil {
		// Check if email is already taken by another user
		exists, err := s.client.User.Query().
			Where(user.Email(*email), user.IDNEQ(id)).
			Exist(ctx)
		if err != nil {
			return nil, fmt.Errorf("failed to check email existence: %w", err)
		}
		if exists {
			return nil, fmt.Errorf("email %s is already taken", *email)
		}
		update = update.SetEmail(*email)
	}
	if username != nil {
		update = update.SetUsername(*username)
	}
	if firstName != nil {
		update = update.SetFirstName(*firstName)
	}
	if lastName != nil {
		update = update.SetLastName(*lastName)
	}

	u, err := update.Save(ctx)
	if err != nil {
		if ent.IsNotFound(err) {
			return nil, fmt.Errorf("user not found: %w", err)
		}
		return nil, fmt.Errorf("failed to update user: %w", err)
	}

	s.logger.Info("User updated",
		zap.String("user_id", id),
	)

	return u, nil
}

// DeleteUser deletes a user (soft delete by setting verified to false, or hard delete).
func (s *UserService) DeleteUser(ctx context.Context, id string) error {
	err := s.client.User.DeleteOneID(id).Exec(ctx)
	if err != nil {
		if ent.IsNotFound(err) {
			return fmt.Errorf("user not found: %w", err)
		}
		return fmt.Errorf("failed to delete user: %w", err)
	}

	s.logger.Info("User deleted",
		zap.String("user_id", id),
	)

	return nil
}

// VerifyEmail verifies a user's email address using a verification token.
func (s *UserService) VerifyEmail(ctx context.Context, token string) error {
	u, err := s.client.User.Query().
		Where(user.EmailVerificationToken(token)).
		Only(ctx)
	if err != nil {
		if ent.IsNotFound(err) {
			return fmt.Errorf("invalid verification token")
		}
		return fmt.Errorf("failed to find user: %w", err)
	}

	// Update user to verified and clear token
	_, err = s.client.User.UpdateOneID(u.ID).
		SetVerified(true).
		ClearEmailVerificationToken().
		Save(ctx)
	if err != nil {
		return fmt.Errorf("failed to verify email: %w", err)
	}

	s.logger.Info("Email verified",
		zap.String("user_id", u.ID),
		zap.String("email", u.Email),
	)

	return nil
}

// RequestPasswordReset requests a password reset token.
func (s *UserService) RequestPasswordReset(ctx context.Context, email string) (string, error) {
	u, err := s.GetUserByEmail(ctx, email)
	if err != nil {
		// Don't reveal if user exists or not (security best practice)
		return "", nil
	}

	// Generate reset token
	resetToken, err := generateToken()
	if err != nil {
		return "", fmt.Errorf("failed to generate reset token: %w", err)
	}

	// Set reset token with expiration (24 hours)
	expiresAt := time.Now().Add(24 * time.Hour)
	_, err = s.client.User.UpdateOneID(u.ID).
		SetPasswordResetToken(resetToken).
		SetPasswordResetExpiresAt(expiresAt).
		Save(ctx)
	if err != nil {
		return "", fmt.Errorf("failed to set reset token: %w", err)
	}

	s.logger.Info("Password reset requested",
		zap.String("user_id", u.ID),
		zap.String("email", email),
	)

	return resetToken, nil
}

// ResetPassword resets a user's password using a reset token.
func (s *UserService) ResetPassword(ctx context.Context, token, newPassword string) error {
	u, err := s.client.User.Query().
		Where(user.PasswordResetToken(token)).
		Only(ctx)
	if err != nil {
		if ent.IsNotFound(err) {
			return fmt.Errorf("invalid reset token")
		}
		return fmt.Errorf("failed to find user: %w", err)
	}

	// Check if token is expired
	if !u.PasswordResetExpiresAt.IsZero() && u.PasswordResetExpiresAt.Before(time.Now()) {
		return fmt.Errorf("reset token has expired")
	}

	// Hash new password
	passwordHash, err := passwordpkg.Hash(newPassword)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Update password and clear reset token
	_, err = s.client.User.UpdateOneID(u.ID).
		SetPasswordHash(passwordHash).
		ClearPasswordResetToken().
		ClearPasswordResetExpiresAt().
		Save(ctx)
	if err != nil {
		return fmt.Errorf("failed to reset password: %w", err)
	}

	s.logger.Info("Password reset",
		zap.String("user_id", u.ID),
	)

	return nil
}

// ChangePassword changes a user's password with old password verification.
func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword string) error {
	u, err := s.GetUser(ctx, userID)
	if err != nil {
		return err
	}

	// Verify old password
	valid, err := passwordpkg.Verify(oldPassword, u.PasswordHash)
	if err != nil {
		return fmt.Errorf("failed to verify password: %w", err)
	}
	if !valid {
		return fmt.Errorf("invalid old password")
	}

	// Hash new password
	passwordHash, err := passwordpkg.Hash(newPassword)
	if err != nil {
		return fmt.Errorf("failed to hash password: %w", err)
	}

	// Update password
	_, err = s.client.User.UpdateOneID(userID).
		SetPasswordHash(passwordHash).
		Save(ctx)
	if err != nil {
		return fmt.Errorf("failed to change password: %w", err)
	}

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

	return nil
}

// VerifyPassword verifies a password against a user's password hash.
func (s *UserService) VerifyPassword(ctx context.Context, email, password string) (*ent.User, error) {
	u, err := s.GetUserByEmail(ctx, email)
	if err != nil {
		return nil, err
	}

	valid, err := passwordpkg.Verify(password, u.PasswordHash)
	if err != nil {
		return nil, fmt.Errorf("failed to verify password: %w", err)
	}
	if !valid {
		return nil, fmt.Errorf("invalid password")
	}

	return u, nil
}
