UserInteraction.java

package org.example.apiInteraction;

import org.example.apiInteraction.RunArgs.FileType;
import org.example.apiInteraction.RunArgs.UserInteractionType;
import org.example.apiInteraction.apiHandling.ApiRecord;
import org.example.apiInteraction.cliInteraction.InteractiveUtils;
import org.example.apiInteraction.cliInteraction.UserResponse;
import org.example.apiInteraction.cliInteraction.WriteMode;
import org.example.apiInteraction.resultFormatting.CSVResultFormatter;
import org.example.apiInteraction.resultFormatting.CustomFormatter;
import org.example.apiInteraction.resultFormatting.JSONResultFormatter;
import org.jetbrains.annotations.NotNull;
import tools.jackson.databind.ObjectMapper;

import java.io.File;
import java.io.IOException;
import java.util.*;

public class UserInteraction {
    private final ApiRecord[] apis;
    private final InteractiveUtils interactiveUtils;
    private final String BASE_FILE_PATH = "src/main/resources/";

    public UserInteraction() {
        ObjectMapper objectMapper = new ObjectMapper();
        String FILE_PATH = BASE_FILE_PATH + "apis.json";
        this.apis = objectMapper.readValue(new File(FILE_PATH), ApiRecord[].class);

        interactiveUtils = new InteractiveUtils(this.apis);
    }

    // Package-private constructor for unit tests. Bypasses file I/O.
    UserInteraction(ApiRecord[] apis, InteractiveUtils interactiveUtils) {
        this.apis = apis;
        this.interactiveUtils = interactiveUtils;
    }

    public void interact() {
        final String apiSelectionMsg = "\nSelect by ID which APIs to call " +
                "(\"all\" for all of them; can do several, ex.: 1,2,12): ";
        final String apiCliOutputMsg = "\nSelect by ID which APIs to output " +
                "(\"all\" for all of them; can do several, ex.: 1,2,12): ";

        int[] apiIDs = interactiveUtils.askUserForApis(apiSelectionMsg);
        int[] outputApiIDs = interactiveUtils.askUserForApis(apiCliOutputMsg, apiIDs);
        WriteMode writeMode = interactiveUtils.askUserWhetherToAppend();
        FileType fileType = interactiveUtils.askUserForFileType();
        int n = interactiveUtils.askUserForMaxConcurrent();
        int t = interactiveUtils.askUserForInterval();

        UserResponse userResponse = new UserResponse(apiIDs, writeMode, outputApiIDs);
        startPolling(userResponse, fileType, n, t, UserInteractionType.INTERACTIVE);
    }

    public void interact(@NotNull FileType fileType, int maxConcurrent, int intervalSeconds) {
        final String apiSelectionMsg = "\nSelect by ID which APIs to call " +
                "(\"all\" for all of them; can do several, ex.: 12): ";

        int[] apiIDs = interactiveUtils.askUserForApis(apiSelectionMsg);
        UserResponse userResponse = new UserResponse(apiIDs, WriteMode.OVERWRITE, new int[]{});
        startPolling(userResponse, fileType, maxConcurrent, intervalSeconds, UserInteractionType.AUTOMATIC);
    }


    private void startPolling(UserResponse userResponse, FileType fileType, int maxConcurrent,
                              int intervalSeconds, UserInteractionType type) {
        final String defaultFileNameFormat = "output.%s";

        Set<ApiRecord> selectedApis = resolveApis(userResponse.apiIdsToCall());
        Map<Integer, String> additionalPaths = resolveAdditionalPaths(selectedApis, type);

        String fileName = String.format(defaultFileNameFormat, fileType.toString().toLowerCase());
        FileHandler fileHandler = new FileHandler(BASE_FILE_PATH + fileName);
        CustomFormatter formatter = buildFormatter(fileType);

        String initialContent = "";
        if (userResponse.writeMode().equals(WriteMode.APPEND)) {
            try {
                initialContent = fileHandler.read();
            } catch (IOException e) {
                System.out.println("ERROR: couldn't read existing file " + e.getMessage());
                return;
            }
        }

        PollingManager pollingManager = new PollingManager(
                selectedApis, additionalPaths, maxConcurrent,
                intervalSeconds, formatter, fileHandler, initialContent
        );

        if (type.equals(UserInteractionType.INTERACTIVE)) {
            runInteractive(pollingManager);
        } else {
            runAutomatic(pollingManager);
        }
    }

    private void runInteractive(PollingManager pollingManager) {
        pollingManager.start();
        System.out.println("\nPolling is running. Type 'stop' and press Enter to stop.\n");

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            if (scanner.nextLine().trim().equalsIgnoreCase("stop")) break;
            System.out.println("(Type 'stop' to halt polling)");
        }

        pollingManager.stop();
        System.out.println("Polling stopped. Results saved to file.");
    }

    private void runAutomatic(PollingManager pollingManager) {
        pollingManager.start();
        System.out.println("Automatic polling started. Press Ctrl+C to stop.");

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("\n[Shutdown] Stopping polling…");
            pollingManager.stop();
        }, "shutdown-hook"));

        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @NotNull Map<Integer, String> resolveAdditionalPaths(@NotNull Set<ApiRecord> selectedApis, UserInteractionType type) {
        Map<Integer, String> result = new HashMap<>();

        for (ApiRecord api : selectedApis) {
            if (!api.additionalPathNeeded()) continue;

            String[] options = api.additionalPaths();
            if (options == null || options.length == 0) {
                throw new IllegalArgumentException(
                        "ERROR: API '" + api.name() + "' needs an additional path "
                                + "but none are configured in apis.json.");
            }

            String chosen;
            if (type.equals(UserInteractionType.INTERACTIVE)) {
                chosen = interactiveUtils.askUserForAdditionalPath(api);
            } else {
                chosen = options[new Random().nextInt(options.length)];
            }

            result.put(api.id(), chosen);
        }

        return result;
    }

    @NotNull Set<ApiRecord> resolveApis(int @NotNull [] apiIds) {
        Set<ApiRecord> result = new HashSet<>();
        for (int id : apiIds) {
            ApiRecord found = null;
            for (ApiRecord api : apis) {
                if (api.id() == id) { found = api; break; }
            }
            if (found == null) {
                throw new IllegalArgumentException("ERROR: API not found for id=" + id);
            }
            result.add(found);
        }
        return result;
    }

    @NotNull CustomFormatter buildFormatter(@NotNull FileType fileType) {
        return switch (fileType) {
            case CSV -> new CSVResultFormatter("");
            case JSON -> new JSONResultFormatter("");
        };
    }
}