/**
 * Page Editor service — data layer for the flat, section-based page
 * editor (`/pages/:id/edit`).
 *
 * A page is a document in the `pages` collection. The editor reads and
 * writes a single JSON field, `data.sections`, holding an ordered list of
 * `PageSection`s. Save draft = `PUT` the sections (+ title/slug/SEO) with
 * no status change; publish = the same payload plus `status: 'PUBLISHED'`.
 *
 * Persistence reuses the existing collection document API, so there is no
 * new endpoint and no Prisma migration — `sections` is declared as a
 * `type: 'json'` field on the `pages` collection so the field-access layer
 * doesn't strip it on write.
 *
 * The section mutation helpers (`addSection`, `moveSection`, …) are pure
 * functions over `PageSection[]` so the editor state and the unit tests
 * share one implementation.
 */
import { cmsApi } from './api.js';
import { getSectionType, createSection, addSection, removeSection, duplicateSection, toggleSectionVisibility, moveSection, reorderSections, updateSectionContent, updateSectionSettings, updateSectionMeta, coerceSections as coerceSectionsCore, validateSectionContent, } from '@actuate-media/cms-core/page-sections';
import { validateSlug } from '@actuate-media/cms-core/client';
import { missingCollectionFields } from './collection-schema.js';
// Re-export the pure section helpers so existing editor imports
// (`../../lib/page-editor-service.js`) keep working after the move to core.
export { createSection, addSection, removeSection, duplicateSection, toggleSectionVisibility, moveSection, reorderSections, updateSectionContent, updateSectionSettings, updateSectionMeta, };
export const PAGES_COLLECTION = 'pages';
// ─── Validation ──────────────────────────────────────────────────────
function isBlank(v) {
    return v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
}
/**
 * Validate a page for save/publish. `forPublish` turns on the stricter
 * rules (at least one visible section, required section fields, valid
 * button URLs, image alt text). SEO gaps are surfaced as warnings.
 *
 * Per-section rules are delegated to the shared `validateSectionContent`
 * in `@actuate-media/cms-core/page-sections`, so the editor and the
 * server/MCP enforce identical content rules. Page-level rules
 * (title/path, "at least one visible section", SEO) stay here because
 * they operate on the whole `EditorPage`.
 */
export function validatePage(page, forPublish = false) {
    const errors = [];
    const warnings = [];
    if (isBlank(page.title))
        errors.push('Page title is required.');
    if (isBlank(page.path))
        errors.push('Page path is required.');
    // Mirror the server publish gate so an invalid/reserved slug surfaces in the
    // validation summary rather than only as a toast after the publish attempt.
    const slugError = validateSlug(page.slug);
    if (slugError)
        errors.push(slugError);
    if (forPublish) {
        const visible = page.sections.filter((s) => s.visible);
        if (visible.length === 0) {
            errors.push('Add at least one visible section before publishing.');
        }
        // Unmanaged (externally-managed) sections have no registered field schema,
        // so they can't be content-validated here and must not block publish.
        for (const section of visible.filter((s) => !s.unmanaged)) {
            const def = getSectionType(section.sectionType);
            const label = section.name || def?.name || 'Section';
            const result = validateSectionContent(section.sectionType, section.content, section.settings);
            for (const issue of result.errors) {
                errors.push(`${label}: ${issue.message}`);
            }
        }
        if (isBlank(page.seoTitle))
            warnings.push('Add an SEO title to improve search results.');
        if (isBlank(page.seoDescription)) {
            warnings.push('Add an SEO description to improve search results.');
        }
    }
    return { errors, warnings };
}
// ─── Schema guard ────────────────────────────────────────────────────
/**
 * The Page Editor persists `sections` / `path` / `metaTitle` /
 * `metaDescription` as top-level keys on a `pages` document. The server's
 * field-access layer only writes keys that are *declared as fields* on the
 * collection — any undeclared key is silently dropped on save. A `pages`
 * collection missing the `sections` field would therefore discard all page
 * content with no error.
 *
 * `missingPageFields` returns the required field keys absent from the
 * `pages` collection so the editor can refuse to operate (and explain why)
 * rather than corrupt data. When `config` (or its collections) is not
 * provided — some embedding/test contexts don't pass it — we can't check,
 * so we return `[]` and trust the consumer.
 */
const REQUIRED_PAGE_FIELDS = ['sections', 'path', 'metaTitle', 'metaDescription'];
export function missingPageFields(config) {
    return missingCollectionFields(config, PAGES_COLLECTION, REQUIRED_PAGE_FIELDS);
}
function isPlainObject(v) {
    return !!v && typeof v === 'object' && !Array.isArray(v);
}
function rowToEditorPage(doc) {
    const data = doc.data ?? {};
    const slug = doc.slug ?? '';
    // The API lifts the stored `_layout` onto `doc.layout`, but tolerate either
    // shape so ownership detection is robust to response variations.
    const builderOwned = isPlainObject(doc.layout) || isPlainObject(data._layout);
    return {
        id: doc.id,
        title: doc.title ?? '',
        slug,
        path: typeof data.path === 'string' && data.path ? data.path : slug ? `/${slug}` : '/',
        status: doc.status ?? 'DRAFT',
        // Preserve unknown (externally-managed) sections so the editor can show
        // them read-only and round-trip them on save instead of dropping them.
        sections: coerceSectionsCore(data.sections, { onUnknown: 'preserve' }),
        seoTitle: typeof data.metaTitle === 'string' ? data.metaTitle : '',
        seoDescription: typeof data.metaDescription === 'string' ? data.metaDescription : '',
        publishedAt: doc.publishedAt ?? null,
        updatedAt: doc.updatedAt ?? null,
        builderOwned,
    };
}
// ─── API calls ───────────────────────────────────────────────────────
export async function fetchPageForEditor(pageId) {
    const res = await cmsApi(`/collections/${PAGES_COLLECTION}/${pageId}`);
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to load page');
    return rowToEditorPage(res.data);
}
// NOTE: `slug` may be empty for a brand-new draft. The `pages` collection
// declares `slug` as a `type: 'slug'` field with `from: 'title'`, so the
// server derives the slug from the title on create when none is supplied;
// the derived value comes back in the response and populates `EditorPage`.
// This is why `validatePage` requires a title but not a slug.
function toWriteBody(page, extra) {
    const payload = {
        title: page.title,
        slug: page.slug,
        path: page.path,
        // Unmanaged sections are part of `page.sections`, so they round-trip on
        // every save (no data loss). Strip the derived `unmanaged` marker — it is
        // recomputed from the registry on read, so persisting it would only stale.
        sections: page.sections.map(({ unmanaged: _unmanaged, ...rest }) => rest),
        metaTitle: page.seoTitle,
        metaDescription: page.seoDescription,
        ...extra,
    };
    return JSON.stringify(payload);
}
/** Create a new page document, returning the new id. */
export async function createPage(page) {
    const res = await cmsApi(`/collections/${PAGES_COLLECTION}`, {
        method: 'POST',
        body: toWriteBody(page),
    });
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to create page');
    return rowToEditorPage(res.data);
}
/** Save the current draft (no status change). */
export async function savePageDraft(page) {
    if (!page.id)
        return createPage(page);
    const res = await cmsApi(`/collections/${PAGES_COLLECTION}/${page.id}`, {
        method: 'PUT',
        body: toWriteBody(page),
    });
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to save draft');
    return rowToEditorPage(res.data);
}
/** Publish: persist the latest content and flip status to PUBLISHED. */
export async function publishPage(page) {
    const endpoint = page.id
        ? `/collections/${PAGES_COLLECTION}/${page.id}`
        : `/collections/${PAGES_COLLECTION}`;
    const res = await cmsApi(endpoint, {
        method: page.id ? 'PUT' : 'POST',
        body: toWriteBody(page, { status: 'PUBLISHED' }),
    });
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to publish page');
    return rowToEditorPage(res.data);
}
/** A blank page scaffold for `/pages/new`. */
export function emptyPage() {
    return {
        id: null,
        title: '',
        slug: '',
        path: '',
        status: 'DRAFT',
        sections: [],
        seoTitle: '',
        seoDescription: '',
        publishedAt: null,
        updatedAt: null,
        builderOwned: false,
    };
}
//# sourceMappingURL=page-editor-service.js.map