/**
 * Merge two tag lists per the spec rules in
 * docs/superpowers/specs/2026-05-19-type-validation-and-json-schema-annotations-design.md.
 *
 * - `@validate(...)`: concatenated across both inputs (alias's validators
 *   first, then use-site's), producing a single combined `@validate` tag.
 *   Users may also stack multiple `@validate(...)` tags or pass multiple
 *   arguments to a single tag — all forms collapse to one chain.
 * - `@jsonSchema(...)`: object-literal arguments merged left-to-right
 *   with use-site keys overriding alias keys (with `description`
 *   concatenating, see `collapseLiteralDescriptions`), producing a
 *   single combined `@jsonSchema` tag. Users may stack multiple
 *   `@jsonSchema(...)` tags on the same side OR pass multiple object
 *   literals to one tag — both shapes flatten into the same merge.
 *   Spreads are preserved verbatim (their evaluation happens at
 *   module-load time).
 *
 * Tag names other than `validate` / `jsonSchema` are deliberately not
 * merged here — they have no alias-vs-use-site semantics defined and may
 * belong to a different feature entirely (e.g. `@goal` / `@optimize`).
 * They flow through `resolveType` untouched on whichever side they
 * appeared, but never combined across the two sides.
 *
 * `aliasTags` come first in the chain, `useSiteTags` apply on top.
 */
export function mergeTagSets(aliasTags, useSiteTags) {
    const alias = aliasTags ?? [];
    const useSite = useSiteTags ?? [];
    if (alias.length === 0 && useSite.length === 0)
        return undefined;
    const combined = [];
    const aliasValidate = alias.filter((t) => t.name === "validate");
    const useSiteValidate = useSite.filter((t) => t.name === "validate");
    if (aliasValidate.length + useSiteValidate.length > 0) {
        const allArgs = [];
        for (const t of [...aliasValidate, ...useSiteValidate]) {
            allArgs.push(...t.arguments);
        }
        combined.push({
            type: "tag",
            name: "validate",
            arguments: allArgs,
            loc: (aliasValidate[0] ?? useSiteValidate[0]).loc,
        });
    }
    const aliasJson = alias.filter((t) => t.name === "jsonSchema");
    const useSiteJson = useSite.filter((t) => t.name === "jsonSchema");
    if (aliasJson.length + useSiteJson.length > 0) {
        const merged = mergeJsonSchemaArgs([...aliasJson, ...useSiteJson]);
        combined.push({
            type: "tag",
            name: "jsonSchema",
            arguments: merged,
            loc: (aliasJson[0] ?? useSiteJson[0]).loc,
        });
    }
    // Pass through any other tags from each side verbatim. We never combine
    // across the two sides because we don't know what their semantics are.
    for (const t of alias) {
        if (t.name !== "validate" && t.name !== "jsonSchema")
            combined.push(t);
    }
    for (const t of useSite) {
        if (t.name !== "validate" && t.name !== "jsonSchema")
            combined.push(t);
    }
    return combined;
}
export function mergeJsonSchemaArgs(tags) {
    const entries = [];
    for (const t of tags) {
        if (!t.arguments || t.arguments.length === 0) {
            const loc = t.loc;
            const where = loc ? ` (line ${loc.line}, col ${loc.col})` : "";
            throw new Error(`@jsonSchema(...) requires at least one object-literal argument${where}.`);
        }
        for (const arg of t.arguments) {
            if (arg.type !== "agencyObject") {
                const loc = arg.loc ?? t.loc;
                const where = loc ? ` (line ${loc.line}, col ${loc.col})` : "";
                throw new Error(`@jsonSchema(...) arguments must be object literals${where}; got ${arg.type}.`);
            }
            for (const e of arg.entries) {
                entries.push(e);
            }
        }
    }
    // Special case: collapse repeated string-literal `description` entries
    // into a single newline-joined description, instead of last-write-wins.
    // The intent is that a reusable alias can attach a description and a
    // use-site can extend it (e.g. add usage context) without losing the
    // original. Only literal-string descriptions participate — non-literal
    // values (variable refs, function calls) and descriptions arriving via
    // `...spread` are left to the normal dedupe path, because we cannot
    // statically combine values we cannot read at type-check time.
    collapseLiteralDescriptions(entries);
    // Dedupe literal-key KVs (last write wins). Splats are preserved.
    const seenKeys = new Set();
    const reversed = [];
    for (let i = entries.length - 1; i >= 0; i--) {
        const e = entries[i];
        if ("key" in e) {
            if (seenKeys.has(e.key))
                continue;
            seenKeys.add(e.key);
            reversed.push(e);
        }
        else {
            reversed.push(e);
        }
    }
    reversed.reverse();
    // First tag becomes the carrier of the merged object.
    const carrierLoc = tags[0].arguments[0]?.loc;
    const mergedObject = {
        type: "agencyObject",
        entries: reversed,
        ...(carrierLoc ? { loc: carrierLoc } : {}),
    };
    return [mergedObject];
}
/**
 * If `expr` is a plain string literal (no interpolation), return its raw
 * text. Otherwise return undefined so the caller can fall back to the
 * default merge behavior.
 */
function readLiteralString(expr) {
    if (expr.type !== "string" && expr.type !== "multiLineString") {
        return undefined;
    }
    const segments = expr.segments ?? [];
    let out = "";
    for (const seg of segments) {
        if (seg.type !== "text")
            return undefined;
        out += seg.value ?? "";
    }
    return out;
}
/**
 * In-place: if `entries` contains 2+ `description` KVs whose values are
 * all plain string literals, replace them with a single description
 * whose text is the originals joined by `\n`. The merged description
 * takes the position of the first one so non-description neighbours
 * keep their relative order; subsequent duplicates are removed so the
 * normal dedupe pass becomes a no-op for `description`. Locations and
 * the carrier literal's `type` (`string` vs `multiLineString`) follow
 * the first description's node.
 */
function collapseLiteralDescriptions(entries) {
    const indices = [];
    for (let i = 0; i < entries.length; i++) {
        const e = entries[i];
        if ("key" in e && e.key === "description")
            indices.push(i);
    }
    if (indices.length < 2)
        return;
    const texts = [];
    for (const i of indices) {
        const kv = entries[i];
        const text = readLiteralString(kv.value);
        // If any description is not a plain literal, bail and let the
        // default dedupe (last-write-wins) handle it.
        if (text === undefined)
            return;
        texts.push(text);
    }
    const joined = texts.join("\n");
    const firstKv = entries[indices[0]];
    const carrier = firstKv.value;
    const newLiteral = {
        ...carrier,
        segments: [{ type: "text", value: joined }],
    };
    entries[indices[0]] = { ...firstKv, value: newLiteral };
    // Remove the trailing duplicate descriptions (back-to-front so the
    // earlier indices stay valid).
    for (let k = indices.length - 1; k >= 1; k--) {
        entries.splice(indices[k], 1);
    }
}
