"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseSpecIndexDocument = parseSpecIndexDocument;
exports.parseSpecIndexDocumentText = parseSpecIndexDocumentText;
exports.serializeSpecIndexDocument = serializeSpecIndexDocument;
exports.renderSpecIndexDocument = renderSpecIndexDocument;
exports.renderValidatedSpecIndexDocument = renderValidatedSpecIndexDocument;
exports.buildSpecIndexDocumentSnapshot = buildSpecIndexDocumentSnapshot;
exports.buildSpecIndexDocumentWithEntries = buildSpecIndexDocumentWithEntries;
exports.applyNonDestructiveSpecIndexEntryUpdate = applyNonDestructiveSpecIndexEntryUpdate;
exports.applySpecIndexEntryFieldUpdates = applySpecIndexEntryFieldUpdates;
const schema_1 = require("./schema");
const read_helpers_1 = require("./read-helpers");
const SPEC_INDEX_DOCUMENT_KEYS = new Set([
    "spec_index_schema_version",
    "generated_at",
    "notes",
    "specs"
]);
function parseSpecIndexDocument(input) {
    const record = (0, read_helpers_1.readRecord)(input, "Spec index document");
    const entriesInput = (0, read_helpers_1.readArray)(record.specs, "Spec index document field 'specs'");
    const entries = entriesInput.map((entryInput, index) => {
        try {
            return (0, schema_1.parseSpecIndexEntry)(entryInput);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : "Unknown entry parse failure.";
            throw new TypeError(`Spec index document entry at specs[${index}] is invalid: ${message}`);
        }
    });
    const additionalMetadata = {};
    for (const [key, value] of Object.entries(record)) {
        if (!SPEC_INDEX_DOCUMENT_KEYS.has(key)) {
            additionalMetadata[key] = value;
        }
    }
    return {
        specIndexSchemaVersion: (0, read_helpers_1.readRequiredNumber)(record, "spec_index_schema_version"),
        generatedAt: (0, read_helpers_1.readGeneratedAt)(record.generated_at),
        notes: (0, read_helpers_1.readStringArray)(record.notes, "Spec index document field 'notes'"),
        entries,
        additionalMetadata
    };
}
function parseSpecIndexDocumentText(jsonText) {
    let parsed;
    try {
        parsed = JSON.parse(jsonText);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : "Could not parse JSON.";
        throw new TypeError(`Spec index document must be valid JSON: ${message}`);
    }
    return parseSpecIndexDocument(parsed);
}
function serializeSpecIndexDocument(document) {
    const serialized = {
        spec_index_schema_version: document.specIndexSchemaVersion,
        generated_at: document.generatedAt,
        notes: [...document.notes],
        specs: document.entries.map((entry) => (0, schema_1.serializeSpecIndexEntry)(entry))
    };
    for (const [key, value] of Object.entries(document.additionalMetadata)) {
        if (!(key in serialized)) {
            serialized[key] = value;
        }
    }
    return serialized;
}
function renderSpecIndexDocument(document, spaces = 2) {
    return `${JSON.stringify(serializeSpecIndexDocument(document), null, spaces)}\n`;
}
function renderValidatedSpecIndexDocument(document, spaces = 2) {
    return renderSpecIndexDocument(parseSpecIndexDocument(serializeSpecIndexDocument(document)), spaces);
}
function buildSpecIndexDocumentSnapshot(input) {
    if (typeof input.path !== "string" || input.path.trim() === "") {
        throw new TypeError("Spec index snapshot path must be non-empty.");
    }
    if (!/^[a-f0-9]{64}$/.test(input.revision)) {
        throw new TypeError("Spec index snapshot revision must be a SHA-256 content revision.");
    }
    if (typeof input.sourceText !== "string") {
        throw new TypeError("Spec index snapshot source text must be a string.");
    }
    return {
        path: input.path,
        revision: input.revision,
        sourceText: input.sourceText,
        document: parseSpecIndexDocumentText(input.sourceText)
    };
}
function buildSpecIndexDocumentWithEntries(currentDocument, nextEntries) {
    return {
        specIndexSchemaVersion: currentDocument.specIndexSchemaVersion,
        generatedAt: currentDocument.generatedAt,
        notes: [...currentDocument.notes],
        entries: [...nextEntries],
        additionalMetadata: { ...currentDocument.additionalMetadata }
    };
}
function applyNonDestructiveSpecIndexEntryUpdate(input) {
    const { currentDocument, targetIndex, nextEntry } = input;
    const currentEntry = currentDocument.entries[targetIndex];
    if (currentEntry === undefined) {
        throw new RangeError(`Spec index target index '${targetIndex}' is out of bounds for non-destructive update.`);
    }
    assertIdentityBearingFieldsArePreserved(currentEntry, nextEntry);
    const nextEntries = currentDocument.entries.map((entry, index) => (index === targetIndex ? nextEntry : entry));
    return buildSpecIndexDocumentWithEntries(currentDocument, nextEntries);
}
function applySpecIndexEntryFieldUpdates(input) {
    const { currentDocument, entryId, updates } = input;
    const targetIndex = currentDocument.entries.findIndex((entry) => entry.id === entryId);
    if (targetIndex === -1) {
        throw new TypeError(`Spec index document is missing entry '${entryId}'.`);
    }
    const currentEntry = currentDocument.entries[targetIndex];
    const nextEntry = {
        ...currentEntry,
        source: {
            ...currentEntry.source,
            additionalMetadata: { ...currentEntry.source.additionalMetadata }
        },
        optional: { ...currentEntry.optional },
        additionalMetadata: { ...currentEntry.additionalMetadata }
    };
    const linkedSlicePatch = {
        slice: undefined,
        path: undefined,
        touched: false
    };
    for (const [key, value] of Object.entries(updates)) {
        applyEntryFieldUpdate(nextEntry, key, value, linkedSlicePatch);
    }
    if (linkedSlicePatch.touched) {
        const currentLinkedSlice = nextEntry.optional.linked_slice;
        const nextSlice = linkedSlicePatch.slice ?? currentLinkedSlice?.slice;
        const nextPath = linkedSlicePatch.path ?? currentLinkedSlice?.path;
        if (typeof nextSlice !== "string" || typeof nextPath !== "string") {
            throw new TypeError("Spec index field updates that touch linked_slice require both linked_slice.slice and linked_slice.path values.");
        }
        nextEntry.optional = {
            ...nextEntry.optional,
            linked_slice: {
                slice: nextSlice,
                path: nextPath
            }
        };
    }
    const validatedEntry = (0, schema_1.parseSpecIndexEntry)((0, schema_1.serializeSpecIndexEntry)(nextEntry));
    return applyNonDestructiveSpecIndexEntryUpdate({
        currentDocument,
        targetIndex,
        nextEntry: validatedEntry
    });
}
function applyEntryFieldUpdate(entry, field, value, linkedSlicePatch) {
    switch (field) {
        case "title": {
            if (typeof value !== "string" || value.trim() === "") {
                throw new TypeError(`Spec index field '${field}' must be a non-empty string.`);
            }
            entry.title = value;
            return;
        }
        case "depth": {
            if (typeof value !== "string" || value.trim() === "") {
                throw new TypeError(`Spec index field '${field}' must be a non-empty string.`);
            }
            entry.depth = value;
            return;
        }
        case "status": {
            if (typeof value !== "string" || value.trim() === "") {
                throw new TypeError(`Spec index field '${field}' must be a non-empty string.`);
            }
            entry.status = value;
            return;
        }
        case "source.classification":
        case "source.type":
        case "source.ref": {
            throw new TypeError(`Spec index field '${field}' is append-only source identity and cannot be rewritten; use spec source add.`);
        }
        case "implementation_order": {
            entry.optional = {
                ...entry.optional,
                implementation_order: value
            };
            return;
        }
        case "registered_at":
        case "updated_at":
        case "archived_at": {
            if (typeof value !== "string" || value.trim() === "") {
                throw new TypeError(`Spec index field '${field}' must be a non-empty string.`);
            }
            entry.optional = {
                ...entry.optional,
                [field]: value
            };
            return;
        }
        case "linked_slice.slice":
        case "linked_slice.path": {
            if (typeof value !== "string" || value.trim() === "") {
                throw new TypeError(`Spec index field '${field}' must be a non-empty string.`);
            }
            linkedSlicePatch.touched = true;
            if (field.endsWith(".slice")) {
                linkedSlicePatch.slice = value;
            }
            else {
                linkedSlicePatch.path = value;
            }
            return;
        }
        case "id":
        case "destination":
        case "spec_path": {
            throw new TypeError(`Spec index field '${field}' is identity-bearing and cannot be rewritten.`);
        }
        default: {
            throw new TypeError(`Unsupported spec index field update '${field}'.`);
        }
    }
}
function assertIdentityBearingFieldsArePreserved(currentEntry, nextEntry) {
    if (currentEntry.id !== nextEntry.id) {
        throw new TypeError("Unsafe spec-registry entry rewrite: field 'id' must be preserved during non-destructive updates.");
    }
    if (currentEntry.destination !== nextEntry.destination) {
        throw new TypeError("Unsafe spec-registry entry rewrite: field 'destination' must be preserved during non-destructive updates.");
    }
    if (currentEntry.spec_path !== nextEntry.spec_path) {
        throw new TypeError("Unsafe spec-registry entry rewrite: field 'spec_path' must be preserved during non-destructive updates.");
    }
    if (currentEntry.source.classification !== nextEntry.source.classification
        || currentEntry.source.type !== nextEntry.source.type
        || currentEntry.source.ref !== nextEntry.source.ref) {
        throw new TypeError("Unsafe spec-registry entry rewrite: source identity fields must be preserved during non-destructive updates.");
    }
}
