// JSON parser adapter (Epic #189, Issue #266). Pure: bytes -> ParserResult.
//
// Emits one ParsedUnit { kind: "json-path", jsonPointer } per LEAF value (string / number /
// boolean / null / empty array / empty object). Pointers follow RFC 6901 — keys are encoded
// with `~0` for `~` and `~1` for `/`. Object keys with the same name are NOT deduplicated;
// callers see one unit per traversal step so duplicate keys (which `JSON.parse` itself
// resolves by last-wins) collapse the same way they do in the parsed object.
//
// Character offsets: every leaf is emitted with a whole-document span [0, text.length) because
// `JSON.parse` discards positions and we have not yet wired a per-leaf re-scanner. The
// JSON Pointer (not the character span) is what downstream layers (#195 chunker, #196 indexer)
// use for citation. Precise per-leaf offsets are deferred to a follow-up issue.
import { decodeUtf8, diagnostic, emptyResult, oversizeDiagnostic, shouldStop, } from "./_internal.js";
const PARSER_ID = "json";
const PARSER_VERSION = "1";
const JSON_EXTENSIONS = new Set(["json", "jsonl", "ndjson"]);
function isJson(input) {
    const ext = input.extension.toLowerCase();
    if (JSON_EXTENSIONS.has(ext))
        return true;
    const media = input.mediaType.toLowerCase();
    return media === "application/json" || media === "application/ld+json";
}
function encodePointerSegment(key) {
    // RFC 6901: `~` -> `~0`, `/` -> `~1`. Order matters: replace `~` first.
    return key.replace(/~/g, "~0").replace(/\//g, "~1");
}
function joinPointer(parent, segment) {
    return `${parent}/${segment}`;
}
function pushLeaf(ctx, pointer, start, end) {
    const limit = shouldStop(ctx.startedAt, ctx.options, ctx.units.length);
    if (limit.stop && limit.code !== undefined && limit.message !== undefined) {
        ctx.diagnostics.push(diagnostic(limit.code, limit.message, ctx.input.documentId, "info"));
        ctx.stopped = true;
        return;
    }
    ctx.units.push({
        kind: "json-path",
        documentId: ctx.input.documentId,
        jsonPointer: pointer,
        characterStart: start,
        characterEnd: end,
    });
}
function isLeaf(value) {
    if (value === null)
        return true;
    const t = typeof value;
    if (t === "string" || t === "number" || t === "boolean")
        return true;
    if (Array.isArray(value))
        return value.length === 0;
    if (t === "object")
        return Object.keys(value).length === 0;
    return true;
}
function descendArray(ctx, value, pointer) {
    for (let i = 0; i < value.length; i += 1) {
        if (ctx.stopped)
            return;
        walk(ctx, value[i], joinPointer(pointer, String(i)));
    }
}
function descendObject(ctx, value, pointer) {
    for (const key of Object.keys(value)) {
        if (ctx.stopped)
            return;
        walk(ctx, value[key], joinPointer(pointer, encodePointerSegment(key)));
    }
}
function walk(ctx, value, pointer) {
    if (ctx.stopped)
        return;
    if (isLeaf(value)) {
        // Character offsets are approximated as [0, text.length) for the root leaf and 0/0 for
        // nested leaves — #195's chunker uses the JSON Pointer, not the character span, for
        // citation, and #196 stores the raw text in the document body anyway. Honest "we did not
        // re-scan" is preferable to inventing fake offsets.
        pushLeaf(ctx, pointer, 0, ctx.text.length);
        return;
    }
    if (Array.isArray(value)) {
        descendArray(ctx, value, pointer);
        return;
    }
    descendObject(ctx, value, pointer);
}
function parseJsonValue(text) {
    try {
        return { ok: true, value: JSON.parse(text) };
    }
    catch (error) {
        return { ok: false, error: error instanceof Error ? error.message : "JSON parse error" };
    }
}
export const jsonParser = Object.freeze({
    capability: Object.freeze({
        parserId: PARSER_ID,
        parserVersion: PARSER_VERSION,
        matches: (input) => isJson(input),
    }),
    parse: (input, options) => {
        if (input.bytes.byteLength > options.maxBytes) {
            return emptyResult(jsonParser.capability, input.documentId, options, [
                oversizeDiagnostic(input.documentId, input.bytes.byteLength, options.maxBytes),
            ]);
        }
        const decoded = decodeUtf8(input.bytes);
        const parsed = parseJsonValue(decoded.text);
        if (!parsed.ok) {
            return emptyResult(jsonParser.capability, input.documentId, options, [
                diagnostic("MALFORMED_INPUT", `JSON parse failed: ${parsed.error}`, input.documentId, "error"),
            ]);
        }
        const ctx = {
            text: decoded.text,
            input,
            options,
            startedAt: options.now(),
            units: [],
            diagnostics: [],
            stopped: false,
        };
        walk(ctx, parsed.value, "");
        return emptyResult(jsonParser.capability, input.documentId, options, ctx.diagnostics, ctx.units);
    },
});
