function [varargout] = runWithProfiling(funcHandle, nOutputs, saveDirectory)
%RUNWITHPROFILING Profile runtime and memory usage, and log stats to JSON file.
%
% Usage:
%   [a, b, c, stats] = runWithProfiling(@() sim.run(), 3, './Results/');
%
% Inputs:
%   funcHandle     - Function handle to run (e.g., @() sim.run())
%   nOutputs       - Number of outputs expected from funcHandle
%   saveDirectory  - Directory path to save stats JSON file
%
% Outputs:
%   varargout      - Outputs of funcHandle (1 to nOutputs)
%                  - Final output is a struct 'stats' with fields:
%                       .runtime              - Wall-clock time (s)
%                       .workspaceMemoryMB    - Total memory used (MB)
%                       .timestamp            - ISO timestamp of run
%                       .hostname             - System hostname

    % Time the function
    tStart = tic;
    [varargout{1:nOutputs}] = funcHandle();
    runtime = toc(tStart);

    % Estimate workspace memory usage
    try
        vars = whos;
        totalBytes = sum([vars.bytes]);
        workspaceMemoryMB = totalBytes / 1e6;
    catch
        workspaceMemoryMB = NaN;
    end

    % Create stats struct
    stats.runtime = runtime;
    stats.workspaceMemoryMB = workspaceMemoryMB;
    stats.timestamp = datestr(now, 'yyyy-mm-dd_HH-MM-SS');
    [~, hostname] = system('hostname');
    stats.hostname = strtrim(hostname);

    % Save as JSON
    try
        if ~exist(saveDirectory, 'dir')
            mkdir(saveDirectory);
        end
        filename = fullfile(saveDirectory, ['profiling_stats_', stats.timestamp, '.json']);
        jsonStr = jsonencode(stats, 'PrettyPrint', true);
        fid = fopen(filename, 'w');
        fwrite(fid, jsonStr, 'char');
        fclose(fid);
    catch ME
        warning('Failed to save profiling stats: %s', ME.message);
    end

    % Return stats
    varargout{nOutputs + 1} = stats;
end
