/**
 * Asset-embed extraction + diffing.
 *
 * Entry bodies reference binary assets two ways:
 *   - Obsidian-style transclusion embeds:  ![[Foo.png]]  /  ![[Foo.png|alt]]
 *   - Standard markdown images:            ![alt](/api/assets/asset/<id>/<file>)
 *
 * Asset *binaries* live outside git (the workspace `.studiograph/assets/`
 * tree for the local backend, or Cloudflare R2), so git history can only ever
 * recover the *reference* — the embed text in the body, not the bytes. These
 * helpers let the write path detect when a whole-body rewrite would drop a
 * reference the prior body had, and let the history tools report which embeds
 * exist at a given revision.
 *
 * Callers pass the markdown body (post-frontmatter). Embed syntax is a body
 * construct; frontmatter is not scanned.
 */
// ![[target]] or ![[target|alias]] — capture the target (pre-pipe).
const TRANSCLUSION_RE = /!\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g;
// ![alt](url) — capture the url. Stops at the first closing paren; asset
// embed URLs in this app never contain parens.
const MARKDOWN_IMAGE_RE = /!\[[^\]]*\]\(([^)]+)\)/g;
// D5 identity (decode-at-parse-boundary): URL segments are percent-encoded on
// emit (`encodeAssetFilename`), while the asset index (`row.filename`) and a
// Fastify-decoded route param speak the raw name. Everything that REASONS
// about identity uses the decoded form; everything that EMITS a URL must
// re-encode. One owner for both directions: core/artifact-asset-urls.
import { decodeAssetFilenameSegment } from './artifact-asset-urls.js';
/**
 * Extract every embedded asset reference from a markdown body,
 * de-duplicated, transclusions first then markdown images. Returns the raw
 * target (transclusion target or image URL) so callers can compare
 * references across revisions and surface them to users/agents.
 */
export function extractAssetEmbeds(body) {
    if (!body)
        return [];
    const out = [];
    const seen = new Set();
    const push = (ref) => {
        const trimmed = ref.trim();
        if (!trimmed || seen.has(trimmed))
            return;
        seen.add(trimmed);
        out.push(trimmed);
    };
    let m;
    TRANSCLUSION_RE.lastIndex = 0;
    while ((m = TRANSCLUSION_RE.exec(body)) !== null)
        push(m[1]);
    MARKDOWN_IMAGE_RE.lastIndex = 0;
    while ((m = MARKDOWN_IMAGE_RE.exec(body)) !== null)
        push(m[1]);
    return out;
}
/**
 * The canonical, render-safe markdown embed for an asset: `![filename](url)`.
 * This is exactly what the editor inserts on drop/paste
 * (web FormattingPalette), so any agent-placed embed that uses this form is
 * guaranteed to render. Agents must NOT invent `![[filename]]` transclusion
 * placeholders — those don't resolve to a stored asset and render as dead text.
 */
export function assetEmbed(filename, url) {
    return `![${filename}](${url})`;
}
/**
 * Derive a human display name from a filename: drop the extension, turn
 * `_`/`-` runs into spaces, and Title-Case each word
 * (`haneya_sake.jpg` → `Haneya Sake`). Shared by every free-standing-asset
 * creation path (manager upload, presign finalize, chat-image promote) so the
 * label rule never drifts. Returns "" for an extension-only name; callers add
 * their own fallback (filename / assetId).
 */
export function deriveAssetDisplayName(filename) {
    const stem = filename.replace(/\.[^.]+$/, '');
    return stem.replace(/[_-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
/**
 * Canonicalize an embed reference for identity comparison: any immutable
 * `/api/assets/<assetId>/<filename>` URL inside it has its filename segment
 * decoded to the RAW stored name. An editor-inserted encoded embed
 * (`page%281%29.png`) and an agent-re-emitted raw one are the SAME asset —
 * comparing the verbatim strings false-tripped ASSET_REMOVAL_BLOCKED on
 * rewrites that didn't drop anything. Non-asset refs pass through unchanged.
 */
function canonicalAssetRef(ref) {
    return ref.replace(IMMUTABLE_ASSET_URL_RE, (_m, assetId, filename) => `/api/assets/${assetId}/${decodeAssetFilenameSegment(filename)}`);
}
/**
 * References present in `before` but absent from `after` — i.e. embeds a
 * body rewrite would drop. Set semantics on the CANONICALIZED reference
 * (decoded asset identity — see {@link canonicalAssetRef}); the returned
 * strings are the original `before` refs so messages show what the body said.
 */
export function droppedAssetEmbeds(before, after) {
    const beforeRefs = extractAssetEmbeds(before);
    if (beforeRefs.length === 0)
        return [];
    const afterRefs = new Set(extractAssetEmbeds(after).map(canonicalAssetRef));
    return beforeRefs.filter((ref) => !afterRefs.has(canonicalAssetRef(ref)));
}
/**
 * Strip a leading YAML frontmatter block (`---\n…\n---`) so embed extraction
 * runs against the body only. Returns the input unchanged when there is no
 * frontmatter. Mirrors the lightweight frontmatter sniff the history tools
 * already use, avoiding a full markdown parse for a read-only report.
 */
export function stripFrontmatter(fileContent) {
    return fileContent.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '');
}
// Three path segments after /api/assets/ (type/id/filename). New-scheme URLs
// have only two (assetId/filename), so they never match — the migration is
// idempotent against already-rewritten content. Segments stop at the chars
// that terminate a URL in markdown/YAML: whitespace, ) " ' ]. The backslash is
// in the terminator set too: inside an artifact's JSON-bundle body a URL is
// serialized as `\"/api/assets/<type>/<id>/<file>\"`, so without it the filename
// group would greedily eat the trailing `\` (capturing `icon.png\`), the blob
// lookup would miss, and the migration would silently skip the embedded URL.
const LEGACY_ASSET_URL_RE = /\/api\/assets\/([^/\s)"'\\\]]+)\/([^/\s)"'\\\]]+)\/([^/\s)"'\\\]]+)/g;
/** Every legacy `/api/assets/{type}/{id}/{filename}` reference in a file,
 *  de-duplicated by URL. Pass the RAW file (frontmatter + body). */
export function extractLegacyAssetUrls(fileContent) {
    if (!fileContent)
        return [];
    const out = [];
    const seen = new Set();
    let m;
    LEGACY_ASSET_URL_RE.lastIndex = 0;
    while ((m = LEGACY_ASSET_URL_RE.exec(fileContent)) !== null) {
        const url = m[0];
        if (seen.has(url))
            continue;
        seen.add(url);
        out.push({ url, type: m[1], id: m[2], filename: m[3], oldKey: `${m[1]}/${m[2]}/${m[3]}` });
    }
    return out;
}
// ── Immutable-key asset URLs ──────────────────────────────────────────────
//
// New-scheme references have exactly two segments after /api/assets/:
// `{assetId}/{filename}`. Uses the SAME terminator set as LEGACY_ASSET_URL_RE
// (whitespace, ) " ' ] \) plus < > for raw HTML `src`, so both grammars classify
// URL boundaries identically. The backslash terminator matters for the share
// read + asset allowlist (share-api.ts), which run this grammar over ANY shared
// body — including an artifact's JSON bundle, where a 2-segment URL is serialized
// as `\"/api/assets/<assetId>/<file>\"`; without it the filename group eats the
// trailing `\` (→ `file.png\`), so the rewrite emits a wrong share URL and the
// embed-allowlist check fails the image closed. The trailing lookahead pins the
// match to two segments (filename is the last segment), so a three-segment legacy
// URL — whose second segment is followed by another `/` — never matches.
const IMMUTABLE_ASSET_URL_RE = /\/api\/assets\/([^/\s)"'\\\]<>]+)\/([^/\s)"'\\\]<>]+)(?=[\s)"'\\\]<>]|$)/g;
/**
 * Rewrite every immutable `/api/assets/{assetId}/{filename}` reference in a
 * body via `replace(assetId, filename)`. Legacy three-segment URLs and
 * `![[transclusions]]` are left untouched. Used by the public share read to
 * point embedded images at the token-scoped `/api/share/{token}/asset/...`
 * surface without leaking the authed `/api/assets` URLs to guests.
 *
 * D5 identity contract: `filename` reaches the callback DECODED (the raw
 * stored name). A callback that emits a URL must percent-encode the segment
 * it splices (`encodeAssetFilename`) — emitting the raw name breaks markdown/
 * HTML URL parsing on spaces and parens.
 */
export function rewriteImmutableAssetUrls(body, replace) {
    return body.replace(IMMUTABLE_ASSET_URL_RE, (_m, assetId, filename) => replace(assetId, decodeAssetFilenameSegment(filename)));
}
/**
 * Every immutable `/api/assets/{assetId}/{filename}` reference embedded in a
 * body, de-duplicated by `{assetId}/{filename}`. Uses the SAME grammar as
 * `rewriteImmutableAssetUrls` (the public share read rewrites exactly these
 * URLs), so the public asset handler can scope itself to the assets a shared
 * entry actually embeds — not every asset that merely shares its repo. Legacy
 * three-segment URLs and `![[transclusions]]` are intentionally ignored, the
 * same way the rewrite leaves them untouched.
 *
 * D5 identity contract: `filename` is the DECODED raw stored name, directly
 * comparable to `assets.db.filename` and to a Fastify-decoded route param —
 * comparing the encoded URL segment against either was the latent
 * special-char 404 on public shares (share-api embedded-asset check).
 */
export function extractImmutableAssetRefs(body) {
    if (!body)
        return [];
    const out = [];
    const seen = new Set();
    let m;
    IMMUTABLE_ASSET_URL_RE.lastIndex = 0;
    while ((m = IMMUTABLE_ASSET_URL_RE.exec(body)) !== null) {
        const filename = decodeAssetFilenameSegment(m[2]);
        const key = `${m[1]}/${filename}`;
        if (seen.has(key))
            continue;
        seen.add(key);
        out.push({ assetId: m[1], filename });
    }
    return out;
}
/**
 * Every immutable assetId an entity embeds — in its markdown `body` OR any
 * string frontmatter value (e.g. a `thumbnail` / `link` url, or an array of
 * urls). De-duplicated. This is the single definition of "what assets does this
 * entry embed", shared by the caption fold (`VectorService.resolveEmbeddedCaptions`,
 * which reads each embedded image's caption into the entry's search text) and
 * the caption sweep's embedder re-index — so the two never disagree on coverage.
 * A cheap substring pre-check skips the regex for the common no-asset entry.
 */
export function embeddedAssetIds(body, frontmatter) {
    const ids = new Set();
    const scan = (s) => {
        if (!s || s.indexOf('/api/assets/') === -1)
            return;
        for (const ref of extractImmutableAssetRefs(s))
            ids.add(ref.assetId);
    };
    scan(body);
    for (const v of Object.values(frontmatter)) {
        if (typeof v === 'string')
            scan(v);
        else if (Array.isArray(v))
            for (const x of v)
                if (typeof x === 'string')
                    scan(x);
    }
    return ids;
}
//# sourceMappingURL=embeds.js.map