(function (root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  root.ForgeCore = api;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
  'use strict';

  const APP_ID = 'shire-marketing-forge-easy';
  const VERSION = 4;
  const OPERATOR = 'Ray Morrison';
  const BOUNDARY = 'SHIRE3D_ONLY';
  const PLATFORMS = ['facebook', 'instagram', 'linkedin', 'tiktok', 'youtube_shorts'];
  const PLATFORM_LIMITS = { facebook: 63206, instagram: 2200, linkedin: 3000, tiktok: 2200, youtube_shorts: 5000 };
  const DEFAULT_PRODUCT_SOURCES = [
    { id: 'shire3d_com', name: 'shire3d.com', url: 'https://shire3d.com/', kind: 'website', enabled: true, autoSync: true, intervalHours: 6, trusted: true },
    { id: 'shire3d_com_au', name: 'shire3d.com.au', url: 'https://shire3d.com.au/s/shop', kind: 'website', enabled: true, autoSync: true, intervalHours: 6, trusted: true },
    { id: 'shire3d_etsy', name: 'shire3d.etsy.com', url: 'https://www.etsy.com/au/shop/Shire3D', kind: 'etsy', enabled: true, autoSync: true, intervalHours: 6, trusted: true }
  ];
  const SOURCE_PRIORITY = { shire3d_com: 1, shire3d_com_au: 2, shire3d_etsy: 3 };
  const BLOCKED_IMPORT_TERMS = [
    'kitchen hero', 'kitchen-hero', 'kitchen_hero', 'recipe', 'recipes',
    'household', 'subscription', 'stripe', 'customer', 'customers'
  ];

  function uid(prefix) {
    const random = Math.random().toString(36).slice(2, 10);
    return `${prefix}_${Date.now().toString(36)}_${random}`;
  }

  function nowIso() {
    return new Date().toISOString();
  }

  function deepClone(value) {
    return JSON.parse(JSON.stringify(value));
  }

  function demoProduct() {
    return {
      id: 'product_demo_spawn_of_hell',
      name: 'DEMO — Spawn of Hell Gothic Dice Tower',
      description: 'A dramatic gothic dice tower concept with an internal dice path and a catching tray. Replace this demo with the approved SHIRE3D listing before publishing.',
      price: 49.95,
      productionCost: 8.4,
      printHours: 18,
      material: 'PLA',
      colours: 'Black, bone, crimson accents',
      stockStatus: 'prototype',
      ebayListing: '',
      imageData: '',
      imageName: '',
      sourceFiles: ['spawn-of-hell-demo.blend', 'spawn-of-hell-demo.stl'],
      approved: false,
      isDemo: true,
      createdAt: nowIso(),
      updatedAt: nowIso()
    };
  }

  function defaultConnections() {
    return PLATFORMS.map((platform) => ({
      id: `connection_${platform}`,
      platform,
      accountName: `SHIRE3D ${platformLabel(platform)}`,
      status: 'not_connected',
      mode: 'official_oauth',
      configured: false,
      connectedAt: null,
      sessionOnly: true,
      hasCredentials: false
    }));
  }

  function createInitialState() {
    return {
      appId: APP_ID,
      version: VERSION,
      boundary: BOUNDARY,
      operator: OPERATOR,
      createdAt: nowIso(),
      updatedAt: nowIso(),
      copyMigrationVersion: 2,
      products: [demoProduct()],
      campaigns: [],
      posts: [],
      queue: [],
      productSources: DEFAULT_PRODUCT_SOURCES.map((source) => ({ ...source, status: 'not_synced', lastSyncAt: null, lastError: '', itemCount: 0 })),
      connections: defaultConnections(),
      control: {
        globalPaused: true,
        emergencyStop: false,
        updatedBy: OPERATOR,
        updatedAt: nowIso()
      },
      audit: [{
        id: uid('audit'),
        type: 'app_created',
        details: 'Easy-mode Forge created locally. Every live product is included automatically. Publishing is paused until Ray approves exact posts and enabled connectors.',
        actor: 'SYSTEM',
        createdAt: nowIso()
      }]
    };
  }

  function ensureState(candidate) {
    if (!candidate || typeof candidate !== 'object' || candidate.appId !== APP_ID) {
      return createInitialState();
    }
    const state = deepClone(candidate);
    state.version = VERSION;
    state.boundary = BOUNDARY;
    state.operator = OPERATOR;
    state.products = Array.isArray(state.products) ? state.products : [];
    state.campaigns = Array.isArray(state.campaigns) ? state.campaigns : [];
    state.posts = Array.isArray(state.posts) ? state.posts : [];
    state.campaigns.forEach((campaign) => {
      campaign.productIds = Array.isArray(campaign.productIds)
        ? campaign.productIds.filter(Boolean)
        : (campaign.productId ? [campaign.productId] : []);
      // New campaigns are always full-catalogue. Historical campaigns keep their product IDs for audit integrity.
      campaign.productMode = campaign.productMode === 'all' ? 'all' : (campaign.status === 'approved' || campaign.status === 'published' ? campaign.productMode || 'legacy' : 'legacy');
      campaign.productCount = Number(campaign.productCount || campaign.productIds.length || (campaign.productId ? 1 : 0));
    });
    state.queue = Array.isArray(state.queue) ? state.queue : [];
    const savedConnections = Array.isArray(state.connections) ? state.connections : [];
    state.connections = defaultConnections().map((base) => {
      const saved = savedConnections.find((item) => item && item.platform === base.platform) || {};
      const savedStatus = saved.status === 'sandbox' ? 'not_connected' : saved.status;
      return { ...base, ...saved, id: base.id, platform: base.platform, status: savedStatus || 'not_connected', hasCredentials: false };
    });
    const savedSources = Array.isArray(state.productSources) ? state.productSources : [];
    state.productSources = DEFAULT_PRODUCT_SOURCES.map((source) => {
      const saved = savedSources.find((item) => item && item.id === source.id) || {};
      return { ...source, ...saved, id: source.id, name: source.name, url: source.url, kind: source.kind, trusted: true };
    });
    state.audit = Array.isArray(state.audit) ? state.audit : [];
    state.control = state.control || { globalPaused: true, emergencyStop: false };
    if (typeof state.control.globalPaused !== 'boolean') state.control.globalPaused = true;
    if (typeof state.control.emergencyStop !== 'boolean') state.control.emergencyStop = false;
    if (Number(state.copyMigrationVersion || 0) < 2) {
      migrateLegacyDraftCopy(state);
      state.copyMigrationVersion = 2;
    }
    return state;
  }

  function addAudit(state, type, details, actor) {
    state.audit.unshift({
      id: uid('audit'),
      type,
      details,
      actor: actor || OPERATOR,
      createdAt: nowIso()
    });
    state.audit = state.audit.slice(0, 500);
    state.updatedAt = nowIso();
  }

  function sanitizeText(value, maxLength) {
    return String(value == null ? '' : value).trim().slice(0, maxLength || 5000);
  }

  function numberOrZero(value) {
    const n = Number(value);
    return Number.isFinite(n) ? n : 0;
  }

  function saveProduct(stateInput, productInput) {
    const state = ensureState(stateInput);
    const input = productInput || {};
    const name = sanitizeText(input.name, 140);
    if (!name) throw new Error('Product name is required.');

    const existingIndex = state.products.findIndex((p) => p.id === input.id);
    const existing = existingIndex >= 0 ? state.products[existingIndex] : null;
    const product = {
      id: existing ? existing.id : uid('product'),
      name,
      description: sanitizeText(input.description, 4000),
      price: numberOrZero(input.price),
      productionCost: numberOrZero(input.productionCost),
      printHours: numberOrZero(input.printHours),
      material: sanitizeText(input.material, 100),
      colours: sanitizeText(input.colours, 300),
      stockStatus: sanitizeText(input.stockStatus || 'ready', 60),
      ebayListing: sanitizeText(input.ebayListing || input.listingUrl, 2000),
      listingUrl: sanitizeText(input.listingUrl || input.ebayListing, 2000),
      imageData: sanitizeText(input.imageData || input.imageUrl, 2000000),
      imageName: sanitizeText(input.imageName, 200),
      sourceId: sanitizeText(input.sourceId || (existing && existing.sourceId), 80),
      sourceName: sanitizeText(input.sourceName || (existing && existing.sourceName), 160),
      sourceUrl: sanitizeText(input.sourceUrl || (existing && existing.sourceUrl), 2000),
      externalId: sanitizeText(input.externalId || (existing && existing.externalId), 200),
      syncedAt: sanitizeText(input.syncedAt || (existing && existing.syncedAt), 80),
      sourceFingerprint: sanitizeText(input.sourceFingerprint || (existing && existing.sourceFingerprint), 300),
      sourceListings: Array.isArray(input.sourceListings) ? input.sourceListings.slice(0, 12).map((listing) => ({
        sourceId: sanitizeText(listing.sourceId, 80), sourceName: sanitizeText(listing.sourceName, 160),
        listingUrl: sanitizeText(listing.listingUrl, 2000), imageUrl: sanitizeText(listing.imageUrl, 2000),
        externalId: sanitizeText(listing.externalId, 200), price: numberOrZero(listing.price),
        currency: sanitizeText(listing.currency || 'AUD', 12), stockStatus: sanitizeText(listing.stockStatus || 'available', 80),
        syncedAt: sanitizeText(listing.syncedAt, 80)
      })) : (existing && Array.isArray(existing.sourceListings) ? existing.sourceListings : []),
      sourceFiles: Array.isArray(input.sourceFiles)
        ? input.sourceFiles.map((x) => sanitizeText(x, 260)).filter(Boolean).slice(0, 50)
        : [],
      approved: Boolean(input.approved),
      trustedSource: input.trustedSource == null ? Boolean(existing && existing.trustedSource) : Boolean(input.trustedSource),
      isDemo: Boolean(input.isDemo),
      createdAt: existing ? existing.createdAt : nowIso(),
      updatedAt: nowIso()
    };

    if (existingIndex >= 0) {
      state.products[existingIndex] = product;
      addAudit(state, 'product_updated', `Updated product: ${product.name}`);
    } else {
      state.products.unshift(product);
      addAudit(state, 'product_added', `Added SHIRE3D product: ${product.name}`);
    }
    return { state, product };
  }

  function deleteProduct(stateInput, productId) {
    const state = ensureState(stateInput);
    const product = state.products.find((p) => p.id === productId);
    if (!product) return state;
    const used = state.campaigns.some((c) => (c.productId === productId || (Array.isArray(c.productIds) && c.productIds.includes(productId))) && c.status !== 'cancelled');
    if (used) throw new Error('This product is used by an active campaign. Cancel or export that campaign first.');
    state.products = state.products.filter((p) => p.id !== productId);
    addAudit(state, 'product_deleted', `Deleted product: ${product.name}`);
    return state;
  }

  function platformLabel(platform) {
    return ({ facebook: 'Facebook', instagram: 'Instagram', linkedin: 'LinkedIn', tiktok: 'TikTok', youtube_shorts: 'YouTube Shorts' })[platform] || platform;
  }

  function monthlyPostCount(frequency) {
    const value = sanitizeText(frequency, 30).toLowerCase();
    if (value.includes('5')) return 20;
    if (value.includes('4')) return 16;
    if (value.includes('2')) return 8;
    if (value.includes('1')) return 4;
    return 12;
  }

  function validMonth(month) {
    return /^\d{4}-\d{2}$/.test(month || '');
  }

  function scheduleDates(month, count) {
    if (!validMonth(month)) throw new Error('Choose a valid campaign month.');
    const [year, monthNumber] = month.split('-').map(Number);
    const daysInMonth = new Date(Date.UTC(year, monthNumber, 0)).getUTCDate();
    const result = [];
    for (let i = 0; i < count; i += 1) {
      const day = Math.min(daysInMonth, Math.max(1, Math.round(2 + (i * (daysInMonth - 3)) / Math.max(1, count - 1))));
      result.push(`${year}-${String(monthNumber).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
    }
    return result;
  }

  function cleanProductName(product) {
    return sanitizeText(product && product.name, 140).replace(/^DEMO\s*[—-]\s*/i, '');
  }

  function plainDescription(product) {
    const raw = sanitizeText(product && product.description, 600)
      .replace(/<[^>]+>/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
    if (!raw || /^approved product$/i.test(raw)) return '';
    const firstSentence = raw.match(/^.*?[.!?](?:\s|$)/);
    const selected = firstSentence ? firstSentence[0].trim() : raw;
    return selected.length > 190 ? `${selected.slice(0, 187).trim()}…` : selected;
  }

  function productShopUrl(product) {
    return sanitizeText((product && (product.listingUrl || product.ebayListing)) || '', 2000);
  }

  function customerCallToAction(product, index) {
    const name = cleanProductName(product);
    const options = [
      `Take a closer look at ${name}.`,
      'Save this one for later.',
      'Share it with someone who would love it.',
      'Choose your favourite and make it yours.',
      'Explore the SHIRE3D range today.'
    ];
    return options[index % options.length];
  }

  function captionFor(product, platform, index) {
    const name = cleanProductName(product);
    const description = plainDescription(product);
    const price = numberOrZero(product && product.price) > 0 ? `From $${numberOrZero(product.price).toFixed(2)} AUD.` : '';
    const shopUrl = productShopUrl(product);
    const hooks = [
      `Meet ${name} — made to bring a little more personality to your space.`,
      `${name} is for people who prefer something memorable over something ordinary.`,
      `A fresh favourite from the SHIRE3D collection: ${name}.`,
      `Small details can completely change a setup. ${name} proves it.`,
      `Made to stand out, made to be enjoyed: ${name}.`,
      `Which version of ${name} would you choose?`,
      `Looking for a gift that feels different? Take a look at ${name}.`,
      `${name} is back in the spotlight — and it deserves a closer look.`
    ];
    const hook = hooks[index % hooks.length];
    const detail = description ? description : 'Designed and 3D printed by SHIRE3D in Australia.';
    const cta = customerCallToAction(product, index);
    const linkLine = shopUrl ? `Shop here: ${shopUrl}` : 'See more from SHIRE3D online.';

    if (platform === 'linkedin') {
      return `${hook}\n\n${detail}\n\n${price}${price ? ' ' : ''}${cta}\n${linkLine}`;
    }
    if (platform === 'youtube_shorts') {
      return `${hook}\n\n${detail}\n\n${price}${price ? ' ' : ''}${cta}\n${linkLine}`;
    }
    if (platform === 'tiktok') {
      return `${hook} ${detail} ${price} ${cta}`.replace(/\s+/g, ' ').trim();
    }
    if (platform === 'instagram') {
      return `${hook}\n\n${detail}\n\n${price}${price ? '\n' : ''}${cta}\n${linkLine}`;
    }
    return `${hook}\n\n${detail}\n\n${price}${price ? ' ' : ''}${cta}\n${linkLine}`;
  }

  function hashtagsFor(product, platform) {
    const text = `${cleanProductName(product)} ${sanitizeText(product && product.description, 500)}`.toLowerCase();
    const tags = ['#SHIRE3D', '#3DPrinting', '#AustralianMade', '#SmallBusinessAustralia'];
    if (/dice|tabletop|dungeon|gaming|miniature/.test(text)) tags.push('#TabletopGaming');
    if (platform === 'youtube_shorts') tags.push('#Shorts');
    if (/gothic|skull|dark|horror/.test(text)) tags.push('#GothicDecor');
    if (/dragon|fantasy|wizard|myth/.test(text)) tags.push('#FantasyDecor');
    if (/holder|organiser|organizer|storage|stand/.test(text)) tags.push('#UsefulPrints');
    if (/planter|decor|ornament|display/.test(text)) tags.push('#HomeDecor');
    if (platform === 'linkedin') return tags.slice(0, 5).join(' ');
    return tags.slice(0, 7).join(' ');
  }

  function marketableProducts(stateInput) {
    const state = ensureState(stateInput);
    const real = state.products.filter((product) => {
      if (product.isDemo) return false;
      const status = String(product.stockStatus || '').toLowerCase();
      return !/(paused|out of stock|sold out|unavailable|discontinued)/.test(status);
    });
    const approved = real.filter((product) => product.trustedSource || product.approved);
    return approved.length ? approved : real;
  }

  function rankedProductsForBoost(state, products) {
    const exposure = new Map();
    state.posts.forEach((post) => {
      if (!post || post.approvalStatus === 'rejected') return;
      exposure.set(post.productId, (exposure.get(post.productId) || 0) + 1);
    });
    return [...products].sort((a, b) => {
      const exposureDiff = (exposure.get(a.id) || 0) - (exposure.get(b.id) || 0);
      if (exposureDiff) return exposureDiff;
      const imageDiff = Number(Boolean(b.imageData)) - Number(Boolean(a.imageData));
      if (imageDiff) return imageDiff;
      const syncedDiff = String(b.syncedAt || b.updatedAt || '').localeCompare(String(a.syncedAt || a.updatedAt || ''));
      if (syncedDiff) return syncedDiff;
      return cleanProductName(a).localeCompare(cleanProductName(b));
    });
  }

  function buildProductRotation(state, products, count) {
    const ranked = rankedProductsForBoost(state, products);
    const rotation = [...ranked];
    let index = 0;
    while (rotation.length < count) {
      rotation.push(ranked[index % ranked.length]);
      index += 1;
    }
    return rotation.slice(0, count);
  }

  function legacyGeneratedCopy(caption) {
    const value = String(caption || '').toLowerCase();
    return value.includes('approved listing')
      || value.includes('development focus is repeatable print quality')
      || value.includes('drive qualified interest')
      || value.includes('honest production information');
  }

  function migrateLegacyDraftCopy(state) {
    state.posts.forEach((post, index) => {
      if (!post || post.frozen || !['ray_review', 'needs_reapproval'].includes(post.approvalStatus)) return;
      if (!legacyGeneratedCopy(post.caption)) return;
      const product = state.products.find((item) => item.id === post.productId);
      if (!product) return;
      post.productName = cleanProductName(product);
      post.title = `${cleanProductName(product)} · ${platformLabel(post.platform)}`;
      post.caption = captionFor(product, post.platform, index);
      post.hashtags = hashtagsFor(product, post.platform);
      post.callToAction = customerCallToAction(product, index);
      post.listingUrl = productShopUrl(product);
      post.altText = product.imageData ? `${cleanProductName(product)} by SHIRE3D.` : '';
      post.copyStyle = 'customer_friendly_v2';
      post.updatedAt = nowIso();
    });
  }

  function generateCampaign(stateInput, options) {
    const state = ensureState(stateInput);
    const opts = options || {};
    const productMode = 'all';
    const products = marketableProducts(state);
    if (!products.length) throw new Error('Sync or approve at least one real SHIRE3D product first.');

    const month = sanitizeText(opts.month, 7);
    if (!validMonth(month)) throw new Error('Choose a campaign month.');
    const platforms = Array.from(new Set((opts.platforms || []).filter((p) => PLATFORMS.includes(p))));
    if (!platforms.length) throw new Error('Choose at least one platform.');
    const frequency = sanitizeText(opts.frequency || '3 posts per week', 40);
    const baseCount = monthlyPostCount(frequency);
    const count = Math.max(baseCount, products.length);
    const rotation = buildProductRotation(state, products, count);
    const dates = scheduleDates(month, count);
    const campaign = {
      id: uid('campaign'),
      productId: '',
      productIds: products.map((product) => product.id),
      productName: 'Every Live SHIRE3D Product',
      productCount: products.length,
      productMode,
      month,
      timezone: 'Australia/Perth',
      platforms,
      frequency,
      goal: sanitizeText(opts.goal || 'Grow awareness and sales across every live SHIRE3D product.', 500),
      tone: sanitizeText(opts.tone || 'Warm, bold, clear and customer-friendly.', 300),
      status: 'ray_review',
      approvedVersion: 0,
      approvedAt: null,
      approvedBy: null,
      createdAt: nowIso(),
      updatedAt: nowIso()
    };

    const timeByPlatform = { facebook: '18:30', instagram: '19:00', linkedin: '08:15', tiktok: '18:00', youtube_shorts: '19:30' };
    const posts = dates.map((date, index) => {
      const platform = platforms[index % platforms.length];
      const product = rotation[index];
      return {
        id: uid('post'),
        campaignId: campaign.id,
        productId: product.id,
        productName: cleanProductName(product),
        platform,
        title: `${cleanProductName(product)} · ${platformLabel(platform)}`,
        caption: captionFor(product, platform, index),
        hashtags: hashtagsFor(product, platform),
        callToAction: customerCallToAction(product, index),
        listingUrl: productShopUrl(product),
        scheduledDate: date,
        scheduledTime: timeByPlatform[platform],
        timezone: 'Australia/Perth',
        mediaData: product.imageData || '',
        mediaName: product.imageName || '',
        altText: product.imageData ? `${cleanProductName(product)} by SHIRE3D.` : '',
        claimsWarning: product.isDemo ? 'Demo product details must be replaced and approved before publishing.' : '',
        approvalStatus: 'ray_review',
        frozen: false,
        frozenSnapshot: null,
        copyStyle: 'customer_friendly_v2',
        revision: 1,
        rejectedReason: '',
        createdAt: nowIso(),
        updatedAt: nowIso()
      };
    });

    state.campaigns.unshift(campaign);
    state.posts.unshift(...posts);
    addAudit(state, 'campaign_prepared', `SHiRE prepared ${posts.length} customer-friendly posts across ${products.length} product${products.length === 1 ? '' : 's'} (${month}).`);
    return { state, campaign, posts };
  }

  function rewriteCampaignCopy(stateInput, campaignId) {
    const state = ensureState(stateInput);
    const campaign = state.campaigns.find((item) => item.id === campaignId);
    if (!campaign) throw new Error('Campaign not found.');
    const posts = state.posts.filter((post) => post.campaignId === campaignId && post.approvalStatus !== 'rejected');
    if (posts.some((post) => post.frozen)) throw new Error('Unfreeze the campaign by editing a post before rewriting approved copy.');
    let rewritten = 0;
    posts.forEach((post, index) => {
      const product = state.products.find((item) => item.id === post.productId);
      if (!product) return;
      post.productName = cleanProductName(product);
      post.title = `${cleanProductName(product)} · ${platformLabel(post.platform)}`;
      post.caption = captionFor(product, post.platform, index);
      post.hashtags = hashtagsFor(product, post.platform);
      post.callToAction = customerCallToAction(product, index);
      post.listingUrl = productShopUrl(product);
      post.mediaData = product.imageData || post.mediaData || '';
      post.mediaName = product.imageName || post.mediaName || '';
      post.altText = post.mediaData ? `${cleanProductName(product)} by SHIRE3D.` : '';
      post.copyStyle = 'customer_friendly_v2';
      post.revision = Number(post.revision || 0) + 1;
      post.updatedAt = nowIso();
      rewritten += 1;
    });
    campaign.status = 'ray_review';
    campaign.updatedAt = nowIso();
    addAudit(state, 'campaign_copy_rewritten', `Rewrote ${rewritten} posts into customer-friendly sales copy without internal production notes.`);
    return { state, rewritten };
  }

  function postBlockers(post) {
    const blockers = [];
    if (!sanitizeText(post.caption, 100000)) blockers.push('Caption is empty.');
    const limit = PLATFORM_LIMITS[post.platform] || 3000;
    if ((post.caption || '').length > limit) blockers.push(`Caption exceeds the ${platformLabel(post.platform)} limit.`);
    if (!post.mediaData) blockers.push('Add an approved product image or render.');
    if (post.mediaData && !sanitizeText(post.altText, 1000)) blockers.push('Add alt text for accessibility.');
    if (sanitizeText(post.claimsWarning, 3000)) blockers.push(post.claimsWarning);
    if (!/^\d{4}-\d{2}-\d{2}$/.test(post.scheduledDate || '')) blockers.push('Choose a valid scheduled date.');
    if (!/^\d{2}:\d{2}$/.test(post.scheduledTime || '')) blockers.push('Choose a valid scheduled time.');
    return blockers;
  }

  function campaignBlockers(stateInput, campaignId) {
    const state = ensureState(stateInput);
    const posts = state.posts.filter((p) => p.campaignId === campaignId && p.approvalStatus !== 'rejected');
    if (!posts.length) return [{ postId: null, title: 'Campaign', reasons: ['At least one active post is required.'] }];
    return posts.map((post) => ({ postId: post.id, title: post.title, reasons: postBlockers(post) }))
      .filter((entry) => entry.reasons.length > 0);
  }

  function updatePost(stateInput, postId, changes) {
    const state = ensureState(stateInput);
    const index = state.posts.findIndex((p) => p.id === postId);
    if (index < 0) throw new Error('Post not found.');
    const current = state.posts[index];
    const wasFrozen = current.frozen;
    const allowed = [
      'title', 'caption', 'hashtags', 'callToAction', 'scheduledDate', 'scheduledTime',
      'mediaData', 'mediaName', 'altText', 'claimsWarning', 'platform'
    ];
    const next = { ...current };
    allowed.forEach((key) => {
      if (Object.prototype.hasOwnProperty.call(changes || {}, key)) next[key] = changes[key];
    });
    next.revision = Number(current.revision || 1) + 1;
    next.updatedAt = nowIso();

    if (wasFrozen) {
      next.frozen = false;
      next.frozenSnapshot = null;
      next.approvalStatus = 'needs_reapproval';
      state.queue.forEach((item) => {
        if (item.postId === postId && !['published', 'cancelled'].includes(item.status)) {
          item.status = 'cancelled';
          item.cancelReason = 'Post edited after approval.';
          item.updatedAt = nowIso();
        }
      });
      const campaign = state.campaigns.find((c) => c.id === current.campaignId);
      if (campaign) {
        campaign.status = 'needs_reapproval';
        campaign.updatedAt = nowIso();
      }
      addAudit(state, 'approval_invalidated', `Edited approved post “${current.title}”; approval was invalidated and its queue item was cancelled.`);
    } else {
      addAudit(state, 'post_updated', `Updated post: ${current.title}`);
    }
    state.posts[index] = next;
    return state;
  }

  function rejectPost(stateInput, postId, reason) {
    const state = ensureState(stateInput);
    const post = state.posts.find((p) => p.id === postId);
    if (!post) throw new Error('Post not found.');
    post.approvalStatus = 'rejected';
    post.rejectedReason = sanitizeText(reason || 'Rejected by Ray Morrison.', 1000);
    post.frozen = false;
    post.frozenSnapshot = null;
    post.updatedAt = nowIso();
    state.queue.forEach((item) => {
      if (item.postId === postId && item.status !== 'published') {
        item.status = 'cancelled';
        item.cancelReason = 'Post rejected.';
      }
    });
    addAudit(state, 'post_rejected', `Rejected post: ${post.title}. ${post.rejectedReason}`);
    return state;
  }

  function restorePost(stateInput, postId) {
    const state = ensureState(stateInput);
    const post = state.posts.find((p) => p.id === postId);
    if (!post) throw new Error('Post not found.');
    post.approvalStatus = 'ray_review';
    post.rejectedReason = '';
    post.updatedAt = nowIso();
    addAudit(state, 'post_restored', `Restored post for review: ${post.title}`);
    return state;
  }

  function approveCampaign(stateInput, campaignId, approver) {
    const state = ensureState(stateInput);
    const campaign = state.campaigns.find((c) => c.id === campaignId);
    if (!campaign) throw new Error('Campaign not found.');
    const blockers = campaignBlockers(state, campaignId);
    if (blockers.length) {
      const count = blockers.reduce((sum, entry) => sum + entry.reasons.length, 0);
      throw new Error(`Campaign has ${count} blocker${count === 1 ? '' : 's'} to fix before approval.`);
    }
    const actor = approver || OPERATOR;
    const activePosts = state.posts.filter((p) => p.campaignId === campaignId && p.approvalStatus !== 'rejected');
    const version = Number(campaign.approvedVersion || 0) + 1;
    const approvedAt = nowIso();

    activePosts.forEach((post) => {
      post.approvalStatus = 'approved';
      post.frozen = true;
      post.frozenSnapshot = {
        version,
        caption: post.caption,
        hashtags: post.hashtags,
        callToAction: post.callToAction,
        platform: post.platform,
        scheduledDate: post.scheduledDate,
        scheduledTime: post.scheduledTime,
        timezone: post.timezone,
        mediaData: post.mediaData,
        mediaName: post.mediaName,
        altText: post.altText,
        approvedAt,
        approvedBy: actor
      };
      post.updatedAt = approvedAt;
      const existing = state.queue.find((item) => item.postId === post.id && !['cancelled', 'failed'].includes(item.status));
      if (!existing) {
        state.queue.push({
          id: uid('queue'),
          campaignId,
          postId: post.id,
          platform: post.platform,
          accountName: `SHIRE3D ${platformLabel(post.platform)}`,
          scheduledAt: `${post.scheduledDate}T${post.scheduledTime}:00+08:00`,
          captionFrozen: post.caption,
          mediaFrozen: post.mediaData,
          status: 'queued',
          attemptCount: 0,
          publishedLink: null,
          lastError: null,
          createdAt: approvedAt,
          updatedAt: approvedAt
        });
      }
    });

    campaign.status = 'scheduled';
    campaign.approvedVersion = version;
    campaign.approvedAt = approvedAt;
    campaign.approvedBy = actor;
    campaign.postCount = activePosts.length;
    campaign.updatedAt = approvedAt;
    addAudit(state, 'campaign_approved', `${actor} approved and froze campaign version ${version} with ${activePosts.length} active posts.` , actor);
    return state;
  }

  function setControl(stateInput, changes, actor) {
    const state = ensureState(stateInput);
    if (Object.prototype.hasOwnProperty.call(changes || {}, 'globalPaused')) {
      state.control.globalPaused = Boolean(changes.globalPaused);
    }
    if (Object.prototype.hasOwnProperty.call(changes || {}, 'emergencyStop')) {
      state.control.emergencyStop = Boolean(changes.emergencyStop);
    }
    state.control.updatedBy = actor || OPERATOR;
    state.control.updatedAt = nowIso();
    addAudit(
      state,
      'publishing_control_changed',
      `Publishing controls: paused=${state.control.globalPaused}, emergencyStop=${state.control.emergencyStop}.`,
      actor || OPERATOR
    );
    return state;
  }

  function publishQueueItem(stateInput, queueId, actor) {
    const state = ensureState(stateInput);
    const item = state.queue.find((q) => q.id === queueId);
    if (!item) throw new Error('Queue item not found.');
    if (state.control.emergencyStop) throw new Error('Emergency stop is active. Nothing can publish.');
    if (state.control.globalPaused) throw new Error('Publishing is paused. Resume publishing first.');
    if (item.status === 'published') throw new Error('Duplicate blocked: this post is already published.');
    const duplicate = state.queue.find((q) => q.postId === item.postId && q.status === 'published');
    if (duplicate) {
      addAudit(state, 'duplicate_blocked', `Duplicate publishing blocked for post ${item.postId}.`, actor || OPERATOR);
      throw new Error('Duplicate blocked: another queue item already published this post.');
    }
    if (!['queued', 'failed'].includes(item.status)) throw new Error(`This item cannot publish from status “${item.status}”.`);
    item.status = 'published';
    item.attemptCount = Number(item.attemptCount || 0) + 1;
    item.lastError = null;
    item.publishedLink = `https://sandbox.${item.platform}.example/${item.postId}`;
    item.updatedAt = nowIso();
    const post = state.posts.find((p) => p.id === item.postId);
    if (post) post.approvalStatus = 'published';
    addAudit(state, 'sandbox_published', `Sandbox-published ${platformLabel(item.platform)} post. No external service was contacted.`, actor || OPERATOR);
    return state;
  }

  function cancelQueueItem(stateInput, queueId, reason, actor) {
    const state = ensureState(stateInput);
    const item = state.queue.find((q) => q.id === queueId);
    if (!item) throw new Error('Queue item not found.');
    if (item.status === 'published') throw new Error('A published record cannot be cancelled.');
    item.status = 'cancelled';
    item.cancelReason = sanitizeText(reason || 'Cancelled by Ray Morrison.', 1000);
    item.updatedAt = nowIso();
    addAudit(state, 'queue_cancelled', `Cancelled queue item for ${platformLabel(item.platform)}. ${item.cancelReason}`, actor || OPERATOR);
    return state;
  }

  function failQueueItem(stateInput, queueId, message) {
    const state = ensureState(stateInput);
    const item = state.queue.find((q) => q.id === queueId);
    if (!item) throw new Error('Queue item not found.');
    if (item.status === 'published') throw new Error('Published items cannot be failed.');
    item.status = 'failed';
    item.attemptCount = Number(item.attemptCount || 0) + 1;
    item.lastError = sanitizeText(message || 'Sandbox failure test.', 1000);
    item.updatedAt = nowIso();
    addAudit(state, 'queue_failed', `Queue item failed: ${item.lastError}`, 'SYSTEM');
    return state;
  }

  function retryQueueItem(stateInput, queueId, actor) {
    const state = ensureState(stateInput);
    const item = state.queue.find((q) => q.id === queueId);
    if (!item) throw new Error('Queue item not found.');
    if (item.status !== 'failed') throw new Error('Only failed items can be retried.');
    const duplicate = state.queue.find((q) => q.postId === item.postId && q.status === 'published');
    if (duplicate) throw new Error('Retry blocked because this post has already been published.');
    item.status = 'queued';
    item.lastError = null;
    item.updatedAt = nowIso();
    addAudit(state, 'queue_retried', `Returned failed ${platformLabel(item.platform)} item to the queue.`, actor || OPERATOR);
    return state;
  }

  function nextAction(stateInput) {
    const state = ensureState(stateInput);
    const realProducts = state.products.filter((p) => !p.isDemo);
    const reviewCampaign = state.campaigns.find((c) => ['ray_review', 'needs_reapproval'].includes(c.status));
    const queued = state.queue.some((q) => ['queued', 'failed'].includes(q.status));
    if (!realProducts.length) return { key: 'products', label: 'Sync your SHIRE3D websites', detail: 'The Forge will collect products, prices, images and listing links automatically.' };
    if (!state.campaigns.length) return { key: 'prepare', label: 'Let SHiRE boost the catalogue', detail: 'SHiRE will rotate every live product and draft customer-friendly posts for the month.' };
    if (reviewCampaign) return { key: 'review', label: 'Review the prepared month', detail: 'Fix any warnings, reject anything unwanted, then approve the exact version.' };
    if (queued) return { key: 'publishing', label: 'Check the publishing queue', detail: state.control.globalPaused ? 'Publishing is safely paused.' : 'Sandbox items are ready for manual release.' };
    return { key: 'prepare', label: 'Prepare another month', detail: 'SHiRE can rotate the full catalogue again or focus on one special item.' };
  }

  function scanBoundary(value, path, matches) {
    const currentPath = path || 'root';
    const found = matches || [];
    if (value == null) return found;
    if (typeof value === 'string') {
      const lower = value.toLowerCase();
      BLOCKED_IMPORT_TERMS.forEach((term) => {
        if (lower.includes(term)) found.push(`${currentPath}: contains “${term}”`);
      });
      return found;
    }
    if (Array.isArray(value)) {
      value.forEach((item, index) => scanBoundary(item, `${currentPath}[${index}]`, found));
      return found;
    }
    if (typeof value === 'object') {
      Object.keys(value).forEach((key) => {
        const keyLower = key.toLowerCase();
        BLOCKED_IMPORT_TERMS.forEach((term) => {
          if (keyLower.includes(term.replace(/[-_ ]/g, ''))) found.push(`${currentPath}.${key}: blocked field name`);
        });
        scanBoundary(value[key], `${currentPath}.${key}`, found);
      });
    }
    return found;
  }

  function normalizeImportedProduct(raw) {
    const item = raw || {};
    const name = sanitizeText(item.name || item.product_name || item.title, 140);
    if (!name) return null;
    return {
      id: uid('product'),
      name,
      description: sanitizeText(item.description || item.product_description || item.summary, 4000),
      price: numberOrZero(item.price || item.sale_price),
      productionCost: numberOrZero(item.productionCost || item.production_cost || item.cost),
      printHours: numberOrZero(item.printHours || item.print_hours || item.production_hours),
      material: sanitizeText(item.material || item.materials, 100),
      colours: sanitizeText(item.colours || item.colors || item.variants, 300),
      stockStatus: sanitizeText(item.stockStatus || item.status || 'ready', 60),
      ebayListing: sanitizeText(item.ebayListing || item.ebay_listing || item.listing_url || item.url, 2000),
      listingUrl: sanitizeText(item.listingUrl || item.listing_url || item.url || item.ebayListing, 2000),
      imageData: sanitizeText(item.imageData || item.image_url || item.imageUrl, 2000000),
      imageName: '',
      sourceId: sanitizeText(item.sourceId || item.source_id, 80),
      sourceName: sanitizeText(item.sourceName || item.source_name, 160),
      sourceUrl: sanitizeText(item.sourceUrl || item.source_url, 2000),
      externalId: sanitizeText(item.externalId || item.external_id, 200),
      syncedAt: sanitizeText(item.syncedAt || item.synced_at, 80),
      sourceListings: [],
      sourceFiles: [],
      approved: Boolean(item.approved),
      isDemo: false,
      createdAt: nowIso(),
      updatedAt: nowIso()
    };
  }

  function importProducts(stateInput, payload) {
    const state = ensureState(stateInput);
    const matches = scanBoundary(payload);
    if (matches.length) {
      throw new Error(`Import blocked by the Kitchen Hero separation boundary. ${matches.slice(0, 3).join('; ')}`);
    }
    let rawProducts = [];
    if (Array.isArray(payload)) rawProducts = payload;
    else if (payload && Array.isArray(payload.products)) rawProducts = payload.products;
    else if (payload && payload.data && Array.isArray(payload.data.Product)) rawProducts = payload.data.Product;
    else if (payload && payload.product) rawProducts = [payload.product];
    else throw new Error('No product list was found. Import a JSON array, { products: [...] }, or a Forge export containing data.Product.');

    const normalized = rawProducts.map(normalizeImportedProduct).filter(Boolean);
    if (!normalized.length) throw new Error('No valid SHIRE3D products were found in the import.');
    normalized.forEach((product) => state.products.unshift(product));
    addAudit(state, 'products_imported', `Imported ${normalized.length} SHIRE3D product record${normalized.length === 1 ? '' : 's'}.`);
    return { state, count: normalized.length };
  }


  function normalizeCatalogKey(value) {
    return sanitizeText(value, 240).toLowerCase()
      .replace(/\bshire\s*3d\b/g, ' ')
      .replace(/\b3d\s*printed\b/g, ' ')
      .replace(/\bships?\s+australia(?:-wide|\s+wide)?\b/g, ' ')
      .replace(/[^a-z0-9]+/g, ' ')
      .replace(/\s+/g, ' ')
      .trim();
  }

  function sourceListingFrom(raw, source, completedAt) {
    return {
      sourceId: source.id,
      sourceName: source.name,
      listingUrl: sanitizeText(raw.listingUrl, 2000),
      imageUrl: sanitizeText(raw.imageUrl, 2000),
      externalId: sanitizeText(raw.externalId, 200),
      price: numberOrZero(raw.price),
      currency: sanitizeText(raw.currency || 'AUD', 12),
      stockStatus: sanitizeText(raw.stockStatus || 'available', 80),
      syncedAt: sanitizeText(raw.syncedAt || completedAt || nowIso(), 80)
    };
  }

  function applyCatalogSync(stateInput, payload) {
    const state = ensureState(stateInput);
    const serialized = JSON.stringify(payload || {}).toLowerCase();
    const matches = ['kitchen hero', 'kitchen-hero', 'kitchen_hero'].filter((term) => serialized.includes(term));
    if (matches.length) throw new Error(`Website sync blocked by the Kitchen Hero separation boundary. Found ${matches.join(', ')}.`);
    if (!payload || !Array.isArray(payload.results)) throw new Error('The website sync response was not valid.');
    let added = 0;
    let updated = 0;
    let unchanged = 0;
    let failed = 0;

    payload.results.forEach((result) => {
      const sourceInfo = result && result.source ? result.source : {};
      const source = state.productSources.find((item) => item.id === sourceInfo.id);
      if (!source) throw new Error('Website sync tried to use a source outside the approved SHIRE3D allowlist.');
      source.lastSyncAt = sanitizeText(result.completedAt || nowIso(), 80);
      source.itemCount = Array.isArray(result.products) ? result.products.length : 0;
      source.lastError = sanitizeText(result.error, 500);
      source.status = result.error ? 'error' : 'synced';
      if (result.error) { failed += 1; return; }

      (result.products || []).forEach((raw) => {
        const name = sanitizeText(raw.name, 140);
        if (!name) return;
        const catalogKey = normalizeCatalogKey(name);
        const listing = sourceListingFrom(raw, source, result.completedAt);
        let product = state.products.find((item) => !item.isDemo && item.sourceListings && item.sourceListings.some((x) =>
          (listing.externalId && x.sourceId === source.id && x.externalId === listing.externalId) ||
          (listing.listingUrl && x.listingUrl === listing.listingUrl)
        ));
        if (!product) product = state.products.find((item) => !item.isDemo && normalizeCatalogKey(item.name) === catalogKey);

        if (!product) {
          const created = {
            id: uid('product'),
            name,
            description: sanitizeText(raw.description, 4000),
            price: numberOrZero(raw.price),
            productionCost: 0,
            printHours: 0,
            material: sanitizeText(raw.material, 100),
            colours: sanitizeText(raw.colours, 300),
            stockStatus: listing.stockStatus,
            ebayListing: listing.listingUrl,
            listingUrl: listing.listingUrl,
            imageData: listing.imageUrl,
            imageName: '',
            sourceId: source.id,
            sourceName: source.name,
            sourceUrl: source.url,
            externalId: listing.externalId,
            syncedAt: listing.syncedAt,
            sourceListings: [listing],
            sourceFiles: [],
            approved: true,
            trustedSource: true,
            isDemo: false,
            createdAt: nowIso(),
            updatedAt: nowIso()
          };
          created.sourceFingerprint = JSON.stringify([created.name, created.description, created.price, created.imageData, created.stockStatus]);
          state.products.unshift(created);
          added += 1;
          return;
        }

        const listings = Array.isArray(product.sourceListings) ? product.sourceListings.filter((x) => x.sourceId !== source.id || (listing.externalId && x.externalId !== listing.externalId)) : [];
        listings.push(listing);
        const currentPriority = SOURCE_PRIORITY[product.sourceId] || 99;
        const incomingPriority = SOURCE_PRIORITY[source.id] || 99;
        const useIncoming = incomingPriority <= currentPriority || !product.sourceId;
        const nextFingerprint = JSON.stringify([
          useIncoming ? name : product.name,
          useIncoming && raw.description ? sanitizeText(raw.description, 4000) : product.description,
          useIncoming && numberOrZero(raw.price) > 0 ? numberOrZero(raw.price) : product.price,
          useIncoming && listing.imageUrl ? listing.imageUrl : product.imageData,
          useIncoming ? listing.stockStatus : product.stockStatus
        ]);
        const comparableListings = (items) => (items || []).map((item) => ({
          sourceId: item.sourceId, sourceName: item.sourceName, listingUrl: item.listingUrl,
          imageUrl: item.imageUrl, externalId: item.externalId, price: item.price,
          currency: item.currency, stockStatus: item.stockStatus
        }));
        const changed = nextFingerprint !== product.sourceFingerprint || JSON.stringify(comparableListings(listings)) !== JSON.stringify(comparableListings(product.sourceListings));
        if (useIncoming) {
          product.name = name;
          if (raw.description) product.description = sanitizeText(raw.description, 4000);
          if (numberOrZero(raw.price) > 0) product.price = numberOrZero(raw.price);
          if (listing.imageUrl) { product.imageData = listing.imageUrl; product.imageName = ''; }
          product.stockStatus = listing.stockStatus;
          product.ebayListing = listing.listingUrl;
          product.listingUrl = listing.listingUrl;
          product.sourceId = source.id;
          product.sourceName = source.name;
          product.sourceUrl = source.url;
          product.externalId = listing.externalId;
        }
        product.sourceListings = listings.slice(-12);
        product.syncedAt = listing.syncedAt;
        product.approved = true;
        product.trustedSource = true;
        product.sourceFingerprint = nextFingerprint;
        product.updatedAt = nowIso();
        changed ? updated += 1 : unchanged += 1;
      });
    });
    addAudit(state, 'websites_synced', `Website autopilot added ${added}, updated ${updated}, left ${unchanged} unchanged; ${failed} source${failed === 1 ? '' : 's'} failed.`);
    return { state, added, updated, unchanged, failed };
  }

  function sourcesDue(stateInput, at) {
    const state = ensureState(stateInput);
    const now = at ? new Date(at).getTime() : Date.now();
    return state.productSources.filter((source) => {
      if (!source.enabled || !source.autoSync) return false;
      if (!source.lastSyncAt) return true;
      const elapsed = now - new Date(source.lastSyncAt).getTime();
      return !Number.isFinite(elapsed) || elapsed >= numberOrZero(source.intervalHours || 6) * 60 * 60 * 1000;
    }).map((source) => source.id);
  }

  function exportState(stateInput) {
    const state = ensureState(stateInput);
    return {
      exportedAt: nowIso(),
      appId: APP_ID,
      version: VERSION,
      boundary: BOUNDARY,
      data: deepClone(state)
    };
  }

  function resetToEmpty() {
    const state = createInitialState();
    state.products = [];
    state.audit = [{
      id: uid('audit'),
      type: 'app_reset',
      details: 'Forge reset to an empty SHIRE3D-only workspace. Publishing remains paused.',
      actor: OPERATOR,
      createdAt: nowIso()
    }];
    return state;
  }

  return {
    APP_ID,
    VERSION,
    OPERATOR,
    BOUNDARY,
    PLATFORMS,
    PLATFORM_LIMITS,
    DEFAULT_PRODUCT_SOURCES,
    createInitialState,
    ensureState,
    saveProduct,
    deleteProduct,
    generateCampaign,
    rewriteCampaignCopy,
    marketableProducts,
    postBlockers,
    campaignBlockers,
    updatePost,
    rejectPost,
    restorePost,
    approveCampaign,
    setControl,
    publishQueueItem,
    cancelQueueItem,
    failQueueItem,
    retryQueueItem,
    nextAction,
    importProducts,
    applyCatalogSync,
    sourcesDue,
    exportState,
    resetToEmpty,
    platformLabel,
    deepClone
  };
});
