Why are new snippets not loading in when the app is in search mode. After I search, only 10 snippets show up, even when I scroll to the end. I do not see the loading indicator at the end of the card, and no additional snippets load in.

Client-side code:

import UIKit
import AVFoundation
import CoreData
import SafariServices


// MARK: - MainViewController

class MainViewController: UIViewController {
    
    // MARK: - UI Properties
    
    private var libraryLayout: LibraryLayoutProtocol?
    private var isLoadingSnippets = false
    private var musicCollectionView: UICollectionView!
    
    private let loadingIndicator: UIActivityIndicatorView = {
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.translatesAutoresizingMaskIntoConstraints = false
        indicator.hidesWhenStopped = true
        return indicator
    }()
    
    private lazy var libraryButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "books.vertical"), for: .normal)
        button.tintColor = .white
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(libraryButtonTapped), for: .touchUpInside)
        return button
    }()
    
    private lazy var leaderboardButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "trophy"), for: .normal)
        button.tintColor = .white
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(leaderboardButtonTapped), for: .touchUpInside)
        return button
    }()
    
    private var libraryView: UIView!
    private var isLibraryVisible = false
    private var libraryItems: [LibraryItem] = []
    private var currentlyPlayingItem: LibraryItem?
    private var isAudioPaused = false
    private var isSearchIconVisible = true
    
    private var authLayout: AuthLayout!
    private var isAuthLayoutVisible = false
    
    private var leaderboardLayout: LeaderboardLayout!
    private var isLeaderboardVisible = false
    
    
    private lazy var userIconButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "person"), for: .normal)
        button.tintColor = .white
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(userIconButtonTapped), for: .touchUpInside)
        return button
    }()

    private var artistLayout: ArtistLayout!
    private var isArtistLayoutVisible = false
    
    private var cachedArtistData: ArtistData?
    private let artistDataQueue = DispatchQueue(label: "com.app.artistDataQueue")
    
    private lazy var searchIconButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "magnifyingglass"), for: .normal)
        button.tintColor = .white
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(searchIconTapped), for: .touchUpInside)
        return button
    }()
    
    private lazy var librarySearchButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "magnifyingglass"), for: .normal)
        button.tintColor = .white
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(librarySearchButtonTapped), for: .touchUpInside)
        button.alpha = 0
        button.isHidden = true
        return button
    }()
    
    private lazy var searchBar: UISearchBar = {
        let searchBar = UISearchBar()
        searchBar.translatesAutoresizingMaskIntoConstraints = false
        searchBar.searchBarStyle = .minimal
        searchBar.placeholder = "Search"
        searchBar.tintColor = .white
        searchBar.barTintColor = .white
        searchBar.backgroundColor = .black
        
        // Customize text field appearance
        if let textField = searchBar.value(forKey: "searchField") as? UITextField {
            textField.textColor = .white
            textField.backgroundColor = .black
            textField.layer.borderColor = UIColor.white.cgColor
            textField.layer.borderWidth = 1
            textField.layer.cornerRadius = 12
        }
        
        searchBar.isHidden = true
        return searchBar
    }()
    
    private lazy var librarySearchBar: UISearchBar = {
        let searchBar = UISearchBar()
        searchBar.translatesAutoresizingMaskIntoConstraints = false
        searchBar.searchBarStyle = .minimal
        searchBar.placeholder = "Search"
        searchBar.tintColor = .white
        searchBar.barTintColor = .white
        searchBar.backgroundColor = .black
        
        // Customize text field appearance
        if let textField = searchBar.value(forKey: "searchField") as? UITextField {
            textField.textColor = .white
            textField.backgroundColor = .black
            textField.layer.borderColor = UIColor.white.cgColor
            textField.layer.borderWidth = 1
            textField.layer.cornerRadius = 12
        }
        
        searchBar.isHidden = true
        searchBar.alpha = 0
        return searchBar
    }()
    
    private lazy var importButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "arrow.down"), for: .normal)
        button.tintColor = .white
        button.backgroundColor = .clear
        button.layer.cornerRadius = 8
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(importButtonTapped), for: .touchUpInside)
        
        let imageConfig = UIImage.SymbolConfiguration(pointSize: 16, weight: .regular)
        button.setPreferredSymbolConfiguration(imageConfig, forImageIn: .normal)
        
        return button
    }()
    
    private lazy var autoPlayButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Enable Auto-Play", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = UIColor(white: 0.2, alpha: 0.8)
        button.layer.cornerRadius = 8
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(autoPlayButtonTapped), for: .touchUpInside)
        return button
    }()
    
    private lazy var previousButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "chevron.left.circle.fill")?.withConfiguration(
            UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
        ), for: .normal)
        button.tintColor = .white
        button.alpha = 0
        button.isHidden = true
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(previousButtonTapped), for: .touchUpInside)
        return button
    }()
    
    private lazy var nextButton: UIButton = {
        let button = UIButton(type: .system)
        button.setImage(UIImage(systemName: "chevron.right.circle.fill")?.withConfiguration(
            UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
        ), for: .normal)
        button.tintColor = .white
        button.alpha = 0
        button.isHidden = true
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)
        return button
    }()

    // Add this property to track the state
    private var isAutoPlayEnabled = false
    
    private var filteredMusicSnippets: [MusicSnippet] = []
    private var filteredLibraryItems: [LibraryItem] = []
    private var isInSearchMode: Bool = false
    
    private var snippetBuffer: SnippetBuffer!
    
    private var isLoadingMore = false

    private var currentSearchPage = 1
    private var isLoadingSearchResults = false
    private var hasMoreSearchResults = true
    private var currentSearchQuery: String?
    private let maxSearchResults = 100
    private let resultsPerPage = 10


    
    // MARK: - Data Properties
    
    var musicSnippets: [MusicSnippet] = []
    
    // MARK: - Audio Properties
    
    private let audioPlayer = AudioPlayer()
    private let snippetManager = MusicSnippetManager()
    private var currentPlayingIndex: Int?
    
    // MARK: - Lifecycle Methods
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .black
        setupCollectionView()
        setupLibraryButton()
        setupSearchBar()
        setupNavigationButtons()
        setupAutoPlayButton()
        setupNotifications()
        setupSwipeGestureRecognizer()
        setupTapGestureRecognizer()
        setupSearchIcon()
        setupUserIconButton()
        setupLeaderboardButton()
        setupLeaderboardLayout()
        setupArtistLayout()
        setupAuthLayout()
        setupLogoutObserver()
        setupLibrarySearchButton()
        setupLibrarySearchBar()
        

        // clearAllCaches()

        libraryLayout = LibraryLayout()
        print("DEBUG: libraryLayout initialized")
        
        setupLibraryView()
        print("DEBUG: setupLibraryView completed")
        
        configureLibraryLayoutCallbacks()
        
        setupLibraryLayout()
        print("DEBUG: setupLibraryLayout completed")
        
        searchBar.isHidden = true
        importButton.isHidden = true
        libraryButton.isHidden = false
        searchIconButton.isHidden = false
        
        audioPlayer.onPlaybackStateChanged = { [weak self] isPlaying in
            guard let self = self, let currentItem = self.currentlyPlayingItem else { return }
            self.updatePlayingState(for: currentItem, isPlaying: isPlaying)
        }
        
        audioPlayer.onNextTrackRequested = { [weak self] in
            guard let self = self,
                  let currentIndex = self.currentPlayingIndex else { return }
            self.moveToNextSnippet(from: currentIndex)
        }

        audioPlayer.onPreviousTrackRequested = { [weak self] in
            guard let self = self,
                  let currentIndex = self.currentPlayingIndex,
                  self.hasPreviousSnippet(from: currentIndex) else { return }
                      
            // Find the previous music snippet
            for index in (0..<currentIndex).reversed() {
                if self.filteredMusicSnippets[index].type == .music {
                    self.scrollToItem(at: index, animated: true)
                    break
                }
            }
        }
        
        if AuthManager.shared.isUserLoggedIn() {
            prefetchArtistData()
        }
        
        NotificationCenter.default.addObserver(self,
                                             selector: #selector(handleAppActive),
                                             name: UIApplication.didBecomeActiveNotification,
                                             object: nil)
        
        
        view.addSubview(loadingIndicator)
        setupLoadingIndicatorConstraints()
        
        musicCollectionView.register(LoadingIndicatorCell.self, forCellWithReuseIdentifier: "LoadingIndicatorCell")


        
        snippetBuffer = SnippetBuffer()
        snippetBuffer.onNewSnippetsAvailable = { [weak self] in
            self?.handleNewSnippets()
        }
        
//        snippetBuffer.onLoadingStateChanged = { [weak self] isLoading in
//            DispatchQueue.main.async {
//                if isLoading {
//                    self?.loadingIndicator.startAnimating()
//                } else {
//                    self?.loadingIndicator.stopAnimating()
//                }
//            }
//        }
        
//        snippetBuffer.onError = { [weak self] error in
//            DispatchQueue.main.async {
//                // Show an alert or toast to the user
//                self?.showErrorAlert("Failed to load new snippets. Please try again.")
//            }
//        }
        musicCollectionView.delegate = self

        
        registerNotifications()
    }
    
    private var hasInitiallyLoadedSnippets = false

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        if !hasInitiallyLoadedSnippets {
            fetchRandomMusicSnippets()
            hasInitiallyLoadedSnippets = true
        }
        audioPlayer.play()
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // Pause playback if needed
        audioPlayer.pauseAudio()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    // MARK: - Setup Methods
    
    private func setupNavigationButtons() {
        view.addSubview(previousButton)
        view.addSubview(nextButton)
        
        NSLayoutConstraint.activate([
            previousButton.centerYAnchor.constraint(equalTo: musicCollectionView.centerYAnchor),
            previousButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            previousButton.widthAnchor.constraint(equalToConstant: 50),
            previousButton.heightAnchor.constraint(equalToConstant: 50),
            
            nextButton.centerYAnchor.constraint(equalTo: musicCollectionView.centerYAnchor),
            nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            nextButton.widthAnchor.constraint(equalToConstant: 50),
            nextButton.heightAnchor.constraint(equalToConstant: 50)
        ])
    }
    
    private func setupCollectionView() {
        let layout = CarouselLayout()
        musicCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        musicCollectionView.translatesAutoresizingMaskIntoConstraints = false
        musicCollectionView.backgroundColor = .clear
        view.addSubview(musicCollectionView)
        
        NSLayoutConstraint.activate([
            musicCollectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            musicCollectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            musicCollectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            musicCollectionView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1/3)
        ])
        
        musicCollectionView.dataSource = self
        musicCollectionView.delegate = self
        musicCollectionView.register(MusicSnippetCell.self, forCellWithReuseIdentifier: "MusicSnippetCell")
        musicCollectionView.register(SpecialCardCell.self, forCellWithReuseIdentifier: "SpecialCardCell")
        musicCollectionView.showsHorizontalScrollIndicator = false
    }
    
    private func setupLibraryButton() {
        view.addSubview(libraryButton)
        
        NSLayoutConstraint.activate([
            libraryButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
            libraryButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            libraryButton.widthAnchor.constraint(equalToConstant: 44),
            libraryButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    private func setupAutoPlayButton() {
        view.addSubview(autoPlayButton)
        
        NSLayoutConstraint.activate([
            autoPlayButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            autoPlayButton.topAnchor.constraint(equalTo: musicCollectionView.topAnchor, constant: 15),
            autoPlayButton.widthAnchor.constraint(equalToConstant: 160),
            autoPlayButton.heightAnchor.constraint(equalToConstant: 36)
        ])
    }
    
    private func setupLeaderboardLayout() {
        leaderboardLayout = LeaderboardLayout()
        view.addSubview(leaderboardLayout)
        leaderboardLayout.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            leaderboardLayout.topAnchor.constraint(equalTo: view.topAnchor),
            leaderboardLayout.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            leaderboardLayout.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            leaderboardLayout.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        leaderboardLayout.isHidden = true
        leaderboardLayout.delegate = self
    }
    
    private func setupLeaderboardButton () {
        view.addSubview(leaderboardButton)
        NSLayoutConstraint.activate([
            leaderboardButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
            leaderboardButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
            leaderboardButton.widthAnchor.constraint(equalToConstant: 44),
            leaderboardButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    private func setupAuthLayout() {
        authLayout = AuthLayout()
        view.addSubview(authLayout)
        authLayout.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            authLayout.topAnchor.constraint(equalTo: view.topAnchor),
            authLayout.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            authLayout.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            authLayout.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        authLayout.isHidden = true
        
        // Set the delegate
        authLayout.delegate = self
        
        // Existing callback assignments
        authLayout.onLoginSuccess = { [weak self] in
            self?.handleAuthSuccess()
        }
        
        authLayout.onRegisterSuccess = { [weak self] in
            self?.handleAuthSuccess()
        }
        
        authLayout.onBackTapped = { [weak self] in
            self?.toggleAuthLayout()
        }
    }

    private func setupSearchBar() {
        view.addSubview(searchBar)
        
        NSLayoutConstraint.activate([
            searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
            searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            searchBar.heightAnchor.constraint(equalToConstant: 40)
        ])
        
        searchBar.delegate = self
    }
    
    private func setupLibrarySearchBar() {
        view.addSubview(librarySearchBar)
        
        NSLayoutConstraint.activate([
            librarySearchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
            librarySearchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            librarySearchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            librarySearchBar.heightAnchor.constraint(equalToConstant: 40)
        ])
        
        librarySearchBar.delegate = self
    }
    
    private func setupSearchIcon() {
        view.addSubview(searchIconButton)
        NSLayoutConstraint.activate([
            searchIconButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
            searchIconButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            searchIconButton.widthAnchor.constraint(equalToConstant: 44),
            searchIconButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    private func setupLibraryView() {
        libraryView = UIView()
        libraryView.backgroundColor = .black
        libraryView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(libraryView)
        
        NSLayoutConstraint.activate([
            libraryView.topAnchor.constraint(equalTo: view.topAnchor, constant: 120),
            libraryView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            libraryView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            libraryView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -120)
        ])
        
        libraryView.isHidden = true
    }
    
    private func setupLibraryLayout() {
        libraryLayout?.onItemTapped = { [weak self] item, itemView in
            print("31")
            self?.handleLibraryItemTap(item: item, itemView: itemView)
        }
        
        libraryLayout?.onItemRenamed = { [weak self] item, newTitle in
            self?.handleLibraryItemRename(item: item, newTitle: newTitle)
        }
        
        libraryLayout?.onItemRemoved = { [weak self] item in
            self?.removeFromLibrary(item: item)
        }
        print("DEBUG: setupLibraryLayout completed")
    }
    
    private func setupUserIconButton() {
        view.addSubview(userIconButton)

        NSLayoutConstraint.activate([
            userIconButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
            userIconButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
            userIconButton.widthAnchor.constraint(equalToConstant: 44),
            userIconButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }

    private func setupArtistLayout() {
        artistLayout = ArtistLayout()
        view.addSubview(artistLayout)

        NSLayoutConstraint.activate([
            artistLayout.topAnchor.constraint(equalTo: view.topAnchor),
            artistLayout.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            artistLayout.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            artistLayout.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])

        artistLayout.isHidden = true
    }
    
    private func setupAPIKey() {
        APIKeyManager.shared.fetchAndStoreAPIKey { result in
            switch result {
            case .success:
                print("API key successfully fetched and stored")
            case .failure(let error):
                print("Failed to fetch API key: \(error)")
                // Handle error (e.g., retry, show alert to user)
            }
        }
    }
    
    private func setupLibrarySearchButton() {
        view.addSubview(librarySearchButton)
        
        NSLayoutConstraint.activate([
            librarySearchButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
            librarySearchButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            librarySearchButton.widthAnchor.constraint(equalToConstant: 44),
            librarySearchButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    private func setupNotifications() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(keyboardWillShow),
                                               name: UIResponder.keyboardWillShowNotification,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(keyboardWillHide),
                                               name: UIResponder.keyboardWillHideNotification,
                                               object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(appDidBecomeActive),
                                               name: UIApplication.didBecomeActiveNotification,
                                               object: nil)
    }
    
    private func registerNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(libraryDidChange(_:)), name: .libraryDidChange, object: nil)
    }
    
    private func configureLibraryLayoutCallbacks() {
        libraryLayout?.onItemTapped = { [weak self] item, itemView in
            guard let self = self else { return }
            self.handleLibraryItemTap(item: item, itemView: itemView)
        }
        
        libraryLayout?.onItemRenamed = { [weak self] item, newTitle in

            self?.handleLibraryItemRename(item: item, newTitle: newTitle)
        }
        
        // Add this new callback implementation
        libraryLayout?.onViewArtistRequested = { [weak self] artistId in
            guard let self = self else { return }
            // Stop any current playback
            self.stopAudio()
            
            // Configure and show the artist layout with a back handler
            self.artistLayout.configure(
                artistId: artistId,
                presentationMode: .viewOnly,
                onBack: { [weak self] in
                    // Show library view again
                    self?.isLibraryVisible = true
                    self?.isArtistLayoutVisible = false
                    self?.artistLayout.hide()
                    self?.updateLibraryVisibility()
                    
                    // Ensure the libraryButton remains visible
                    self?.libraryButton.isHidden = false
                    UIView.animate(withDuration: 0.15) {
                        self?.libraryButton.alpha = 1
                    }
                }
            )
            
            // Hide library view and show artist layout
            self.isLibraryVisible = false
            self.updateLibraryVisibility()
            self.isArtistLayoutVisible = true
            self.artistLayout.show()
            
            // **Update UI elements to hide both buttons**
            UIView.animate(withDuration: 0) {
                self.libraryButton.alpha = 0
                self.searchIconButton.alpha = 0
            } completion: { _ in
                self.libraryButton.isHidden = true
                self.searchIconButton.isHidden = true
            }
        }
    }
    
    private func setupLoadingIndicatorConstraints() {
        NSLayoutConstraint.activate([
            loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
    
    private func setupSwipeGestureRecognizer() {
        let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipeGesture(_:)))
        swipeGesture.direction = .down
        view.addGestureRecognizer(swipeGesture)
    }
    
    private func setupTapGestureRecognizer() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
        tapGesture.cancelsTouchesInView = false
        view.addGestureRecognizer(tapGesture)
    }
    
    // MARK: - Notification Handlers
    
    @objc private func appDidBecomeActive() {
        // refreshLibraryLayout()
    }
    
    @objc private func libraryDidChange(_ notification: Notification) {
        // Fetch the updated library items
        if !Thread.isMainThread {
            DispatchQueue.main.async { [weak self] in
                self?.libraryDidChange(notification)
            }
            return
        }
        
        libraryItems = LibraryManager.shared.fetchLibraryItems()
        
        // Update filteredLibraryItems based on current search text
        if let searchText = librarySearchBar.text, !searchText.isEmpty {
            filterLibraryItems(with: searchText)
        } else {
            filteredLibraryItems = libraryItems
        }
        
        // Refresh the library layout
        refreshLibraryLayout()
    }

    
    private func setupLogoutObserver() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(handleLogout),
                                               name: .userDidLogout,
                                               object: nil)
    }


    @objc private func handleLogout() {
        // First, reset all states
        isAuthLayoutVisible = false
        isArtistLayoutVisible = false
        isLibraryVisible = false  // Add this line to reset library visibility
        
        // Stop any playing audio
        stopAudio()
        
        // Hide layouts
        authLayout.hide()
        artistLayout.hide()
        libraryView.isHidden = true  // Add this line to hide library view
        
        // Reset user button to person icon
        userIconButton.setImage(UIImage(systemName: "person"), for: .normal)
        libraryButton.setImage(UIImage(systemName: "books.vertical"), for: .normal)  // Reset library button icon
        
        // Clear and refresh the library view
        if let containerView = libraryView.subviews.first {
            containerView.subviews.forEach { $0.removeFromSuperview() }
        }
        
        // Show current content
        showCurrentContentViews()
        
        // Explicitly show main UI elements AFTER showCurrentContentViews
        libraryButton.isHidden = false
        searchIconButton.isHidden = false
        musicCollectionView.isHidden = false
        libraryButton.alpha = 1
        searchIconButton.alpha = 1
        
        // Force refresh the library to ensure it's empty
        LibraryManager.shared.forceRefresh()
        
        // Update the UI with empty state
        updateLibraryVisibility()
    }

    // MARK: - AutoPlay
    
    @objc private func autoPlayButtonTapped() {
        isAutoPlayEnabled.toggle()
        let buttonTitle = isAutoPlayEnabled ? "Disable Auto-Play" : "Enable Auto-Play"
        autoPlayButton.setTitle(buttonTitle, for: .normal)
        
        if let layout = musicCollectionView.collectionViewLayout as? CarouselLayout {
            layout.isAutoPlayEnabled = isAutoPlayEnabled
        }
        
        audioPlayer.enableBackgroundPlayback(isEnabled: isAutoPlayEnabled)

        if isAutoPlayEnabled && currentPlayingIndex != nil && !audioPlayer.isPlaying {
            audioPlayer.play()
        }
        // Show/hide navigation buttons with animation
        if isAutoPlayEnabled {
            previousButton.isHidden = false
            nextButton.isHidden = false

            UIView.animate(withDuration: 0.15) {
                self.previousButton.alpha = 1
                self.nextButton.alpha = 1
            }
        } else {
            UIView.animate(withDuration: 0.15) {
                self.previousButton.alpha = 0
                self.nextButton.alpha = 0
            } completion: { _ in
                self.previousButton.isHidden = true
                self.nextButton.isHidden = true
            }
        }
    }
    
    func moveToNextSnippet(from index: Int) {
        var nextIndex = index + 1
        if nextIndex >= filteredMusicSnippets.count {
            nextIndex = 0 // Loop back to the beginning
        }
        if nextIndex == index {
            // Only one snippet, avoid infinite loop
            return
        }
        let nextSnippet = filteredMusicSnippets[nextIndex]
        if nextSnippet.type == .music, let url = nextSnippet.url {
            self.scrollToItem(at: nextIndex, animated: true)
            // No need to call playAudioFromUrl here, scrollToItem will handle it
        } else {
            // Skip non-music snippets
            moveToNextSnippet(from: nextIndex)
        }
    }
    
    private func hasPreviousSnippet(from currentIndex: Int) -> Bool {
        guard currentIndex > 0 else { return false }
        // Check all previous indices for a music snippet
        for index in (0..<currentIndex).reversed() {
            if filteredMusicSnippets[index].type == .music {
                return true
            }
        }
        return false
    }

    private func hasNextSnippet(from currentIndex: Int) -> Bool {
        guard currentIndex < filteredMusicSnippets.count - 1 else { return false }
        // Check all next indices for a music snippet
        for index in (currentIndex + 1)..<filteredMusicSnippets.count {
            if filteredMusicSnippets[index].type == .music {
                return true
            }
        }
        return false
    }

    private func updateNavigationButtonStates() {
        guard let currentIndex = currentPlayingIndex else {
            previousButton.isEnabled = false
            nextButton.isEnabled = false
            return
        }
        
        previousButton.isEnabled = hasPreviousSnippet(from: currentIndex)
        nextButton.isEnabled = hasNextSnippet(from: currentIndex)
        
        // Update alpha for visual feedback
        previousButton.alpha = previousButton.isEnabled ? 1.0 : 0.3
        nextButton.alpha = nextButton.isEnabled ? 1.0 : 0.3
    }

    private func disableAutoPlay() {
        if isAutoPlayEnabled {
            isAutoPlayEnabled = false
            autoPlayButton.setTitle("Enable Auto-Play", for: .normal)
            
            if let layout = musicCollectionView.collectionViewLayout as? CarouselLayout {
                layout.isAutoPlayEnabled = false
            }
            
            audioPlayer.enableBackgroundPlayback(isEnabled: false)
            
            // Hide navigation buttons with animation
            UIView.animate(withDuration: 0.15) {
                self.previousButton.alpha = 0
                self.nextButton.alpha = 0
            } completion: { _ in
                self.previousButton.isHidden = true
                self.nextButton.isHidden = true
            }
        }
    }
    
    // MARK: - Button Actions
    
    @objc private func libraryButtonTapped() {
        handleViewSwitch(to: isLibraryVisible ? .main : .library)
    }
    
    @objc private func searchIconTapped() {
        searchBar.isHidden = false
        searchIconButton.isHidden = true
        libraryButton.isHidden = true
        
        searchBar.alpha = 0
        
        UIView.animate(withDuration: 0.15) {
            self.searchBar.alpha = 1
            self.musicCollectionView.alpha = 1
        } completion: { _ in
            self.searchBar.becomeFirstResponder()
            self.checkCollectionViewVisibility()
        }
    }
    
    @objc private func importButtonTapped() {
        if let text = searchBar.text, !text.isEmpty {
            performSearch(with: text)
        }
        view.endEditing(true)
    }
    
    @objc private func previousButtonTapped() {
        guard let currentIndex = currentPlayingIndex,
              hasPreviousSnippet(from: currentIndex) else { return }
              
        // Find the previous music snippet
        for index in (0..<currentIndex).reversed() {
            if filteredMusicSnippets[index].type == .music {
                scrollToItem(at: index, animated: true)
                break
            }
        }
    }
    
    @objc private func nextButtonTapped() {
        guard let currentIndex = currentPlayingIndex else { return }
        moveToNextSnippet(from: currentIndex)
    }
    
    @objc private func userIconButtonTapped() {
        if AuthManager.shared.isUserLoggedIn() {
            toggleArtistLayout()
        } else {
            toggleAuthLayout()
        }
    }

    private func hideUserIconButton() {
        UIView.animate(withDuration: 0.15) {
            self.userIconButton.alpha = 0
        } completion: { _ in
            self.userIconButton.isHidden = true
        }
    }

    private func showUserIconButton() {
        self.userIconButton.isHidden = false
        UIView.animate(withDuration: 0.15) {
            self.userIconButton.alpha = 1
        }
    }
    
    // MARK: - Gesture Handlers
    
    @objc private func handleSwipeGesture(_ gesture: UISwipeGestureRecognizer) {
        if gesture.state == .ended {
            view.endEditing(true)
        }
    }
    
    @objc private func handleTapGesture(_ gesture: UITapGestureRecognizer) {
        // Dismiss any active keyboard
        view.endEditing(true)
        
        let location = gesture.location(in: view)
        
        // Handle hiding of search bars if necessary
        if !librarySearchBar.isHidden && !librarySearchBar.frame.contains(location) {
            hideLibrarySearchBar()
        }
        
        if !searchBar.isHidden && !searchBar.frame.contains(location) {
            hideSearchBar()
        }
        
        if musicCollectionView.frame.contains(location) {
            print("Tap inside collection view")
        } else {
            print("Tap outside collection view")
        }
    }
    
    // MARK: - Infinite Scroll
    
    private func handleNewSnippets() {
        isLoadingMore = false

        // Preserve current playing snippet info
        let currentlyPlayingSnippetID = currentPlayingIndex != nil ? filteredMusicSnippets[currentPlayingIndex!].id : nil
        let wasPlaying = audioPlayer.isPlaying
        let oldCount = filteredMusicSnippets.count
        
        // Update data source
        filteredMusicSnippets = snippetBuffer.getSnippets()
        let newCount = filteredMusicSnippets.count
        
        // If counts are different, we must do a full reload
        if oldCount != newCount {
            currentPlayingIndex = currentlyPlayingSnippetID.flatMap { playingID in
                filteredMusicSnippets.firstIndex { $0.id == playingID }
            }
            musicCollectionView.reloadData()
        } else {
            // Counts are same, safe to do partial update
            if let playingID = currentlyPlayingSnippetID,
               let newIndex = filteredMusicSnippets.firstIndex(where: { $0.id == playingID }) {
                currentPlayingIndex = newIndex
                musicCollectionView.reloadData()
            } else {
                currentPlayingIndex = nil
                audioPlayer.stopAudio()
                musicCollectionView.reloadData()
            }
        }
        
        // Restore playback if it was playing
        if wasPlaying, let index = currentPlayingIndex {
            // Don't restart the audio if it's the same snippet still playing
            updateCellPlayingState(at: index, isPlaying: true)
        }
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if isInSearchMode {
            if !isLoadingSearchResults && hasMoreSearchResults {
                let visibleIndexPaths = musicCollectionView.indexPathsForVisibleItems
                let visibleIndices = visibleIndexPaths.map { $0.item }
                guard let maxVisibleIndex = visibleIndices.max(),
                      maxVisibleIndex < filteredMusicSnippets.count else {
                    return
                }
                if (filteredMusicSnippets.count - maxVisibleIndex) <= 5 {
                    // Fetch next page
                    fetchNextSearchResults()
                }
            }
        } else {
            // Existing code to fetch more random snippets
            guard !isInSearchMode else { return }
            
            let visibleIndexPaths = musicCollectionView.indexPathsForVisibleItems
            let visibleIndices = visibleIndexPaths.map { $0.item }
            
            // Only proceed if we have visible indices and they're within bounds
            guard let maxVisibleIndex = visibleIndices.max(),
                  maxVisibleIndex < filteredMusicSnippets.count else {
                return
            }
            
            if snippetBuffer.shouldFetchMoreSnippets(currentIndex: maxVisibleIndex) && !isLoadingMore {
                isLoadingMore = true
                
                // Only reload the section to add the loading indicator
                let section = IndexSet(integer: 0)
                musicCollectionView.reloadSections(section)
                
                snippetBuffer.fetchMoreSnippets()
            }
        }
    }
    
    // MARK: - UI Helpers
    
    private func hideSearchBar() {
        searchBar.resignFirstResponder()
        
        UIView.animate(withDuration: 0.05) {
            self.searchBar.alpha = 0
            self.musicCollectionView.alpha = 1
        } completion: { _ in
            self.searchBar.isHidden = true
            self.searchIconButton.isHidden = false
            self.libraryButton.isHidden = false
            self.searchIconButton.alpha = 0
            self.libraryButton.alpha = 0
            
            UIView.animate(withDuration: 0.05) {
                self.searchIconButton.alpha = 1
                self.libraryButton.alpha = 1
            }
            
            self.checkCollectionViewVisibility()
        }
    }
    
//    private func updatePlaceholderVisibility() {
//        searchBar.setPlaceholderVisibility(searchBar.text.isEmpty)
//    }
    
    private func resetAllAnimations() {
        for cell in musicCollectionView.visibleCells {
            if let musicCell = cell as? MusicSnippetCell {
                musicCell.animationView.fullReset()
            }
        }
    }
    
    func checkCollectionViewVisibility() {
        print("Collection view is in hierarchy: \(musicCollectionView.superview != nil)")
        print("Collection view is hidden: \(musicCollectionView.isHidden)")
        print("Collection view frame: \(musicCollectionView.frame)")
        print("Collection view bounds: \(musicCollectionView.bounds)")
        print("Number of items: \(musicCollectionView.numberOfItems(inSection: 0))")
    }
    
    func scrollToItem(at index: Int, animated: Bool) {
        guard index < filteredMusicSnippets.count else { return }
        let indexPath = IndexPath(item: index, section: 0)
        musicCollectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated)
        
        // Add this to trigger playback when scrolling programmatically
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
            let snippet = self?.filteredMusicSnippets[index]
            if snippet?.type == .music, let url = snippet?.url {
                self?.playAudioFromUrl(url, at: index)
            }
        }
    }
    
    private func resetItemViewAppearance(_ itemView: UIView) {
        itemView.backgroundColor = .black
        itemView.subviews.compactMap { $0 as? UILabel }.forEach { label in
            if label.text == itemView.subviews.compactMap({ $0 as? UILabel }).first?.text {
                label.textColor = .white // Title label
            } else {
                label.textColor = .lightGray // Artist label
            }
        }
    }
    
    private func hideCurrentContentViews() {
        
        libraryView.isHidden = true
        musicCollectionView.isHidden = true
        artistLayout.isHidden = true
        leaderboardButton.isHidden = true
        autoPlayButton.isHidden = true  // Add this line
        
        
        // Add these lines
        if isAutoPlayEnabled {
            UIView.animate(withDuration: 0.15) {
                self.previousButton.alpha = 0
                self.nextButton.alpha = 0
            } completion: { _ in
                self.previousButton.isHidden = true
                self.nextButton.isHidden = true
            }
        }
        
        disableAutoPlay()  // Add this line

    }


    private func showCurrentContentViews() {
        libraryView.isHidden = !isLibraryVisible
        musicCollectionView.isHidden = isLibraryVisible
        artistLayout.isHidden = !isArtistLayoutVisible
        libraryButton.isHidden = false
        searchIconButton.isHidden = false
        leaderboardButton.isHidden = false
        autoPlayButton.isHidden = isLibraryVisible  // Add this line

        
        // Add these lines
        if isAutoPlayEnabled && !isLibraryVisible && !isArtistLayoutVisible {
            previousButton.isHidden = false
            nextButton.isHidden = false
            UIView.animate(withDuration: 0.15) {
                self.previousButton.alpha = 1
                self.nextButton.alpha = 1
            }
        }
    }
    
    private func handleViewSwitch(to view: ViewType) {
        // Stop any currently playing audio before switching views
        stopAudio()
        
        switch view {
        case .library:
            isLibraryVisible = true
            updateLibraryVisibility()
        case .main:
            isLibraryVisible = false
            updateLibraryVisibility()
        }
    }

    // MARK: Auth Stuff
    
    private func handleAuthSuccess() {
        // Add a check to ensure we have a valid artist ID
        guard let artistId = AuthManager.shared.getArtistId() else {
            print("Error: No artist ID available after login")
            return
        }
        LibraryManager.shared.forceRefresh()
        
        toggleAuthLayout()
        
        // Give a small delay to ensure all states are updated
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
            self?.toggleArtistLayout()
        }
    }
    
    private func prefetchUserGlobeTexture() {
        guard let artistId = AuthManager.shared.getArtistId() else { return }
        
        ArtistManager.shared.fetchArtistData(artistId: artistId) { result in
            switch result {
            case .success(let artistData):
                if let globeTextureState = artistData.globeTexture,
                   let image = UIImage(data: globeTextureState.textureData) {
                    // Cache the globe texture
                    ArtistManager.globeTextureCache[artistId] = image
                }
            case .failure(let error):
                print("Error prefetching user globe texture: \(error)")
            }
        }
    }
    
    private func prefetchArtistData() {
        guard let artistId = AuthManager.shared.getArtistId() else { return }
        
        // Start loading indicator if needed
        artistDataQueue.async { [weak self] in
            ArtistManager.shared.fetchArtistData(artistId: artistId, forceRefresh: true) { [weak self] result in
                switch result {
                case .success(let artistData):
                    self?.cachedArtistData = artistData
                    
                    // Pre-fetch and cache the globe texture
                    if let globeTextureState = artistData.globeTexture,
                       let image = UIImage(data: globeTextureState.textureData) {
                        ArtistManager.globeTextureCache[artistId] = image
                    }
                    
                    // Also pre-fetch songs
                    self?.prefetchArtistSongs(artistId: artistId)
                    
                case .failure(let error):
                    print("Failed to pre-fetch artist data: \(error)")
                }
            }
        }
    }
    
    private func prefetchArtistSongs(artistId: String) {
        ArtistManager.shared.fetchArtistSongs(for: artistId) { result in
            switch result {
            case .success(let response):
                print("Successfully pre-fetched \(response.songs.count) songs")
            case .failure(let error):
                print("Failed to pre-fetch artist songs: \(error)")
            }
        }
    }
    
    @objc private func handleAppActive() {
        if AuthManager.shared.isUserLoggedIn() {
            prefetchArtistData()
        }
    }
    
    private func toggleAuthLayout() {
        isAuthLayoutVisible.toggle()

        if isAuthLayoutVisible {
            stopAudio()
            hideCurrentContentViews()
            authLayout.show()
            
            UIView.animate(withDuration: 0.15, animations: {
                self.libraryButton.alpha = 0
                self.searchIconButton.alpha = 0
                self.userIconButton.alpha = 0
            }) { _ in
                self.libraryButton.isHidden = true
                self.searchIconButton.isHidden = true
                self.userIconButton.isHidden = true
            }
        } else {
            authLayout.hide()
            showCurrentContentViews()
            
            self.libraryButton.isHidden = false
            self.searchIconButton.isHidden = false
            self.userIconButton.isHidden = false
            UIView.animate(withDuration: 0.15, animations: {
                self.libraryButton.alpha = 1
                self.searchIconButton.alpha = 1
                self.userIconButton.alpha = 1
            })
        }
    }
    
    // MARK: Leaderboard Stuff
    
    @objc private func leaderboardButtonTapped() {
        toggleLeaderboardLayout()
    }
    
    private func toggleLeaderboardLayout() {
        isLeaderboardVisible.toggle()

        if isLeaderboardVisible {
            stopAudio()
            hideCurrentContentViews()
            leaderboardLayout.show()
            
            UIView.animate(withDuration: 0.15, animations: {
                self.libraryButton.alpha = 0
                self.searchIconButton.alpha = 0
                self.userIconButton.alpha = 0
            }) { _ in
                self.libraryButton.isHidden = true
                self.searchIconButton.isHidden = true
                self.userIconButton.isHidden = true
            }
        } else {
            stopAudio()
            leaderboardLayout.hide()
            showCurrentContentViews()
            
            self.libraryButton.isHidden = false
            self.searchIconButton.isHidden = false
            self.userIconButton.isHidden = false
            UIView.animate(withDuration: 0.15) {
                self.libraryButton.alpha = 1
                self.searchIconButton.alpha = 1
                self.userIconButton.alpha = 1
            }
        }
    }
    
    // MARK: Artist Stuff
    
    @objc private func dismissArtistLayout() {
        toggleArtistLayout()
    }

    private func toggleArtistLayout() {
        guard AuthManager.shared.isUserLoggedIn() else {
            toggleAuthLayout()
            return
        }
        
        isArtistLayoutVisible.toggle()
        
        if isArtistLayoutVisible {
            hideCurrentContentViews()
            
            // Use cached data if available
            if let artistId = AuthManager.shared.getArtistId() {
                artistLayout.configure(
                    artistId: artistId,
                    presentationMode: .owned
                )
                
                // If we have cached data, apply it immediately
                if let cachedData = cachedArtistData {
                    DispatchQueue.main.async {
                        self.artistLayout.show()
                        // The layout will use the cached data from ArtistManager
                    }
                } else {
                    // If no cached data, show layout normally
                    artistLayout.show()
                }
            }
            
            view.bringSubviewToFront(userIconButton)
            view.bringSubviewToFront(libraryButton)
            view.bringSubviewToFront(searchIconButton)
            
            stopAudio()
            userIconButton.setImage(UIImage(systemName: "square.fill"), for: .normal)
            
            UIView.animate(withDuration: 0.15) {
                self.libraryButton.alpha = 0
                self.searchIconButton.alpha = 0
            } completion: { _ in
                self.libraryButton.isHidden = true
                self.searchIconButton.isHidden = true
            }
        } else {
            UIView.animate(withDuration: 0.15) {
                self.artistLayout.alpha = 0
            } completion: { _ in
                self.artistLayout.hide()
                self.showCurrentContentViews()
                self.userIconButton.setImage(UIImage(systemName: "person"), for: .normal)
                
                self.libraryButton.isHidden = false
                self.searchIconButton.isHidden = false
                UIView.animate(withDuration: 0.15) {
                    self.libraryButton.alpha = 1
                    self.searchIconButton.alpha = 1
                }
            }
        }
    }

    // MARK: - Library Visibility
    
    private func updateLibraryVisibility() {
        if isLibraryVisible {
            // Reset search states
            librarySearchBar.resignFirstResponder()
            librarySearchBar.isHidden = true
            librarySearchBar.alpha = 0
            librarySearchButton.isHidden = false
            
            // Setup library view initial state
            libraryView.isHidden = false
            libraryView.alpha = 0
            
            // Fetch items based on current auth state
            libraryItems = LibraryManager.shared.fetchLibraryItems()
            filteredLibraryItems = libraryItems
            
            // Prepare layout
            libraryLayout?.resetItemVisibility()
            
            // Apply layout and animate
            libraryLayout?.apply(to: libraryView, with: filteredLibraryItems) { [weak self] in
                guard let self = self else { return }
                
                // Hide main content
                self.hideMainContent { [weak self] in
                    guard let self = self else { return }
                    
                    // Show library content
                    UIView.animate(withDuration: 0.15) {
                        self.libraryView.alpha = 1
                        self.librarySearchButton.alpha = 1
                    }
                }
            }
        } else {
            // Hide library content
            UIView.animate(withDuration: 0.15) {
                self.librarySearchButton.alpha = 0
                self.librarySearchBar.alpha = 0
                self.libraryView.alpha = 0
            } completion: { [weak self] _ in
                guard let self = self else { return }
                
                self.librarySearchButton.isHidden = true
                self.librarySearchBar.isHidden = true
                self.libraryView.isHidden = true
                
                // Show main content
                self.showMainContent()
            }
        }
        
        updateLoadingIndicatorVisibility()
        updateLibraryButtonIcon()
    }
    // Helper methods
    private func hideMainContent(completion: @escaping () -> Void) {
        UIView.animate(withDuration: 0.15) {
            self.musicCollectionView.alpha = 0
            self.searchIconButton.alpha = 0
            self.leaderboardButton.alpha = 0
            self.userIconButton.alpha = 0
            self.autoPlayButton.alpha = 0  // Add this line
        } completion: { _ in
            self.musicCollectionView.isHidden = true
            self.searchIconButton.isHidden = true
            self.leaderboardButton.isHidden = true
            self.userIconButton.isHidden = true
            self.autoPlayButton.isHidden = true  // Add this line
            completion()
        }
    }

    private func showMainContent() {
        self.musicCollectionView.isHidden = false
        self.searchIconButton.isHidden = false
        self.leaderboardButton.isHidden = false
        self.userIconButton.isHidden = false
        self.autoPlayButton.isHidden = false  // Add this line
        
        UIView.animate(withDuration: 0.15) {
            self.musicCollectionView.alpha = 1
            self.searchIconButton.alpha = 1
            self.leaderboardButton.alpha = 1
            self.userIconButton.alpha = 1
            self.autoPlayButton.alpha = 1  // Add this line
        }
    }


    
    private func updateLibraryButtonIcon() {
        let iconName = isLibraryVisible ? "square.fill" : "books.vertical"
        libraryButton.setImage(UIImage(systemName: iconName), for: .normal)
    }
    
    private func updateLoadingIndicatorVisibility() {
        if isLibraryVisible {
            loadingIndicator.stopAnimating()
        } else {
            if isLoadingSnippets {
                loadingIndicator.startAnimating()
            } else {
                loadingIndicator.stopAnimating()
            }
        }
    }
    
    private func hideLibrarySearchBar() {
        librarySearchBar.resignFirstResponder()
        
        UIView.animate(withDuration: 0.15) {
            self.librarySearchBar.alpha = 0
        } completion: { _ in
            self.librarySearchBar.isHidden = true
            self.librarySearchButton.isHidden = false
            
            UIView.animate(withDuration: 0.15) {
                self.librarySearchButton.alpha = 1
            }
            
            self.libraryButton.isHidden = false
        }
    }
    
    private func filterLibraryItems(with searchText: String) {
        if searchText.isEmpty {
            filteredLibraryItems = libraryItems
        } else {
            filteredLibraryItems = libraryItems.filter { item in
                let titleMatch = item.title?.range(of: searchText, options: .caseInsensitive) != nil
                let artistMatch = item.artist?.range(of: searchText, options: .caseInsensitive) != nil
                return titleMatch || artistMatch
            }
        }
        
        libraryLayout?.apply(to: libraryView, with: filteredLibraryItems) {
            // Completion code if needed
        }
    }
    
    // MARK: - Data Loading
    
    func fetchRandomMusicSnippets() {
        // If ArtistLayout is visible, do not fetch or play snippets
        if isArtistLayoutVisible {
            print("ArtistLayout is visible. Skipping fetching random music snippets.")
            return
        }
        
        isInSearchMode = false
        autoPlayButton.isHidden = false

        
        stopAudio()
        isLoadingSnippets = true
        updateLoadingIndicatorVisibility()
        musicCollectionView.isHidden = true
        
        snippetManager.fetchRandomMusicSnippets { [weak self] result in
            guard let self = self else { return }
            
            DispatchQueue.main.async {
                self.isLoadingSnippets = false
                self.updateLoadingIndicatorVisibility()
                self.musicCollectionView.isHidden = false
                switch result {
                case .success(let snippets):
                    self.musicSnippets = snippets
                    self.filteredMusicSnippets = self.filterSnippets(snippets)
                    self.resetAllAnimations()
                    self.musicCollectionView.reloadData()
                    
                    if let firstMusicIndex = self.filteredMusicSnippets.firstIndex(where: { $0.type == .music }) {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                            // Check again if ArtistLayout is visible before playing
                            if !self.isArtistLayoutVisible {
                                self.scrollToItem(at: firstMusicIndex, animated: true)
                            } else {
                                print("ArtistLayout became visible. Skipping snippet playback.")
                            }
                        }
                    }
                    
                    print("DEBUG: Random music snippets fetched and collection view reloaded")
                    
                case .failure(let error):
                    if case APIError.unauthorized = error {
                        self.handleUnauthorizedError()
                    } else {
                        print("Error fetching recommendations: \(error)")
                        // Handle other errors
                    }
                }
            }
        }
    }

    
    func performSearch(with query: String) {
        isInSearchMode = true
        currentSearchPage = 1
        isLoadingSearchResults = false
        hasMoreSearchResults = true
        currentSearchQuery = query
        
        stopAudio()
        isLoadingSnippets = true
        updateLoadingIndicatorVisibility()
        musicCollectionView.isHidden = true
        
        snippetManager.fetchRecommendations(for: query, page: currentSearchPage) { [weak self] result in
            guard let self = self else { return }
            
            DispatchQueue.main.async {
                self.isLoadingSnippets = false
                self.updateLoadingIndicatorVisibility()
                self.musicCollectionView.isHidden = false
                
                switch result {
                case .success(let snippets):
                    self.musicSnippets = snippets
                    self.filteredMusicSnippets = self.filterSnippets(snippets)
                    self.resetAllAnimations()
                    self.musicCollectionView.reloadData()
                    
                    if self.filteredMusicSnippets.count >= self.maxSearchResults || snippets.count < self.resultsPerPage {
                        self.hasMoreSearchResults = false
                    }
                    
                    self.hideSearchBar()
                    
                    if let firstMusicIndex = self.filteredMusicSnippets.firstIndex(where: { $0.type == .music }) {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                            self.scrollToItem(at: firstMusicIndex, animated: true)
                        }
                    }
                    
                case .failure(let error):
                    if case APIError.unauthorized = error {
                        self.handleUnauthorizedError()
                    } else {
                        print("Error fetching recommendations: \(error)")
                        // Handle other errors
                    }
                }
            }
        }
    }
    
    private func fetchNextSearchResults() {
        guard let query = currentSearchQuery,
              !isLoadingSearchResults,
              hasMoreSearchResults else {
            return
        }
        isLoadingSearchResults = true
        currentSearchPage += 1
        snippetManager.fetchRecommendations(for: query, page: currentSearchPage) { [weak self] result in
            guard let self = self else { return }
            DispatchQueue.main.async {
                self.isLoadingSearchResults = false
                switch result {
                case .success(let snippets):
                    if snippets.isEmpty {
                        self.hasMoreSearchResults = false
                    } else {
                        self.musicSnippets.append(contentsOf: snippets)
                        self.filteredMusicSnippets = self.filterSnippets(self.musicSnippets)
                        self.musicCollectionView.reloadData()
                        
                        if self.filteredMusicSnippets.count >= self.maxSearchResults || snippets.count < self.resultsPerPage {
                            self.hasMoreSearchResults = false
                        }
                    }
                case .failure(let error):
                    print("Error fetching more search results: \(error)")
                    self.hasMoreSearchResults = false
                }
            }
        }
    }
    
    private func filterSnippets(_ snippets: [MusicSnippet]) -> [MusicSnippet] {
        var filtered = snippets.filter { $0.type != .search } // Always remove search button
        
        // Include the .reload snippet only in search mode
        if isInSearchMode && currentSearchPage == 1 {
            // Ensure only one .reload snippet is present
            if let reloadSnippet = snippets.first(where: { $0.type == .reload }) {
                filtered.removeAll { $0.type == .reload }
                filtered.insert(reloadSnippet, at: 0) // Insert at the beginning
            }
        } else {
            filtered = filtered.filter { $0.type != .reload }
        }
        
        return filtered
    }
    
    private func handleUnauthorizedError() {
        print("Unauthorized access. Please check your API key.")
        // In a real app, consider showing an alert or triggering re-authentication
    }
    
    private func filterSearchButton(from snippets: [MusicSnippet]) -> [MusicSnippet] {
        return snippets.filter { $0.type != .search }
    }
    
    func clearAllCaches() {
        // Clear ArtistManager cache
        if let artistId = AuthManager.shared.getArtistId() {
            ArtistManager.shared.clearCache(for: artistId)
        }
        
        // Clear any API response caches
        URLCache.shared.removeAllCachedResponses()
        
        // Refresh data
        fetchRandomMusicSnippets()
    }
    
    // MARK: - Audio Playback
    
    func playAudioFromUrl(_ urlString: String, at index: Int) {
        // Check if ArtistLayout is visible
        if isArtistLayoutVisible {
            print("ArtistLayout is visible. Skipping snippet playback.")
            return
        }
        
        
        
        // Stop any existing audio and reset state
        stopAudio()
        currentlyPlayingItem = nil  // Reset library item reference
        
        print("DEBUG: Attempting to play audio from URL: \(urlString)")
        
        if index == currentPlayingIndex {
            togglePlayPause()
            return
        }
        
        if let oldIndex = currentPlayingIndex {
            updateCellPlayingState(at: oldIndex, isPlaying: false)
        }
        
        let snippet = filteredMusicSnippets[index]
        
        audioPlayer.onAudioPositionUpdate = { [weak self] currentTime, duration in
            DispatchQueue.main.async {
                if let cell = self?.musicCollectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? MusicSnippetCell {
                    cell.animationView.updateAnimationProgress(currentTime)
                }
            }
        }
        
        audioPlayer.onPlaybackStarted = { [weak self] in
            DispatchQueue.main.async {
                self?.updateCellPlayingState(at: index, isPlaying: true)
            }
        }
        
        audioPlayer.onPlaybackPaused = { [weak self] in
            DispatchQueue.main.async {
                self?.updateCellPlayingState(at: index, isPlaying: false)
            }
        }
        
        audioPlayer.onPlaybackResumed = { [weak self] in
            DispatchQueue.main.async {
                self?.updateCellPlayingState(at: index, isPlaying: true)
            }
        }
        
        audioPlayer.playAudioFromUrl(urlString,
                                    title: snippet.title,
                                    artist: snippet.artist) { [weak self] success in
            DispatchQueue.main.async {
                if success {
                    self?.currentPlayingIndex = index
                    self?.updateNavigationButtonStates()
                } else {
                    print("DEBUG: Failed to play audio for index: \(index)")
                }
            }
        }
        
        audioPlayer.onPlaybackEnded = { [weak self] in
            DispatchQueue.main.async {
                guard let self = self else { return }
                if self.isAutoPlayEnabled {
                    // When autoplay is enabled, move to next snippet
                    self.moveToNextSnippet(from: index)
                } else {
                    // When autoplay is disabled, replay the current snippet
                    if let url = self.filteredMusicSnippets[index].url {
                        self.playAudioFromUrl(url, at: index)
                    }
                }
            }
        }
    }

    
    private func playAudioFromUrl(_ urlString: String, item: LibraryItem, itemView: UIView) {
        audioPlayer.onPlaybackStarted = { [weak self] in
            print("Playback started")
            DispatchQueue.main.async {
                self?.isAudioPaused = false
                self?.updatePlayingState(for: item, isPlaying: true)
            }
        }
        
        audioPlayer.onPlaybackPaused = { [weak self] in
            print("Playback paused")
            DispatchQueue.main.async {
                self?.isAudioPaused = true
                self?.updatePlayingState(for: item, isPlaying: false)
            }
        }
        
        audioPlayer.onPlaybackResumed = { [weak self] in
            print("Playback resumed")
            DispatchQueue.main.async {
                self?.isAudioPaused = false
                self?.updatePlayingState(for: item, isPlaying: true)
            }
        }
        
        audioPlayer.playAudioFromUrl(urlString) { [weak self] success in
            DispatchQueue.main.async {
                if success {
                    print("Now playing: \(item.title ?? "Unknown") by \(item.artist ?? "Unknown")")
                    self?.currentlyPlayingItem = item
                    self?.isAudioPaused = false
                } else {
                    print("Failed to play audio")
                    self?.updatePlayingState(for: item, isPlaying: false)
                    self?.isAudioPaused = true
                }
            }
        }
    }
    
    private func updateCellPlayingState(at index: Int, isPlaying: Bool) {
        if let cell = musicCollectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? MusicSnippetCell {
            let snippet = filteredMusicSnippets[index]
            cell.configure(with: snippet, isPlaying: isPlaying)
            cell.setNumber(index)
        }
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        checkCollectionViewVisibility()
        let center = view.convert(musicCollectionView.center, to: musicCollectionView)
        if let indexPath = musicCollectionView.indexPathForItem(at: center) {
            print("Center item index: \(indexPath.item)")
            
            // Add bounds check before accessing the array
            guard indexPath.item < filteredMusicSnippets.count else {
                print("Index out of range: \(indexPath.item) >= \(filteredMusicSnippets.count)")
                return
            }
            
            let snippet = filteredMusicSnippets[indexPath.item]
            if snippet.type == .music {
                if indexPath.item != currentPlayingIndex {
                    if let url = snippet.url {
                        // Check if ArtistLayout is visible before playing
                        if !isArtistLayoutVisible {
                            playAudioFromUrl(url, at: indexPath.item)
                        } else {
                            print("ArtistLayout is visible. Skipping snippet playback.")
                        }
                    }
                }
            }
        } else {
            print("No item at center")
        }
    }
    
    private func stopAudio() {
        audioPlayer.stopAudio()
        
        // Reset main view state
        if let currentIndex = currentPlayingIndex {
            updateCellPlayingState(at: currentIndex, isPlaying: false)
        }
        currentPlayingIndex = nil
        
        // Reset library state
        if let currentItem = currentlyPlayingItem {
            updatePlayingState(for: currentItem, isPlaying: false)
        }
        currentlyPlayingItem = nil
        
        // Reset all library items' playing states
        LibraryManager.shared.resetAllPlayingStates()
    }
    
    func togglePlayPause() {
        audioPlayer.togglePlayPause()
    }
    
    // MARK: - Library Management
    
    private func handleLibraryItemTap(item: LibraryItem, itemView: UIView) {
        print("tapped")
        if item == currentlyPlayingItem {
            if audioPlayer.isPlaying {
                audioPlayer.togglePlayPause()
                updatePlayingState(for: item, isPlaying: false)
            } else {
                audioPlayer.togglePlayPause()
                updatePlayingState(for: item, isPlaying: true)
            }
        } else {
            stopAudio()
            playLibraryItem(item, itemView: itemView)
        }
    }
    
    private func handleLibraryItemRename(item: LibraryItem, newTitle: String) {
        print("DEBUG: Entering handleLibraryItemRename method")
        print("DEBUG: Attempting to rename item in Core Data")
        
        // Update the item's title
        item.title = newTitle
        
        // Save the changes to Core Data
        do {
            try CoreDataStack.shared.saveLibraryContext()
            print("DEBUG: Successfully renamed item to: \(newTitle) in Core Data")
            
            // Update the libraryItems array
            if let index = libraryItems.firstIndex(where: { $0.objectID == item.objectID }) {
                libraryItems[index] = item
                print("DEBUG: Updated libraryItems array with new title")
            } else {
                print("DEBUG: Could not find item in libraryItems array")
            }
            
            // Update the filteredLibraryItems based on current search text
            if let searchText = librarySearchBar.text, !searchText.isEmpty {
                filterLibraryItems(with: searchText)
            } else {
                filteredLibraryItems = libraryItems
            }
            
            // Refresh the library view with the updated filteredLibraryItems
            DispatchQueue.main.async { [weak self] in
                self?.libraryLayout?.apply(to: self?.libraryView ?? UIView(), with: self?.filteredLibraryItems ?? []) {
                    print("DEBUG: Library view refreshed after rename")
                }
            }
            
        } catch {
            print("ERROR: Failed to save renamed item: \(error)")
            
            // Optionally, you can revert the title change or notify the user
            // For example:
            // item.title = oldTitle
            // Show an alert to the user about the failure
            let alert = UIAlertController(title: "Rename Failed",
                                          message: "Unable to rename the item. Please try again.",
                                          preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            present(alert, animated: true, completion: nil)
        }
    }
    
    private func playLibraryItem(_ item: LibraryItem, itemView: UIView) {
        guard let gsutilUri = item.gsutilUri else {
            print("Error: No gsutilUri found for library item")
            return
        }
        
        fetchAuthenticatedURL(for: gsutilUri) { [weak self] authenticatedURL in
            guard let self = self, let url = authenticatedURL else { return }
            DispatchQueue.main.async {
                // Reset all other items' playing states
                self.libraryItems.forEach { $0.isPlaying = false }
                
                // Set the current item as playing
                item.isPlaying = true
                self.updatePlayingState(for: item, isPlaying: true)
                
                self.audioPlayer.playAudioFromUrl(url) { success in
                    if success {
                        print("Now playing: \(item.title ?? "Unknown") by \(item.artist ?? "Unknown")")
                        self.currentlyPlayingItem = item
                    } else {
                        print("Failed to play audio")
                        self.updatePlayingState(for: item, isPlaying: false)
                    }
                }
            }
        }
    }
    
    func addToLibrary(snippet: MusicSnippet) {
        let song = ModelConverters.convertToSong(from: snippet)
        LibraryManager.shared.addToLibrary(song) { result in
            switch result {
            case .success:
                print("Successfully added to library")
                // Handle success (e.g., update UI, show confirmation)
            case .failure(let error):
                print("Failed to add to library: \(error)")
                // Handle error (e.g., show error alert)
            }
        }
        updateLibraryStatus(for: snippet, isInLibrary: true)
        print("Successfully added to library")

        // Fetch the updated library items
        libraryItems = LibraryManager.shared.fetchLibraryItems()

        // Update filteredLibraryItems based on current search text
        if let searchText = librarySearchBar.text, !searchText.isEmpty {
            filterLibraryItems(with: searchText)
        } else {
            filteredLibraryItems = libraryItems
        }

        // Refresh the library layout
        DispatchQueue.main.async { [weak self] in
            self?.libraryLayout?.apply(to: self?.libraryView ?? UIView(), with: self?.filteredLibraryItems ?? []) {
                print("Library layout refreshed after adding new item")
            }
        }
    }

    
    private func removeFromLibrary(item: LibraryItem) {
        guard let gsutilUri = item.gsutilUri else {
            print("Error: No gsutilUri found for library item")
            return
        }
        
        // Step 1: Optimistically remove the item from the local data source and UI
        if let index = libraryItems.firstIndex(where: { $0.objectID == item.objectID }) {
            libraryItems.remove(at: index)
            print("DEBUG: Removed item from libraryItems array")
            
            // Update the filteredLibraryItems based on current search text
            if let searchText = librarySearchBar.text, !searchText.isEmpty {
                filterLibraryItems(with: searchText)
            } else {
                filteredLibraryItems = libraryItems
            }
            
            // Refresh the library view with the updated filteredLibraryItems
            DispatchQueue.main.async { [weak self] in
                self?.libraryLayout?.apply(to: self?.libraryView ?? UIView(), with: self?.filteredLibraryItems ?? []) {
                    print("DEBUG: Library view refreshed after item removal")
                }
            }
        } else {
            print("DEBUG: Could not find item in libraryItems array to remove")
        }
        
        // Step 2: Save the changes to Core Data
        do {
            try CoreDataStack.shared.saveLibraryContext()
            print("DEBUG: Successfully removed item from Core Data")
        } catch {
            print("ERROR: Failed to remove item from Core Data: \(error)")
            // Since Core Data failed, you might want to revert the UI changes
            // Optionally, re-add the item to libraryItems and refresh the UI
            libraryItems.append(item)
            if let searchText = librarySearchBar.text, !searchText.isEmpty {
                filterLibraryItems(with: searchText)
            } else {
                filteredLibraryItems = libraryItems
            }
            DispatchQueue.main.async { [weak self] in
                self?.libraryLayout?.apply(to: self?.libraryView ?? UIView(), with: self?.filteredLibraryItems ?? []) {
                    print("DEBUG: Library view refreshed after Core Data failure")
                }
                // Show an error alert to the user
                self?.showErrorAlert("Unable to remove the item from your library. Please try again.")
            }
            return // Exit early since Core Data failed
        }
        
        // Step 3: Perform the removal operation in the background
        LibraryManager.shared.removeFromLibrary(gsutilUri: gsutilUri) { [weak self] result in
            switch result {
            case .success:
                print("Successfully removed from library on server")
                // Optionally, you can perform additional actions on success
            case .failure(let error):
                print("Failed to remove from library on server: \(error)")
                
                // Step 4: Handle the error by reverting UI changes and informing the user
                DispatchQueue.main.async {
                    // Re-add the item to the local data source
                    self?.libraryItems.append(item)
                    if let searchText = self?.librarySearchBar.text, !searchText.isEmpty {
                        self?.filterLibraryItems(with: searchText)
                    } else {
                        self?.filteredLibraryItems = self?.libraryItems ?? []
                    }
                    
                    // Refresh the library view with the reverted data
                    self?.libraryLayout?.apply(to: self?.libraryView ?? UIView(), with: self?.filteredLibraryItems ?? []) {
                        print("DEBUG: Library view refreshed after server removal failure")
                    }
                    
                    // Show an error alert to the user
                    self?.showErrorAlert("Failed to remove the item from your library. Please try again.")
                }
            }
        }
    }

    // Helper method to present an error alert
    private func showErrorAlert(_ message: String) {
        let alert = UIAlertController(title: "Removal Failed",
                                      message: message,
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }
    
    private func updateLibraryStatus(for snippet: MusicSnippet, isInLibrary: Bool) {
        if let index = musicSnippets.firstIndex(where: { $0.id == snippet.id }) {
            musicSnippets[index].isInLibrary = isInLibrary
        }
        if let filteredIndex = filteredMusicSnippets.firstIndex(where: { $0.id == snippet.id }) {
            filteredMusicSnippets[filteredIndex].isInLibrary = isInLibrary
            musicCollectionView.reloadItems(at: [IndexPath(item: filteredIndex, section: 0)])
        }
    }
    
    private func updatePlayingState(for item: LibraryItem, isPlaying: Bool) {
        LibraryManager.shared.updatePlayingState(for: item, isPlaying: isPlaying)
        libraryLayout?.updateItemState(for: item, isPlaying: isPlaying)
    }
    
    private func refreshLibraryLayout() {
        guard isLibraryVisible else { return }
        libraryLayout?.apply(to: libraryView, with: filteredLibraryItems) {
            print("Library layout refreshed in MainViewController")
        }
    }
    
    @objc private func librarySearchButtonTapped() {
        self.libraryButton.isHidden = true
        
        librarySearchButton.isHidden = true
        librarySearchBar.isHidden = false
        
        UIView.animate(withDuration: 0.15) {
            self.librarySearchBar.alpha = 1
        } completion: { _ in
            self.librarySearchBar.becomeFirstResponder()
        }
    }
    
    // MARK: - API Interaction
    
    private func fetchAuthenticatedURL(for gsutilUri: String, completion: @escaping (String?) -> Void) {
        let endpoint = "/api/authenticate-uri/"
        let parameters = ["gsutil_uri": gsutilUri]
        
        guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else {
            print("Failed to serialize JSON parameters.")
            completion(nil)
            return
        }
        
        APIClient.shared.perform(endpoint, method: "POST", body: jsonData) { (result: Result<[String: String], APIError>) in
            switch result {
            case .success(let response):
                if let authenticatedURL = response["authenticated_url"] {
                    completion(authenticatedURL)
                } else {
                    print("Failed to parse authenticated URL from response")
                    completion(nil)
                }
            case .failure(let error):
                print("Error fetching authenticated URL: \(error)")
                completion(nil)
            }
        }
    }
    
    // MARK: - Keyboard Handling
    
    @objc private func keyboardWillShow(notification: NSNotification) {
        guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        
        let keyboardHeight = keyboardFrame.height
        let collectionViewBottomInset = view.frame.height - musicCollectionView.frame.maxY
        let inset = max(0, keyboardHeight - collectionViewBottomInset)
        
        UIView.animate(withDuration: 0.15) {
            self.musicCollectionView.contentInset.bottom = inset
            self.musicCollectionView.verticalScrollIndicatorInsets.bottom = inset
            self.musicCollectionView.layoutIfNeeded()
        }
    }
    
    @objc private func keyboardWillHide(notification: NSNotification) {
        UIView.animate(withDuration: 0.15) {
            self.musicCollectionView.contentInset.bottom = 0
            self.musicCollectionView.verticalScrollIndicatorInsets.bottom = 0
            self.musicCollectionView.layoutIfNeeded()
        }
    }
}

// MARK: - UICollectionViewDataSource

extension MainViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        let additionalCell = (isLoadingMore || isLoadingSearchResults) ? 1 : 0
        return filteredMusicSnippets.count + additionalCell
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        if (isLoadingMore || isLoadingSearchResults) && indexPath.item == filteredMusicSnippets.count {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LoadingIndicatorCell", for: indexPath) as! LoadingIndicatorCell
            cell.startAnimating()
            return cell
        }
        
        if isLoadingMore && indexPath.item == filteredMusicSnippets.count {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LoadingIndicatorCell", for: indexPath) as! LoadingIndicatorCell
            cell.startAnimating()
            return cell
        }
        
        // Guard against index out of bounds
        guard indexPath.item < filteredMusicSnippets.count else {
            return UICollectionViewCell()
        }
        
        if indexPath.item == filteredMusicSnippets.count {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "LoadingIndicatorCell", for: indexPath) as! LoadingIndicatorCell
            cell.startAnimating()
            return cell
        }
        
        let snippet = filteredMusicSnippets[indexPath.item]

        switch snippet.type {
        case .music:
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MusicSnippetCell", for: indexPath) as? MusicSnippetCell else {
                return UICollectionViewCell()
            }
            cell.animationView.fullReset()
            let isPlaying = indexPath.item == currentPlayingIndex
            cell.configure(with: snippet, isPlaying: isPlaying)
            cell.setNumber(indexPath.item)
            
            cell.onCellTapped = { [weak self] in
                if let url = snippet.url {
                    self?.playAudioFromUrl(url, at: indexPath.item)
                }
            }
            
            cell.onAddToLibraryTapped = { [weak self] in
                if snippet.isInLibrary {
                    // self?.removeFromLibrary(snippet: snippet)
                } else {
                    self?.addToLibrary(snippet: snippet)
                }
            }
            
            // Add the new handler for viewing artist
            cell.onViewArtistTapped = { [weak self] artistId in
                // Stop any current playback
                self?.stopAudio()
                
                // Configure and show the artist layout
                self?.artistLayout.configure(
                    artistId: artistId,
                    presentationMode: .viewOnly,
                    onBack: { [weak self] in
                        // Hide artist layout and show main content
                        self?.isArtistLayoutVisible = false
                        self?.artistLayout.hide()
                        self?.showCurrentContentViews()
                        
                        // Show UI elements
                        self?.libraryButton.isHidden = false
                        self?.searchIconButton.isHidden = false
                        UIView.animate(withDuration: 0.15) {
                            self?.libraryButton.alpha = 1
                            self?.searchIconButton.alpha = 1
                        }
                    }
                )
                
                // Hide main content and show artist layout
                self?.hideCurrentContentViews()
                self?.isArtistLayoutVisible = true
                self?.artistLayout.show()
                
                // Update UI elements
                UIView.animate(withDuration: 0.15) {
                    self?.libraryButton.alpha = 0
                    self?.searchIconButton.alpha = 0
                } completion: { _ in
                    self?.libraryButton.isHidden = true
                    self?.searchIconButton.isHidden = true
                }
            }
            
            cell.onViewFullSongTapped = { [weak self] snippet in
                guard let self = self else { return }
                
                let streamingLinksLayout = StreamingLinksLayout(frame: self.view.bounds)
                streamingLinksLayout.delegate = self
                streamingLinksLayout.configure(with: snippet)  // Now we can pass snippet directly
                self.view.addSubview(streamingLinksLayout)
                streamingLinksLayout.show()
            }
            
            return cell
            
        case .search, .reload:
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "SpecialCardCell", for: indexPath) as? SpecialCardCell else {
                return UICollectionViewCell()
            }
            cell.configure(with: snippet)
            return cell
        }
    }
}

// MARK: - UICollectionViewDelegate

extension MainViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        print("DEBUG: Cell selected at index: \(indexPath.item)")
        
        guard indexPath.item < filteredMusicSnippets.count else {
            return
        }
        
        let snippet = filteredMusicSnippets[indexPath.item]
        
        switch snippet.type {
        case .music:
            if indexPath.item == currentPlayingIndex {
                audioPlayer.togglePlayPause()
                updateCellPlayingState(at: indexPath.item, isPlaying: audioPlayer.isPlaying)
            } else {
                if let url = snippet.url {
                    playAudioFromUrl(url, at: indexPath.item)
                } else {
                    print("DEBUG: No URL available for this snippet")
                }
            }
        case .search:
            print("DEBUG: Search action triggered")
        case .reload:
            print("DEBUG: Reload action triggered")
            fetchRandomMusicSnippets()
        }
    }
}

// MARK: - UICollectionViewDelegateFlowLayout

extension MainViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView,
                        layout collectionViewLayout: UICollectionViewLayout,
                        sizeForItemAt indexPath: IndexPath) -> CGSize {
        let screenWidth = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height)
        let referenceWidth: CGFloat = 428 // Width of iPhone 15 screen in portrait
        let referenceItemWidth: CGFloat = 200
        let referenceItemHeight: CGFloat = 160
        
        let widthScale = screenWidth / referenceWidth
        let itemWidth = referenceItemWidth * widthScale
        let itemHeight = referenceItemHeight * widthScale
        
        return CGSize(width: itemWidth, height: itemHeight)
    }
}

// MARK: - UITextViewDelegate

extension MainViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        if textView == searchBar {
            // Existing search bar logic
            // updatePlaceholderVisibility()
            importButton.isHidden = textView.text.isEmpty
        } else if textView == librarySearchBar {
            // Library search bar logic
            // librarySearchBar.setPlaceholderVisibility(textView.text.isEmpty)
            filterLibraryItems(with: textView.text)
        }
    }
    
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        if text == "\n" {
            if textView == searchBar {
                performSearch(with: textView.text)
                hideSearchBar()
            } else if textView == librarySearchBar {
                textView.resignFirstResponder() // Dismiss keyboard
            }
            return false
        }
        return true
    }
}

extension MainViewController: StreamingLinksLayoutDelegate {
    func streamingLinksLayout(_ layout: StreamingLinksLayout, didSelectURL url: URL) {
        // Handle streaming link selection
        let safariVC = SFSafariViewController(url: url)
        safariVC.modalPresentationStyle = .pageSheet
        present(safariVC, animated: true)
    }
}


// MARK: - AuthLayout Delegate

extension MainViewController: AuthLayoutDelegate {
    func authLayout(_ authLayout: AuthLayout, didShowError message: String) {
        // Present an alert to inform the user about the error
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }
}

extension MainViewController: LeaderboardLayoutDelegate {
    func leaderboardLayout(_ layout: LeaderboardLayout, didTapClose: Void) {
        layout.hide()
        
        // Update the state flag
        isLeaderboardVisible = false
        
        // Show all main content buttons again
        musicCollectionView.isHidden = false
        libraryButton.isHidden = false
        searchIconButton.isHidden = false
        userIconButton.isHidden = false
        leaderboardButton.isHidden = false
        autoPlayButton.isHidden = false  // Add this line to show autoplay button
        
        UIView.animate(withDuration: 0.15) {
            self.musicCollectionView.alpha = 1
            self.libraryButton.alpha = 1
            self.searchIconButton.alpha = 1
            self.userIconButton.alpha = 1
            self.leaderboardButton.alpha = 1
            self.autoPlayButton.alpha = 1  // Add this line to animate autoplay button
        }
    }
    
    func leaderboardLayout(_ layout: LeaderboardLayout, didRequestProfileView artistId: String) {
        let artistLayout = ArtistLayout(frame: view.bounds)
        view.addSubview(artistLayout)
        
        artistLayout.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            artistLayout.topAnchor.constraint(equalTo: view.topAnchor),
            artistLayout.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            artistLayout.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            artistLayout.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        // Configure with view-only mode and back handler
        artistLayout.configure(
            artistId: artistId,
            presentationMode: .viewOnly,
            onBack: { [weak self, weak layout] in
                // Hide artist layout and show leaderboard again
                artistLayout.removeFromSuperview()
                
                // Make sure leaderboard is visible
                layout?.isHidden = false
                UIView.animate(withDuration: 0.15) {
                    layout?.alpha = 1
                }
            }
        )
        
        // Hide leaderboard without removing it
        layout.alpha = 0
        layout.isHidden = true
        
        // Show the artist layout
        artistLayout.show()
    }
}

// MARK: - SpecialCardCell

class SpecialCardCell: UICollectionViewCell {
    
    // MARK: - UI Components
    
    private let actionLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16, weight: .bold)
        label.textColor = .white
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    // MARK: - Initializers
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - Setup Methods
    
    private func setupViews() {
        contentView.backgroundColor = UIColor(red: 5/255, green: 5/255, blue: 5/255, alpha: 1)
        contentView.layer.cornerRadius = 4
        contentView.clipsToBounds = true
        
        contentView.addSubview(actionLabel)
        
        NSLayoutConstraint.activate([
            actionLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            actionLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
        ])
    }
    
    // MARK: - Configuration
    
    func configure(with snippet: MusicSnippet) {
        switch snippet.type {
        case .search:
            actionLabel.text = "Search"
        case .reload:
            actionLabel.text = "Random"
        case .music:
            actionLabel.text = "Play"
        }
    }
}

// MARK: - MusicSnippetCell

class MusicSnippetCell: UICollectionViewCell {
    
    // MARK: - Callbacks
    
    var onSelectButtonTapped: (() -> Void)?
    var onAddToLibraryTapped: (() -> Void)?
    var onCellTapped: (() -> Void)?
    var onViewArtistTapped: ((String) -> Void)?
    var onViewFullSongTapped: ((MusicSnippet) -> Void)?

    private var currentSnippet: MusicSnippet?
    
    // MARK: - UI Components
    
    private var isSearchMode: Bool = false
    private var isInLibrary: Bool = false
    
    private let numberLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 14, weight: .medium)
        label.textColor = .white
        label.backgroundColor = UIColor(white: 0.3, alpha: 0.7)
        label.textAlignment = .center
        label.layer.cornerRadius = 10
        label.clipsToBounds = true
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let animationView: AnimationView = {
        let view = AnimationView()
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = .clear
        return view
    }()
    
    let optionsButton: UIButton = {
        let button = UIButton(type: .system)
        if let image = UIImage(systemName: "ellipsis")?.rotated(by: .pi / 2) {
            button.setImage(image, for: .normal)
        } else {
            button.setImage(UIImage(systemName: "ellipsis"), for: .normal)
        }
        button.tintColor = UIColor(white: 0.3, alpha: 0.9)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    let addToLibraryButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Save", for: .normal)
        button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
        button.setTitleColor(UIColor.black.withAlphaComponent(0.9), for: .normal)
        button.backgroundColor = .clear
        button.layer.cornerRadius = 8
        button.layer.borderWidth = 1
        button.layer.borderColor = UIColor.black.withAlphaComponent(0.9).cgColor
        button.translatesAutoresizingMaskIntoConstraints = false
        button.setContentHuggingPriority(.required, for: .vertical)
        button.setContentCompressionResistancePriority(.required, for: .vertical)
        return button
    }()
    
    // MARK: - Initializers
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupViews()
    }
    
    // MARK: - Setup Methods
    
    private func setupViews() {
        let backgroundImage = UIImage(named: "IMG_20231217_0007")
        contentView.layer.contents = backgroundImage?.cgImage
        contentView.layer.contentsGravity = .resizeAspectFill
        contentView.layer.cornerRadius = 4
        contentView.clipsToBounds = true
        
        // contentView.addSubview(numberLabel)
        contentView.addSubview(addToLibraryButton)
        contentView.addSubview(optionsButton)
        contentView.addSubview(animationView)
        
        NSLayoutConstraint.activate([
            addToLibraryButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8),
            addToLibraryButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            addToLibraryButton.widthAnchor.constraint(equalToConstant: 100),
            addToLibraryButton.heightAnchor.constraint(equalToConstant: 34),
            
//            numberLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
//            numberLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
//            numberLabel.widthAnchor.constraint(equalToConstant: 20),
//            numberLabel.heightAnchor.constraint(equalToConstant: 20),
            
            optionsButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
            optionsButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4),
            optionsButton.widthAnchor.constraint(equalToConstant: 20),
            optionsButton.heightAnchor.constraint(equalToConstant: 20),
            
            animationView.topAnchor.constraint(equalTo: contentView.topAnchor),
            animationView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            animationView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            animationView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
        ])
        
        addToLibraryButton.addTarget(self, action: #selector(addToLibraryButtonTapped), for: .touchUpInside)
        
        // Configure the options button's menu
        optionsButton.showsMenuAsPrimaryAction = true
        optionsButton.menu = createContextMenu()
    }
    
    // MARK: - Configuration
    
    func configure(with musicSnippet: MusicSnippet, isPlaying: Bool) {
        self.isInLibrary = musicSnippet.isInLibrary
        self.currentSnippet = musicSnippet // Store the current snippet
        
        updateLibraryButtonVisibility()
        addToLibraryButton.removeTarget(nil, action: nil, for: .allEvents)
        addToLibraryButton.addTarget(self, action: #selector(addToLibraryButtonTapped), for: .touchUpInside)
        
        animationView.updateWithSnippet(musicSnippet)
        numberLabel.text = ""
        
        // Update the context menu
        optionsButton.menu = createContextMenu()
    }
    
    private func createContextMenu() -> UIMenu {
        var menuItems: [UIAction] = []

        // View Artist action
        let viewArtistAction = UIAction(
            title: "View Artist",
            image: UIImage(systemName: "person")
        ) { [weak self] _ in
            guard let self = self,
                  let snippet = self.currentSnippet,
                  let artistId = snippet.artistId else {
                return
            }
            self.onViewArtistTapped?(artistId)
        }
        menuItems.append(viewArtistAction)
        
        // View Full Song action (only show if streaming links exist)
        if let snippet = currentSnippet,
           let streamingLinks = snippet.streamingLinks,
           !streamingLinks.isEmpty {
            let viewFullSongAction = UIAction(
                title: "View Full Song",
                image: UIImage(systemName: "music.note")
            ) { [weak self] _ in
                guard let self = self,
                      let snippet = self.currentSnippet else { return }
                self.onViewFullSongTapped?(snippet)
            }
            menuItems.append(viewFullSongAction)
        }
        
        return UIMenu(title: "", children: menuItems)
    }
    
    func updateLibraryButtonState(isInLibrary: Bool) {
        if isInLibrary {
            // Configure button for "Remove" state if needed
        } else {
            addToLibraryButton.setTitle("Save", for: .normal)
            addToLibraryButton.backgroundColor = .clear
        }
    }
    
    private func updateLibraryButtonVisibility() {
        addToLibraryButton.isHidden = isInLibrary
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        addToLibraryButton.isHidden = false
        addToLibraryButton.removeTarget(nil, action: nil, for: .allEvents)
    }
    
    func setNumber(_ number: Int) {
        numberLabel.text = "\(number + 1)"
    }
    
    // MARK: - Actions
    
    @objc private func addToLibraryButtonTapped() {
        isInLibrary = true
        updateLibraryButtonVisibility()
        onAddToLibraryTapped?()
    }
    
    @objc private func cellTapped() {
        onCellTapped?()
    }
    
    // MARK: - Additional Methods
    
    func showAddToLibraryButton() {
        isInLibrary = false
        updateLibraryButtonVisibility()
    }
}

// MARK: - NSLayoutConstraint Extension

extension NSLayoutConstraint {
    func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint {
        self.priority = priority
        return self
    }
}

private enum ViewType {
    case main
    case library
}

extension MainViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        if searchBar == self.searchBar {
            if let text = searchBar.text, !text.isEmpty {
                performSearch(with: text)
            }
            searchBar.resignFirstResponder()
        } else if searchBar == librarySearchBar {
            searchBar.resignFirstResponder()
        }
    }
    
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        if searchBar == librarySearchBar {
            filterLibraryItems(with: searchText)
        }
    }
    
    // Add this method to handle the cancel button
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        if searchBar == self.searchBar {
            hideSearchBar()
        } else if searchBar == librarySearchBar {
            hideLibrarySearchBar()
        }
    }
}

class LoadingIndicatorCell: UICollectionViewCell {
    private let activityIndicator: UIActivityIndicatorView = {
        let indicator = UIActivityIndicatorView(style: .medium)
        indicator.color = .white
        indicator.translatesAutoresizingMaskIntoConstraints = false
        indicator.hidesWhenStopped = true
        return indicator
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        contentView.backgroundColor = .clear
        contentView.addSubview(activityIndicator)
        
        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor)
        ])
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        activityIndicator.stopAnimating()
    }
    
    func startAnimating() {
        activityIndicator.startAnimating()
    }
}


import Foundation

class MusicSnippetManager {
    var musicSnippets: [MusicSnippet] = []
    let baseURL = APIClient.shared.baseURL

    public let maxSearchResults = 100
    public let resultsPerPage = 10
    
    private func createMusicSnippet(from song: Song) -> MusicSnippet {
        return MusicSnippet(
            type: .music,
            gsutilUri: song.gsutilUri,
            url: song.authenticatedUrl,
            artist: song.artist,
            artistId: song.artistId,
            title: song.title,
            amplitudes: song.amplitudes,
            spectralCentroids: song.spectralCentroids,
            onsetTimes: song.onsetTimes,
            duration: song.duration,
            streamingLinks: song.streamingLinks,
            odesliUrl: song.odesliUrl,
            odesliTitle: song.odesliTitle,
            odesliArtist: song.odesliArtist
        )
    }
    
    func fetchRandomMusicSnippets(completion: @escaping (Result<[MusicSnippet], Error>) -> Void) {
        let endpoint = "/events/"
        let parameters: [String: Any] = ["event_type": "random_generation"]
        
        guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else {
            completion(.failure(APIError.invalidResponse))
            return
        }
        
        APIClient.shared.perform(endpoint, method: "POST", body: jsonData) { (result: Result<UserMusicEvent, APIError>) in
            switch result {
            case .success(let event):
                let snippets = event.songs.map { self.createMusicSnippet(from: $0) }
                completion(.success(snippets)) // No special buttons added
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
        
    func fetchRecommendations(for text: String, page: Int = 1, completion: @escaping (Result<[MusicSnippet], Error>) -> Void) {
        print("DEBUG: Fetching recommendations for page \(page)")
        let endpoint = "/events/"
        let parameters: [String: Any] = [
            "event_type": "recommendation",
            "input_text": text,
            "page": page,
            "per_page": resultsPerPage
        ]
        
        guard let jsonData = try? JSONSerialization.data(withJSONObject: parameters) else {
            completion(.failure(APIError.invalidResponse))
            return
        }
        
        APIClient.shared.perform(endpoint, method: "POST", body: jsonData) { (result: Result<UserMusicEvent, APIError>) in
            switch result {
            case .success(let event):
                let snippets = event.songs.map { self.createMusicSnippet(from: $0) }
                let snippetsWithButtons = page == 1 ? self.addSpecialButtons(to: snippets) : snippets
                print("DEBUG: Fetched \(snippets.count) snippets for page \(page)")
                completion(.success(snippetsWithButtons))
            case .failure(let error):
                print("DEBUG: Error fetching page \(page): \(error)")
                completion(.failure(error))
            }
        }
    }
    
    func downloadAudioData(for snippet: MusicSnippet, completion: @escaping (Result<Data, Error>) -> Void) {
        guard let urlString = snippet.url, let url = URL(string: urlString) else {
            completion(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: nil)))
            return
        }

        URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let data = data else {
                completion(.failure(NSError(domain: "No data", code: 0, userInfo: nil)))
                return
            }
            
            completion(.success(data))
        }.resume()
    }
    
    public func addSpecialButtons(to snippets: [MusicSnippet]) -> [MusicSnippet] {
        var allSnippets = [MusicSnippet(type: .search)]
        allSnippets.append(contentsOf: snippets)
        allSnippets.append(MusicSnippet(type: .reload))
        return allSnippets
    }
}

// SnippetBuffer.swift

import Foundation

class SnippetBuffer {
    
    // MARK: - Properties
    
    private var snippets: [MusicSnippet] = []
    private var isFetching = false
    private let snippetManager = MusicSnippetManager()
    private let bufferThreshold = 5  // Number of items left before fetching more
    private let maxBufferSize = 100  // Optional maximum buffer size to prevent unlimited growth
    
    // Callback to notify when new snippets are available
    var onNewSnippetsAvailable: (() -> Void)?
    // var onLoadingStateChanged: ((Bool) -> Void)?
    var onError: ((Error) -> Void)?


    // MARK: - Initialization
    
    init() {
        // Initial fetch of snippets
        fetchMoreSnippets()
    }
    
    // MARK: - Public Methods
    
    func getSnippets() -> [MusicSnippet] {
        return snippets
    }
    
    func getSnippet(at index: Int) -> MusicSnippet? {
        guard index >= 0 && index < snippets.count else { return nil }
        return snippets[index]
    }
    
    func totalSnippetsCount() -> Int {
        return snippets.count
    }
    
    func shouldFetchMoreSnippets(currentIndex: Int) -> Bool {
        return !isFetching && (totalSnippetsCount() - currentIndex) <= bufferThreshold
    }
    
    func fetchMoreSnippets() {
        guard !isFetching else { return }
        isFetching = true
        // onLoadingStateChanged?(true)

        snippetManager.fetchRandomMusicSnippets { [weak self] result in
            guard let self = self else { return }
            self.isFetching = false
            // onLoadingStateChanged?(false)

            switch result {
            case .success(let newSnippets):
                // Remove duplicates
                let uniqueSnippets = newSnippets.filter { newSnippet in
                    !self.snippets.contains(where: { $0.id == newSnippet.id })
                }
                
                self.snippets.append(contentsOf: uniqueSnippets)
                
                // Optionally limit buffer size
                if self.snippets.count > self.maxBufferSize {
                    self.snippets.removeFirst(self.snippets.count - self.maxBufferSize)
                }
                
                DispatchQueue.main.async {
                    self.onNewSnippetsAvailable?()
                }
            case .failure(let error):
                print("Error fetching more snippets: \(error)")
                self.onError?(error)

                // Handle error as needed
            }
        }
    }
}

//
//  UserMusicEvent.swift
//  iOSdeejMessage
//
//  Created by DJ Bray on 6/25/24.
//

import Foundation

struct UserMusicEvent: Codable {
    let id: Int
    let eventType: String
    let songs: [Song]
    let inputText: String
    let timestamp: Date
    let recommendationType: String?
    let interactionDuration: Int?
    
    enum CodingKeys: String, CodingKey {
        case id, songs, timestamp
        case eventType = "event_type"
        case inputText = "input_text"
        case recommendationType = "recommendation_type"
        case interactionDuration = "interaction_duration"
    }
}

Server-side code:

Here is the UserMusicEventView:

class UserMusicEventView(generics.CreateAPIView):
    permission_classes = [HasValidAPIKey]
    serializer_class = UserMusicEventSerializer
    
    def post(self, request, *args, **kwargs):
        try:
            serializer = self.get_serializer(data=request.data)
            serializer.is_valid(raise_exception=True)
            event = serializer.save()

            page = int(request.data.get('page', 1))
            per_page = int(request.data.get('per_page', 10))

            if event.event_type in ['recommendation', 'random_generation']:
                if event.event_type == 'recommendation' and event.input_text:
                    results = generate_recommendations(
                        event.input_text, 
                        page=page,
                        per_page=per_page
                    )
                    recommended_songs = results['songs']
                    pagination = results['pagination']
                    event.recommendation_type = 'text_message'
                else:
                    recommended_songs = generate_random()
                    pagination = {
                        'total': len(recommended_songs),
                        'page': 1,
                        'per_page': len(recommended_songs),
                        'total_pages': 1
                    }
                    event.recommendation_type = 'random'

                event.song_uris = ','.join([song['gsutil_uri'] for song in recommended_songs])
                event.save()

            response_data = UserMusicEventSerializer(event).data
            response_data.update({
                'songs': recommended_songs,
                'pagination': pagination
            })
            
            return Response(response_data, status=status.HTTP_201_CREATED)
        except Exception as e:
            logging.error(f"Error in UserMusicEventView: {str(e)}", exc_info=True)
            return Response(
                {"error": "An unexpected error occurred", "details": str(e)}, 
                status=status.HTTP_500_INTERNAL_SERVER_ERROR
            )

Here are the recommendation functions:

import os
import sys
import subprocess
import requests
import pymongo
from pymongo import MongoClient
import random
import ssl
import logging
import torch
import torch.nn as nn
from google.cloud import storage
from google.oauth2 import service_account
from django.conf import settings
import datetime
from deejapp.audio_analysis import analyze_audio


# Create a global connection pool
client = MongoClient(settings.MONGODB_URI)
db = client[settings.MONGODB_NAME]
songs_collection = db["music"]

# Configure logging
logging.basicConfig(filename='recommender.log', level=logging.DEBUG)

def generate_text_embedding(text):
    try:
        logging.info(f"Generating text embedding for: {text}")

        # Wrap the input text into an array with itself
        text_array = [text, text]

        response = requests.post('http://localhost:5001/generate_embedding', json={'text': text_array})
        response.raise_for_status()  # Raise an exception for 4xx or 5xx status codes

        if response.status_code == 200:
            embedding = response.json()['embedding']
            embedding = embedding[:len(embedding)//2]
            logging.info(f"Generated text embedding: {embedding}")
            return embedding
        else:
            logging.warning(f"Unexpected response status code: {response.status_code}")
            return None

    except requests.exceptions.RequestException as e:
        logging.error(f"Error occurred while generating text embedding: {e}")
        return None

    except (KeyError, TypeError, ValueError) as e:
        logging.error(f"Error occurred while parsing the response: {e}")
        return None 

def generate_recommendations(text, page=1, per_page=10):
    logging.info(f"Generating recommendations for text: {text}, page: {page}")
    client = MongoClient(settings.MONGODB_URI)
    db = client[settings.MONGODB_NAME]
    songs_collection = db["music"]

    try:
        text_embedding = generate_text_embedding(text)
        text_embedding_flattened = [num for inner_list in text_embedding for num in inner_list]

        if text_embedding is not None:
            skip = (page - 1) * per_page
            total_count = 100  # Maximum results from vectorSearch

            result = songs_collection.aggregate([
                {
                    "$vectorSearch": {
                        "index": "music_embeddings",
                        "queryVector": text_embedding_flattened,
                        "path": "embedding",
                        "numCandidates": 100,
                        "limit": 100
                    }
                },
                {"$skip": skip},
                {"$limit": per_page},
                {
                    "$project": {
                        "audio_url": 1,
                        "embedding": 1,
                        "_id": 0,
                        "artist": 1,
                        "title": 1,
                        "artist_id": 1,
                        "streaming_links": 1,
                        "odesli_url": 1,
                        "odesli_title": 1,
                        "odesli_artist": 1,
                        "duration": 1,
                        "amplitudes": 1,
                        "spectral_centroids": 1,
                        "onset_times": 1
                    }
                }
            ])

            recommended_songs = []
            for song in result:
                gsutil_uri = song["audio_url"]
                authenticated_url = get_authenticated_url(gsutil_uri)
                
                recommended_songs.append({
                    "gsutil_uri": gsutil_uri,
                    "authenticated_url": authenticated_url,
                    "artist": song.get("artist", ""),
                    "artist_id": str(song.get("artist_id", "")),
                    "title": song.get("title", ""),
                    "duration": song.get("duration"),
                    "amplitudes": song.get("amplitudes"),
                    "spectral_centroids": song.get("spectral_centroids"),
                    "onset_times": song.get("onset_times"),
                    "streaming_links": song.get("streaming_links"),
                    "odesli_url": song.get("odesli_url"),
                    "odesli_title": song.get("odesli_title"),
                    "odesli_artist": song.get("odesli_artist")
                })

            return {
                'songs': recommended_songs,
                'pagination': {
                    'total': total_count,
                    'page': page,
                    'per_page': per_page,
                    'total_pages': -(-total_count // per_page)
                }
            }
        else:
            return {
                'songs': [],
                'pagination': {
                    'total': 0,
                    'page': page,
                    'per_page': per_page,
                    'total_pages': 0
                }
            }
    finally:
        client.close()

# Update generate_random function similarly
def generate_random():
    logging.info("Generating random recommendations")
    client = MongoClient(settings.MONGODB_URI)
    db = client[settings.MONGODB_NAME]
    songs_collection = db["music"]

    try:
        total_songs = songs_collection.count_documents({})
        if total_songs == 0:
            logging.warning("The 'music' collection is empty.")
            return []

        num_songs = min(10, total_songs)
        random_indices = random.sample(range(total_songs), num_songs)

        random_songs = []
        for index in random_indices:
            song = songs_collection.find().limit(-1).skip(index).next()
            gsutil_uri = song['audio_url']
            authenticated_url = get_authenticated_url(gsutil_uri)
            
            random_songs.append({
                "gsutil_uri": gsutil_uri,
                "authenticated_url": authenticated_url,
                "artist": song.get("artist", ""),
                "artist_id": str(song.get("artist_id", "")),
                "title": song.get("title", ""),
                "duration": song.get("duration"),
                "amplitudes": song.get("amplitudes"),
                "spectral_centroids": song.get("spectral_centroids"),
                "onset_times": song.get("onset_times"),
                "streaming_links": song.get("streaming_links"),
                "odesli_url": song.get("odesli_url"),
                "odesli_title": song.get("odesli_title"),
                "odesli_artist": song.get("odesli_artist")
            })

        logging.info(f"Generated {len(random_songs)} random recommendations")
        return random_songs
    finally:
        client.close()
# Helper function to get authenticated URL (you may want to move this to the Song model)
def get_authenticated_url(gsutil_uri):
    credentials = service_account.Credentials.from_service_account_file(settings.GCS_CREDENTIALS_FILE)
    storage_client = storage.Client(credentials=credentials)
    
    bucket_name = gsutil_uri.split('/')[2]
    blob_name = '/'.join(gsutil_uri.split('/')[3:])
    
    bucket = storage_client.bucket(bucket_name)
    blob = bucket.blob(blob_name)
    
    expiration = datetime.datetime.utcnow() + datetime.timedelta(hours=1)
    return blob.generate_signed_url(expiration)

def update_audio_features(gsutil_uri):
    logging.info(f"Updating audio features for {gsutil_uri}")
    client = MongoClient(settings.MONGODB_URI)
    db = client[settings.MONGODB_NAME]
    songs_collection = db["music"]
    
    song = songs_collection.find_one({"audio_url": gsutil_uri})
    
    if song and "amplitudes" in song and "spectral_centroids" in song and "duration" in song and "onset_times" in song:
        logging.info(f"Found existing audio features for {gsutil_uri}")
        return {
            "duration": song["duration"],
            "amplitudes": song["amplitudes"],
            "spectral_centroids": song["spectral_centroids"],
            "onset_times": song["onset_times"]
        }
    
    logging.info(f"Computing new audio features for {gsutil_uri}")
    authenticated_url = get_authenticated_url(gsutil_uri)
    try:
        audio_features = analyze_audio(authenticated_url)
        
        songs_collection.update_one(
            {"audio_url": gsutil_uri},
            {"$set": {
                "duration": audio_features["duration"],
                "amplitudes": audio_features["amplitudes"],
                "spectral_centroids": audio_features["spectral_centroids"],
                "onset_times": audio_features["onset_times"]
            }}
        )
        
        logging.info(f"Successfully computed and stored audio features for {gsutil_uri}")
        return audio_features
    except Exception as e:
        logging.error(f"Error computing audio features for {gsutil_uri}: {str(e)}")
        return None
    finally:
        client.close()

def generate_audio_embedding_from_file(file_path):
    url = 'http://localhost:5001/generate_audio_embedding'
    files = {'audio': open(file_path, 'rb')}
    response = requests.post(url, files=files)
    response.raise_for_status()

    if response.status_code == 200:
        embedding = response.json()['embedding']
        return embedding
    else:
        logging.warning(f"Unexpected response status code: {response.status_code}")
        return None

# # New function to fetch song data from MongoDB
# def fetch_song_data_from_mongodb(gsutil_uri):
#     client = MongoClient(settings.MONGODB_URI)
#     db = client[settings.MONGODB_NAME]
#     songs_collection = db["music"]

#     song_data = songs_collection.find_one({"audio_url": gsutil_uri})
    
#     client.close()

#     if song_data:
#         return {
#             "artist": song_data.get("artist", ""),
#             "embedding": song_data.get("embedding", [])
#             # Add any other fields you want to retrieve
#         }
#     else:
#         return None