001package com.studentgui.apphelpers;
002
003import java.io.IOException;
004import java.nio.file.Files;
005import java.nio.file.Path;
006import java.time.Instant;
007import java.time.ZoneId;
008import java.time.format.DateTimeFormatter;
009import java.util.HashMap;
010import java.util.Map;
011
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import com.fasterxml.jackson.databind.ObjectMapper;
016
017/**
018 * Helper to write per-session JSON exports for app pages.
019 */
020public final class SessionJsonWriter {
021    private static final Logger LOG = LoggerFactory.getLogger(SessionJsonWriter.class);
022    private static final ObjectMapper MAPPER = new ObjectMapper();
023
024    private SessionJsonWriter() {}
025
026    /**
027     * Write a per-session JSON file into the student's StudentDataFiles folder.
028     * The filename will include a unix timestamp to ensure uniqueness per session.
029     *
030     * @param student display name of the student
031     * @param pageName short page identifier (e.g. "Abacus")
032     * @param payload arbitrary payload object to serialize (Map or POJO)
033     * @return the path to the written file, or null on failure
034     */
035    public static Path writeSessionJson(final String student, final String pageName, final Object payload) {
036        return writeSessionJson(student, pageName, payload, null);
037    }
038
039    /**
040     * Write a per-session JSON file and optionally include an explicit sessionId.
041     * If the explicit sessionId is null, this method will look for a "sessionId"
042     * entry inside the payload Map and use that if present. The envelope written
043     * to disk will include the sessionId when available.
044     *
045    * Filename format: {@code PageName-<epoch>-<readable>[-session-<sessionId>].json}
046     *
047     * @param student display name of the student
048     * @param pageName short page identifier (e.g. "Abacus")
049     * @param payload arbitrary payload object to serialize (Map or POJO)
050     * @param explicitSessionId optional session id to use in the envelope and filename
051     * @return the path to the written file, or null on failure
052     */
053    public static Path writeSessionJson(final String student, final String pageName, final Object payload, final String explicitSessionId) {
054        if (student == null || student.trim().isEmpty() || pageName == null) {
055            return null;
056        }
057        try {
058            Path outDir = Helpers.studentCollectedDataDir(student);
059            Files.createDirectories(outDir);
060            long ts = Instant.now().toEpochMilli();
061            // format for readability too
062            String readable = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmssSSS").withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(ts));
063
064            // Determine sessionId preference: explicit param first, then payload if it implements SessionPayload
065            String sid = explicitSessionId;
066            if (sid == null && payload instanceof com.studentgui.apphelpers.dto.SessionPayload) {
067                int s = ((com.studentgui.apphelpers.dto.SessionPayload) payload).getSessionId();
068                if (s != 0) {
069                    sid = Integer.toString(s);
070                }
071            }
072
073            String filename = String.format("%s-%d-%s%s.json", pageName, ts, readable, (sid != null ? "-session-" + sid : ""));
074            Path outFile = outDir.resolve(filename);
075
076            Map<String, Object> envelope = new HashMap<>();
077            envelope.put("student", student);
078            envelope.put("timestamp", ts);
079            envelope.put("timestampIso", readable);
080            envelope.put("page", pageName);
081            if (sid != null) {
082                envelope.put("sessionId", sid);
083            }
084            envelope.put("payload", payload);
085
086            byte[] bytes = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsBytes(envelope);
087            Files.write(outFile, bytes);
088            LOG.info("Wrote session JSON for {} page {} to {}", student, pageName, outFile);
089            return outFile;
090        } catch (IOException ex) {
091            LOG.warn("Unable to write session JSON for {} page {}: {}", student, pageName, ex.toString());
092            return null;
093        }
094    }
095
096    /**
097     * Convenience overload that accepts an int sessionId to avoid callers
098     * converting to String. Delegates to the string-based overload.
099     *
100     * @param student display name of the student
101     * @param pageName short page identifier
102     * @param payload arbitrary payload object
103     * @param explicitSessionId numeric session id
104     * @return written file path or null
105     */
106    public static Path writeSessionJson(final String student, final String pageName, final Object payload, final int explicitSessionId) {
107        return writeSessionJson(student, pageName, payload, Integer.toString(explicitSessionId));
108    }
109
110    /**
111     * Backwards-compatible convenience method for callers that still have
112     * (codes,scores) arrays. It wraps them in a small Map and delegates to
113     * the main payload-based writer.
114     *
115     * @param student the student's display name
116     * @param pageName short page identifier (e.g. "Abacus")
117     * @param codes array of part codes to include in the payload
118     * @param scores array of scores corresponding to the codes
119     * @return path to the written JSON file, or null on failure
120     */
121    public static Path writeSessionJson(final String student, final String pageName, final String[] codes, final int[] scores) {
122        com.studentgui.apphelpers.dto.AssessmentPayload payload = new com.studentgui.apphelpers.dto.AssessmentPayload(0, codes, scores);
123        return writeSessionJson(student, pageName, payload);
124    }
125}