/**
 * Post Editor service — data layer for the flat, section-based Post editor
 * (`/posts/:type/:id/edit`) and the per-type Post Template editor
 * (`/posts/types/:slug/template`).
 *
 * A post is a document in its post-type collection (e.g. `blog`). The editor
 * reads/writes the post's fields (title, slug, excerpt, featured image,
 * body, SEO) plus a `data.sections` JSON list of `PageSection`s — the same
 * flat model the Page Editor uses. New posts inherit their section list from
 * the post type's template.
 *
 * A template is a single document per post type in the hidden
 * `post-templates` collection, matched by `slug` == the post-type slug. It
 * stores the design-driven header layout (`data.header`) and the default
 * section list (`data.sections`).
 *
 * Persistence reuses the existing collection document API, so there is no
 * new endpoint and no Prisma migration — `sections` is declared as `json`
 * on each post-type collection (and `header`/`sections` on `post-templates`)
 * so the field-access layer doesn't strip them on write.
 */
import { cmsApi } from './api.js';
import { missingCollectionFields } from './collection-schema.js';
import { getSectionType, generateSectionId, createSection, addSection, removeSection, duplicateSection, toggleSectionVisibility, moveSection, reorderSections, updateSectionContent, updateSectionSettings, updateSectionMeta, coerceSections, coercePostHeader, createPostTemplate, validateSectionContent, } from '@actuate-media/cms-core/page-sections';
import { validateSlug } from '@actuate-media/cms-core/client';
// Re-export the pure section helpers so the post editor imports them from
// one place (mirrors page-editor-service).
export { createSection, addSection, removeSection, duplicateSection, toggleSectionVisibility, moveSection, reorderSections, updateSectionContent, updateSectionSettings, updateSectionMeta, };
export const TEMPLATES_COLLECTION = 'post-templates';
// ─── Schema guard ────────────────────────────────────────────────────
/**
 * The Post Editor persists these as top-level keys on the post-type
 * collection's documents. The server's field-access layer silently drops any
 * key not declared as a field on the collection, so a post-type collection
 * missing e.g. `sections` would discard all section content on save with no
 * error. Mirrors the Page Editor's `missingPageFields` guard.
 */
const REQUIRED_POST_FIELDS = [
    'sections',
    'excerpt',
    'featuredImage',
    'body',
    'category',
    'publishDate',
    'metaTitle',
    'metaDescription',
];
export function missingPostFields(config, postType) {
    return missingCollectionFields(config, postType, REQUIRED_POST_FIELDS);
}
// ─── Validation ──────────────────────────────────────────────────────
function isBlank(v) {
    return v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
}
/**
 * Validate a post for save/publish. Section rules are delegated to the
 * shared `validateSectionContent` so the editor and the server/MCP enforce
 * identical content rules; post-level rules (title/slug, SEO) live here.
 */
export function validatePost(post, forPublish = false) {
    const errors = [];
    const warnings = [];
    if (isBlank(post.title))
        errors.push('Post title is required.');
    if (isBlank(post.slug))
        errors.push('Post slug 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(post.slug);
    if (slugError)
        errors.push(slugError);
    if (forPublish) {
        const visible = post.sections.filter((s) => s.visible);
        // 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}`);
            for (const issue of result.warnings)
                warnings.push(`${label}: ${issue.message}`);
        }
        if (isBlank(post.seoTitle))
            warnings.push('Add an SEO title to improve search results.');
        if (isBlank(post.seoDescription)) {
            warnings.push('Add an SEO description to improve search results.');
        }
    }
    return { errors, warnings };
}
/** Validate a template for save (section content only — no post fields). */
export function validateTemplate(template) {
    const errors = [];
    const warnings = [];
    for (const section of template.sections.filter((s) => s.visible && !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}`);
        for (const issue of result.warnings)
            warnings.push(`${label}: ${issue.message}`);
    }
    return { errors, warnings };
}
function dataStr(data, key) {
    const v = data[key];
    return typeof v === 'string' ? v : '';
}
/**
 * Read stored tags into plain strings. The `tagsField` preset stores
 * `[{ tag: 'AI' }]` rows; older/hand-rolled configs may store `['AI']`
 * directly — accept both so no site loses tags in the editor.
 */
export function parseTags(raw) {
    if (!Array.isArray(raw))
        return [];
    const out = [];
    for (const item of raw) {
        const value = typeof item === 'string'
            ? item
            : item && typeof item === 'object'
                ? item.tag
                : undefined;
        if (typeof value === 'string' && value.trim())
            out.push(value.trim());
    }
    return out;
}
function rowToEditorPost(postType, doc) {
    const data = doc.data ?? {};
    const dataTitle = dataStr(data, 'title');
    return {
        id: doc.id,
        postType,
        // Prefer the JSON field — the denormalized `doc.title` column can lag or
        // reflect slug-derived fallbacks on imported/legacy rows.
        title: dataTitle || (doc.title ?? ''),
        slug: doc.slug ?? dataStr(data, 'slug'),
        excerpt: dataStr(data, 'excerpt'),
        featuredImage: dataStr(data, 'featuredImage'),
        body: dataStr(data, 'body'),
        category: dataStr(data, 'category'),
        tags: parseTags(data.tags),
        status: doc.status ?? 'DRAFT',
        publishDate: data.publishDate ?? doc.scheduledAt ?? null,
        scheduledAt: doc.scheduledAt ?? null,
        scheduledUnpublishAt: doc.scheduledUnpublishAt ?? null,
        // Preserve unknown (externally-managed) sections so the editor can show
        // them read-only and round-trip them on save instead of dropping them.
        sections: coerceSections(data.sections, { onUnknown: 'preserve' }),
        seoTitle: dataStr(data, 'metaTitle'),
        seoDescription: dataStr(data, 'metaDescription'),
        publishedAt: doc.publishedAt ?? null,
        updatedAt: doc.updatedAt ?? null,
    };
}
function templateEndpoint(postType) {
    return `/post-templates/${encodeURIComponent(postType)}`;
}
/**
 * Load a post type's template via the dedicated, audited `/post-templates/:type`
 * endpoint (scoped + logged), rather than the raw collection API. When the type
 * has no saved template yet the endpoint returns the default starting template;
 * if the request fails outright we still hand back that default so the editor
 * is never blank.
 */
export async function fetchTemplateForEditor(postType) {
    const res = await cmsApi(templateEndpoint(postType));
    if (res.error || !res.data) {
        const seed = createPostTemplate(postType);
        return { docId: null, postType, header: seed.header, sections: seed.sections };
    }
    const r = res.data;
    const docId = r.docId ?? null;
    const header = coercePostHeader(r.header);
    let sections = coerceSections(r.sections, { onUnknown: 'preserve' });
    // Defend against an older server (or an empty stored template): an unsaved
    // type should still seed the default sections so authors aren't faced with a
    // blank canvas.
    if (!docId && sections.length === 0) {
        sections = createPostTemplate(postType).sections;
    }
    return { docId, postType, header, sections };
}
/** Persist a post type's template via `PUT /post-templates/:type`. */
export async function saveTemplate(template) {
    const res = await cmsApi(templateEndpoint(template.postType), {
        method: 'PUT',
        body: JSON.stringify({
            header: template.header,
            sections: template.sections.map(stripUnmanagedMarker),
        }),
    });
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to save template');
    const r = res.data;
    return {
        docId: r.docId ?? null,
        postType: template.postType,
        header: coercePostHeader(r.header),
        sections: coerceSections(r.sections, { onUnknown: 'preserve' }),
    };
}
// ─── Posts ───────────────────────────────────────────────────────────
export async function fetchPostForEditor(postType, id) {
    const res = await cmsApi(`/collections/${encodeURIComponent(postType)}/${id}`);
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to load post');
    return rowToEditorPost(postType, res.data);
}
/**
 * Strip the derived `unmanaged` marker before persisting — it is recomputed
 * from the registry on read, so persisting it would only stale.
 */
function stripUnmanagedMarker(section) {
    const { unmanaged: _unmanaged, ...rest } = section;
    return rest;
}
function toWriteBody(post, extra) {
    const payload = {
        title: post.title,
        slug: post.slug,
        excerpt: post.excerpt,
        featuredImage: post.featuredImage,
        body: post.body,
        category: post.category,
        // Store the canonical `tagsField` row shape so server-side array-field
        // validation and the public renderer see the schema they expect.
        tags: post.tags.map((tag) => ({ tag })),
        publishDate: post.publishDate,
        // Unmanaged sections are part of `post.sections`, so they round-trip on
        // every save (no data loss).
        sections: post.sections.map(stripUnmanagedMarker),
        metaTitle: post.seoTitle,
        metaDescription: post.seoDescription,
        ...extra,
    };
    return JSON.stringify(payload);
}
export async function createPost(post) {
    const res = await cmsApi(`/collections/${encodeURIComponent(post.postType)}`, {
        method: 'POST',
        body: toWriteBody(post),
    });
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to create post');
    return rowToEditorPost(post.postType, res.data);
}
export async function savePostDraft(post) {
    if (!post.id)
        return createPost(post);
    const res = await cmsApi(`/collections/${encodeURIComponent(post.postType)}/${post.id}`, { method: 'PUT', body: toWriteBody(post) });
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to save draft');
    return rowToEditorPost(post.postType, res.data);
}
export async function publishPost(post) {
    const endpoint = post.id
        ? `/collections/${encodeURIComponent(post.postType)}/${post.id}`
        : `/collections/${encodeURIComponent(post.postType)}`;
    const res = await cmsApi(endpoint, {
        method: post.id ? 'PUT' : 'POST',
        body: toWriteBody(post, { status: 'PUBLISHED' }),
    });
    if (res.error || !res.data)
        throw new Error(res.error ?? 'Failed to publish post');
    return rowToEditorPost(post.postType, res.data);
}
/** A blank post scaffold seeded with the post type's template sections. */
export async function emptyPostFromTemplate(postType) {
    let sections = [];
    try {
        const template = await fetchTemplateForEditor(postType);
        // Clone with fresh ids so the post owns its own sections, decoupled from
        // the template (editing the post never mutates the type template).
        // `generateSectionId` (not `createSection`) because preserved unmanaged
        // template sections have no registered type and `createSection` throws.
        sections = template.sections.map((s) => ({ ...s, id: generateSectionId() }));
    }
    catch {
        // No template / fetch failure → start from an empty section list.
        sections = [];
    }
    return {
        id: null,
        postType,
        title: '',
        slug: '',
        excerpt: '',
        featuredImage: '',
        body: '',
        category: '',
        tags: [],
        status: 'DRAFT',
        publishDate: null,
        scheduledAt: null,
        scheduledUnpublishAt: null,
        sections,
        seoTitle: '',
        seoDescription: '',
        publishedAt: null,
        updatedAt: null,
    };
}
//# sourceMappingURL=post-editor-service.js.map