package memory

import (
	"context"
	"sync"
	"time"

	"{{GO_MODULE}}/pkg/auth"
	"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/domain"
	"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/port"
)

// Compile-time interface check.
var _ port.UserRepository = (*UserRepository)(nil)

// UserRepository is an in-memory user store with bcrypt password hashing.
// Pre-populated with demo users for standalone development.
type UserRepository struct {
	mu        sync.RWMutex
	users     map[domain.UserID]*domain.User
	passwords map[domain.UserID]string   // bcrypt hashes
	roles     map[domain.UserID][]string // role lists
	byEmail   map[string]domain.UserID   // email → user ID index
}

// NewUserRepository creates a new in-memory user repository seeded with demo users.
// If devEmail is non-empty, an additional user is seeded with that email and devPassword
// so the developer's account survives server restarts without re-registering.
func NewUserRepository(devEmail, devPassword string) *UserRepository {
	repo := &UserRepository{
		users:     make(map[domain.UserID]*domain.User),
		passwords: make(map[domain.UserID]string),
		roles:     make(map[domain.UserID][]string),
		byEmail:   make(map[string]domain.UserID),
	}

	// Seed demo users with bcrypt-hashed passwords.
	// Passwords meet complexity requirements (min 8 chars, uppercase, lowercase, digit).
	repo.seedUser("usr_test_001", "test@example.com", "Test User", "Password123", []string{"user"})
	repo.seedUser("usr_admin_001", "admin@example.com", "Admin User", "Admin1234", []string{"admin", "user"})

	// Seed the developer's own account if DEV_USER_EMAIL is configured.
	// This ensures the email is always registered after restarts without manual re-registration.
	if devEmail != "" {
		repo.seedUser("usr_dev_001", devEmail, "Dev User", devPassword, []string{"admin", "user"})
	}

	return repo
}

func (r *UserRepository) seedUser(id, email, name, password string, userRoles []string) {
	uid := domain.UserID(id)
	now := time.Now()

	hash, err := auth.HashPassword(password)
	if err != nil {
		panic("failed to hash seed password: " + err.Error())
	}

	r.users[uid] = &domain.User{
		ID:            uid,
		Email:         email,
		EmailVerified: true,
		Name:          name,
		Status:        domain.UserStatusActive,
		Roles:         userRoles,
		CreatedAt:     now,
		UpdatedAt:     now,
	}
	r.passwords[uid] = hash
	r.roles[uid] = userRoles
	r.byEmail[email] = uid
}

func (r *UserRepository) copyUser(u *domain.User) *domain.User {
	cp := *u
	cp.Roles = make([]string, len(u.Roles))
	copy(cp.Roles, u.Roles)
	return &cp
}

func (r *UserRepository) Create(_ context.Context, user *domain.User) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, exists := r.byEmail[user.Email]; exists {
		return domain.ErrDuplicateEmail
	}

	r.users[user.ID] = r.copyUser(user)
	r.byEmail[user.Email] = user.ID
	r.roles[user.ID] = make([]string, len(user.Roles))
	copy(r.roles[user.ID], user.Roles)
	return nil
}

func (r *UserRepository) Get(_ context.Context, id domain.UserID) (*domain.User, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	u, ok := r.users[id]
	if !ok {
		return nil, domain.ErrUserNotFound
	}
	return r.copyUser(u), nil
}

func (r *UserRepository) GetByEmail(_ context.Context, email string) (*domain.User, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	uid, ok := r.byEmail[email]
	if !ok {
		return nil, domain.ErrUserNotFound
	}
	return r.copyUser(r.users[uid]), nil
}

func (r *UserRepository) Update(_ context.Context, user *domain.User) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	existing, ok := r.users[user.ID]
	if !ok {
		return domain.ErrUserNotFound
	}

	// If email changed, update the index.
	if existing.Email != user.Email {
		if _, taken := r.byEmail[user.Email]; taken {
			return domain.ErrDuplicateEmail
		}
		delete(r.byEmail, existing.Email)
		r.byEmail[user.Email] = user.ID
	}

	user.UpdatedAt = time.Now()
	r.users[user.ID] = r.copyUser(user)
	return nil
}

func (r *UserRepository) UpdateLastLogin(_ context.Context, id domain.UserID) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	u, ok := r.users[id]
	if !ok {
		return domain.ErrUserNotFound
	}
	now := time.Now()
	u.LastLoginAt = &now
	return nil
}

func (r *UserRepository) ExistsByEmail(_ context.Context, email string) (bool, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	_, ok := r.byEmail[email]
	return ok, nil
}

func (r *UserRepository) SetPassword(_ context.Context, userID domain.UserID, hash string) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, ok := r.users[userID]; !ok {
		return domain.ErrUserNotFound
	}
	r.passwords[userID] = hash
	return nil
}

func (r *UserRepository) GetPasswordHash(_ context.Context, userID domain.UserID) (string, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	hash := r.passwords[userID]
	return hash, nil
}

func (r *UserRepository) HasPassword(_ context.Context, userID domain.UserID) (bool, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	_, ok := r.passwords[userID]
	return ok, nil
}

func (r *UserRepository) AddRole(_ context.Context, userID domain.UserID, role string) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	u, ok := r.users[userID]
	if !ok {
		return domain.ErrUserNotFound
	}

	for _, existing := range r.roles[userID] {
		if existing == role {
			return nil
		}
	}
	r.roles[userID] = append(r.roles[userID], role)
	u.Roles = make([]string, len(r.roles[userID]))
	copy(u.Roles, r.roles[userID])
	return nil
}

func (r *UserRepository) RemoveRole(_ context.Context, userID domain.UserID, role string) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	u, ok := r.users[userID]
	if !ok {
		return domain.ErrUserNotFound
	}

	filtered := make([]string, 0, len(r.roles[userID]))
	for _, existing := range r.roles[userID] {
		if existing != role {
			filtered = append(filtered, existing)
		}
	}
	r.roles[userID] = filtered
	u.Roles = make([]string, len(filtered))
	copy(u.Roles, filtered)
	return nil
}

func (r *UserRepository) GetRoles(_ context.Context, userID domain.UserID) ([]string, error) {
	r.mu.RLock()
	defer r.mu.RUnlock()

	if _, ok := r.users[userID]; !ok {
		return nil, domain.ErrUserNotFound
	}

	roles := r.roles[userID]
	result := make([]string, len(roles))
	copy(result, roles)
	return result, nil
}
