import { n as slugify } from "./slugify-Cce3dTdg.mjs";
import { o as RESERVED_FIELD_SLUGS } from "./types-tM44hEcf.mjs";
import mime from "mime/lite";

//#region src/import/utils.ts
/** Internal WordPress post types that should be excluded from import */
const INTERNAL_POST_TYPES = [
	"revision",
	"nav_menu_item",
	"custom_css",
	"customize_changeset",
	"oembed_cache",
	"wp_global_styles",
	"wp_navigation",
	"wp_template",
	"wp_template_part",
	"attachment",
	"wp_block"
];
/** Internal meta key prefixes to filter out */
const INTERNAL_META_PREFIXES = ["_edit_", "_wp_"];
const NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
const TRAILING_SLASHES = /\/+$/;
const WP_JSON_SUFFIX = /\/wp-json\/?.*$/;
/** Specific internal meta keys */
const INTERNAL_META_KEYS = [
	"_edit_last",
	"_edit_lock",
	"_pingme",
	"_encloseme"
];
/** Base fields required for any WordPress import */
const BASE_REQUIRED_FIELDS = [
	{
		slug: "title",
		label: "Title",
		type: "string",
		required: true,
		searchable: true
	},
	{
		slug: "content",
		label: "Content",
		type: "portableText",
		required: false,
		searchable: true
	},
	{
		slug: "excerpt",
		label: "Excerpt",
		type: "text",
		required: false
	}
];
/** Featured image field - only added to post types that have _thumbnail_id */
const FEATURED_IMAGE_FIELD = {
	slug: "featured_image",
	label: "Featured Image",
	type: "image",
	required: false
};
/**
* Check if a post type is internal/should be excluded
*/
function isInternalPostType(type) {
	return INTERNAL_POST_TYPES.includes(type);
}
/**
* Check if a meta key is internal/should be filtered out
*/
function isInternalMetaKey(key) {
	if (INTERNAL_META_KEYS.includes(key)) return true;
	for (const prefix of INTERNAL_META_PREFIXES) if (key.startsWith(prefix)) return true;
	if (key === "_thumbnail_id") return false;
	if (key.startsWith("_yoast_")) return false;
	if (key.startsWith("_rank_math_")) return false;
	if (key.startsWith("_")) return true;
	return false;
}
/**
* Map WordPress status to normalized status
*/
function mapWpStatus(status) {
	switch (status) {
		case "publish": return "publish";
		case "draft": return "draft";
		case "pending": return "pending";
		case "private": return "private";
		case "future": return "future";
		default: return "draft";
	}
}
/** Default mappings from WordPress post types to EmDash collections */
const POST_TYPE_TO_COLLECTION = {
	post: "posts",
	page: "pages",
	attachment: "media",
	product: "products",
	portfolio: "portfolio",
	testimonial: "testimonials",
	team: "team",
	event: "events",
	faq: "faqs"
};
/**
* Map WordPress post type to EmDash collection name
*/
function mapPostTypeToCollection(postType) {
	return POST_TYPE_TO_COLLECTION[postType] || postType;
}
/**
* Map WordPress meta key to EmDash field slug
*/
function mapMetaKeyToField(key) {
	if (key === "_yoast_wpseo_title") return "seo_title";
	if (key === "_yoast_wpseo_metadesc") return "seo_description";
	if (key === "_rank_math_title") return "seo_title";
	if (key === "_rank_math_description") return "seo_description";
	if (key === "_thumbnail_id") return "featured_image";
	if (key.startsWith("_")) return key.slice(1);
	return key;
}
/**
* Infer field type from meta key name and sample value
*/
function inferMetaType(key, value) {
	if (key.endsWith("_id") || key === "_thumbnail_id") return "string";
	if (key.endsWith("_date") || key.endsWith("_time")) return "date";
	if (key.endsWith("_count") || key.endsWith("_number")) return "number";
	if (!value) return "string";
	if (value.startsWith("a:") || value.startsWith("{") || value.startsWith("[")) return "json";
	if (NUMERIC_PATTERN.test(value)) return "number";
	if ([
		"0",
		"1",
		"true",
		"false"
	].includes(value)) return "boolean";
	return "string";
}
/**
* Meta prefixes written by well-known WordPress plugins as operational
* bookkeeping (sync state, counters, cache keys) — not content. Without
* this filter, a mature site's analysis suggests dozens of junk fields
* per post type and the real content fields drown in them.
*
* ponytail: curated list of the plugins we've seen in the wild, not a
* taxonomy of the WP ecosystem. Unknown plugins' meta still gets through;
* extend the list as real sites surface new offenders.
*/
const PLUGIN_META_PREFIXES = [
	"aawp_",
	"algolia_",
	"amazon_polly_",
	"ampforwp_",
	"classifai_",
	"essb_",
	"eg_",
	"gnpub_",
	"jetpack_",
	"mashsb_",
	"monsterinsights_",
	"onesignal_",
	"penci_",
	"perfmatters_",
	"pys_",
	"rank_math_",
	"rp4wp_",
	"saswp_",
	"sbg_",
	"snap_",
	"spay_",
	"tie_",
	"wl_",
	"wpil_",
	"wprm_",
	"wpswa_",
	"wpuf_",
	"yarpp_"
];
/** Exact meta keys that are plugin/core bookkeeping, not content. */
const PLUGIN_META_KEYS = new Set([
	"entity_same_as",
	"exclude_from_search",
	"footnotes",
	"inline_featured_image",
	"os_meta",
	"thirstydata"
]);
/**
* Check whether a meta key is well-known plugin bookkeeping that should
* not become a content field. Hyphens are normalized to underscores
* before matching (e.g. `ampforwp-amp-on-off`).
*/
function isPluginBookkeepingMeta(key) {
	const normalized = key.replaceAll("-", "_");
	if (PLUGIN_META_KEYS.has(normalized)) return true;
	return PLUGIN_META_PREFIXES.some((prefix) => normalized.startsWith(prefix));
}
const INVALID_FIELD_SLUG_CHARS = /[^a-z0-9_]+/g;
const LEADING_NON_ALPHA_CHARS = /^[^a-z]+/;
/**
* Sanitize a WordPress meta/ACF key into a valid EmDash field slug
* (`/^[a-z][a-z0-9_]*$/`, max 63 chars, not reserved).
*
* Must be applied consistently on both sides of an import: once when
* creating fields from the analysis, and again when matching incoming
* meta keys onto schema fields — otherwise keys like `my-field` create
* `my_field` but never receive values.
*/
function sanitizeFieldSlug(key) {
	const sanitized = key.toLowerCase().replace(INVALID_FIELD_SLUG_CHARS, "_").replace(LEADING_NON_ALPHA_CHARS, "").slice(0, 63);
	if (!sanitized) return "field";
	if (RESERVED_FIELD_SLUGS.includes(sanitized)) return `wp_${sanitized}`;
	return sanitized;
}
const REGEX_SPECIALS = /[.*+?^${}()|[\]\\]/g;
const LEADING_WWW = /^www\./;
/**
* Turn an absolute URL into a root-relative one when it points at the
* source site (www-insensitive). Returns null when the URL should be
* left alone: external links, non-http(s) schemes, and `/wp-content/`
* media files — those stay absolute so the later media pass can match
* them against its old-URL -> new-URL map.
*/
function relativizeUrl(url, sourceHost) {
	if (!url.startsWith("http://") && !url.startsWith("https://")) return null;
	try {
		const parsed = new URL(url);
		if (parsed.hostname.replace(LEADING_WWW, "") !== sourceHost) return null;
		if (parsed.pathname.startsWith("/wp-content/")) return null;
		return `${parsed.pathname}${parsed.search}${parsed.hash}` || "/";
	} catch {
		return null;
	}
}
function relativizeMarkDefs(markDefs, sourceHost) {
	for (const def of markDefs ?? []) if (def._type === "link" && typeof def.href === "string") def.href = relativizeUrl(def.href, sourceHost) ?? def.href;
}
/**
* Rewrite internal links in imported content to root-relative URLs, in
* place. Without this, imported posts keep linking back to the old
* WordPress domain (e.g. `https://oldsite.com/companies/google/`)
* instead of staying on the new site.
*
* ponytail: path structures are kept as-is (WP permalink /2024/05/slug/
* stays /2024/05/slug/) — mapping old paths onto the new site's routes
* is the planned permalink->redirect-map feature.
*/
function relativizeContentLinks(blocks, siteUrl) {
	let sourceHost;
	try {
		sourceHost = new URL(siteUrl).hostname.replace(LEADING_WWW, "");
	} catch {
		return;
	}
	const hrefPattern = new RegExp(`href=(["']?)https?://(?:www\\.)?${sourceHost.replace(REGEX_SPECIALS, "\\$&")}(/[^"'\\s>]*)?\\1`, "gi");
	for (const block of blocks) switch (block._type) {
		case "block":
			relativizeMarkDefs(block.markDefs, sourceHost);
			break;
		case "image":
			if (block.link) block.link = relativizeUrl(block.link, sourceHost) ?? block.link;
			break;
		case "table":
			for (const row of block.rows) for (const cell of row.cells) relativizeMarkDefs(cell.markDefs, sourceHost);
			break;
		case "columns":
			for (const column of block.columns) relativizeContentLinks(column.content, siteUrl);
			break;
		case "cover":
			relativizeContentLinks(block.content, siteUrl);
			break;
		case "button":
			if (block.url) block.url = relativizeUrl(block.url, sourceHost) ?? block.url;
			break;
		case "buttons":
			for (const button of block.buttons) if (button.url) button.url = relativizeUrl(button.url, sourceHost) ?? button.url;
			break;
		case "htmlBlock":
			block.html = block.html.replace(hrefPattern, (_m, _quote, path) => {
				return `href="${path || "/"}"`;
			});
			break;
		case "code":
		case "embed":
		case "gallery":
		case "break":
		case "file":
		case "pullquote": break;
		default:
	}
}
/**
* Normalize URL for API requests
*/
function normalizeUrl(url) {
	let normalized = url.trim();
	if (!normalized.startsWith("http")) normalized = `https://${normalized}`;
	normalized = normalized.replace(TRAILING_SLASHES, "");
	normalized = normalized.replace(WP_JSON_SUFFIX, "");
	return normalized;
}
/**
* Extract filename from URL
*/
function getFilenameFromUrl(url) {
	try {
		return new URL(url).pathname.split("/").filter(Boolean).pop();
	} catch {
		return;
	}
}
/**
* Guess MIME type from filename
*/
function guessMimeType(filename) {
	return mime.getType(filename) ?? void 0;
}
/**
* Build a map of attachment IDs to URLs for resolving featured images
*/
function buildAttachmentMap(attachments) {
	const map = /* @__PURE__ */ new Map();
	for (const att of attachments) if (att.id && att.url) map.set(String(att.id), att.url);
	return map;
}
/**
* Check if two field types are compatible for import
*/
function isTypeCompatible(requiredType, existingType) {
	if (requiredType === existingType) return true;
	return {
		string: [
			"string",
			"text",
			"slug"
		],
		text: ["string", "text"],
		portableText: ["portableText", "json"],
		number: ["number", "integer"],
		integer: ["number", "integer"]
	}[requiredType]?.includes(existingType) ?? false;
}
const MAX_SLUG_COLLISION_ATTEMPTS = 1e3;
/**
* Find or create a unique byline slug, capped at MAX_SLUG_COLLISION_ATTEMPTS.
*/
async function ensureUniqueBylineSlug(bylineRepo, baseSlug) {
	let candidate = baseSlug;
	let suffix = 2;
	while (await bylineRepo.findBySlug(candidate)) {
		if (suffix > MAX_SLUG_COLLISION_ATTEMPTS) throw new Error(`Byline slug collision limit exceeded for base slug "${baseSlug}". Tried ${MAX_SLUG_COLLISION_ATTEMPTS} variants.`);
		candidate = `${baseSlug}-${suffix}`;
		suffix++;
	}
	return candidate;
}
/**
* Resolve (find-or-create) a byline for an imported WordPress author.
* Caches results in `cache` keyed by `authorLogin:mappedUserId`.
*/
async function resolveImportByline(authorLogin, displayName, mappedUserId, bylineRepo, cache) {
	if (!authorLogin) return void 0;
	const cacheKey = `${authorLogin}:${mappedUserId ?? ""}`;
	const cached = cache.get(cacheKey);
	if (cached) return cached;
	if (mappedUserId) {
		const existingForUser = await bylineRepo.findByUserId(mappedUserId);
		if (existingForUser) {
			cache.set(cacheKey, existingForUser.id);
			return existingForUser.id;
		}
	}
	const name = displayName || authorLogin;
	const slug = await ensureUniqueBylineSlug(bylineRepo, slugify(authorLogin) || "author");
	const created = await bylineRepo.create({
		slug,
		displayName: name,
		userId: mappedUserId ?? null,
		isGuest: !mappedUserId
	});
	cache.set(cacheKey, created.id);
	return created.id;
}
/**
* Check schema compatibility between required fields and existing collection
*/
function checkSchemaCompatibility(requiredFields, existingCollection) {
	if (!existingCollection) {
		const fieldStatus = {};
		for (const field of requiredFields) fieldStatus[field.slug] = {
			status: "missing",
			requiredType: field.type
		};
		return {
			exists: false,
			fieldStatus,
			canImport: true
		};
	}
	const fieldStatus = {};
	const incompatibleFields = [];
	for (const field of requiredFields) {
		const existingField = existingCollection.fields.get(field.slug);
		if (!existingField) fieldStatus[field.slug] = {
			status: "missing",
			requiredType: field.type
		};
		else if (isTypeCompatible(field.type, existingField.type)) fieldStatus[field.slug] = {
			status: "compatible",
			existingType: existingField.type,
			requiredType: field.type
		};
		else {
			fieldStatus[field.slug] = {
				status: "type_mismatch",
				existingType: existingField.type,
				requiredType: field.type
			};
			incompatibleFields.push(field.slug);
		}
	}
	const canImport = incompatibleFields.length === 0;
	return {
		exists: true,
		fieldStatus,
		canImport,
		reason: canImport ? void 0 : `Incompatible field types: ${incompatibleFields.join(", ")}`
	};
}

//#endregion
export { sanitizeFieldSlug as _, getFilenameFromUrl as a, isInternalMetaKey as c, mapMetaKeyToField as d, mapPostTypeToCollection as f, resolveImportByline as g, relativizeContentLinks as h, checkSchemaCompatibility as i, isInternalPostType as l, normalizeUrl as m, FEATURED_IMAGE_FIELD as n, guessMimeType as o, mapWpStatus as p, buildAttachmentMap as r, inferMetaType as s, BASE_REQUIRED_FIELDS as t, isPluginBookkeepingMeta as u };
//# sourceMappingURL=utils-BJXElxdw.mjs.map