import { Injectable, computed, inject, signal } from '@angular/core';
import { take } from 'rxjs/operators';
import { BootstrapConfig, DEFAULT_CATALOG_CONFIG, DEFAULT_HEADER_CONFIG, DEFAULT_PRODUCT_PAGE_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../../shared/models/config';
import { NavigationItemConfig, NavigationLocalizedText } from '../../../shared/models/config';
import { ConfigService } from '../../../core/config/config.service';
import { ProjectEditorIoService } from '../services/project-editor-io.service';
import { ProjectEditorPreviewService } from '../services/project-editor-preview.service';
import { LocaleSyncService } from '../services/locale-sync.service';
import { ProjectEditorState } from '../models/project-editor.model';
import { PlatformRuntimeService } from '../../../core/runtime/platform-runtime.service';
import { ProjectValidator, ProjectValidationIssue } from '../services/project-validator.service';
import { ProjectEditorSectionId } from '../models/project-editor.model';
import { EditorSchemaService } from '../schema/editor-schema.service';
import { FieldSchema } from '../schema/field-schema.model';
import { History, commit as commitHistory, emptyHistory, redo as redoHistory, undo as undoHistory } from '../schema/history.util';
import { ProjectEditorDraftStorageService } from '../services/project-editor-draft-storage.service';
import { EDITOR_SECTION_BOOTSTRAP_KEYS, BuilderSectionStatus } from '../models/project-editor.model';
import { BUILDER_GROUPS, BuilderGroupId } from '../builder/builder-groups.model';

function isFilled(value: unknown): boolean {
  if (value === null || value === undefined) {
    return false;
  }
  if (typeof value === 'string') {
    return value.trim().length > 0;
  }
  if (Array.isArray(value)) {
    return value.length > 0;
  }
  return true;
}

const HISTORY_DEBOUNCE_MS = 300;

export interface ChangeSummaryRow {
  readonly fieldKey: string;
  /** i18n key for the field's label. */
  readonly labelKey: string;
  readonly section?: ProjectEditorSectionId;
  readonly before: string;
  readonly after: string;
}

@Injectable({ providedIn: 'root' })
export class ProjectEditorFacade {
  private readonly configService = inject(ConfigService);
  private readonly ioService = inject(ProjectEditorIoService);
  private readonly previewService = inject(ProjectEditorPreviewService);
  private readonly localeSync = inject(LocaleSyncService);
  private readonly runtime = inject(PlatformRuntimeService);
  private readonly validator = inject(ProjectValidator);
  private readonly draftStorage = inject(ProjectEditorDraftStorageService);
  private readonly schema = inject(EditorSchemaService);

  private readonly history = signal<History<BootstrapConfig>>(emptyHistory());
  /** Snapshot captured at the start of an edit burst; committed to history once edits settle. */
  private pendingBaseline: BootstrapConfig | null = null;
  private historyTimer: ReturnType<typeof setTimeout> | null = null;

  private readonly state = signal<ProjectEditorState>({
    bootstrap: null,
    originalBootstrap: null,
    importError: null,
    activeSection: 'general',
    status: 'draft',
    lastSavedBootstrap: null,
    lastSavedAt: null,
    lastPublishedAt: null,
    draftRestored: false,
  });

  readonly bootstrap = computed(() => this.state().bootstrap);
  readonly importError = computed(() => this.state().importError);
  readonly activeSection = computed(() => this.state().activeSection);
  readonly homepagePage = computed(() => this.bootstrap()?.pages.find(page => page.key === 'home' || page.route.path === '/') ?? null);
  readonly homepageWidgets = computed(() => this.homepagePage()?.sections.flatMap(section => section.widgets.map(widget => ({ sectionId: section.id, sectionType: section.type, widget }))) ?? []);
  readonly status = computed(() => this.state().status);
  readonly lastSavedAt = computed(() => this.state().lastSavedAt);
  readonly lastPublishedAt = computed(() => this.state().lastPublishedAt);
  readonly draftRestored = computed(() => this.state().draftRestored);
  /** The originally loaded/published `staticPages` snapshot, for per-page modified-state comparison in the Static Pages editor. */
  readonly originalStaticPages = computed(() => this.state().originalBootstrap?.staticPages ?? null);
  readonly validationIssues = computed(() => {
    const current = this.bootstrap();
    return current ? this.validator.validate(current) : [];
  });
  /** Issues that block publishing (`severity === 'error'`), in declaration order. */
  readonly blockingIssues = computed(() => this.validationIssues().filter(issue => issue.severity === 'error'));
  readonly hasBlockingIssues = computed(() => this.blockingIssues().length > 0);
  /** Issues grouped by the schema field key they map to, for inline field errors. */
  readonly issuesByField = computed<Map<string, ProjectValidationIssue[]>>(() => {
    const map = new Map<string, ProjectValidationIssue[]>();
    for (const issue of this.validationIssues()) {
      if (!issue.fieldKey) {
        continue;
      }
      const bucket = map.get(issue.fieldKey);
      if (bucket) {
        bucket.push(issue);
      } else {
        map.set(issue.fieldKey, [issue]);
      }
    }
    return map;
  });
  /** Blocking-issue count per editor section, for nav badges. */
  readonly issuesBySection = computed<Map<ProjectEditorSectionId, number>>(() => {
    const map = new Map<ProjectEditorSectionId, number>();
    for (const issue of this.blockingIssues()) {
      if (!issue.section) {
        continue;
      }
      map.set(issue.section, (map.get(issue.section) ?? 0) + 1);
    }
    return map;
  });
  readonly dirty = computed(() => {
    const current = this.bootstrap();
    if (!current) {
      return false;
    }
    return JSON.stringify(current) !== JSON.stringify(this.state().lastSavedBootstrap);
  });
  readonly canUndo = computed(() => this.history().past.length > 0);
  readonly canRedo = computed(() => this.history().future.length > 0);
  /** Schema field keys whose current value differs from the originally loaded/published config. */
  readonly modifiedFields = computed<Set<string>>(() => {
    const current = this.bootstrap();
    const original = this.state().originalBootstrap;
    const set = new Set<string>();
    if (!current || !original) {
      return set;
    }
    for (const field of this.schema.all()) {
      if (JSON.stringify(this.schema.getByPath(current, field.key)) !== JSON.stringify(this.schema.getByPath(original, field.key))) {
        set.add(field.key);
      }
    }
    return set;
  });
  /** Editor sections that contain at least one modified field, for the nav modified indicator. */
  readonly modifiedSections = computed<Set<ProjectEditorSectionId>>(() => {
    const sections = new Set<ProjectEditorSectionId>();
    for (const key of this.modifiedFields()) {
      const field = this.schema.getField(key);
      if (field) {
        sections.add(field.section);
      }
    }
    return sections;
  });
  /** Required-field completeness per section (Sprint 3 Builder IA) - real schema data only, never guessed. */
  readonly sectionStatus = computed<Map<ProjectEditorSectionId, BuilderSectionStatus>>(() => {
    const current = this.bootstrap();
    const map = new Map<ProjectEditorSectionId, BuilderSectionStatus>();
    if (!current) {
      return map;
    }
    const modified = this.modifiedSections();
    const bySection = new Map<ProjectEditorSectionId, FieldSchema[]>();
    for (const field of this.schema.all()) {
      const list = bySection.get(field.section) ?? [];
      list.push(field);
      bySection.set(field.section, list);
    }
    for (const [section, fields] of bySection) {
      const required = fields.filter(f => f.required);
      if (required.length === 0) {
        map.set(section, modified.has(section) ? 'in-progress' : 'unknown');
        continue;
      }
      const filled = required.filter(f => isFilled(this.schema.getByPath(current, f.key))).length;
      map.set(section, filled === required.length ? 'complete' : filled === 0 ? 'not-started' : 'in-progress');
    }
    return map;
  });

  /** Rolls sectionStatus up to the Builder's business-oriented groups. */
  readonly groupStatus = computed<Map<BuilderGroupId, BuilderSectionStatus>>(() => {
    const sections = this.sectionStatus();
    const result = new Map<BuilderGroupId, BuilderSectionStatus>();
    for (const group of BUILDER_GROUPS) {
      const statuses = group.sections.map(id => sections.get(id) ?? 'unknown');
      if (statuses.some(s => s === 'in-progress') || (statuses.some(s => s === 'not-started') && statuses.some(s => s === 'complete'))) {
        result.set(group.id, 'in-progress');
      } else if (statuses.every(s => s === 'not-started')) {
        result.set(group.id, 'not-started');
      } else {
        const known = statuses.filter(s => s !== 'unknown');
        result.set(group.id, known.length > 0 && known.every(s => s === 'complete') ? 'complete' : 'unknown');
      }
    }
    return result;
  });

  /** % of schema-required fields filled across the whole config - the Builder overview's readiness number. Real data only. */
  readonly readinessPercent = computed<number | null>(() => {
    const current = this.bootstrap();
    if (!current) {
      return null;
    }
    const required = this.schema.all().filter(f => f.required);
    if (required.length === 0) {
      return null;
    }
    const filled = required.filter(f => isFilled(this.schema.getByPath(current, f.key))).length;
    return Math.round((filled / required.length) * 100);
  });

  /** First section with a required field still missing, in group display order - the overview's "recommended next step". */
  readonly nextRecommendedSection = computed<ProjectEditorSectionId | null>(() => {
    if (!this.bootstrap()) {
      return null;
    }
    const statuses = this.sectionStatus();
    for (const group of BUILDER_GROUPS) {
      for (const section of group.sections) {
        const status = statuses.get(section);
        if (status === 'not-started' || status === 'in-progress') {
          return section;
        }
      }
    }
    return null;
  });

  /** Per-field before/after diff vs the originally loaded/published config, for the pre-publish preview. */
  readonly changeSummary = computed<ChangeSummaryRow[]>(() => {
    const current = this.bootstrap();
    const original = this.state().originalBootstrap;
    if (!current || !original) {
      return [];
    }
    const rows: ChangeSummaryRow[] = [];
    for (const key of this.modifiedFields()) {
      const field = this.schema.getField(key);
      rows.push({
        fieldKey: key,
        labelKey: field?.labelKey ?? key,
        section: field?.section,
        before: this.stringifyValue(this.schema.getByPath(original, key)),
        after: this.stringifyValue(this.schema.getByPath(current, key)),
      });
    }
    return rows;
  });

  private stringifyValue(value: unknown): string {
    if (value === null || value === undefined) {
      return '';
    }
    if (typeof value === 'object') {
      const json = JSON.stringify(value);
      return json.length > 80 ? `${json.slice(0, 77)}…` : json;
    }
    return String(value);
  }

  loadBootstrap(): void {
    this.configService.loadBootstrap(true).pipe(take(1)).subscribe({
      next: config => {
        const normalized = this.normalize(JSON.parse(JSON.stringify(config)) as BootstrapConfig);
        const storedDraft = this.draftStorage.load(normalized.tenant.id);
        const restoredFromDraft = storedDraft !== null;
        const bootstrap = restoredFromDraft ? this.normalize(storedDraft!.bootstrap) : normalized;
        this.state.update(current => ({
          ...current,
          bootstrap,
          originalBootstrap: normalized,
          importError: null,
          lastSavedBootstrap: normalized,
          lastSavedAt: restoredFromDraft ? storedDraft!.savedAt : null,
          status: 'draft',
          draftRestored: restoredFromDraft,
        }));
        this.clearHistory();
      },
      error: () => this.state.update(current => ({ ...current, bootstrap: null, importError: 'builder.importError' })),
    });
  }

  updateBootstrap(updater: (current: BootstrapConfig) => BootstrapConfig): void {
    const current = this.state().bootstrap;
    if (!current) {
      return;
    }

    // Capture the pre-burst snapshot so a run of rapid edits collapses into one
    // undo step. The baseline is cleared when the debounce fires (or on undo).
    // A fresh burst also invalidates any redo stack immediately (not just once
    // the debounced commit lands) - otherwise Redo would stay enabled for up to
    // HISTORY_DEBOUNCE_MS after a post-undo edit and silently discard it.
    if (this.pendingBaseline === null) {
      this.pendingBaseline = current;
      if (this.history().future.length > 0) {
        this.history.update(history => ({ ...history, future: [] }));
      }
    }
    const next = this.normalize(updater(JSON.parse(JSON.stringify(current)) as BootstrapConfig));
    this.state.update(state => ({ ...state, bootstrap: next, draftRestored: false }));
    this.draftStorage.save(next);
    this.scheduleHistoryCommit();
  }

  private scheduleHistoryCommit(): void {
    if (this.historyTimer !== null) {
      clearTimeout(this.historyTimer);
    }
    this.historyTimer = setTimeout(() => this.flushHistory(), HISTORY_DEBOUNCE_MS);
  }

  /** Commits any pending edit burst to the undo stack immediately. */
  private flushHistory(): void {
    if (this.historyTimer !== null) {
      clearTimeout(this.historyTimer);
      this.historyTimer = null;
    }
    if (this.pendingBaseline === null) {
      return;
    }
    const baseline = this.pendingBaseline;
    this.pendingBaseline = null;
    this.history.update(history => commitHistory(history, baseline));
  }

  private clearHistory(): void {
    if (this.historyTimer !== null) {
      clearTimeout(this.historyTimer);
      this.historyTimer = null;
    }
    this.pendingBaseline = null;
    this.history.set(emptyHistory());
  }

  private applyHistoryState(bootstrap: BootstrapConfig): void {
    const restored = this.normalize(JSON.parse(JSON.stringify(bootstrap)) as BootstrapConfig);
    this.state.update(state => ({ ...state, bootstrap: restored, draftRestored: false }));
    this.draftStorage.save(restored);
  }

  /** Reverts the last edit burst. Flushes any in-flight burst first. */
  undo(): void {
    this.flushHistory();
    const current = this.state().bootstrap;
    if (!current) {
      return;
    }
    const step = undoHistory(this.history(), current);
    if (!step) {
      return;
    }
    this.history.set(step.history);
    this.applyHistoryState(step.value);
  }

  /** Re-applies the most recently undone burst. */
  redo(): void {
    const current = this.state().bootstrap;
    if (!current) {
      return;
    }
    const step = redoHistory(this.history(), current);
    if (!step) {
      return;
    }
    this.history.set(step.history);
    this.applyHistoryState(step.value);
  }

  /** i18n message key of the first issue mapped to a field, or null. Reactive: reads `issuesByField`. */
  fieldError(fieldKey: string): string | null {
    const issues = this.issuesByField().get(fieldKey);
    return issues && issues.length > 0 ? issues[0].message : null;
  }

  dismissDraftRestoredNotice(): void {
    this.state.update(current => ({ ...current, draftRestored: false }));
  }

  exportBootstrap(): string {
    const current = this.state().bootstrap;
    return current ? this.ioService.exportBootstrap(current) : '';
  }

  /**
   * Replaces the whole draft with an imported bootstrap. Routed through
   * updateBootstrap() (not a direct state.update) so an import gets the same
   * undo-history entry and draftStorage persistence as any other edit -
   * otherwise the import "worked" on screen but silently wasn't saved to the
   * localStorage draft, and Undo couldn't step back through it.
   */
  importBootstrap(raw: string): void {
    try {
      const imported = this.normalize(this.ioService.importBootstrap(raw));
      this.state.update(current => ({ ...current, importError: null }));
      this.updateBootstrap(() => imported);
    } catch {
      this.state.update(current => ({ ...current, importError: 'builder.importError' }));
    }
  }

  preview(): void {
    const current = this.state().bootstrap;
    if (!current) {
      return;
    }

    this.previewService.preview(current);
  }

  setActiveSection(activeSection: ProjectEditorState['activeSection']): void {
    this.state.update(current => ({ ...current, activeSection }));
  }

  addLocale(locale: string): void {
    this.updateBootstrap(current => this.localeSync.addLocale(current, locale));
  }

  removeLocale(locale: string): void {
    this.updateBootstrap(current => this.localeSync.removeLocale(current, locale));
  }

  setDefaultLocale(locale: string): void {
    this.updateBootstrap(current => ({
      ...current,
      tenant: { ...current.tenant, defaultLocale: locale },
      localization: { ...current.localization, defaultLocale: locale },
    }));
  }

  save(): void {
    const current = this.state().bootstrap;
    if (!current) {
      return;
    }
    const savedAt = this.draftStorage.save(current);
    this.state.update(state => ({ ...state, lastSavedBootstrap: JSON.parse(JSON.stringify(current)), lastSavedAt: savedAt }));
  }

  /** Reverts one section's bootstrap keys to the originally loaded/published snapshot. Caller is responsible for confirmation UX. */
  resetSection(sectionId: ProjectEditorState['activeSection']): void {
    const original = this.state().originalBootstrap;
    const keys = EDITOR_SECTION_BOOTSTRAP_KEYS[sectionId];
    if (!original || !keys) {
      return;
    }
    this.updateBootstrap(current => {
      const patch: Partial<BootstrapConfig> = {};
      for (const key of keys) {
        (patch as Record<string, unknown>)[key] = JSON.parse(JSON.stringify(original[key]));
      }
      return { ...current, ...patch };
    });
  }

  /**
   * Reverts a single schema-registered field to its originally loaded/published value.
   * Field-level counterpart to resetSection - same "revert to original" semantics, scoped to
   * one dot-path instead of a whole bootstrap key. No confirmation needed: it's a single-value
   * change that flows through updateBootstrap like any edit, so undo covers it.
   */
  resetField(key: string): void {
    const original = this.state().originalBootstrap;
    if (!original) {
      return;
    }
    const value = JSON.parse(JSON.stringify(this.schema.getByPath(original, key)));
    this.updateBootstrap(current => this.setByPath(current, key, value));
  }

  /** Immutable dot-path setter, the write-side counterpart to EditorSchemaService.getByPath. Only used for resetField's scalar/object schema paths - none of the registered field keys index into array elements. */
  private setByPath<T extends object>(source: T, key: string, value: unknown): T {
    const [head, ...rest] = key.split('.');
    if (rest.length === 0) {
      return { ...source, [head]: value };
    }
    const child = (source as Record<string, unknown>)[head];
    return {
      ...source,
      [head]: this.setByPath(typeof child === 'object' && child !== null ? child : {}, rest.join('.'), value),
    };
  }

  /** Discards the entire draft, reverting to the originally loaded/published bootstrap. Caller is responsible for confirmation UX. */
  resetDraft(): void {
    const original = this.state().originalBootstrap;
    if (!original) {
      return;
    }
    const reset = this.normalize(JSON.parse(JSON.stringify(original)) as BootstrapConfig);
    this.draftStorage.clear();
    this.state.update(state => ({
      ...state,
      bootstrap: reset,
      // Reset also clears the persisted draft, so there is nothing pending
      // to save at this point - keep `lastSavedBootstrap` in sync too,
      // otherwise `dirty` (computed against it) immediately flags
      // "unsaved changes" right after a full discard.
      lastSavedBootstrap: JSON.parse(JSON.stringify(reset)),
      lastSavedAt: null,
      draftRestored: false
    }));
    this.clearHistory();
  }

  publish(): boolean {
    const current = this.state().bootstrap;
    if (!current || this.hasBlockingIssues()) {
      return false;
    }
    // FH-E.5 / PHASE-9 §5. This is a LOCAL preview publish: it applies the
    // config to the in-memory runtime and saves the draft to localStorage.
    // localStorage is a recovery cache here (see draftRestored), never the
    // published source of truth. Real publish must round-trip through the
    // revision API - POST .../revisions/{id}/publish - which creates an
    // immutable server-side revision and flips the published pointer in one
    // transaction. Until that endpoint exists this stays local-only and must
    // not be treated as authoritative; when it ships, this method awaits the
    // server response and only then marks status 'published'.
    this.runtime.reloadFromBootstrap(current);
    const savedAt = this.draftStorage.save(current);
    this.state.update(state => ({
      ...state,
      status: 'published',
      lastSavedBootstrap: JSON.parse(JSON.stringify(current)),
      originalBootstrap: JSON.parse(JSON.stringify(current)),
      lastSavedAt: savedAt,
      lastPublishedAt: savedAt,
    }));
    this.clearHistory();
    return true;
  }

  /**
   * Inserts a nav link that points at a static page by id rather than a raw
   * route. `type: 'staticPage'` is already understood by the resolvers
   * (StaticPageResolverService / FooterResolverService derive the label and
   * route from the page itself) - this only closes the editor-side gap of
   * never being able to create that item shape.
   */
  addStaticPageNavLink(target: 'header' | 'footer', pageId: string): void {
    this.updateBootstrap(current => {
      const list = current.navigation[target];
      if (!this.isFlatNavList(list) || !pageId) {
        return current;
      }
      const items = list as NavigationItemConfig[];
      const newItem: NavigationItemConfig = {
        id: `nav-page-${Date.now()}`,
        type: 'staticPage',
        key: pageId,
        order: items.length + 1,
        visible: true,
      };
      return { ...current, navigation: { ...current.navigation, [target]: [...items, newItem] } };
    });
  }

  addNavLink(target: 'header' | 'footer'): void {
    this.updateBootstrap(current => {
      const list = current.navigation[target];
      if (!this.isFlatNavList(list)) {
        return current;
      }
      const items = list as NavigationItemConfig[];
      const newItem: NavigationItemConfig = {
        id: `nav-${Date.now()}`,
        label: 'New link',
        route: '/',
        order: items.length + 1,
        visible: true,
      };
      return { ...current, navigation: { ...current.navigation, [target]: [...items, newItem] } };
    });
  }

  removeNavLink(target: 'header' | 'footer', id: string): void {
    this.updateBootstrap(current => {
      const list = current.navigation[target];
      if (!this.isFlatNavList(list)) {
        return current;
      }
      return {
        ...current,
        navigation: { ...current.navigation, [target]: (list as NavigationItemConfig[]).filter(item => item.id !== id) },
      };
    });
  }

  updateNavLink(target: 'header' | 'footer', id: string, patch: Partial<NavigationItemConfig>): void {
    this.updateBootstrap(current => {
      const list = current.navigation[target];
      if (!this.isFlatNavList(list)) {
        return current;
      }
      return {
        ...current,
        navigation: {
          ...current.navigation,
          [target]: (list as NavigationItemConfig[]).map(item => (item.id !== id ? item : { ...item, ...patch })),
        },
      };
    });
  }

  /**
   * Updates a nav item's label without destroying other locales' translations.
   * If the item's current label is a plain string (or unset), it's replaced outright.
   * If it's already a NavigationLocalizedText object, only the current default
   * locale's key is overwritten; every other locale key is preserved untouched.
   */
  updateNavLinkLabel(target: 'header' | 'footer', id: string, value: string, locale?: string): void {
    this.updateBootstrap(current => {
      const list = current.navigation[target];
      if (!this.isFlatNavList(list)) {
        return current;
      }
      const defaultLocale = current.localization.defaultLocale ?? 'en';
      const targetLocale = locale ?? defaultLocale;
      return {
        ...current,
        navigation: {
          ...current.navigation,
          [target]: (list as NavigationItemConfig[]).map(item => {
            if (item.id !== id) {
              return item;
            }
            const currentLabel = item.label;
            let nextLabel: string | NavigationLocalizedText;
            if (currentLabel && typeof currentLabel === 'object') {
              nextLabel = { ...currentLabel, [targetLocale]: value };
            } else if (targetLocale === defaultLocale) {
              nextLabel = value;
            } else {
              nextLabel = { [defaultLocale]: currentLabel ?? '', [targetLocale]: value };
            }
            return { ...item, label: nextLabel };
          }),
        },
      };
    });
  }

  reorderNavLink(target: 'header' | 'footer', id: string, direction: -1 | 1): void {
    this.updateBootstrap(current => {
      const list = current.navigation[target];
      if (!this.isFlatNavList(list)) {
        return current;
      }
      const items = [...(list as NavigationItemConfig[])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
      const index = items.findIndex(item => item.id === id);
      const nextIndex = index + direction;
      if (index < 0 || nextIndex < 0 || nextIndex >= items.length) {
        return current;
      }
      const tmp = items[index];
      items[index] = items[nextIndex];
      items[nextIndex] = tmp;
      return {
        ...current,
        navigation: { ...current.navigation, [target]: items.map((item, order) => ({ ...item, order: order + 1 })) },
      };
    });
  }

  private isFlatNavList(list: NavigationItemConfig[] | { items: unknown }[]): list is NavigationItemConfig[] {
    return list.length === 0 || !('items' in list[0]);
  }

  private normalize(config: BootstrapConfig): BootstrapConfig {
    return {
      ...config,
      header: {
        ...DEFAULT_HEADER_CONFIG,
        ...(config.header ?? {})
      },
      catalog: {
        ...DEFAULT_CATALOG_CONFIG,
        ...(config.catalog ?? {})
      },
      productPage: {
        ...DEFAULT_PRODUCT_PAGE_CONFIG,
        ...(config.productPage ?? {})
      },
      userExperience: {
        ...DEFAULT_USER_EXPERIENCE_CONFIG,
        ...(config.userExperience ?? {})
      },
      localization: {
        ...config.localization,
        supportedLocales: config.localization?.supportedLocales?.length ? config.localization.supportedLocales : config.tenant.supportedLocales,
        defaultLocale: config.localization?.defaultLocale || config.tenant.defaultLocale,
        currencyByLocale: config.localization?.currencyByLocale ?? {},
        dictionaries: config.localization?.dictionaries ?? []
      },
      pages: [...(config.pages ?? [])].sort((left, right) => {
        const leftOrder = Math.min(...left.sections.map(section => section.order));
        const rightOrder = Math.min(...right.sections.map(section => section.order));
        return leftOrder - rightOrder;
      }),
      staticPages: this.normalizeStaticPagesShape(config.staticPages),
    };
  }

  /**
   * Backfills `enabled`/`status` on record-format static pages at the
   * bootstrap-normalization layer, mirroring (not importing - would create a
   * project-editor <-> content-management circular dependency, since
   * ContentManagementFacade already depends on this facade) the same default
   * ContentPageService.normalizePage applies for display: a page with no
   * enabled/status predates this field and was always live, so it defaults
   * to enabled+published rather than silently disappearing. Without this,
   * exportBootstrap() on a page nobody has touched this session would omit
   * both fields (present in the editor's display via normalize-on-read, but
   * literally absent from the stored/exported JSON) - the storefront
   * resolver was never at risk (it normalizes on every read), but export
   * fidelity should match what the editor shows.
   */
  private normalizeStaticPagesShape(staticPages: BootstrapConfig['staticPages']): BootstrapConfig['staticPages'] {
    if (!staticPages || Array.isArray(staticPages)) {
      return staticPages;
    }
    const result: typeof staticPages = {};
    for (const [key, page] of Object.entries(staticPages)) {
      result[key] = {
        ...page,
        enabled: page.enabled ?? true,
        status: page.status ?? 'published',
      };
    }
    return result;
  }
}
