'use client';
/**
 * This Source Code is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright (c) Infonomic Company Limited
 */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getAdminConfig } from '@byline/core';
import { useTranslation } from '@byline/i18n/react';
import { Alert, Button, ComboButton, LoaderEllipsis } from '@byline/ui/react';
import cx from 'clsx';
import { sliceFieldAdmin } from '../fields/field-admin';
import { FieldRenderer } from '../fields/field-renderer';
import { useBylineFieldServices } from '../fields/field-services-context';
import { AdminGroup } from '../presentation/group';
import { AdminRow } from '../presentation/row';
import { AdminTabs } from '../presentation/tabs';
import { AvailableLocalesWidget } from './available-locales-widget';
import { DocumentActions } from './document-actions';
import { FormProvider, useFieldValue, useFormContext } from './form-context';
import { NavigationGuardModal, SystemFieldsConfirmModal, UnsavedChangesModal } from './form-modals';
import styles from './form-renderer.module.css';
import { FormStatusDisplay } from './form-status-display';
import { useNavigationGuardAdapter } from './navigation-guard';
import { PathWidget } from './path-widget';
import { ScheduledPublicationCell, ScheduledPublicationNotice, useScheduledPublication, } from './scheduled-publication-control';
import { computeStatusTransitions } from './status-transitions';
import { TreePlacementWidget } from './tree-placement-widget';
import { executeUploadsWithProgress } from './upload-executor';
import { useFormLayout } from './use-form-layout';
const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRevision, onMutationError, onTreeMutationCommitted, scheduledPublicationsNeedReconfirmation = false, scheduledPublicationsHref, onReloadDocument, fields, onSubmit, onCancel, onStatusChange, onUnpublish, scheduledPublication, onSchedulePublication, onConfirmScheduledPublication, onCancelScheduledPublication, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, showPath = true, heading, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange, }) => {
    const { getFieldValues, runFieldHooks, validateForm, errors: initialErrors, hasChanges: hasChangesFn, resetHasChanges, getPatches, getDirtyBreakdown, getSystemPath, getSystemAvailableLocales, subscribeErrors, subscribeMeta, setFieldValue, setFieldError, getPendingUploads, clearPendingUploads, setFieldUploading, } = useFormContext();
    const { t } = useTranslation('byline-admin');
    const [errors, setErrors] = useState(initialErrors);
    const [hasChanges, setHasChanges] = useState(hasChangesFn());
    const [statusBusy, setStatusBusy] = useState(false);
    const [isUploading, setIsUploading] = useState(false);
    const submittingRef = useRef(false);
    const [isSubmitting, setIsSubmitting] = useState(false);
    const isBusy = isUploading || isSubmitting;
    const formRef = useRef(null);
    const focusBeforeBusyRef = useRef(null);
    const restoreFocusAfterBusyRef = useRef(false);
    // Block-only "save first" guard. Set true when the editor triggers a
    // guarded action (status change, duplicate, copy-to-locale) while the form
    // is dirty — those actions operate on the saved version, so unsaved edits
    // would be silently excluded.
    const [showUnsavedModal, setShowUnsavedModal] = useState(false);
    // Holds the pending Save payload while the editor confirms an immediate,
    // non-versioned system-field write (path / advertised locales). Non-null
    // means the confirmation modal is open. See docs/08-internationalization/index.md.
    const [pendingSystemFieldsSubmit, setPendingSystemFieldsSubmit] = useState(null);
    const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
    // Scheduled publication owns three placements — a status-bar cell, an
    // escalated notice, and the schedule modal — so its state lives here and the
    // surfaces are rendered where each belongs.
    const scheduling = useScheduledPublication({
        disabled: mutationsBlocked,
        schedule: scheduledPublication ?? null,
        onSchedule: onSchedulePublication,
        onConfirm: onConfirmScheduledPublication,
        onCancel: onCancelScheduledPublication,
        hasUnsavedChanges: hasChanges,
        onUnsavedChanges: () => setShowUnsavedModal(true),
    });
    const { uploadField } = useBylineFieldServices();
    // Path-widget wiring. The live preview must use the installation's
    // client-side slugifier (same function as `ServerConfig.slugifier`) so it
    // agrees with the persisted path. And when the `useAsPath` source is a
    // server-assigned `counter` or a read-only field, its value can't be
    // reproduced or changed through the form, so the widget suppresses its
    // source-derived preview and "Regenerate" affordance.
    const pathSlugifier = getAdminConfig().slugifier;
    const pathSourceLocked = useMemo(() => {
        if (!useAsPath)
            return false;
        const source = fields.find((f) => f.name === useAsPath);
        return source != null && (source.type === 'counter' || source.readOnly === true);
    }, [useAsPath, fields]);
    // Sync contentLocale when the route re-fetches with a different locale.
    useEffect(() => {
        if (initialLocale)
            setContentLocale(initialLocale);
    }, [initialLocale]);
    // Layout primitives + lookup tables — pure derivations of `adminConfig` +
    // `fields`. The validator at startup guarantees every reachable name
    // resolves and every schema field is placed at most once, so the render-time
    // lookups below are unguarded. See ./use-form-layout.
    const { fieldByName, tabSetByName, rowByName, groupByName, layout, fieldToTabPath } = useFormLayout(adminConfig, fields);
    // ---------------------------------------------------------------------
    // Active-tab state — one tab name per declared tab set.
    // Lifted into FormRenderer via `_activeTabBySet` / `_onTabChange` so the
    // user's tab choices survive the locale-change remount triggered by
    // FormProvider's `key` prop.
    // ---------------------------------------------------------------------
    const tabSets = adminConfig?.tabSets ?? [];
    const initialActiveTabBySet = useMemo(() => {
        const result = {};
        for (const set of tabSets) {
            const saved = _activeTabBySet?.[set.name];
            if (saved && set.tabs.some((t) => t.name === saved)) {
                result[set.name] = saved;
            }
            else {
                result[set.name] = set.tabs[0]?.name ?? '';
            }
        }
        return result;
        // initial-only; subsequent updates flow through setActiveTabBySet.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [tabSets, _activeTabBySet]);
    const [activeTabBySet, setActiveTabBySet] = useState(initialActiveTabBySet);
    const handleTabChange = useCallback((tabSetName, tabName) => {
        setActiveTabBySet((prev) => ({ ...prev, [tabSetName]: tabName }));
        _onTabChange?.(tabSetName, tabName);
    }, [_onTabChange]);
    // Track live form data so TabDefinition.condition functions can react to
    // field changes. Re-evaluated per keystroke via the meta-subscribe loop.
    const [formData, setFormData] = useState(() => getFieldValues());
    // Live document heading — tracks the useAsTitle field as the user types
    const liveTitle = useFieldValue(useAsTitle ?? '');
    const computedHeading = heading ||
        liveTitle ||
        (headingLabel
            ? mode === 'create'
                ? t('forms.heading.createLabel', { label: headingLabel })
                : t('forms.heading.editLabel', { label: headingLabel })
            : mode === 'create'
                ? t('forms.heading.create')
                : t('forms.heading.edit'));
    // Navigation guard — block router navigation and browser unload when dirty.
    // The guard hook is injected by the consuming framework (prop > context > no-op fallback).
    const guardFromContext = useNavigationGuardAdapter();
    const useGuard = useNavigationGuardProp ?? guardFromContext;
    const [discarding, setDiscarding] = useState(false);
    const [reloadFailed, setReloadFailed] = useState(false);
    const warningRef = useRef(null);
    const mutationBlockedRef = useRef(mutationsBlocked);
    mutationBlockedRef.current = mutationsBlocked || discarding;
    const guard = useGuard(hasChanges && !discarding);
    useEffect(() => {
        if (mutationIssue)
            warningRef.current?.focus();
    }, [mutationIssue]);
    useEffect(() => {
        if (!discarding)
            return;
        // Let the guard's beforeunload listener detach before the explicit discard.
        Promise.resolve()
            .then(() => (onReloadDocument ? onReloadDocument() : window.location.reload()))
            .catch(() => {
            setDiscarding(false);
            setReloadFailed(true);
        });
    }, [discarding, onReloadDocument]);
    // Compute available status transitions
    const currentStatus = initialData?.status;
    const { primaryStatus, secondaryStatuses, isTerminal } = computeStatusTransitions(currentStatus, workflowStatuses, nextStatus);
    useEffect(() => {
        return subscribeErrors((newErrors) => setErrors(newErrors));
    }, [subscribeErrors]);
    useEffect(() => {
        return subscribeMeta(() => setHasChanges(hasChangesFn()));
    }, [subscribeMeta, hasChangesFn]);
    // Keep formData in sync for evaluating TabDefinition.condition functions
    useEffect(() => {
        return subscribeMeta(() => setFormData(getFieldValues()));
    }, [subscribeMeta, getFieldValues]);
    // `inert` removes the active control from the tab order while a save is in
    // flight. Restore the editor's position after React has removed `inert`;
    // when the original control became disabled, fall back to the first usable
    // form control instead of leaving focus on <body>.
    useEffect(() => {
        if (isBusy || !restoreFocusAfterBusyRef.current)
            return;
        restoreFocusAfterBusyRef.current = false;
        if (mutationIssue) {
            warningRef.current?.focus();
            focusBeforeBusyRef.current = null;
            return;
        }
        const original = focusBeforeBusyRef.current;
        focusBeforeBusyRef.current = null;
        const originalCanReceiveFocus = original?.isConnected === true && !original.matches(':disabled, [aria-disabled="true"]');
        const target = originalCanReceiveFocus
            ? original
            : formRef.current?.querySelector('input:not(:disabled), textarea:not(:disabled), select:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex="-1"])');
        target?.focus({ preventScroll: true });
    }, [isBusy, mutationIssue]);
    const captureFocusBeforeBusy = useCallback(() => {
        if (focusBeforeBusyRef.current != null)
            return;
        const activeElement = document.activeElement;
        if (!(activeElement instanceof HTMLElement) || !formRef.current?.contains(activeElement))
            return;
        focusBeforeBusyRef.current = activeElement;
        restoreFocusAfterBusyRef.current = true;
    }, []);
    const handleCancel = () => {
        if (onCancel && typeof onCancel === 'function') {
            onCancel();
        }
    };
    // Await the host handler. Resolution means the save succeeded and the clean
    // baseline can be committed; rejection preserves dirty state so the editor
    // does not lose work and the navigation guard keeps blocking. Host handlers
    // that surface their own toast MUST rethrow afterwards.
    // Re-entry guard lives in a ref so the callback identity does not change
    // mid-flight; the mirrored state drives the Save button's disabled prop.
    const submitPayload = useCallback(async (payload) => {
        if (mutationBlockedRef.current || typeof onSubmit !== 'function')
            return;
        if (submittingRef.current)
            return;
        submittingRef.current = true;
        captureFocusBeforeBusy();
        setIsSubmitting(true);
        try {
            await onSubmit(payload);
            resetHasChanges();
        }
        catch {
            // Intentionally swallowed here — the host has already reported the
            // failure to the user. Dirty state is preserved by not resetting.
        }
        finally {
            submittingRef.current = false;
            setIsSubmitting(false);
        }
    }, [captureFocusBeforeBusy, onSubmit, resetHasChanges]);
    const handleSubmit = (e) => {
        if (mutationBlockedRef.current) {
            e.preventDefault();
            return;
        }
        e.preventDefault();
        // Run field-level beforeValidate hooks (submit-time), then validate
        void (async () => {
            const hookErrors = await runFieldHooks(fields);
            const formErrors = validateForm(fields);
            const allErrors = [...hookErrors, ...formErrors];
            if (allErrors.length > 0) {
                console.error('Form validation failed:', allErrors);
                return;
            }
            if (mutationBlockedRef.current)
                return;
            // Execute any pending uploads before submitting
            const pendingUploads = getPendingUploads();
            if (pendingUploads.size > 0) {
                captureFocusBeforeBusy();
                setIsUploading(true);
                try {
                    const uploadResult = await executeUploadsWithProgress(pendingUploads, uploadField, ({ fieldPath, status }) => {
                        setFieldUploading(fieldPath, status === 'uploading');
                    }, {
                        // Document context for server-side upload hooks: the persisted
                        // document id (edit mode only) plus any `upload.context` form
                        // values declared on the schema field. See UploadConfig.context
                        // in @byline/core.
                        documentId: mode === 'edit' && typeof initialData?.id === 'string' ? initialData.id : undefined,
                        fields,
                        getFormValues: getFieldValues,
                    });
                    // Check for upload errors
                    if (!uploadResult.allSucceeded) {
                        // Set field-level errors for failed uploads
                        for (const [fieldPath, errorMessage] of uploadResult.errors.entries()) {
                            setFieldError(fieldPath, t('forms.uploadFailedFieldError', { message: errorMessage }));
                        }
                        console.error('One or more uploads failed:', uploadResult.errors);
                        setIsUploading(false);
                        return;
                    }
                    // Replace pending StoredFileValues with real ones in form data
                    for (const [fieldPath, storedFile] of uploadResult.successful.entries()) {
                        setFieldValue(fieldPath, storedFile);
                    }
                    // Clear pending uploads (blob URLs already revoked by clearPendingUploads)
                    clearPendingUploads();
                }
                catch (err) {
                    console.error('Upload execution error:', err);
                    setIsUploading(false);
                    return;
                }
                setIsUploading(false);
            }
            const data = getFieldValues();
            const patches = getPatches();
            const { contentDirty, pathDirty, availableLocalesDirty, reason } = getDirtyBreakdown();
            const systemPath = getSystemPath();
            // Only emit the advertised-locale set for collections that opted into the
            // widget — otherwise leave it undefined so the write path never touches
            // `byline_document_available_locales` for non-advertising collections.
            const systemAvailableLocales = advertiseLocales ? getSystemAvailableLocales() : undefined;
            const payload = {
                data,
                patches,
                contentDirty,
                pathDirty,
                systemPath,
                availableLocalesDirty,
                systemAvailableLocales,
            };
            // Editing the document-grain system fields (path / advertised locales) is
            // an immediate, non-versioned write that does NOT reset workflow status,
            // so confirm it before saving. Create mode writes everything as part of
            // the initial version, so no confirmation applies there.
            if (mode === 'edit' && (reason === 'direct-write' || reason === 'both')) {
                setPendingSystemFieldsSubmit(payload);
                return;
            }
            await submitPayload(payload);
        })();
    };
    // Per-tab-set error counts: { [tabSetName]: { [tabName]: count } }.
    // Each <Tabs> bar consumes its own slice.
    const tabErrorCountsBySet = useMemo(() => {
        const result = {};
        for (const err of errors) {
            const path = fieldToTabPath.get(err.field);
            if (!path)
                continue;
            result[path.tabSetName] ??= {};
            result[path.tabSetName][path.tabName] = (result[path.tabSetName]?.[path.tabName] ?? 0) + 1;
        }
        return result;
    }, [errors, fieldToTabPath]);
    // -------------------------------------------------------------------
    // Layout walk — recursively dispatches each name in a region to the
    // appropriate primitive renderer or to <FieldRenderer>.
    // -------------------------------------------------------------------
    const renderField = (fieldName) => {
        const field = fieldByName.get(fieldName);
        if (!field)
            return null;
        return (<FieldRenderer key={field.name} field={field} defaultValue={initialData?.fields?.[field.name]} contentLocale={contentLocale} components={adminConfig?.fields?.[field.name]?.components} editor={adminConfig?.fields?.[field.name]?.editor} fieldAdmin={sliceFieldAdmin(adminConfig?.fields, field.name)}/>);
    };
    const renderItem = (name) => {
        const tabSet = tabSetByName.get(name);
        if (tabSet)
            return renderTabSet(tabSet);
        const group = groupByName.get(name);
        if (group)
            return renderGroup(group);
        const row = rowByName.get(name);
        if (row)
            return renderRow(row);
        return renderField(name);
    };
    const renderRow = (row) => (<AdminRow key={`row:${row.name}`}>{row.fields.map((name) => renderField(name))}</AdminRow>);
    const renderGroup = (group) => (<AdminGroup key={`group:${group.name}`} label={group.label}>
      {group.fields.map((name) => renderItem(name))}
    </AdminGroup>);
    const renderTabSet = (set) => {
        const visibleTabs = set.tabs.filter((tab) => !tab.condition || tab.condition(formData));
        const requested = activeTabBySet[set.name] ?? '';
        const resolvedActive = visibleTabs.length > 0 && !visibleTabs.some((t) => t.name === requested)
            ? (visibleTabs[0]?.name ?? requested)
            : requested;
        const activeTab = visibleTabs.find((t) => t.name === resolvedActive);
        return (<div key={`tabset:${set.name}`} className={cx('byline-form-tabset', styles.tabset)}>
        {visibleTabs.length > 0 && (<AdminTabs tabs={visibleTabs} activeTab={resolvedActive} onChange={(tabName) => handleTabChange(set.name, tabName)} errorCounts={tabErrorCountsBySet[set.name]} className={cx('byline-form-tabset-tabs', styles['tabset-tabs'])}/>)}
        {activeTab && (<div className={cx('byline-form-tabset-fields', styles['tabset-fields'])}>
            {activeTab.fields.map((name) => renderItem(name))}
          </div>)}
      </div>);
    };
    const busyAnnouncement = isUploading
        ? t('forms.actions.uploading')
        : isSubmitting
            ? t('forms.actions.saving')
            : '';
    return (<>
      <span className={cx('byline-form-busy-status', styles['busy-status'])} role="status" aria-live="polite">
        {busyAnnouncement}
      </span>
      <div aria-busy={isBusy}>
        <form ref={formRef} method="post" noValidate onSubmit={handleSubmit} className={cx('byline-form', styles.form)} inert={isBusy ? true : undefined}>
          <div className={cx('byline-form-heading-row', styles['heading-row'])}>
            <h1 className={cx('byline-form-heading', styles.heading)}>{computedHeading}</h1>
            {/* Source-locale anchor indicator removed pending heading-layout work.
        To re-enable: render `<SourceLocaleBadge locale={sourceLocale} />`
        here from `initialData.sourceLocale` (mismatch-only is the intended
        end state). See docs/08-internationalization/index.md. */}
            {headerSlot}
          </div>
          <div className={cx('byline-form-status-bar', styles['status-bar'])}>
            <div className={cx('byline-form-status-details', styles['status-details'])}>
              <FormStatusDisplay disabled={mutationsBlocked || discarding} initialData={initialData} workflowStatuses={workflowStatuses} publishedVersion={publishedVersion} onUnpublish={onUnpublish} afterStatusCells={<ScheduledPublicationCell state={scheduling.state} timeZone={scheduling.timeZone}/>}/>
            </div>
            <div className={cx('byline-form-actions', styles.actions)}>
              <Button className={cx('byline-form-actions-button', styles['actions-button'])} size="sm" intent="noeffect" type="button" onClick={handleCancel}>
                {hasChanges === false ? t('common.actions.close') : t('common.actions.cancel')}
              </Button>
              <Button className={cx('byline-form-actions-button', styles['actions-button'])} size="sm" type="submit" disabled={mutationsBlocked ||
            discarding ||
            hasChanges === false ||
            isUploading ||
            isSubmitting} aria-label={isSubmitting ? t('common.actions.save') : undefined}>
                {isUploading ? (t('forms.actions.uploading')) : (<span className={cx('byline-form-save-content', styles['save-content'])}>
                    <span className={cx('byline-form-save-label', styles['save-label'], isSubmitting && styles['save-label-hidden'])}>
                      {t('common.actions.save')}
                    </span>
                    {isSubmitting ? (<span className={cx('byline-form-save-loader', styles['save-loader'])}>
                        <LoaderEllipsis size={28} aria-hidden="true"/>
                      </span>) : null}
                  </span>)}
              </Button>
              {primaryStatus && onStatusChange && (<div className={cx('byline-form-actions-status-wrap', styles['actions-status-wrap'])}>
                  <ComboButton buttonClassName={cx('byline-form-actions-combo-button', styles['actions-combo-button'])} triggerClassName={cx('byline-form-actions-combo-trigger', styles['actions-combo-trigger'])} options={secondaryStatuses.map((s) => ({
                label: isTerminal
                    ? t('forms.actions.revertTo', { label: s.label ?? s.name })
                    : (s.verb ?? s.label ?? s.name),
                value: s.name,
            }))} sideOffset={5} size="sm" type="button" intent={isTerminal ? 'info' : 'success'} disabled={mutationsBlocked || discarding || statusBusy} onOptionSelect={async (value) => {
                if (mutationBlockedRef.current)
                    return;
                if (hasChanges) {
                    setShowUnsavedModal(true);
                    return;
                }
                setStatusBusy(true);
                try {
                    await onStatusChange(value);
                }
                catch (error) {
                    onMutationError?.(error);
                }
                finally {
                    setStatusBusy(false);
                }
            }} onButtonClick={isTerminal
                ? undefined
                : async () => {
                    if (mutationBlockedRef.current)
                        return;
                    if (hasChanges) {
                        setShowUnsavedModal(true);
                        return;
                    }
                    setStatusBusy(true);
                    try {
                        await onStatusChange(primaryStatus.name);
                    }
                    catch (error) {
                        onMutationError?.(error);
                    }
                    finally {
                        setStatusBusy(false);
                    }
                }}>
                    {statusBusy
                ? '...'
                : isTerminal
                    ? (primaryStatus.label ?? primaryStatus.name)
                    : (primaryStatus.verb ?? primaryStatus.label ?? primaryStatus.name)}
                  </ComboButton>
                </div>)}
              <DocumentActions disabled={mutationsBlocked || discarding} publishedVersion={publishedVersion} onUnpublish={onUnpublish} onDelete={onDelete} onDuplicate={onDuplicate} sourceTitle={useAsTitle != null && initialData != null
            ? initialData[useAsTitle]
            : null} onCopyToLocale={onCopyToLocale} sourceLocale={contentLocale} contentLocales={contentLocales} hasUnsavedChanges={hasChanges} onUnsavedChanges={() => setShowUnsavedModal(true)} onDeleteLocale={onDeleteLocale} defaultLocale={defaultLocale} availableLocales={initialData?._availableVersionLocales} scheduledPublicationState={scheduling.state} onSchedulePublication={scheduling.openSchedule} onConfirmScheduledPublication={scheduling.confirm} onCancelScheduledPublication={scheduling.cancel}/>
            </div>
          </div>
          {(mutationIssue || scheduledPublicationsNeedReconfirmation) && (<div ref={warningRef} tabIndex={-1} role="alert" aria-live="assertive" className={cx('byline-document-concurrency', styles.concurrency)}>
              {mutationIssue && (<Alert intent="warning" icon close={false} title={t(`documentConcurrency.${mutationIssue}Title`)}>
                  <p>{t(`documentConcurrency.${mutationIssue}`)}</p>
                  {mutationIssue !== 'committed' && (<Button type="button" disabled={discarding} onClick={() => {
                        setReloadFailed(false);
                        setDiscarding(true);
                    }}>
                      {t('documentConcurrency.reloadAction')}
                    </Button>)}
                  {reloadFailed && <p>{t('documentConcurrency.reloadFailed')}</p>}
                </Alert>)}
              {scheduledPublicationsNeedReconfirmation && (<Alert intent="warning" icon close={false} title={t('documentConcurrency.schedulesTitle')}>
                  <p>{t('documentConcurrency.schedules')}</p>
                  {scheduledPublicationsHref && (<a href={scheduledPublicationsHref}>
                      {t('documentConcurrency.reviewSchedules')}
                    </a>)}
                </Alert>)}
            </div>)}
          <ScheduledPublicationNotice state={scheduling.state} timeZone={scheduling.timeZone} busy={mutationsBlocked || discarding || scheduling.busy} onConfirm={scheduling.confirm} onReschedule={scheduling.openSchedule} onCancel={scheduling.cancel}/>
          {scheduling.modal}
          {restoreWarnings && restoreWarnings.length > 0 && (<Alert className="m-0 mt-4" intent="warning" icon={true} close={false} title={t('forms.restoreWarnings.title')}>
              <p>{t('forms.restoreWarnings.body', { count: restoreWarnings.length })}</p>
              <ul>
                {restoreWarnings.map((w) => (<li key={w}>{w}</li>))}
              </ul>
            </Alert>)}
          <div className={cx('byline-form-layout', styles.layout)}>
            <div className={cx('byline-form-content', styles.content)}>
              {layout.main.map((name) => renderItem(name))}
            </div>
            <div className={cx('byline-form-sidebar', styles.sidebar)}>
              {showPath &&
            (useAsPath ||
                (typeof initialData?.path === 'string' && initialData.path.length > 0)) && (<PathWidget disabled={mutationsBlocked || discarding} useAsPath={useAsPath} collectionPath={collectionPath ?? ''} defaultLocale={defaultLocale} activeLocale={contentLocale} mode={mode} slugifier={pathSlugifier} sourceLocked={pathSourceLocked}/>)}
              {tree && mode === 'edit' && typeof initialData?.id === 'string' && (<TreePlacementWidget disabled={mutationsBlocked || discarding} onMutationError={onMutationError} onCommitted={onTreeMutationCommitted} expectedRevision={observedRevision ?? initialData.revision} collectionPath={collectionPath ?? ''} documentId={initialData.id} useAsTitle={useAsTitle}/>)}
              {advertiseLocales && (<AvailableLocalesWidget disabled={mutationsBlocked || discarding} contentLocales={contentLocales ?? []} availableVersionLocales={initialData?._availableVersionLocales ?? []}/>)}
              {(layout.sidebar ?? []).map((name) => renderItem(name))}
            </div>
          </div>
          {showUnsavedModal && <UnsavedChangesModal onClose={() => setShowUnsavedModal(false)}/>}
          {!mutationsBlocked && !discarding && pendingSystemFieldsSubmit != null && (<SystemFieldsConfirmModal contentDirty={pendingSystemFieldsSubmit.contentDirty} pathDirty={pendingSystemFieldsSubmit.pathDirty} availableLocalesDirty={pendingSystemFieldsSubmit.availableLocalesDirty} onCancel={() => setPendingSystemFieldsSubmit(null)} onConfirm={() => {
                const payload = pendingSystemFieldsSubmit;
                setPendingSystemFieldsSubmit(null);
                void submitPayload(payload);
            }}/>)}
          {guard.isBlocked && (<NavigationGuardModal onStay={guard.stay} onProceed={guard.proceed}/>)}
        </form>
      </div>
    </>);
};
export const FormRenderer = (props) => {
    const { mode, initialData, initialLocale, collectionPath } = props;
    // Persists per-tab-set active tab across locale-change remounts of FormContent.
    // useRef so mutations never trigger a re-render of FormRenderer itself.
    const savedTabsRef = useRef({});
    return (<FormProvider key={`${initialLocale ?? 'default'}-${initialData?.versionId ?? ''}`} initialData={initialData} documentId={mode === 'edit' && typeof initialData?.id === 'string' ? initialData.id : null} collectionPath={collectionPath ?? null}>
      {/* Forwarded wholesale. An enumerated prop list here silently dropped
            every prop added to FormRendererProps but not copied down — which is
            how the scheduled-publication handlers reached FormContent as
            `undefined` and the control never rendered. */}
      <FormContent {...props} _activeTabBySet={savedTabsRef.current} _onTabChange={(tabSetName, tabName) => {
            savedTabsRef.current = { ...savedTabsRef.current, [tabSetName]: tabName };
        }}/>
    </FormProvider>);
};
