001package gudusoft.gsqlparser.ir.semantic.diff;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.List;
006import java.util.Map;
007
008/**
009 * Hand-rolled deterministic JSON exporter for {@link DivergenceReport}, in
010 * the same style as
011 * {@code gudusoft.gsqlparser.ir.semantic.export.SemanticIRJsonExporter}.
012 *
013 * <p>Edges are sorted via {@link CanonicalLineageEdge#ORDER}; aggregate
014 * map keys are sorted lexicographically; divergences are sorted via
015 * {@link Divergence#ORDER}.
016 */
017public final class DivergenceJsonExporter {
018
019    private static final String INDENT = "  ";
020
021    private DivergenceJsonExporter() {}
022
023    public static String toJson(DivergenceReport report) {
024        if (report == null) {
025            throw new IllegalArgumentException("report must not be null");
026        }
027        StringBuilder sb = new StringBuilder();
028        sb.append("{\n");
029        writeKey(sb, 1, "sql");
030        writeString(sb, report.getSqlName());
031        sb.append(",\n");
032        writeKey(sb, 1, "irProjection");
033        writeProjection(sb, 1, report.getIrModel());
034        sb.append(",\n");
035        writeKey(sb, 1, "dlineageProjection");
036        writeProjection(sb, 1, report.getDlineageModel());
037        sb.append(",\n");
038        writeKey(sb, 1, "divergences");
039        writeDivergences(sb, 1, report.getDivergences());
040        sb.append(",\n");
041        writeKey(sb, 1, "summary");
042        writeSummary(sb, 1, report.getSummary());
043        sb.append("\n}\n");
044        return sb.toString();
045    }
046
047    private static void writeProjection(StringBuilder sb, int depth, CanonicalLineageModel m) {
048        sb.append("{\n");
049        writeKey(sb, depth + 1, "edges");
050        writeEdges(sb, depth + 1, m);
051        sb.append(",\n");
052        writeKey(sb, depth + 1, "outputNames");
053        writeStringArray(sb, sortedCopy(m.getOutputNames()));
054        sb.append(",\n");
055        writeKey(sb, depth + 1, "aggregates");
056        writeAggregates(sb, depth + 1, m.getAggregateByOutput());
057        sb.append("\n");
058        indent(sb, depth);
059        sb.append("}");
060    }
061
062    private static void writeEdges(StringBuilder sb, int depth, CanonicalLineageModel m) {
063        List<CanonicalLineageEdge> sorted = new ArrayList<>(m.getEdges());
064        Collections.sort(sorted, CanonicalLineageEdge.ORDER);
065        if (sorted.isEmpty()) {
066            sb.append("[]");
067            return;
068        }
069        sb.append("[\n");
070        for (int i = 0; i < sorted.size(); i++) {
071            indent(sb, depth + 1);
072            writeEdgeInline(sb, sorted.get(i));
073            if (i < sorted.size() - 1) sb.append(",");
074            sb.append("\n");
075        }
076        indent(sb, depth);
077        sb.append("]");
078    }
079
080    private static void writeEdgeInline(StringBuilder sb, CanonicalLineageEdge e) {
081        sb.append("{");
082        writeKeyInline(sb, "role");
083        writeString(sb, e.getRole().name());
084        sb.append(", ");
085        writeKeyInline(sb, "outputName");
086        if (e.getOutputName() == null) {
087            sb.append("null");
088        } else {
089            writeString(sb, e.getOutputName());
090        }
091        sb.append(", ");
092        writeKeyInline(sb, "baseTable");
093        writeString(sb, e.getBaseTable());
094        sb.append(", ");
095        writeKeyInline(sb, "baseColumn");
096        writeString(sb, e.getBaseColumn());
097        sb.append("}");
098    }
099
100    private static void writeAggregates(StringBuilder sb, int depth, Map<String, Boolean> aggregates) {
101        if (aggregates.isEmpty()) {
102            sb.append("{}");
103            return;
104        }
105        List<String> keys = sortedCopy(aggregates.keySet());
106        sb.append("{\n");
107        for (int i = 0; i < keys.size(); i++) {
108            String k = keys.get(i);
109            indent(sb, depth + 1);
110            writeKeyInline(sb, k);
111            sb.append(aggregates.get(k) ? "true" : "false");
112            if (i < keys.size() - 1) sb.append(",");
113            sb.append("\n");
114        }
115        indent(sb, depth);
116        sb.append("}");
117    }
118
119    private static void writeStringArray(StringBuilder sb, List<String> values) {
120        if (values.isEmpty()) {
121            sb.append("[]");
122            return;
123        }
124        sb.append("[");
125        for (int i = 0; i < values.size(); i++) {
126            if (i > 0) sb.append(", ");
127            writeString(sb, values.get(i));
128        }
129        sb.append("]");
130    }
131
132    private static void writeDivergences(StringBuilder sb, int depth, List<Divergence> divs) {
133        if (divs.isEmpty()) {
134            sb.append("[]");
135            return;
136        }
137        sb.append("[\n");
138        for (int i = 0; i < divs.size(); i++) {
139            indent(sb, depth + 1);
140            writeDivergenceInline(sb, divs.get(i));
141            if (i < divs.size() - 1) sb.append(",");
142            sb.append("\n");
143        }
144        indent(sb, depth);
145        sb.append("]");
146    }
147
148    private static void writeDivergenceInline(StringBuilder sb, Divergence d) {
149        sb.append("{");
150        writeKeyInline(sb, "kind");
151        writeString(sb, d.getKind().name());
152        sb.append(", ");
153        writeKeyInline(sb, "outputName");
154        if (d.getOutputName() == null) {
155            sb.append("null");
156        } else {
157            writeString(sb, d.getOutputName());
158        }
159        sb.append(", ");
160        writeKeyInline(sb, "detail");
161        writeString(sb, d.getDetail());
162        sb.append("}");
163    }
164
165    private static void writeSummary(StringBuilder sb, int depth, Map<DivergenceClass, Integer> summary) {
166        sb.append("{\n");
167        // Iterate in DivergenceClass declaration order — provided by the report.
168        int i = 0;
169        int n = summary.size();
170        for (Map.Entry<DivergenceClass, Integer> e : summary.entrySet()) {
171            indent(sb, depth + 1);
172            writeKeyInline(sb, e.getKey().name());
173            sb.append(e.getValue());
174            if (i < n - 1) sb.append(",");
175            sb.append("\n");
176            i++;
177        }
178        indent(sb, depth);
179        sb.append("}");
180    }
181
182    private static List<String> sortedCopy(java.util.Collection<String> values) {
183        List<String> sorted = new ArrayList<>(values);
184        Collections.sort(sorted);
185        return sorted;
186    }
187
188    private static void writeKey(StringBuilder sb, int depth, String key) {
189        indent(sb, depth);
190        sb.append('"').append(escape(key)).append("\": ");
191    }
192
193    private static void writeKeyInline(StringBuilder sb, String key) {
194        sb.append('"').append(escape(key)).append("\": ");
195    }
196
197    private static void writeString(StringBuilder sb, String value) {
198        sb.append('"').append(escape(value)).append('"');
199    }
200
201    private static void indent(StringBuilder sb, int depth) {
202        for (int i = 0; i < depth; i++) sb.append(INDENT);
203    }
204
205    private static String escape(String s) {
206        StringBuilder out = new StringBuilder(s.length() + 2);
207        for (int i = 0; i < s.length(); i++) {
208            char c = s.charAt(i);
209            switch (c) {
210                case '"':  out.append("\\\""); break;
211                case '\\': out.append("\\\\"); break;
212                case '\n': out.append("\\n"); break;
213                case '\r': out.append("\\r"); break;
214                case '\t': out.append("\\t"); break;
215                case '\b': out.append("\\b"); break;
216                case '\f': out.append("\\f"); break;
217                default:
218                    if (c < 0x20) {
219                        out.append(String.format("\\u%04x", (int) c));
220                    } else {
221                        out.append(c);
222                    }
223            }
224        }
225        return out.toString();
226    }
227}