(function () {
  'use strict';

  const Core = window.ForgeCore;
  const APP_VERSION = '1.3.0';
  const STORAGE_KEY = 'shireMarketingForgeEasyV1';
  const HAD_LOCAL_STATE_AT_START = Boolean(localStorage.getItem(STORAGE_KEY));
  const root = document.getElementById('root');
  const modalRoot = document.getElementById('modal-root');
  const toastRoot = document.getElementById('toast-root');
  let selectedCampaignId = null;
  let websiteBridgeOnline = false;
  let syncInProgress = false;
  let autoSyncTimer = null;
  let socialBridgeOnline = false;
  let oauthStatuses = {};
  let oauthSetup = {};

  function escapeHtml(value) {
    return String(value == null ? '' : value)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  function nl2br(value) {
    return escapeHtml(value).replace(/\n/g, '<br>');
  }

  function money(value) {
    const number = Number(value || 0);
    return new Intl.NumberFormat('en-AU', { style: 'currency', currency: 'AUD' }).format(number);
  }

  function formatDate(value, options) {
    if (!value) return 'Not set';
    const date = new Date(value.length === 10 ? `${value}T12:00:00+08:00` : value);
    if (Number.isNaN(date.getTime())) return value;
    return new Intl.DateTimeFormat('en-AU', options || { day: 'numeric', month: 'short', year: 'numeric' }).format(date);
  }

  function loadState() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      return Core.ensureState(raw ? JSON.parse(raw) : Core.createInitialState());
    } catch (error) {
      console.error(error);
      return Core.createInitialState();
    }
  }

  let state = loadState();

  async function persistServerState() {
    if (!location.protocol.startsWith('http')) return;
    try {
      await fetch('/api/agent-state', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ state })
      });
    } catch (_) {}
  }

  async function hydrateAgentState() {
    if (!location.protocol.startsWith('http') || HAD_LOCAL_STATE_AT_START) return;
    try {
      const response = await fetch('/api/agent-state', { cache: 'no-store' });
      const payload = await response.json();
      if (response.ok && payload && payload.state) {
        state = Core.ensureState(payload.state);
        localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
        render();
      }
    } catch (_) {}
  }

  function persist(nextState, message) {
    state = Core.ensureState(nextState);
    localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
    void persistServerState();
    render();
    if (message) toast('Done', message, 'success');
  }

  function toast(title, message, type) {
    toastRoot.innerHTML = `
      <div class="toast ${escapeHtml(type || '')}">
        <strong>${escapeHtml(title)}</strong>
        <span>${escapeHtml(message)}</span>
      </div>`;
    window.clearTimeout(toast.timer);
    toast.timer = window.setTimeout(() => { toastRoot.innerHTML = ''; }, 4300);
  }

  function currentRoute() {
    const route = (location.hash || '#home').slice(1).split('?')[0];
    return ['home', 'products', 'prepare', 'review', 'accounts', 'publishing', 'advanced'].includes(route) ? route : 'home';
  }

  function go(route) {
    if (currentRoute() === route) render();
    else location.hash = route;
  }

  function routeMeta(route) {
    return {
      home: ['Home', 'SHiRE does the work. Ray reviews the result.'],
      products: ['Products', 'Automatically collected from the approved SHIRE3D websites.'],
      prepare: ['Prepare', 'SHiRE rotates the whole catalogue and drafts the month.'],
      review: ['Review', 'Edit, reject or approve the exact prepared version.'],
      accounts: ['Accounts', 'Connect SHIRE3D social accounts through official sign-in.'],
      publishing: ['Publishing', 'Approval-gated queue, pause controls and duplicate protection.'],
      advanced: ['Advanced', 'Website sync, imports, exports and audit history.']
    }[route];
  }

  function navButton(route, icon, label, badge) {
    const active = currentRoute() === route ? 'active' : '';
    return `<button class="nav-button ${active}" data-action="navigate" data-route="${route}">
      <span class="nav-icon">${icon}</span>
      <span class="nav-label">${label}</span>
      ${badge != null ? `<span class="nav-badge">${badge}</span>` : '<span></span>'}
    </button>`;
  }

  function mobileButton(route, icon, label) {
    return `<button class="${currentRoute() === route ? 'active' : ''}" data-action="navigate" data-route="${route}"><span>${icon}</span>${label}</button>`;
  }

  function statusTag(status) {
    const label = String(status || 'unknown').replace(/_/g, ' ');
    const cls = status === 'published' || status === 'approved' || status === 'scheduled' ? 'green'
      : status === 'failed' || status === 'rejected' || status === 'cancelled' ? 'danger'
      : status === 'needs_reapproval' ? 'purple' : 'amber';
    return `<span class="tag ${cls}">${escapeHtml(label.toUpperCase())}</span>`;
  }

  function platformTag(platform) {
    return `<span class="tag">${escapeHtml(Core.platformLabel(platform))}</span>`;
  }

  function oauthStatus(platform) {
    return oauthStatuses[platform] || { platform, configured: false, connected: false, status: 'setup_required', accountName: '' };
  }

  function oauthProviderName(platform) {
    return { facebook: 'Facebook', instagram: 'Meta', linkedin: 'LinkedIn', tiktok: 'TikTok', youtube_shorts: 'Google' }[platform] || Core.platformLabel(platform);
  }

  function connectionBadge(platform) {
    const item = oauthStatus(platform);
    if (item.connected) return '<span class="tag green">CONNECTED</span>';
    if (item.configured) return '<span class="tag amber">READY TO LOGIN</span>';
    return '<span class="tag purple">ONE-TIME SETUP</span>';
  }

  function sourceStatusTag(source) {
    if (syncInProgress) return '<span class="tag amber">SYNCING</span>';
    if (source.status === 'synced') return '<span class="tag green">CONNECTED</span>';
    if (source.status === 'error') return '<span class="tag danger">NEEDS ATTENTION</span>';
    return '<span class="tag amber">READY</span>';
  }

  function sourceLastSync(source) {
    return source.lastSyncAt ? formatDate(source.lastSyncAt, { day: 'numeric', month: 'short', hour: 'numeric', minute: '2-digit' }) : 'Not synced yet';
  }

  function websitesPanel() {
    const sources = state.productSources || [];
    const totalFound = sources.reduce((sum, source) => sum + Number(source.itemCount || 0), 0);
    return `<section class="card website-panel">
      <div class="card-header"><div><div class="eyebrow">Product autopilot</div><h2 style="margin-top:8px">Your websites feed the Forge.</h2><p>When the app starts correctly, it checks these three approved SHIRE3D storefronts and refreshes again every six hours while running.</p></div><div class="button-row"><span class="status-pill ${websiteBridgeOnline ? 'safe' : 'warn'}">${websiteBridgeOnline ? 'AUTOPILOT ONLINE' : 'START SCRIPT REQUIRED'}</span><button class="btn primary" data-action="sync-websites" ${syncInProgress || !websiteBridgeOnline ? 'disabled' : ''}>${syncInProgress ? 'Checking websites…' : '↻ Sync now'}</button></div></div>
      <div class="grid three source-grid">${sources.map((source) => `<div class="source-tile"><div class="source-top"><strong>${escapeHtml(source.name)}</strong>${sourceStatusTag(source)}</div><a href="${escapeHtml(source.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(source.url.replace(/^https?:\/\/(www\.)?/, ''))}</a><div class="source-meta"><span>${Number(source.itemCount || 0)} listings found</span><span>${escapeHtml(sourceLastSync(source))}</span></div>${source.lastError ? `<div class="source-error">${escapeHtml(source.lastError)}</div>` : ''}</div>`).join('')}</div>
      <div class="inline-note" style="margin-top:16px"><strong>Safe boundary:</strong> only these fixed domains can be read. No store login, Etsy password, customer records, orders, payment data or Kitchen Hero information is requested. ${totalFound ? `${totalFound} source listings were seen during the latest checks.` : ''}</div>
    </section>`;
  }

  function renderShell(content) {
    const route = currentRoute();
    const meta = routeMeta(route);
    const pendingCampaigns = state.campaigns.filter((c) => ['ray_review', 'needs_reapproval'].includes(c.status)).length;
    const queueCount = state.queue.filter((q) => ['queued', 'failed'].includes(q.status)).length;
    const realProductCount = state.products.filter((p) => !p.isDemo).length;
    const connectedCount = Core.PLATFORMS.filter((platform) => oauthStatus(platform).connected).length;
    const controlClass = state.control.emergencyStop ? 'danger' : state.control.globalPaused ? 'warn' : 'safe';
    const controlText = state.control.emergencyStop ? 'EMERGENCY STOP' : state.control.globalPaused ? 'PUBLISHING PAUSED' : 'SANDBOX READY';

    root.innerHTML = `
      <div class="app-shell">
        <aside class="sidebar">
          <div class="brand">
            <img src="icons/forge-mark.svg" alt="SHIRE Marketing Forge mark">
            <div><strong>SHIRE FORGE</strong><span>Easy Mode · v${APP_VERSION}</span></div>
          </div>
          <nav class="nav" aria-label="Main navigation">
            ${navButton('home', '⌂', 'Home')}
            ${navButton('products', '◆', 'Products', realProductCount)}
            ${navButton('prepare', '✦', 'SHiRE prepares')}
            ${navButton('review', '✓', 'Ray reviews', pendingCampaigns)}
            ${navButton('accounts', '◎', 'Accounts', connectedCount)}
            ${navButton('publishing', '↗', 'Publishing', queueCount)}
            ${navButton('advanced', '⚙', 'Advanced')}
          </nav>
          <div class="sidebar-bottom">
            <div class="boundary-card"><strong>SHIRE3D DATA ONLY</strong><p>Kitchen Hero repositories, users, recipes, payments, accounts and credentials are blocked from this workspace.</p></div>
            <div class="authority">Ray Morrison · Sole approval authority<br>Passwords never enter the Forge · OAuth tokens stay server-side</div>
          </div>
        </aside>
        <div class="main-column">
          <header class="topbar">
            <div class="topbar-title"><strong>${escapeHtml(meta[0])}</strong><span>${escapeHtml(meta[1])}</span></div>
            <div class="topbar-actions">
              <span class="status-pill safe">SHIRE3D ONLY</span>
              <span class="status-pill ${controlClass}">${controlText}</span>
            </div>
          </header>
          <div class="safety-strip"><span>🛡</span><div><strong>Approval boundary:</strong> Every live SHIRE3D product is included automatically. Official account login never gives SHiRE permission to publish unapproved work; only Ray can release an exact frozen campaign.</div></div>
          <main class="content">${content}</main>
        </div>
        <nav class="mobile-nav" aria-label="Mobile navigation">
          ${mobileButton('home', '⌂', 'Home')}
          ${mobileButton('products', '◆', 'Products')}
          ${mobileButton('review', '✓', 'Review')}
          ${mobileButton('accounts', '◎', 'Accounts')}
          ${mobileButton('publishing', '↗', 'Publish')}
        </nav>
      </div>`;
  }

  function metricCard(label, value, note, cls) {
    return `<div class="card metric ${cls || ''}"><div class="metric-label">${escapeHtml(label)}</div><div class="metric-value">${escapeHtml(value)}</div><div class="metric-note">${escapeHtml(note)}</div></div>`;
  }

  function homePage() {
    const next = Core.nextAction(state);
    const realProducts = state.products.filter((p) => !p.isDemo).length;
    const reviewPosts = state.posts.filter((p) => ['ray_review', 'needs_reapproval'].includes(p.approvalStatus)).length;
    const approvedPosts = state.posts.filter((p) => p.frozen).length;
    const queued = state.queue.filter((q) => q.status === 'queued').length;
    const failed = state.queue.filter((q) => q.status === 'failed').length;
    const published = state.queue.filter((q) => q.status === 'published').length;
    const latest = state.campaigns[0];
    const connectedAccounts = Core.PLATFORMS.filter((platform) => oauthStatus(platform).connected).length;

    return `
      <section class="page-head">
        <div><div class="eyebrow">Guided command centre</div><h1>One obvious next step.</h1><p class="page-intro">Products come in automatically from your SHIRE3D websites. SHiRE rotates every live item, writes customer-friendly posts and gives under-promoted products extra attention. You review the finished month before anything moves forward.</p></div>
      </section>
      <section class="hero-action">
        <div class="card next-card">
          <div><div class="next-number">1</div><div class="eyebrow">Do this next</div><h2>${escapeHtml(next.label)}</h2><p>${escapeHtml(next.detail)}</p></div>
          <div class="button-row"><button class="btn primary large" data-action="next-action" data-next="${escapeHtml(next.key)}">Continue →</button></div>
        </div>
        <div class="grid two">
          ${metricCard('Real products', realProducts, 'Demo products not counted')}
          ${metricCard('Waiting for Ray', reviewPosts, 'Prepared posts needing review', reviewPosts ? 'amber' : '')}
          ${metricCard('Frozen posts', approvedPosts, 'Exact approved versions', approvedPosts ? 'green' : '')}
          ${metricCard('Connected accounts', connectedAccounts, `${Core.PLATFORMS.length} supported platforms`, connectedAccounts ? 'green' : '')}
          ${metricCard('Queue issues', failed, queued ? `${queued} safely queued` : 'No queued jobs', failed ? 'danger' : '')}
        </div>
      </section>
      <section class="grid two">
        <div class="card">
          <div class="card-header"><div><h3>How easy mode works</h3><p>The specialist controls still exist, but they no longer get in your way.</p></div><span class="tag amber">5 STEPS</span></div>
          <div class="grid two">
            <div class="inline-note"><strong class="text-amber">1. Products</strong><br>Your websites fill and update the product shelf.</div>
            <div class="inline-note"><strong class="text-amber">2. SHiRE prepares</strong><br>Every live product is rotated automatically.</div>
            <div class="inline-note"><strong class="text-amber">3. Ray reviews</strong><br>Edit, reject, then approve the exact version.</div>
            <div class="inline-note"><strong class="text-amber">4. Accounts</strong><br>Sign in through each platform's official OAuth screen.</div>
            <div class="inline-note"><strong class="text-amber">5. Publishing</strong><br>Nothing releases until the frozen campaign is approved.</div>
          </div>
        </div>
        <div class="card ${latest ? '' : 'soft'}">
          <div class="card-header"><div><h3>${latest ? 'Latest campaign' : 'Nothing prepared yet'}</h3><p>${latest ? `${escapeHtml(latest.productName)} · ${escapeHtml(latest.month)}` : 'Sync the catalogue, then SHiRE can promote the entire range.'}</p></div>${latest ? statusTag(latest.status) : '<span class="tag">EMPTY</span>'}</div>
          ${latest ? `<div class="tags">${latest.platforms.map(platformTag).join('')}</div><p>${escapeHtml(latest.goal)}</p><div class="card-footer"><span class="faint">${state.posts.filter((p) => p.campaignId === latest.id && p.approvalStatus !== 'rejected').length} active posts</span><button class="btn" data-action="open-campaign" data-id="${latest.id}">Open review</button></div>`
            : `<div class="empty"><div class="empty-icon">◆</div><h3>Sync your SHIRE3D websites</h3><p>The product shelf will fill automatically from your approved storefronts.</p><button class="btn primary" data-action="navigate" data-route="products">Open product autopilot</button></div>`}
        </div>
      </section>
      <section class="grid three" style="margin-top:16px">
        ${metricCard('Queued', queued, state.control.globalPaused ? 'Paused by default' : 'Sandbox release available')}
        ${metricCard('Sandbox published', published, 'No external service contacted', published ? 'green' : '')}
        ${metricCard('Kitchen Hero access', '0', 'Boundary remains intact', 'green')}
      </section>`;
  }

  function productCard(product) {
    const margin = Number(product.price || 0) - Number(product.productionCost || 0);
    return `<article class="card product-card">
      <div class="product-image">${product.imageData ? `<img src="${escapeHtml(product.imageData)}" alt="${escapeHtml(product.name)}">` : `<div class="product-image-placeholder">NO APPROVED IMAGE YET</div>`}</div>
      <div class="product-body">
        <div class="card-header"><div><div class="tags">${product.isDemo ? '<span class="tag danger">DEMO</span>' : ''}${product.trustedSource ? '<span class="tag green">AUTO-SYNCED</span>' : (product.approved ? '<span class="tag green">PRODUCT APPROVED</span>' : '<span class="tag amber">DETAILS NEED APPROVAL</span>')}${product.sourceName ? `<span class="tag purple">${escapeHtml(product.sourceName)}</span>` : ''}</div><h3 style="margin-top:10px">${escapeHtml(product.name)}</h3></div></div>
        <p class="product-description">${escapeHtml(product.description || 'No description added.')}</p>
        <div class="product-stats">
          <div class="product-stat"><span>Price</span><strong>${money(product.price)}</strong></div>
          <div class="product-stat"><span>Margin*</span><strong>${money(margin)}</strong></div>
          <div class="product-stat"><span>Print</span><strong>${product.printHours ? `${escapeHtml(product.printHours)} hr` : '—'}</strong></div>
        </div>
        <div class="card-footer"><div class="tags">${product.material ? `<span class="tag">${escapeHtml(product.material)}</span>` : ''}${product.sourceFiles && product.sourceFiles.length ? `<span class="tag purple">${product.sourceFiles.length} FILES</span>` : ''}${product.sourceListings && product.sourceListings.length > 1 ? `<span class="tag">${product.sourceListings.length} STORES</span>` : ''}</div><div class="button-row">${product.listingUrl || product.ebayListing ? `<a class="btn small" href="${escapeHtml(product.listingUrl || product.ebayListing)}" target="_blank" rel="noopener noreferrer">Open listing ↗</a>` : ''}<button class="btn small" data-action="edit-product" data-id="${product.id}">Edit</button><span class="tag green">INCLUDED AUTOMATICALLY</span></div></div>
      </div>
    </article>`;
  }

  function productsPage() {
    const products = state.products;
    return `
      <section class="page-head"><div><div class="eyebrow">Step 1</div><h1>Your SHIRE3D products—automatically.</h1><p class="page-intro">The Forge checks shire3d.com, shire3d.com.au and shire3d.etsy.com, then adds new listings and refreshes changed names, prices, descriptions, images and links.</p></div><div class="button-row"><button class="btn" data-action="go-import">Import backup</button><button class="btn" data-action="add-product">+ Add private product</button></div></section>
      ${websitesPanel()}
      ${products.length ? `<section class="grid three" style="margin-top:18px">${products.map(productCard).join('')}</section>` : `<section class="empty" style="margin-top:18px"><div class="empty-icon">◆</div><h3>No SHIRE3D products yet</h3><p>Start the app with the included start script, then press Sync now. The product shelf will populate from your approved websites.</p><button class="btn primary large" data-action="sync-websites" ${websiteBridgeOnline ? '' : 'disabled'}>Sync websites</button></section>`}
      <p class="faint" style="margin-top:18px;font-size:11px">* Margin is price minus the production-cost figure entered here. Website sync cannot know filament, labour, postage or marketplace fees, so add production costs later when available.</p>`;
  }

  function currentMonthValue() {
    const now = new Date();
    return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
  }

  function preparePage() {
    const usable = Core.marketableProducts(state);
    if (!usable.length) {
      return `<section class="page-head"><div><div class="eyebrow">Step 2</div><h1>Let SHiRE prepare.</h1></div></section><section class="empty"><div class="empty-icon">◆</div><h3>Sync your products first</h3><p>SHiRE needs at least one real SHIRE3D product before it can prepare customer-ready marketing.</p><button class="btn primary" data-action="navigate" data-route="products">Open product autopilot</button></section>`;
    }
    const basePosts = 12;
    const minimumPosts = Math.max(basePosts, usable.length);
    return `
      <section class="page-head"><div><div class="eyebrow">Step 2</div><h1>Every product. Automatically.</h1><p class="page-intro">There is no single-item campaign mode anymore. SHiRE uses every live product found across your approved stores, features each one before repeating anything, and gives extra turns to products that have had less exposure.</p></div></section>
      <form id="prepare-form" class="card" style="max-width:900px">
        <div class="catalogue-lock">
          <div><div class="eyebrow">Locked campaign coverage</div><h2>${usable.length} live products included</h2><p>New products discovered before preparation are included automatically. Sold-out, paused and discontinued listings are skipped.</p></div>
          <span class="status-pill safe">EVERY ITEM</span>
        </div>
        <div class="form-grid" style="margin-top:18px">
          <div class="field"><label for="prepare-month">Month</label><input id="prepare-month" name="month" type="month" value="${currentMonthValue()}" required></div>
          <div class="field"><label for="prepare-frequency">Posting pace</label><select id="prepare-frequency" name="frequency"><option>2 posts per week</option><option selected>3 posts per week</option><option>4 posts per week</option><option>5 posts per week</option></select></div>
          <div class="field full"><label>Platforms</label><div class="check-grid">${Core.PLATFORMS.map((platform) => { const connection = oauthStatus(platform); return `<div class="check-tile"><input type="checkbox" id="platform-${platform}" name="platforms" value="${platform}" checked><label for="platform-${platform}"><strong>${Core.platformLabel(platform)}</strong><br><span class="help">${connection.connected ? `Connected as ${escapeHtml(connection.accountName || 'SHIRE3D')}` : connection.configured ? 'Login ready in Accounts' : 'Draft now · connect later'}</span></label></div>`; }).join('')}</div></div>
        </div>
        <div class="divider"></div>
        <div class="inline-note"><strong>Full-catalogue autopilot:</strong> all ${usable.length} live products receive a feature before any product repeats. The month will contain at least ${minimumPosts} posts at the default pace so nothing is forgotten.</div>
        <details style="margin-top:16px"><summary class="faint" style="cursor:pointer">Optional campaign direction</summary><div class="form-grid" style="margin-top:16px"><div class="field full"><label for="prepare-goal">Private direction for SHiRE</label><textarea id="prepare-goal" name="goal">Help customers discover every live SHIRE3D product and encourage genuine product interest.</textarea><span class="help">This direction guides the plan but is never pasted into customer posts.</span></div><div class="field full"><label for="prepare-tone">Brand voice</label><input id="prepare-tone" name="tone" value="Warm, bold, clear and customer-friendly."></div></div></details>
        <div class="button-row" style="margin-top:20px"><button class="btn primary large" type="submit">✦ SHiRE, prepare every product</button><button class="btn" type="button" data-action="navigate" data-route="accounts">Connect social accounts</button></div>
      </form>`;
  }

  function chooseCampaign() {
    if (selectedCampaignId && state.campaigns.some((c) => c.id === selectedCampaignId)) return selectedCampaignId;
    const pending = state.campaigns.find((c) => ['ray_review', 'needs_reapproval'].includes(c.status));
    selectedCampaignId = (pending || state.campaigns[0] || {}).id || null;
    return selectedCampaignId;
  }

  function postCard(post) {
    const blockers = Core.postBlockers(post);
    const classes = `${post.approvalStatus === 'rejected' ? 'rejected' : ''} ${post.frozen ? 'frozen' : ''}`;
    const product = state.products.find((item) => item.id === post.productId);
    const productName = post.productName || (product && product.name) || 'SHIRE3D product';
    return `<article class="post-card ${classes}">
      <div class="post-top"><div><div class="tags">${platformTag(post.platform)}<span class="tag purple">${escapeHtml(productName)}</span>${statusTag(post.approvalStatus)}${post.frozen ? '<span class="tag green">FROZEN</span>' : ''}</div><h3 style="margin-top:10px">${escapeHtml(post.title)}</h3><div class="queue-meta">${formatDate(post.scheduledDate)} · ${escapeHtml(post.scheduledTime)} · Australia/Perth</div></div><div class="button-row">${post.approvalStatus === 'rejected' ? `<button class="btn small" data-action="restore-post" data-id="${post.id}">Restore</button>` : `<button class="btn small" data-action="edit-post" data-id="${post.id}">Edit</button><button class="btn danger small" data-action="reject-post" data-id="${post.id}">Reject</button>`}</div></div>
      ${post.mediaData ? `<img class="post-media" src="${escapeHtml(post.mediaData)}" alt="${escapeHtml(post.altText || '')}">` : ''}
      <div class="post-caption">${nl2br(post.caption)}</div>
      ${post.hashtags ? `<div class="faint" style="font-size:12px">${escapeHtml(post.hashtags)}</div>` : ''}
      ${post.approvalStatus === 'rejected' ? `<div class="inline-note" style="border-color:var(--danger);background:var(--danger-soft);margin-top:12px">Rejected: ${escapeHtml(post.rejectedReason)}</div>` : ''}
      ${blockers.length && post.approvalStatus !== 'rejected' ? `<ul class="warning-list">${blockers.map((blocker) => `<li>⚠ ${escapeHtml(blocker)}</li>`).join('')}</ul>` : ''}
    </article>`;
  }

  function reviewPage() {
    const campaignId = chooseCampaign();
    if (!campaignId) {
      return `<section class="page-head"><div><div class="eyebrow">Step 3</div><h1>Ray reviews.</h1><p class="page-intro">Customer-ready posts will appear here.</p></div></section><section class="empty"><div class="empty-icon">✦</div><h3>No prepared campaign yet</h3><p>Let SHiRE rotate the full catalogue and prepare the month.</p><button class="btn primary large" data-action="navigate" data-route="prepare">Prepare catalogue marketing</button></section>`;
    }
    const campaign = state.campaigns.find((item) => item.id === campaignId);
    const posts = state.posts.filter((post) => post.campaignId === campaignId);
    const activePosts = posts.filter((post) => post.approvalStatus !== 'rejected');
    const blockers = Core.campaignBlockers(state, campaignId);
    const blockerCount = blockers.reduce((sum, item) => sum + item.reasons.length, 0);
    const frozenCount = activePosts.filter((post) => post.frozen).length;
    const productCount = Number(campaign.productCount || new Set(activePosts.map((post) => post.productId)).size || 1);
    const canRewrite = !activePosts.some((post) => post.frozen);

    return `
      <section class="page-head"><div><div class="eyebrow">Step 3</div><h1>Customer-friendly posts, ready to check.</h1><p class="page-intro">The copy now speaks to buyers—not to developers or internal reviewers. SHiRE rotates the catalogue, while you can edit or reject anything before approval.</p></div><div class="field" style="min-width:min(360px,100%)"><label for="campaign-select">Campaign</label><select id="campaign-select">${state.campaigns.map((item) => `<option value="${item.id}" ${item.id === campaignId ? 'selected' : ''}>${escapeHtml(item.productName)} · ${escapeHtml(item.month)}</option>`).join('')}</select></div></section>
      <section class="review-layout">
        <div class="review-list">
          <div class="card soft"><div class="card-header"><div><h3>${escapeHtml(campaign.productName)}</h3><p>${escapeHtml(campaign.month)} · ${escapeHtml(campaign.frequency)} · ${productCount} product${productCount === 1 ? '' : 's'} · Australia/Perth</p></div>${statusTag(campaign.status)}</div><div class="tags">${campaign.platforms.map(platformTag).join('')}<span class="tag green">CUSTOMER COPY</span>${campaign.productMode === 'all' ? '<span class="tag purple">FULL CATALOGUE</span>' : ''}</div><p>Every live product is featured before SHiRE repeats an item. Products with less previous exposure are placed earlier in the rotation.</p><div class="button-row"><button class="btn" data-action="rewrite-campaign" data-id="${campaign.id}" ${canRewrite ? '' : 'disabled'}>↻ Refresh customer-friendly copy</button></div></div>
          ${posts.map(postCard).join('')}
        </div>
        <aside class="card approval-panel ${blockerCount ? 'danger' : 'green'}">
          <div class="eyebrow">Approval gate</div><div class="approval-count">${blockerCount}</div><h3>${blockerCount ? 'things to fix' : 'ready for Ray'}</h3>
          <p>${blockerCount ? 'Every warning is shown directly on its post. Rejected posts are excluded.' : `This will freeze ${activePosts.length} customer-ready posts covering ${productCount} product${productCount === 1 ? '' : 's'}.`}</p>
          <div class="grid two" style="margin:18px 0"><div class="product-stat"><span>Products</span><strong>${productCount}</strong></div><div class="product-stat"><span>Active posts</span><strong>${activePosts.length}</strong></div><div class="product-stat"><span>Rejected</span><strong>${posts.length - activePosts.length}</strong></div><div class="product-stat"><span>Version</span><strong>${Number(campaign.approvedVersion || 0) + 1}</strong></div></div>
          ${blockers.length ? `<ul class="warning-list">${blockers.slice(0, 6).map((blocker) => `<li><strong>${escapeHtml(blocker.title)}:</strong> ${escapeHtml(blocker.reasons[0])}</li>`).join('')}${blockers.length > 6 ? `<li>Plus ${blockers.length - 6} more post warnings.</li>` : ''}</ul>` : ''}
          <button class="btn success large" style="width:100%;margin-top:18px" data-action="approve-campaign" data-id="${campaign.id}" ${blockerCount ? 'disabled' : ''}>✓ Approve & freeze month</button>
          <p class="faint" style="font-size:11px;line-height:1.5">Editing a frozen post automatically cancels its queue item and returns the campaign to needs reapproval.</p>
        </aside>
      </section>`;
  }

  function accountDescription(platform) {
    return ({
      facebook: 'Connect the SHIRE3D Facebook Page through Meta Login.',
      instagram: 'Connect the SHIRE3D professional Instagram account through Meta Login.',
      linkedin: 'Connect Ray or the approved SHIRE3D LinkedIn identity through LinkedIn OAuth.',
      tiktok: 'Connect the SHIRE3D TikTok account through TikTok Desktop Login Kit.',
      youtube_shorts: 'Connect the SHIRE3D YouTube channel for Shorts through Google OAuth.'
    })[platform] || 'Connect through the platform’s official sign-in.';
  }

  function accountCard(platform) {
    const item = oauthStatus(platform);
    const setup = oauthSetup[platform] || {};
    const action = item.connected
      ? `<button class="btn danger" data-action="disconnect-platform" data-platform="${platform}">Disconnect</button>`
      : item.configured
        ? `<button class="btn primary" data-action="connect-platform" data-platform="${platform}">Sign in with ${escapeHtml(Core.platformLabel(platform))}</button>`
        : `<button class="btn" data-action="setup-platform" data-platform="${platform}">One-time setup</button>`;
    return `<article class="card account-card ${item.connected ? 'connected' : ''}">
      <div class="card-header"><div><div class="tags">${platformTag(platform)}${connectionBadge(platform)}</div><h3 style="margin-top:12px">${escapeHtml(item.connected ? (item.accountName || `SHIRE3D ${Core.platformLabel(platform)}`) : Core.platformLabel(platform))}</h3></div><div class="account-dot ${item.connected ? 'online' : ''}"></div></div>
      <p>${escapeHtml(accountDescription(platform))}</p>
      <div class="account-meta"><span>Official OAuth</span><span>${item.connected ? `Connected ${escapeHtml(formatDate(item.connectedAt, { day: 'numeric', month: 'short', hour: 'numeric', minute: '2-digit' }))}` : item.configured ? 'Developer app configured' : 'Developer app details required once'}</span></div>
      ${setup.note ? `<div class="faint" style="font-size:11px;margin-top:10px">${escapeHtml(setup.note)}</div>` : ''}
      <div class="card-footer"><span class="faint">Passwords are entered only on ${escapeHtml(oauthProviderName(platform))}.</span>${action}</div>
    </article>`;
  }

  function accountsPage() {
    const connected = Core.PLATFORMS.filter((platform) => oauthStatus(platform).connected).length;
    return `
      <section class="page-head"><div><div class="eyebrow">Step 4</div><h1>Connect your SHIRE3D accounts.</h1><p class="page-intro">Use each platform’s real sign-in and permission screen. The Forge never asks for your social password. Account login does not bypass Ray’s campaign approval gate.</p></div><div class="button-row"><span class="status-pill ${socialBridgeOnline ? 'safe' : 'warn'}">${socialBridgeOnline ? 'OAUTH BRIDGE ONLINE' : 'START SCRIPT REQUIRED'}</span><button class="btn" data-action="refresh-connections">↻ Refresh</button></div></section>
      <section class="card connection-summary"><div><div class="eyebrow">Account status</div><h2>${connected} of ${Core.PLATFORMS.length} connected</h2><p>Facebook, Instagram, LinkedIn, TikTok and YouTube Shorts are supported. Each service requires a free developer application before its OAuth button can work.</p></div><button class="btn primary" data-action="open-social-guide">Open setup guide</button></section>
      <section class="grid two" style="margin-top:18px">${Core.PLATFORMS.map(accountCard).join('')}</section>
      <section class="card" style="margin-top:18px"><div class="card-header"><div><h3>What connecting allows</h3><p>Only the permissions shown on the platform consent screen.</p></div><span class="tag green">RAY CONTROLS RELEASE</span></div><div class="grid three"><div class="inline-note"><strong>1. Official login</strong><br>Your password stays with the social platform.</div><div class="inline-note"><strong>2. Exact account choice</strong><br>Ray selects the approved SHIRE3D page or channel.</div><div class="inline-note"><strong>3. Frozen campaign only</strong><br>Connecting never publishes drafts by itself.</div></div></section>`;
  }

  function socialSetupModal(platform) {
    const setup = oauthSetup[platform] || {};
    const vars = Array.isArray(setup.requiredEnvironment) ? setup.requiredEnvironment : [];
    showModal(
      `${Core.platformLabel(platform)} connection setup`,
      'This is a one-time platform requirement—not a SHiRE password request.',
      `<div class="inline-note"><strong>Redirect URI to register:</strong><br><code>${escapeHtml(setup.redirectUri || `http://127.0.0.1:4173/api/oauth/callback/${platform}`)}</code></div>
       <ol class="setup-steps"><li>Create or open the SHIRE Marketing Forge developer app on ${escapeHtml(Core.platformLabel(platform))}.</li><li>Enable the login/content-publishing product and request the permissions listed by that platform.</li><li>Register the redirect URI above exactly.</li><li>Run <code>configure-social-logins-windows.ps1</code> and enter the app ID/key and secret when asked.</li><li>Restart the Forge, return here and press Sign in.</li></ol>
       ${vars.length ? `<div class="field"><label>Local environment fields used</label><div class="tags">${vars.map((name) => `<span class="tag">${escapeHtml(name)}</span>`).join('')}</div></div>` : ''}
       <div class="inline-note"><strong>Security:</strong> the Windows setup helper encrypts secrets with your Windows user account. OAuth access tokens stay in server memory for this build and disappear when the Forge closes.</div>`,
      `<button class="btn" data-action="close-modal">Close</button>${setup.developerPortal ? `<a class="btn primary" href="${escapeHtml(setup.developerPortal)}" target="_blank" rel="noopener noreferrer">Open developer portal ↗</a>` : ''}`,
      true
    );
  }

  function queueItemCard(item) {
    const post = state.posts.find((p) => p.id === item.postId);
    const canPublish = ['queued', 'failed'].includes(item.status) && !state.control.globalPaused && !state.control.emergencyStop;
    return `<article class="queue-item">
      <div><div class="tags">${platformTag(item.platform)}${statusTag(item.status)}</div><div class="queue-title" style="margin-top:8px">${escapeHtml(post ? post.title : item.postId)}</div><div class="queue-meta">${formatDate(item.scheduledAt, { day: 'numeric', month: 'short', year: 'numeric', hour: 'numeric', minute: '2-digit' })} · ${escapeHtml(item.accountName)}</div>${item.lastError ? `<div class="queue-error">${escapeHtml(item.lastError)}</div>` : ''}${item.publishedLink ? `<div class="queue-meta">Sandbox record: ${escapeHtml(item.publishedLink)}</div>` : ''}</div>
      <div class="button-row">
        ${item.status === 'queued' ? `<button class="btn small" data-action="fail-queue" data-id="${item.id}">Test failure</button><button class="btn danger small" data-action="cancel-queue" data-id="${item.id}">Cancel</button><button class="btn success small" data-action="publish-queue" data-id="${item.id}" ${canPublish ? '' : 'disabled'}>Publish sandbox</button>` : ''}
        ${item.status === 'failed' ? `<button class="btn small" data-action="retry-queue" data-id="${item.id}">Retry</button><button class="btn danger small" data-action="cancel-queue" data-id="${item.id}">Cancel</button>` : ''}
      </div>
    </article>`;
  }

  function publishingPage() {
    const queue = [...state.queue].sort((a, b) => String(a.scheduledAt).localeCompare(String(b.scheduledAt)));
    const active = queue.filter((q) => ['queued', 'failed'].includes(q.status));
    const history = queue.filter((q) => ['published', 'cancelled'].includes(q.status));
    const blocked = state.control.globalPaused || state.control.emergencyStop;
    return `
      <section class="page-head"><div><div class="eyebrow">Step 5</div><h1>Publishing stays controlled.</h1><p class="page-intro">Connected accounts use official OAuth, but this release still keeps final publishing in controlled test mode until each platform developer app is approved and Ray explicitly enables live release.</p></div></section>
      <section class="grid two">
        <div class="card ${state.control.globalPaused ? 'amber' : 'green'}"><div class="safety-control"><div><h3>Pause all publishing</h3><p>Overrides every queued item.</p></div><label class="switch"><input id="pause-toggle" type="checkbox" ${state.control.globalPaused ? 'checked' : ''}><span></span></label></div></div>
        <div class="card ${state.control.emergencyStop ? 'danger' : ''}"><div class="safety-control"><div><h3>Emergency stop</h3><p>Hard stop. Nothing can publish.</p></div><label class="switch danger"><input id="emergency-toggle" type="checkbox" ${state.control.emergencyStop ? 'checked' : ''}><span></span></label></div></div>
      </section>
      ${blocked ? `<div class="inline-note" style="margin:16px 0"><strong>${state.control.emergencyStop ? 'Emergency stop active.' : 'Publishing is paused.'}</strong> Queue items remain frozen and unchanged.</div>` : `<div class="inline-note" style="margin:16px 0;border-color:var(--green);background:var(--green-soft)"><strong class="text-green">Controlled test release enabled.</strong> ${Core.PLATFORMS.filter((platform) => oauthStatus(platform).connected).length} official account connection${Core.PLATFORMS.filter((platform) => oauthStatus(platform).connected).length === 1 ? '' : 's'} available; live API posting is not enabled in this build.</div>`}
      <section class="grid two">
        <div class="card"><div class="card-header"><div><h3>Current queue</h3><p>Failed jobs stay visible and retries cannot create duplicates.</p></div><span class="tag amber">${active.length} ACTIVE</span></div>${active.length ? active.map(queueItemCard).join('') : `<div class="empty"><div class="empty-icon">↗</div><h3>Queue is empty</h3><p>Approve a prepared month to create frozen sandbox jobs.</p><button class="btn primary" data-action="navigate" data-route="review">Open review</button></div>`}</div>
        <div class="card"><div class="card-header"><div><h3>Completed and cancelled</h3><p>Permanent local audit records.</p></div><span class="tag">${history.length}</span></div>${history.length ? history.slice(0, 30).map(queueItemCard).join('') : `<div class="empty"><div class="empty-icon">✓</div><h3>No history yet</h3><p>Sandbox publishing and cancellations will appear here.</p></div>`}</div>
      </section>`;
  }

  function advancedPage() {
    return `
      <section class="page-head"><div><div class="eyebrow">Specialist controls</div><h1>Advanced stays out of the way.</h1><p class="page-intro">Use this area for portable backups, website-sync details, SHIRE3D imports and the audit trail.</p></div></section>
      <section class="grid two">
        <div class="card"><div class="card-header"><div><h3>Portable data</h3><p>All exports remain local files.</p></div><span class="tag green">NO SECRETS</span></div><div class="grid two"><button class="btn primary large" data-action="export-json">Download full JSON</button><button class="btn large" data-action="trigger-import">Import products JSON</button></div><input id="import-file" type="file" accept="application/json,.json" hidden><div class="inline-note" style="margin-top:16px">Imports accept product arrays, <code>{ products: [...] }</code>, or legacy Forge exports containing <code>data.Product</code>. Kitchen Hero-shaped records are blocked.</div></div>
        <div class="card"><div class="card-header"><div><h3>Workspace housekeeping</h3><p>Destructive actions always require a confirmation.</p></div></div><div class="button-row"><button class="btn" data-action="clear-demo">Remove demo products</button><button class="btn danger" data-action="reset-workspace">Reset everything</button></div><p class="faint" style="font-size:11px">Reset removes local products, campaigns, posts, queue records and audit events from this browser only.</p></div>
      </section>
      <section class="card" style="margin-top:16px"><div class="card-header"><div><h3>Audit history</h3><p>Newest activity first. Approval records are never fabricated.</p></div><span class="tag">${state.audit.length} EVENTS</span></div><div class="audit-list">${state.audit.length ? state.audit.map((a) => `<div class="audit-row"><strong>${escapeHtml(String(a.type).replace(/_/g, ' ').toUpperCase())}</strong><p>${escapeHtml(a.details)}</p><time>${formatDate(a.createdAt, { day: 'numeric', month: 'short', year: 'numeric', hour: 'numeric', minute: '2-digit' })} · ${escapeHtml(a.actor)}</time></div>`).join('') : '<p>No audit events.</p>'}</div></section>`;
  }

  function pageContent() {
    const route = currentRoute();
    if (route === 'products') return productsPage();
    if (route === 'prepare') return preparePage();
    if (route === 'review') return reviewPage();
    if (route === 'accounts') return accountsPage();
    if (route === 'publishing') return publishingPage();
    if (route === 'advanced') return advancedPage();
    return homePage();
  }

  function render() {
    renderShell(pageContent());
    bindRenderedControls();
  }

  function showModal(title, subtitle, body, footer, wide) {
    modalRoot.innerHTML = `<div class="modal-backdrop" data-action="close-modal-background"><section class="modal ${wide ? 'wide' : ''}" role="dialog" aria-modal="true" aria-label="${escapeHtml(title)}"><header class="modal-head"><div><h2>${escapeHtml(title)}</h2>${subtitle ? `<p>${escapeHtml(subtitle)}</p>` : ''}</div><button class="icon-button" data-action="close-modal" aria-label="Close">✕</button></header><div class="modal-body">${body}</div>${footer ? `<footer class="modal-foot">${footer}</footer>` : ''}</section></div>`;
  }

  function closeModal() {
    modalRoot.innerHTML = '';
  }

  async function resizeImage(file) {
    if (!file || !file.type.startsWith('image/')) throw new Error('Choose a JPG, PNG, WEBP or other image file.');
    const source = await new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(new Error('The image could not be read.'));
      reader.readAsDataURL(file);
    });
    const image = await new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = () => reject(new Error('The selected image is not valid.'));
      img.src = source;
    });
    const max = 1200;
    const scale = Math.min(1, max / Math.max(image.width, image.height));
    const width = Math.max(1, Math.round(image.width * scale));
    const height = Math.max(1, Math.round(image.height * scale));
    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    canvas.getContext('2d').drawImage(image, 0, 0, width, height);
    return canvas.toDataURL('image/jpeg', 0.82);
  }

  function productModal(productId) {
    const product = state.products.find((p) => p.id === productId) || null;
    let imageData = product ? product.imageData : '';
    let imageName = product ? product.imageName : '';
    let sourceFiles = product && Array.isArray(product.sourceFiles) ? [...product.sourceFiles] : [];
    showModal(product ? 'Edit SHIRE3D product' : 'Add SHIRE3D product', 'Website products update automatically. Use this form for private products or production details the storefront cannot know.', `
      <form id="product-form">
        <div class="form-grid">
          <div class="field full"><label for="product-name">Product name *</label><input id="product-name" name="name" value="${escapeHtml(product ? product.name : '')}" required maxlength="140"></div>
          <div class="field full"><label for="product-description">Plain description</label><textarea id="product-description" name="description" maxlength="4000">${escapeHtml(product ? product.description : '')}</textarea></div>
          <div class="field"><label for="product-price">Selling price (AUD)</label><input id="product-price" name="price" type="number" min="0" step="0.01" value="${product ? escapeHtml(product.price) : ''}"></div>
          <div class="field"><label for="product-cost">Production cost (AUD)</label><input id="product-cost" name="productionCost" type="number" min="0" step="0.01" value="${product ? escapeHtml(product.productionCost) : ''}"></div>
          <div class="field"><label for="product-hours">Print time (hours)</label><input id="product-hours" name="printHours" type="number" min="0" step="0.1" value="${product ? escapeHtml(product.printHours) : ''}"></div>
          <div class="field"><label for="product-material">Material</label><input id="product-material" name="material" value="${escapeHtml(product ? product.material : 'PLA')}"></div>
          <div class="field"><label for="product-colours">Colours / variants</label><input id="product-colours" name="colours" value="${escapeHtml(product ? product.colours : '')}"></div>
          <div class="field"><label for="product-status">Production status</label><select id="product-status" name="stockStatus">${['prototype','ready','made to order','in stock','paused'].map((x) => `<option ${product && product.stockStatus === x ? 'selected' : ''}>${x}</option>`).join('')}</select></div>
          <div class="field full"><label for="product-listing">Product listing URL or notes</label><textarea id="product-listing" name="ebayListing">${escapeHtml(product ? product.ebayListing : '')}</textarea></div>
          <div class="field full"><label>Approved product image or render</label><div class="upload-box">${imageData ? `<img id="product-image-preview" class="post-media" src="${escapeHtml(imageData)}" alt="Current product image">` : '<div id="product-image-preview" class="faint" style="margin-bottom:10px">No image selected.</div>'}<input id="product-image-input" type="file" accept="image/*"><span class="help">The image is resized before being stored locally in this browser.</span></div></div>
          <div class="field full"><label>STL / Blender / production files</label><div class="upload-box"><input id="product-files-input" type="file" multiple accept=".stl,.3mf,.obj,.blend,.step,.stp,.fbx,.gcode"><div id="product-files-list" class="tags" style="margin-top:10px">${sourceFiles.map((f) => `<span class="tag purple">${escapeHtml(f)}</span>`).join('')}</div><span class="help">For safety and size, the app stores file names only—not the file contents.</span></div></div>
          <div class="field full"><div class="check-tile"><input id="product-approved" name="approved" type="checkbox" ${product && product.approved ? 'checked' : ''}><label for="product-approved">✓ I have checked these product details for marketing use</label></div></div>
        </div>
      </form>`, `<button class="btn" data-action="close-modal">Cancel</button>${product ? `<button class="btn danger" data-action="delete-product" data-id="${product.id}">Delete</button>` : ''}<button class="btn primary" id="save-product-button">Save product</button>`, true);

    const imageInput = document.getElementById('product-image-input');
    imageInput.addEventListener('change', async () => {
      try {
        const file = imageInput.files[0];
        if (!file) return;
        imageData = await resizeImage(file);
        imageName = file.name;
        const preview = document.getElementById('product-image-preview');
        preview.outerHTML = `<img id="product-image-preview" class="post-media" src="${imageData}" alt="Selected product image">`;
      } catch (error) { toast('Image not added', error.message, 'error'); }
    });
    const filesInput = document.getElementById('product-files-input');
    filesInput.addEventListener('change', () => {
      sourceFiles = Array.from(filesInput.files).map((f) => f.name).slice(0, 50);
      document.getElementById('product-files-list').innerHTML = sourceFiles.map((f) => `<span class="tag purple">${escapeHtml(f)}</span>`).join('');
    });
    document.getElementById('save-product-button').addEventListener('click', () => {
      const form = document.getElementById('product-form');
      if (!form.reportValidity()) return;
      const data = new FormData(form);
      try {
        const result = Core.saveProduct(state, {
          id: product ? product.id : undefined,
          name: data.get('name'),
          description: data.get('description'),
          price: data.get('price'),
          productionCost: data.get('productionCost'),
          printHours: data.get('printHours'),
          material: data.get('material'),
          colours: data.get('colours'),
          stockStatus: data.get('stockStatus'),
          ebayListing: data.get('ebayListing'),
          imageData,
          imageName,
          sourceFiles,
          approved: data.get('approved') === 'on',
          isDemo: false
        });
        closeModal();
        persist(result.state, product ? 'Product updated.' : 'Product added.');
      } catch (error) { toast('Could not save product', error.message, 'error'); }
    });
  }

  function postModal(postId) {
    const post = state.posts.find((p) => p.id === postId);
    if (!post) return;
    let mediaData = post.mediaData || '';
    let mediaName = post.mediaName || '';
    showModal('Edit prepared post', post.frozen ? 'Saving changes will invalidate the frozen approval and cancel its queue item.' : 'You are editing a draft that has not been approved.', `
      <form id="post-form">
        <div class="form-grid">
          <div class="field"><label for="post-platform">Platform</label><select id="post-platform" name="platform">${Core.PLATFORMS.map((p) => `<option value="${p}" ${post.platform === p ? 'selected' : ''}>${Core.platformLabel(p)}</option>`).join('')}</select></div>
          <div class="field"><label for="post-title">Internal title</label><input id="post-title" name="title" value="${escapeHtml(post.title)}"></div>
          <div class="field full"><label for="post-caption">Caption</label><textarea id="post-caption" name="caption" style="min-height:210px">${escapeHtml(post.caption)}</textarea></div>
          <div class="field full"><label for="post-hashtags">Hashtags</label><input id="post-hashtags" name="hashtags" value="${escapeHtml(post.hashtags)}"></div>
          <div class="field"><label for="post-date">Date</label><input id="post-date" name="scheduledDate" type="date" value="${escapeHtml(post.scheduledDate)}"></div>
          <div class="field"><label for="post-time">Time</label><input id="post-time" name="scheduledTime" type="time" value="${escapeHtml(post.scheduledTime)}"></div>
          <div class="field full"><label>Image / approved render</label><div class="upload-box">${mediaData ? `<img id="post-image-preview" class="post-media" src="${escapeHtml(mediaData)}" alt="Current post image">` : '<div id="post-image-preview" class="faint" style="margin-bottom:10px">No image selected.</div>'}<input id="post-image-input" type="file" accept="image/*"></div></div>
          <div class="field full"><label for="post-alt">Alt text</label><textarea id="post-alt" name="altText">${escapeHtml(post.altText)}</textarea></div>
          <div class="field full"><label for="post-warning">Claim or approval warning</label><textarea id="post-warning" name="claimsWarning">${escapeHtml(post.claimsWarning)}</textarea><span class="help">Clear this only after replacing demo facts or verifying the claim.</span></div>
        </div>
      </form>`, `<button class="btn" data-action="close-modal">Cancel</button><button class="btn primary" id="save-post-button">Save changes</button>`, true);
    document.getElementById('post-image-input').addEventListener('change', async (event) => {
      try {
        const file = event.target.files[0];
        if (!file) return;
        mediaData = await resizeImage(file);
        mediaName = file.name;
        document.getElementById('post-image-preview').outerHTML = `<img id="post-image-preview" class="post-media" src="${mediaData}" alt="Selected post image">`;
      } catch (error) { toast('Image not added', error.message, 'error'); }
    });
    document.getElementById('save-post-button').addEventListener('click', () => {
      const form = document.getElementById('post-form');
      const data = new FormData(form);
      try {
        const next = Core.updatePost(state, post.id, {
          platform: data.get('platform'), title: data.get('title'), caption: data.get('caption'), hashtags: data.get('hashtags'),
          scheduledDate: data.get('scheduledDate'), scheduledTime: data.get('scheduledTime'), mediaData, mediaName,
          altText: data.get('altText'), claimsWarning: data.get('claimsWarning')
        });
        closeModal();
        persist(next, post.frozen ? 'Post changed. Its previous approval was invalidated safely.' : 'Post updated.');
      } catch (error) { toast('Could not save post', error.message, 'error'); }
    });
  }

  function rejectModal(postId) {
    const post = state.posts.find((p) => p.id === postId);
    if (!post) return;
    showModal('Reject this post?', post.title, `<div class="field"><label for="reject-reason">Reason</label><textarea id="reject-reason">Not right for this campaign.</textarea><span class="help">Rejected posts are excluded from whole-month approval.</span></div>`, `<button class="btn" data-action="close-modal">Keep post</button><button class="btn danger" id="confirm-reject">Reject post</button>`);
    document.getElementById('confirm-reject').addEventListener('click', () => {
      try {
        const next = Core.rejectPost(state, postId, document.getElementById('reject-reason').value);
        closeModal();
        persist(next, 'Post rejected and excluded from campaign approval.');
      } catch (error) { toast('Could not reject post', error.message, 'error'); }
    });
  }

  function approvalModal(campaignId) {
    const campaign = state.campaigns.find((c) => c.id === campaignId);
    const activePosts = state.posts.filter((p) => p.campaignId === campaignId && p.approvalStatus !== 'rejected');
    if (!campaign) return;
    showModal('Approve and freeze this month?', 'This approval is bound only to the exact version shown.', `
      <div class="card green"><div class="eyebrow">Approval summary</div><h3 style="margin-top:10px">${escapeHtml(campaign.productName)} · ${escapeHtml(campaign.month)}</h3><div class="grid two" style="margin-top:16px"><div class="product-stat"><span>Active posts</span><strong>${activePosts.length}</strong></div><div class="product-stat"><span>Version</span><strong>${Number(campaign.approvedVersion || 0) + 1}</strong></div><div class="product-stat"><span>Timezone</span><strong>Australia/Perth</strong></div><div class="product-stat"><span>Connectors</span><strong>Sandbox only</strong></div></div></div>
      <div class="inline-note" style="margin-top:16px"><strong>After approval:</strong> captions, images, accounts, dates and times are frozen into queue records. Any later edit invalidates approval and cancels the affected queue item.</div>
      <p style="font-size:12px">Approver recorded: <strong>Ray Morrison</strong>. No real social account will be contacted.</p>`, `<button class="btn" data-action="close-modal">Cancel</button><button class="btn success" id="confirm-approval">Approve exact version</button>`);
    document.getElementById('confirm-approval').addEventListener('click', () => {
      try {
        const next = Core.approveCampaign(state, campaignId, 'Ray Morrison');
        closeModal();
        persist(next, 'Campaign approved, frozen and placed into the paused sandbox queue.');
        go('publishing');
      } catch (error) { toast('Approval blocked', error.message, 'error'); }
    });
  }

  function confirmModal(title, message, confirmLabel, onConfirm, danger) {
    showModal(title, '', `<p style="line-height:1.65">${escapeHtml(message)}</p>`, `<button class="btn" data-action="close-modal">Cancel</button><button class="btn ${danger ? 'danger' : 'primary'}" id="generic-confirm">${escapeHtml(confirmLabel)}</button>`);
    document.getElementById('generic-confirm').addEventListener('click', () => {
      try { onConfirm(); closeModal(); } catch (error) { toast('Action stopped', error.message, 'error'); }
    });
  }

  function bindRenderedControls() {
    const prepareForm = document.getElementById('prepare-form');
    if (prepareForm) prepareForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const data = new FormData(prepareForm);
      try {
        const result = Core.generateCampaign(state, {
          productMode: 'all',
          month: data.get('month'), frequency: data.get('frequency'),
          platforms: data.getAll('platforms'), goal: data.get('goal'), tone: data.get('tone')
        });
        selectedCampaignId = result.campaign.id;
        persist(result.state, `SHiRE prepared ${result.posts.length} customer-friendly posts across ${result.campaign.productCount} product${result.campaign.productCount === 1 ? '' : 's'}. Nothing has been approved.`);
        go('review');
      } catch (error) { toast('Could not prepare campaign', error.message, 'error'); }
    });


    const campaignSelect = document.getElementById('campaign-select');
    if (campaignSelect) campaignSelect.addEventListener('change', () => { selectedCampaignId = campaignSelect.value; render(); });

    const pauseToggle = document.getElementById('pause-toggle');
    if (pauseToggle) pauseToggle.addEventListener('change', () => {
      const requested = pauseToggle.checked;
      confirmModal(requested ? 'Pause all publishing?' : 'Resume sandbox publishing?', requested ? 'Every queue item will remain frozen and blocked.' : 'This enables only the manual sandbox release buttons. No live connector exists.', requested ? 'Pause all' : 'Resume sandbox', () => persist(Core.setControl(state, { globalPaused: requested }), requested ? 'Publishing paused.' : 'Sandbox publishing resumed.'), false);
      pauseToggle.checked = state.control.globalPaused;
    });

    const emergencyToggle = document.getElementById('emergency-toggle');
    if (emergencyToggle) emergencyToggle.addEventListener('change', () => {
      const requested = emergencyToggle.checked;
      confirmModal(requested ? 'Activate emergency stop?' : 'Release emergency stop?', requested ? 'This activates the hard stop and also pauses all publishing.' : 'Publishing will remain paused after the emergency stop is released.', requested ? 'Activate stop' : 'Release stop', () => {
        const next = Core.setControl(state, requested ? { emergencyStop: true, globalPaused: true } : { emergencyStop: false, globalPaused: true });
        persist(next, requested ? 'Emergency stop activated.' : 'Emergency stop released. Publishing remains paused.');
      }, requested);
      emergencyToggle.checked = state.control.emergencyStop;
    });

    const importInput = document.getElementById('import-file');
    if (importInput) importInput.addEventListener('change', async () => {
      const file = importInput.files[0];
      if (!file) return;
      try {
        const payload = JSON.parse(await file.text());
        const result = Core.importProducts(state, payload);
        persist(result.state, `Imported ${result.count} SHIRE3D product record${result.count === 1 ? '' : 's'}.`);
        importInput.value = '';
      } catch (error) { toast('Import blocked', error.message, 'error'); importInput.value = ''; }
    });
  }

  async function refreshConnections(quiet) {
    if (!location.protocol.startsWith('http')) {
      socialBridgeOnline = false;
      if (!quiet) toast('Account bridge is not running', 'Start the Forge with start-windows.bat or start-linux.sh.', 'error');
      render();
      return;
    }
    try {
      const response = await fetch('/api/oauth/status', { cache: 'no-store' });
      const payload = await response.json();
      if (!response.ok) throw new Error(payload.error || 'Could not read account status.');
      socialBridgeOnline = Boolean(payload.ok);
      oauthStatuses = Object.fromEntries((payload.providers || []).map((item) => [item.platform, item]));
      oauthSetup = Object.fromEntries((payload.providers || []).map((item) => [item.platform, item]));
      render();
      if (!quiet) toast('Accounts refreshed', 'The latest connection status is shown.', 'success');
    } catch (error) {
      socialBridgeOnline = false;
      if (!quiet) toast('Could not refresh accounts', error.message, 'error');
      render();
    }
  }

  async function disconnectPlatform(platform) {
    try {
      const response = await fetch(`/api/oauth/disconnect/${encodeURIComponent(platform)}`, { method: 'POST' });
      const payload = await response.json();
      if (!response.ok) throw new Error(payload.error || 'Could not disconnect this account.');
      await refreshConnections(true);
      toast('Account disconnected', `${Core.platformLabel(platform)} was disconnected from this Forge session.`, 'success');
    } catch (error) { toast('Disconnect failed', error.message, 'error'); }
  }

  function showSocialGuide() {
    showModal('Connect social accounts', 'A one-time developer-app setup is required by every platform.',
      `<p>The Forge now has real OAuth routes for Facebook, Instagram, LinkedIn, TikTok and YouTube Shorts. Social networks do not allow an unknown local program to ask for account access, so each one first needs a developer app ID and secret.</p>
       <div class="grid two"><div class="inline-note"><strong>Windows</strong><br>Run <code>configure-social-logins-windows.ps1</code>. Secrets are encrypted using your Windows account.</div><div class="inline-note"><strong>Linux</strong><br>Copy <code>social-oauth.env.sh.example</code>, fill it locally and source it before starting the Forge.</div></div>
       <p class="faint">Open <code>SOCIAL-ACCOUNT-SETUP.md</code> for the exact platform-by-platform checklist and callback addresses.</p>`,
      '<button class="btn primary" data-action="close-modal">Got it</button>', true);
  }

  function handleOAuthReturn() {
    const params = new URLSearchParams(location.search);
    const result = params.get('oauth');
    const platform = params.get('platform');
    const message = params.get('message');
    if (!result) return;
    history.replaceState({}, '', `${location.pathname}${location.hash || '#accounts'}`);
    if (result === 'connected') toast('Account connected', `${Core.platformLabel(platform)} is connected for this Forge session.`, 'success');
    else toast('Account connection stopped', message || `${Core.platformLabel(platform)} could not be connected.`, 'error');
  }

  async function syncWebsites(force, quiet) {
    if (syncInProgress) return;
    if (!location.protocol.startsWith('http')) {
      toast('Autopilot is not running', 'Close this tab and start the app with start-windows.bat or start-linux.sh.', 'error');
      return;
    }
    syncInProgress = true;
    render();
    try {
      const sourceIds = (state.productSources || []).filter((source) => source.enabled).map((source) => source.id);
      const response = await fetch('/api/sync-products', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sourceIds, force: Boolean(force) })
      });
      const payload = await response.json();
      if (!response.ok) throw new Error(payload.error || 'The website sync service returned an error.');
      const result = Core.applyCatalogSync(state, payload);
      syncInProgress = false;
      state = result.state;
      localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
      void persistServerState();
      render();
      if (!quiet || result.added || result.updated || result.failed) {
        const message = `Added ${result.added}, updated ${result.updated}, unchanged ${result.unchanged}${result.failed ? `; ${result.failed} source failed` : ''}.`;
        toast(result.failed ? 'Website sync finished with a warning' : 'Websites synced', message, result.failed ? 'error' : 'success');
      }
    } catch (error) {
      syncInProgress = false;
      render();
      toast('Website sync could not run', error.message, 'error');
    }
  }

  async function startWebsiteAutopilot() {
    if (!location.protocol.startsWith('http')) { websiteBridgeOnline = false; render(); return; }
    try {
      const response = await fetch('/api/health', { cache: 'no-store' });
      const health = await response.json();
      websiteBridgeOnline = Boolean(response.ok && health.ok && health.boundary === 'SHIRE3D_ONLY' && health.version === APP_VERSION);
      if (response.ok && health.ok && health.version !== APP_VERSION) {
        toast('Old Forge server detected', `Close the old Forge and run this v${APP_VERSION} start-windows.bat. The running server is v${health.version || 'unknown'}.`, 'error');
      }
    } catch (_) { websiteBridgeOnline = false; }
    await refreshConnections(true);
    render();
    if (websiteBridgeOnline) {
      const due = Core.sourcesDue(state);
      if (due.length) syncWebsites(false, true);
      window.clearInterval(autoSyncTimer);
      autoSyncTimer = window.setInterval(() => {
        if (Core.sourcesDue(state).length) syncWebsites(false, true);
      }, 30 * 60 * 1000);
    }
  }

  function downloadJson() {
    const payload = Core.exportState(state);
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const anchor = document.createElement('a');
    anchor.href = url;
    anchor.download = `shire-marketing-forge-easy-${new Date().toISOString().slice(0,10)}.json`;
    anchor.click();
    URL.revokeObjectURL(url);
    toast('Export created', 'The complete local Forge JSON was downloaded.', 'success');
  }

  document.addEventListener('click', (event) => {
    const target = event.target.closest('[data-action]');
    if (!target) return;
    const action = target.dataset.action;
    if (action === 'navigate') go(target.dataset.route);
    else if (action === 'next-action') go(target.dataset.next === 'products' ? 'products' : target.dataset.next);
    else if (action === 'add-product') productModal();
    else if (action === 'sync-websites') syncWebsites(true, false);
    else if (action === 'refresh-connections') refreshConnections(false);
    else if (action === 'connect-platform') { const item = oauthStatus(target.dataset.platform); if (!item.configured) socialSetupModal(target.dataset.platform); else window.location.href = `/api/oauth/start/${encodeURIComponent(target.dataset.platform)}`; }
    else if (action === 'disconnect-platform') disconnectPlatform(target.dataset.platform);
    else if (action === 'setup-platform') socialSetupModal(target.dataset.platform);
    else if (action === 'open-social-guide') showSocialGuide();
    else if (action === 'edit-product') productModal(target.dataset.id);
    else if (action === 'prepare-product') go('prepare');
    else if (action === 'open-campaign') { selectedCampaignId = target.dataset.id; go('review'); }
    else if (action === 'rewrite-campaign') {
      try {
        const result = Core.rewriteCampaignCopy(state, target.dataset.id);
        persist(result.state, `Rewrote ${result.rewritten} posts as customer-friendly product marketing.`);
      } catch (error) { toast('Could not rewrite campaign', error.message, 'error'); }
    }
    else if (action === 'edit-post') postModal(target.dataset.id);
    else if (action === 'reject-post') rejectModal(target.dataset.id);
    else if (action === 'restore-post') {
      try { persist(Core.restorePost(state, target.dataset.id), 'Post restored for review.'); } catch (error) { toast('Could not restore post', error.message, 'error'); }
    }
    else if (action === 'approve-campaign') approvalModal(target.dataset.id);
    else if (action === 'apply-product-image') {
      const product = state.products.find((p) => p.id === target.dataset.product);
      if (!product || !product.imageData) return;
      let next = state;
      state.posts.filter((p) => p.campaignId === target.dataset.campaign && p.approvalStatus !== 'rejected').forEach((post) => {
        next = Core.updatePost(next, post.id, { mediaData: product.imageData, mediaName: product.imageName, altText: `${product.name} SHIRE3D product photograph or approved render.` });
      });
      persist(next, 'Product image applied to every active post.');
    }
    else if (action === 'publish-queue') {
      try { persist(Core.publishQueueItem(state, target.dataset.id, 'Ray Morrison'), 'Sandbox publish recorded. No external service was contacted.'); } catch (error) { toast('Publishing blocked', error.message, 'error'); }
    }
    else if (action === 'fail-queue') {
      try { persist(Core.failQueueItem(state, target.dataset.id, 'Demonstration failure: connector unavailable.'), 'Test failure recorded. Use Retry to prove duplicate-safe recovery.'); } catch (error) { toast('Could not test failure', error.message, 'error'); }
    }
    else if (action === 'retry-queue') {
      try { persist(Core.retryQueueItem(state, target.dataset.id, 'Ray Morrison'), 'Failed item returned to the existing queue record.'); } catch (error) { toast('Retry blocked', error.message, 'error'); }
    }
    else if (action === 'cancel-queue') {
      confirmModal('Cancel this queue item?', 'The frozen post remains in the campaign, but this publishing job will be cancelled.', 'Cancel queue item', () => persist(Core.cancelQueueItem(state, target.dataset.id, 'Cancelled by Ray Morrison.', 'Ray Morrison'), 'Queue item cancelled.'), true);
    }
    else if (action === 'export-json') downloadJson();
    else if (action === 'trigger-import' || action === 'go-import') { if (currentRoute() !== 'advanced') { go('advanced'); setTimeout(() => document.getElementById('import-file')?.click(), 80); } else document.getElementById('import-file')?.click(); }
    else if (action === 'clear-demo') {
      confirmModal('Remove demo products?', 'Only products marked DEMO will be removed. Real SHIRE3D products stay intact.', 'Remove demos', () => {
        const next = Core.deepClone(state);
        next.products = next.products.filter((p) => !p.isDemo);
        persist(next, 'Demo products removed.');
      }, true);
    }
    else if (action === 'reset-workspace') {
      confirmModal('Reset the entire local Forge?', 'This permanently removes all local products, campaigns, posts, queue jobs and audit history from this browser. Export first if records matter.', 'Reset everything', () => { selectedCampaignId = null; persist(Core.resetToEmpty(), 'Workspace reset. Publishing remains paused.'); }, true);
    }
    else if (action === 'delete-product') {
      const id = target.dataset.id;
      confirmModal('Delete this product?', 'This cannot be undone unless you have an export.', 'Delete product', () => { closeModal(); persist(Core.deleteProduct(state, id), 'Product deleted.'); }, true);
    }
    else if (action === 'close-modal') closeModal();
    else if (action === 'close-modal-background' && event.target === target) closeModal();
  });

  window.addEventListener('hashchange', render);
  window.addEventListener('keydown', (event) => { if (event.key === 'Escape') closeModal(); });
  handleOAuthReturn();
  if (!location.hash) location.hash = 'home';
  else render();
  window.setTimeout(async () => { await hydrateAgentState(); await startWebsiteAutopilot(); }, 250);

  if ('serviceWorker' in navigator && location.protocol.startsWith('http')) {
    navigator.serviceWorker.register('service-worker.js').catch(() => {});
  }
})();
