import concurrent.futures

class BackgroundProcessManager:
    _instance: "BackgroundProcessManager | None" = None

    def __init__(self):
        if not hasattr(self, 'executor'):
            # Using ThreadPoolExecutor as it avoids pickling issues with complex Plotly objects
            # and works well for I/O bound tasks (waiting for Kaleido subprocess).
            self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=None)
            self.futures = []

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    def submit_task(self, func, *args, **kwargs):
        future = self.executor.submit(func, *args, **kwargs)
        self.futures.append(future)
        return future

    def wait_all(self):
        # Wait for all futures to complete
        for future in concurrent.futures.as_completed(self.futures):
            # Retrieve result to raise any exceptions that occurred during execution
            try:
                future.result()
            except Exception as e:
                print(f"Background task failed: {e}")
                # Depending on requirements, we might want to re-raise or log
                raise e
        
        # Clear futures after completion
        self.futures.clear()

# Global instance
manager = BackgroundProcessManager.get_instance()
