← back to Designer Wallcoverings

DW-Agents/dw-agents/architectural-update-agent.ts

4701 lines

/**
 * DW-Agents: Architectural Update Agent
 *
 * Web UI for updating commercial vendors to Architectural Wallcoverings branding:
 * 1. Vendor dropdown selector (loads from Shopify)
 * 2. Batch size options (1,2,5,10,20,30,50,100,250,500,all)
 * 3. Start/Stop/Resume controls
 * 4. Real-time progress with product thumbnails
 * 5. Process history (collapsed view)
 *
 * Title Format: Pattern Name Color (Real mfr#) | Architectural Wallcoverings
 * No MFR#: Pattern Name Color | Architectural Wallcoverings
 *
 * Uses:
 * - Agent-SKU skill for manufacturer specifications
 * - Interior Designer color tag skill for AI color analysis
 *
 * Port: 9897
 */

import Anthropic from '@anthropic-ai/sdk';
import express from 'express';
import { requireGlobalAuth } from './shared-global-auth';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import fetch from 'node-fetch';
import dotenv from 'dotenv';
import { AgentMemory } from './shared-memory-system';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { chromium } from 'playwright';
import pg from 'pg';
const { Pool } = pg;
import crypto from 'crypto';

// PostgreSQL connection for tracking updates
const pool = new Pool({
  connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:5432/dw_unified')
});

// Current batch ID for tracking
let currentBatchId: string | null = null;

// ES Module __dirname equivalent
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Import shared UI components
import { getUniversalHeader, getThemedPageWrapper, getThemeStyles } from './shared-ui-components.js';

// Koroseal SKU to Collection mappings (from koroseal-spec-scraper)
const KOROSEAL_SKU_COLLECTIONS: Record<string, string> = {
  'C921': 'chateau', 'C923': 'chateau',
  'RA21': 'arco', 'SPR1': 'spire',
  'AL21': 'adalyn-weave', 'AS21': 'astra',
  'DK-WT': 'digital', 'DK-ST': 'digital', 'DK-': 'digital',
  'MB0': 'mandy-budan', 'SRD-': 'sarah-rowland',
  '2R15': 'rememberance-lite', '2R': 'rememberance',
  'TAK-': 'specialty',
};

// Koroseal SKU to Color Name mappings
const KOROSEAL_SKU_COLORS: Record<string, string> = {
  'C921-01': 'Whitewashed', 'C921-02': 'Bleached', 'C921-03': 'Weathered',
  'C921-04': 'Beach', 'C921-05': 'Tuscan', 'C921-06': 'Clove',
  'C921-07': 'Pining', 'C921-08': 'Drift', 'C921-09': 'Ancient Oak',
  'C921-10': 'Aged Walnut', 'C921-11': 'Espresso', 'C921-12': 'Smoked',
  'AL21-01': 'Moonlight', 'AL21-02': 'Hope', 'AL21-03': 'Sunshine',
  'AL21-04': 'Playful', 'AL21-05': 'Cloud', 'AL21-06': 'Future',
  'AL21-07': 'Confident', 'AL21-08': 'Lagoon', 'AL21-09': 'Mindful',
  'AL21-10': 'Earthen', 'AL21-11': 'Mood', 'AL21-12': 'Forest',
  'AL21-13': 'Compassion', 'AL21-14': 'Peaceful', 'AL21-15': 'Gratitude',
  'SPR1-01': 'Hermitage', 'SPR1-02': 'Versailles', 'SPR1-03': 'Windsor',
};

// Collection to Pattern Name mappings
const KOROSEAL_PATTERN_NAMES: Record<string, string> = {
  'chateau': 'Chateau',
  'adalyn-weave': 'Adalyn Weave',
  'astra': 'Astra',
  'arco': 'Arco',
  'spire': 'Spire',
  'rememberance-lite': 'Rememberance Lite',
  'rememberance': 'Rememberance',
  'digital': 'Digital Print',
  'mandy-budan': 'Mandy Budan',
  'sarah-rowland': 'Sarah Rowland',
};

// Load environment variables from multiple locations
dotenv.config({ path: '/root/Projects/Designer-Wallcoverings/.env' });
dotenv.config();

const app = express();
const PORT = 9897;

// Initialize agent memory system
const agentMemory = new AgentMemory('architectural-update');
console.log('📚 Memory system initialized for architectural-update');

// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(session({
  secret: 'architectural-update-secret-2024',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: false, maxAge: 24 * 60 * 60 * 1000 }
}));

// Apply global auth
app.use(requireGlobalAuth);

// Shopify configuration - LIVE STORE (designer-laboratory-sandbox = designerwallcoverings.com)
// NEVER use designerwallcoverings.myshopify.com - it doesn't work!
const SHOPIFY_CONFIG = {
  shop: 'designer-laboratory-sandbox.myshopify.com',
  accessToken: (process.env.SHOPIFY_ADMIN_TOKEN || ''),
  apiVersion: '2024-01'
};

// Anthropic Claude
const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY,
});

// Update specifications - supports both Architectural and Residential modes
const UPDATE_SPECS_ARCHITECTURAL = {
  suffix: 'Architectural Wallcoverings',
  seoDescription: 'Architectural wallcoverings for commercial and high-end interiors. Specification-grade materials from your one stop resource for over 20 years.',
  samplePrice: 4.25,
  category: 'Building Materials',
  productType: 'Architectural Wallcoverings'
};

const UPDATE_SPECS_RESIDENTIAL = {
  suffix: 'Designer Wallcoverings',
  seoDescription: 'Premium designer wallcoverings for residential and boutique interiors. Curated selection from your trusted source for over 20 years.',
  samplePrice: 4.25,
  category: 'Home & Garden',
  productType: 'Designer Wallcoverings'
};

// Default to Architectural (can be changed via UI)
let currentSkill: 'architectural' | 'residential' = 'architectural';

// Legacy reference for backwards compatibility
const UPDATE_SPECS = UPDATE_SPECS_ARCHITECTURAL;

function getCurrentUpdateSpecs() {
  return currentSkill === 'residential' ? UPDATE_SPECS_RESIDENTIAL : UPDATE_SPECS_ARCHITECTURAL;
}

// Process state
interface ProcessState {
  running: boolean;
  paused: boolean;
  vendor: string | null;
  batchSize: number;
  currentIndex: number;
  totalProducts: number;
  processed: number;
  errors: Array<{ id: string; error: string }>;
  products: Array<{ id: string; title: string; image: string | null; handle?: string; tags?: string; variants?: any[]; images?: any[] }>;
  startTime: Date | null;
  logs: string[];
  syncImages: boolean;  // Whether to sync images from manufacturer
  overrides: Record<string, { title?: string; pattern?: string; color?: string; mfrSku?: string; description?: string }>;  // User overrides for products
  productUpdates: Map<string, {
    status: 'pending' | 'updating' | 'completed' | 'error';
    oldTitle: string;
    newTitle?: string;
    sku?: string;
    timestamp?: Date;
    error?: string;
    imagesUpdated?: boolean;
    imageCount?: number;
    // Extended details for full display
    pattern?: string;
    color?: string;
    mfrSku?: string;
    tags?: string[];
    oldTags?: string[];
    description?: string;
    productType?: string;
    category?: string;
    colorAnalysis?: any;
    mfrData?: any;
  }>;  // Track individual product updates with full details
}

let processState: ProcessState = {
  running: false,
  paused: false,
  vendor: null,
  batchSize: 10,
  currentIndex: 0,
  totalProducts: 0,
  processed: 0,
  errors: [],
  products: [],
  startTime: null,
  logs: [],
  syncImages: true,  // Default to enabled
  overrides: {},  // User overrides for products
  productUpdates: new Map()  // Track individual product updates
};

// Process history
const historyFile = path.join(__dirname, 'data', 'architectural-update-history.json');

function loadHistory(): Array<any> {
  try {
    if (fs.existsSync(historyFile)) {
      return JSON.parse(fs.readFileSync(historyFile, 'utf8'));
    }
  } catch (e) {}
  return [];
}

function saveHistoryEntry(entry: any): void {
  const dir = path.dirname(historyFile);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  const history = loadHistory();
  history.unshift(entry);
  if (history.length > 50) history.pop();
  fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
}

// Persistent store for updated SKUs - tracks all successfully updated products
const updatedSkusFile = path.join(__dirname, 'data', 'architectural-updated-skus.json');

interface UpdatedSkuRecord {
  sku: string;
  productId: string;
  title: string;
  pattern?: string;
  color?: string;
  mfrSku?: string;
  updatedAt: string;
  vendor?: string;
}

function loadUpdatedSkus(): Map<string, UpdatedSkuRecord> {
  try {
    if (fs.existsSync(updatedSkusFile)) {
      const data = JSON.parse(fs.readFileSync(updatedSkusFile, 'utf8'));
      return new Map(Object.entries(data));
    }
  } catch (e) {
    console.error('[Updated SKUs] Error loading:', e);
  }
  return new Map();
}

function saveUpdatedSku(record: UpdatedSkuRecord): void {
  const dir = path.dirname(updatedSkusFile);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
  const skus = loadUpdatedSkus();
  skus.set(record.sku, record);
  const obj: Record<string, UpdatedSkuRecord> = {};
  skus.forEach((v, k) => { obj[k] = v; });
  fs.writeFileSync(updatedSkusFile, JSON.stringify(obj, null, 2));
}

function isSkuPreviouslyUpdated(sku: string): UpdatedSkuRecord | null {
  const skus = loadUpdatedSkus();
  return skus.get(sku) || null;
}

// Activity log
const activityLog: Array<{
  timestamp: Date;
  type: 'update' | 'error' | 'info' | 'complete';
  message: string;
}> = [];

function logActivity(type: 'update' | 'error' | 'info' | 'complete', message: string) {
  activityLog.unshift({ timestamp: new Date(), type, message });
  if (activityLog.length > 100) activityLog.pop();
  processState.logs.push(`[${new Date().toLocaleTimeString()}] ${message}`);
  if (processState.logs.length > 50) processState.logs.shift();
}

// Statistics
let stats = {
  totalUpdated: 0,
  totalErrors: 0,
  lastVendor: null as string | null,
  lastRunTime: null as Date | null,
  vendorsProcessed: [] as string[]
};

/**
 * Format title according to spec
 * For artworks (Mandy Budan, Sarah Rowland): "Artwork Name (SKU) | Artist Name | Suffix"
 * For regular products: "Pattern Color (SKU) | Suffix"
 */
function formatTitle(patternName: string, color: string | null, mfrNumber: string | null, options?: { artist?: string | null, isArtwork?: boolean }): string {
  const currentSpecs = getCurrentUpdateSpecs();

  // For artwork collections, include artist name in title
  if (options?.isArtwork && options?.artist) {
    const artworkName = patternName.trim();
    if (mfrNumber && mfrNumber.trim()) {
      const cleanSku = mfrNumber.trim().replace(/-SAMPLE$/i, '');
      return `${artworkName} (${cleanSku}) | ${options.artist} | ${currentSpecs.suffix}`;
    }
    return `${artworkName} | ${options.artist} | ${currentSpecs.suffix}`;
  }

  // Standard format: Pattern Color (SKU) | Suffix
  const basePart = color ? `${patternName} ${color}`.trim() : patternName.trim();
  if (mfrNumber && mfrNumber.trim()) {
    // Remove -SAMPLE suffix from SKU (not needed in titles)
    const cleanSku = mfrNumber.trim().replace(/-SAMPLE$/i, '');
    return `${basePart} (${cleanSku}) | ${currentSpecs.suffix}`;
  }
  return `${basePart} | ${currentSpecs.suffix}`;
}

/**
 * Analyze product image for color tags using Claude AI
 */
async function analyzeColorsWithAI(imageUrl: string, productTitle: string): Promise<any> {
  try {
    const response = await anthropic.messages.create({
      model: 'claude-opus-4-8',
      max_tokens: 1024,
      messages: [{
        role: 'user',
        content: [
          {
            type: 'image',
            source: { type: 'url', url: imageUrl }
          },
          {
            type: 'text',
            text: `Analyze this wallcovering image for color tagging. Product: "${productTitle}"

Using interior design terminology, provide:
1. Background Color (the dominant color)
2. Up to 3 color names per color family present
3. Style/Period tags (1-2: Victorian, Art Deco, Mid-Century Modern, Contemporary, Traditional, etc.)
4. Application tags for architectural/commercial use (Hospitality, Healthcare, Corporate, Residential, Retail)
5. Material type if identifiable (Vinyl, Grasscloth, Paper, Non-woven, etc.)

IMPORTANT: Natural materials (Grasscloth, Sisal, Jute, Paper, Silk) are NEVER Type II. Only Vinyl/Synthetic can be Type II Commercial.

Return as JSON:
{
  "backgroundColor": "string",
  "colorTags": ["color1", "color2", "color3"],
  "styleTags": ["style1"],
  "applicationTags": ["Hospitality", "Corporate"],
  "materialTags": ["Vinyl"],
  "isTypeII": boolean
}`
          }
        ]
      }]
    });

    const text = response.content[0].type === 'text' ? response.content[0].text : '';
    const jsonMatch = text.match(/\{[\s\S]*\}/);
    if (jsonMatch) {
      return JSON.parse(jsonMatch[0]);
    }
    return null;
  } catch (e: any) {
    console.error('AI color analysis error:', e.message);
    return null;
  }
}

/**
 * Interface for manufacturer data
 */
interface ManufacturerData {
  mfrSku: string | null;        // Real manufacturer SKU (e.g., "2R15-21", "AL21-15")
  patternName: string | null;   // Pattern name (e.g., "Rememberance Lite", "Adalyn Weave")
  colorName: string | null;     // Color name (e.g., "Canfu", "Gratitude")
  artist?: string | null;       // Artist name for artwork collections (e.g., "Mandy Budan", "Sarah Rowland")
  isArtwork?: boolean;          // True if this is an artwork/artist collection (don't split pattern/color)
  material?: string | null;
  backing?: string | null;
  fireRating?: string | null;
  korosealUrl?: string | null;
}

/**
 * Extract real manufacturer SKU from various product data sources
 * Live site handle format: dwk-30865-koroseal-koroseal-type-2-vinyl-2r15-21-rememberance-lite-canfu
 */
function extractRealMfrSku(product: any): string | null {
  const handle = product.handle || '';

  // SKU patterns to match: 2R15-21, AL21-15, C921-10, TAK-DA01-01
  const skuPatterns = [
    /\b(2r\d{1,2}-\d{1,2})\b/i,              // 2R15-21 (Rememberance)
    /\b([a-z]{2}\d{2}-\d{1,2})\b/i,          // AL21-15, C921-10
    /\b(tak-[a-z]{2}\d{2}-\d{2})\b/i,        // TAK-DA01-01
    /\b(spr\d-\d{2})\b/i,                     // SPR1-01
    /\b(ra\d{2}-\d{1,2})\b/i,                // RA21-01 (Arco)
    /\b(as\d{2}-\d{1,2})\b/i,                // AS21-01 (Astra)
    /\b(dk-[a-z]{2}-\d+)\b/i,                // DK-WT-001 (Digital)
  ];

  for (const pattern of skuPatterns) {
    const match = handle.match(pattern);
    if (match) {
      return match[1].toUpperCase();
    }
  }

  // Try to find in title (if properly formatted)
  const titleSkuMatch = product.title?.match(/\(([A-Z0-9-]+)\)/i);
  if (titleSkuMatch) {
    const sku = titleSkuMatch[1].toUpperCase();
    if (!sku.startsWith('DWK-')) {
      return sku;
    }
  }

  // Check variant SKUs (not DWK prefixed)
  if (product.variants?.length > 0) {
    for (const variant of product.variants) {
      if (variant.sku && !variant.sku.startsWith('DWK-')) {
        return variant.sku.toUpperCase();
      }
    }
  }

  return null;
}

/**
 * Parse handle to extract pattern and color
 * Supports TWO formats:
 * 1. LIVE: dwk-30865-koroseal-koroseal-type-2-vinyl-2r15-21-rememberance-lite-canfu
 *    → SKU before pattern-color
 * 2. SANDBOX: adalyn-weave-cloud-wallcovering-al21-05-sample-architectural-wallcoverings
 *    → pattern-color before SKU
 */
function parseHandleForPatternAndColor(handle: string, mfrSku: string | null): { pattern: string | null; color: string | null } {
  if (!handle) return { pattern: null, color: null };

  // FALLBACK: If no SKU or SKU not in handle, try to parse common handle formats
  // Format: pattern-color-wallpaper-wallcovering-vendor (e.g., bird-sonnet-blush-wallpaper-wallcovering-1838-wallcoverings)
  if (!mfrSku || handle.toLowerCase().indexOf(mfrSku.toLowerCase()) === -1) {
    // Try to extract pattern and color from handle
    const wallpaperIndex = handle.indexOf('-wallpaper');
    const wallcoveringIndex = handle.indexOf('-wallcovering');
    const cutoffIndex = wallpaperIndex !== -1 ? wallpaperIndex : wallcoveringIndex;

    if (cutoffIndex > 0) {
      const beforeWallpaper = handle.substring(0, cutoffIndex);
      const parts = beforeWallpaper.split('-').filter(p => p && p.length > 1 && !p.match(/^dwk?$/i));

      if (parts.length >= 2) {
        // Last part is color, rest is pattern
        const color = parts[parts.length - 1];
        const patternParts = parts.slice(0, -1);
        const pattern = patternParts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ');
        return {
          pattern: pattern,
          color: color.charAt(0).toUpperCase() + color.slice(1)
        };
      } else if (parts.length === 1 && parts[0].length > 2) {
        // Single-word pattern (e.g., "damier" from handle "damier-wallcovering-innovationsusa")
        const pattern = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
        return { pattern, color: null };
      }
    }
    return { pattern: null, color: null };
  }

  const skuLower = mfrSku.toLowerCase();
  const skuIndex = handle.toLowerCase().indexOf(skuLower);

  // Check if this is SANDBOX format (pattern-color BEFORE sku)
  // Indicated by "-wallcovering-" appearing after sku
  const afterSku = handle.substring(skuIndex + skuLower.length);
  const beforeSku = handle.substring(0, skuIndex);

  if (beforeSku.includes('wallcovering') || afterSku.startsWith('-sample')) {
    // SANDBOX FORMAT: adalyn-weave-cloud-wallcovering-al21-05-sample-...
    // Pattern and color come BEFORE the SKU
    const beforeWallcovering = handle.split('-wallcovering')[0];
    const parts = beforeWallcovering.split('-').filter(p => p && p.length > 0);

    if (parts.length >= 2) {
      // Last part is color, rest is pattern
      const color = parts[parts.length - 1];
      const patternParts = parts.slice(0, -1);
      const pattern = patternParts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ');
      return { pattern, color: color.charAt(0).toUpperCase() + color.slice(1) };
    }
  } else {
    // LIVE FORMAT: dwk-30865-...-2r15-21-rememberance-lite-canfu
    // Pattern and color come AFTER the SKU
    const parts = afterSku.split('-').filter(p => p && p.length > 0);

    if (parts.length >= 2) {
      // Last part is color, rest is pattern
      const color = parts[parts.length - 1];
      const patternParts = parts.slice(0, -1);
      const pattern = patternParts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(' ');
      return { pattern, color: color.charAt(0).toUpperCase() + color.slice(1) };
    } else if (parts.length === 1) {
      // Only color, no pattern
      return { pattern: null, color: parts[0].charAt(0).toUpperCase() + parts[0].slice(1) };
    }
  }

  return { pattern: null, color: null };
}

/**
 * Get pattern name from SKU or handle
 */
function getPatternFromSkuOrHandle(mfrSku: string | null, handle: string): string | null {
  // Try from SKU prefix first
  if (mfrSku) {
    for (const [prefix, collection] of Object.entries(KOROSEAL_SKU_COLLECTIONS)) {
      if (mfrSku.toUpperCase().startsWith(prefix)) {
        return KOROSEAL_PATTERN_NAMES[collection] || collection.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
      }
    }
  }

  // Try to extract from handle
  // Format: ...-patternname-colorname
  const handleParts = handle.split('-');

  // Look for known pattern names in handle
  for (const [key, name] of Object.entries(KOROSEAL_PATTERN_NAMES)) {
    const slugified = key.replace(/-/g, '');
    if (handle.toLowerCase().includes(key) || handle.toLowerCase().includes(slugified)) {
      return name;
    }
  }

  // Try to extract pattern name from end of handle (before color)
  if (handleParts.length > 5) {
    // Skip first parts (dwk-id-koroseal-...) and try to find pattern/color
    const lastParts = handleParts.slice(-3);
    // Assume pattern-name-color format
    if (lastParts.length >= 2) {
      const potentialPattern = lastParts.slice(0, -1).join(' ')
        .split(' ')
        .map(w => w.charAt(0).toUpperCase() + w.slice(1))
        .join(' ');
      return potentialPattern;
    }
  }

  return null;
}

/**
 * Get color name from SKU or handle
 */
function getColorFromSkuOrHandle(mfrSku: string | null, handle: string): string | null {
  // Try exact SKU match first
  if (mfrSku) {
    const upperSku = mfrSku.toUpperCase();
    if (KOROSEAL_SKU_COLORS[upperSku]) {
      return KOROSEAL_SKU_COLORS[upperSku];
    }
  }

  // Extract from handle (last part is usually color)
  const handleParts = handle.split('-');
  if (handleParts.length > 3) {
    const colorSlug = handleParts[handleParts.length - 1];
    // Capitalize
    return colorSlug.charAt(0).toUpperCase() + colorSlug.slice(1);
  }

  return null;
}

/**
 * Extended manufacturer data with images
 */
interface ManufacturerDataWithImages extends ManufacturerData {
  images?: string[];  // Array of image URLs from manufacturer website
}

/**
 * Scrape Koroseal website for product data AND images
 */
async function scrapeKorosealData(mfrSku: string): Promise<Partial<ManufacturerDataWithImages>> {
  try {
    // Build potential URL
    let url: string | null = null;

    for (const [prefix, collection] of Object.entries(KOROSEAL_SKU_COLLECTIONS)) {
      if (mfrSku.toUpperCase().startsWith(prefix)) {
        const color = KOROSEAL_SKU_COLORS[mfrSku.toUpperCase()];
        if (color) {
          url = `https://koroseal.com/products/wallcoverings/${collection}/${color.toLowerCase().replace(/\s+/g, '-')}`;
        } else {
          url = `https://koroseal.com/products/wallcoverings/${collection}`;
        }
        break;
      }
    }

    if (!url) {
      console.log(`  No URL mapping for SKU: ${mfrSku}`);
      return {};
    }

    console.log(`  Scraping: ${url}`);

    const browser = await chromium.launch({ headless: true });
    const page = await browser.newPage();

    await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
    await page.waitForTimeout(2000);

    const data = await page.evaluate(() => {
      const result: any = { images: [] };
      const text = document.body.innerText;

      // Extract specs
      const patterns: [string, RegExp][] = [
        ['material', /Material\s*:\s*([^\n]+)/i],
        ['backing', /Backing\s*:\s*([^\n]+)/i],
        ['fireRating', /Fire Rating\s*:\s*([^\n]+)/i],
      ];

      for (const [key, regex] of patterns) {
        const match = text.match(regex);
        if (match) result[key] = match[1].trim();
      }

      // Get pattern/color from page title or header
      const h1 = document.querySelector('h1');
      if (h1) result.pageTitle = h1.textContent?.trim();

      // Extract product images
      // Main product image
      const mainImg = document.querySelector('.productcontain img, .pbpic img, .product-image img') as HTMLImageElement;
      if (mainImg?.src) {
        result.images.push(mainImg.src);
      }

      // Additional images from gallery/thumbnails
      document.querySelectorAll('.thumbscontain img, .gallery img, .product-gallery img').forEach((img: Element) => {
        const imgEl = img as HTMLImageElement;
        if (imgEl.src && !result.images.includes(imgEl.src)) {
          // Get full-size version if available (remove thumb suffix)
          let fullSrc = imgEl.src.replace(/_thumb|_small|_medium/gi, '');
          result.images.push(fullSrc);
        }
      });

      // Also check for background-image style (common in Koroseal)
      document.querySelectorAll('.tppic, .thumbprev span[style*="background"]').forEach((el: Element) => {
        const style = (el as HTMLElement).style.backgroundImage;
        const match = style?.match(/url\(['"]?([^'"]+)['"]?\)/);
        if (match && match[1] && !result.images.includes(match[1])) {
          result.images.push(match[1]);
        }
      });

      return result;
    });

    await browser.close();

    return {
      material: data.material,
      backing: data.backing,
      fireRating: data.fireRating,
      korosealUrl: url,
      images: data.images || []
    };
  } catch (e: any) {
    console.log(`  Scrape error: ${e.message}`);
    return {};
  }
}

/**
 * Scrape product images directly from Koroseal collection page by SKU
 */
async function scrapeKorosealImages(mfrSku: string, handle: string): Promise<string[]> {
  try {
    const browser = await chromium.launch({ headless: true });
    const page = await browser.newPage();

    // First, try to find collection URL from SKU
    let collectionUrl: string | null = null;
    for (const [prefix, collection] of Object.entries(KOROSEAL_SKU_COLLECTIONS)) {
      if (mfrSku.toUpperCase().startsWith(prefix)) {
        collectionUrl = `https://koroseal.com/products/wallcoverings/${collection}`;
        break;
      }
    }

    if (!collectionUrl) {
      await browser.close();
      return [];
    }

    console.log(`  Fetching images from: ${collectionUrl}`);
    await page.goto(collectionUrl, { waitUntil: 'networkidle', timeout: 30000 });
    await page.waitForTimeout(2000);

    // Find the specific colorway by SKU
    const images = await page.evaluate((sku) => {
      const results: string[] = [];

      // Look for colorway thumbnails with matching SKU
      document.querySelectorAll('.thumbscontain a.thumbprev, .colorway-thumb').forEach((el) => {
        const skuText = el.querySelector('.tptext')?.textContent?.trim() || '';
        if (skuText.toUpperCase().includes(sku.toUpperCase())) {
          // Found matching colorway - get its image
          const picEl = el.querySelector('.tppic, span[style*="background"]') as HTMLElement;
          if (picEl) {
            const bgStyle = picEl.style.backgroundImage;
            const match = bgStyle?.match(/url\(['"]?([^'"]+)['"]?\)/);
            if (match && match[1]) {
              results.push(match[1]);
            }
          }

          // Also check for img tag
          const img = el.querySelector('img') as HTMLImageElement;
          if (img?.src && !results.includes(img.src)) {
            results.push(img.src);
          }
        }
      });

      // If no match found, get the main product image
      if (results.length === 0) {
        const mainImg = document.querySelector('.pbpic img, .product-main img') as HTMLImageElement;
        if (mainImg?.src) {
          results.push(mainImg.src);
        }
      }

      return results;
    }, mfrSku);

    await browser.close();
    return images;
  } catch (e: any) {
    console.log(`  Image scrape error: ${e.message}`);
    return [];
  }
}

/**
 * Extract manufacturer data from product TAGS
 * Tags contain: Pattern Name, Real SKU (AL21-15, C921-10, 1469311), Color Name, Background Color, etc.
 */
function extractDataFromTags(tags: string): { mfrSku: string | null; patternName: string | null; colorName: string | null } {
  if (!tags) return { mfrSku: null, patternName: null, colorName: null };

  // Split tags into array (comma-separated)
  const tagList = tags.split(',').map(t => t.trim()).filter(t => t);

  let mfrSku: string | null = null;
  let patternName: string | null = null;
  let colorName: string | null = null;

  // Known pattern names (two-word names that appear in tags)
  const knownPatterns = [
    'Adalyn Weave', 'Chateau Wood', 'Rememberance Lite', 'Mandy Budan', 'Sarah Rowland',
    'Digital Print', 'Astra', 'Arco', 'Spire', 'Chateau', 'Ajijic', 'Cancun',
    'Tulum', 'Isla Mujeres', 'Puerto Vallarta', 'Cozumel', 'Playa del Carmen'
  ];

  // SKU patterns: Various manufacturer SKU formats found in tags
  const skuPatterns = [
    /^DW[A-Z]{0,2}-\d{5,6}(-SAMPLE)?$/i, // DWWC-505590-SAMPLE, DWK-31959
    /^[A-Z]{2,4}\d{1,2}-\d{1,2}$/i,      // AL21-15, C921-10, 2R15-21
    /^[A-Z]{3}-[A-Z]{2}\d{2}-\d{2}$/i,   // TAK-DA01-01
    /^\d{6,8}$/,                          // 1469311 (numeric)
    /^[A-Z]{2}\d{2}[A-Z]?-\d{1,2}$/i,    // Another SKU format
    /^[A-Z]{1,3}\d{3,6}[A-Z]?$/i,        // W12345, AB123456
    /^[A-Z]{2,4}-\d{4,6}$/i,             // WXYZ-12345
  ];

  // Look for SKU first (usually first number or SKU-pattern tag)
  for (const tag of tagList) {
    // Check against SKU patterns
    for (const pattern of skuPatterns) {
      if (pattern.test(tag)) {
        mfrSku = tag.toUpperCase();
        break;
      }
    }
    if (mfrSku) break;
  }

  // Look for known pattern names
  for (const tag of tagList) {
    for (const known of knownPatterns) {
      if (tag.toLowerCase() === known.toLowerCase()) {
        patternName = known;
        break;
      }
    }
    if (patternName) break;
  }

  // If no known pattern found, look for two-word names that could be patterns
  if (!patternName) {
    for (const tag of tagList) {
      // Skip if it's an SKU, a color tag, or other system tags
      if (skuPatterns.some(p => p.test(tag))) continue;
      if (tag.startsWith('Background Color')) continue;
      if (tag === 'Wallcovering' || tag === 'Architectural' || tag === 'Koroseal') continue;
      if (tag === 'Type II' || tag === 'Commercial Grade') continue;

      // Two-word pattern names
      const words = tag.split(' ');
      if (words.length === 2 && words[0].length > 2 && words[1].length > 2) {
        // Could be a pattern name like "Adalyn Weave"
        patternName = tag;
        break;
      }
    }
  }

  // Look for color name (single word, not a system tag, not SKU)
  // Color names are usually capitalized single words like "Moonlight", "Gratitude", "Canfu"
  for (const tag of tagList) {
    if (mfrSku && tag === mfrSku) continue;
    if (patternName && tag === patternName) continue;
    if (tag.startsWith('Background Color')) continue;
    if (tag === 'Wallcovering' || tag === 'Architectural' || tag === 'Koroseal') continue;
    if (tag === 'Type II' || tag === 'Commercial Grade') continue;
    if (skuPatterns.some(p => p.test(tag))) continue;

    // Single word capitalized - likely a color name
    if (tag.split(' ').length === 1 && /^[A-Z][a-z]+$/.test(tag)) {
      // Check against known Koroseal colors
      const colorValues = Object.values(KOROSEAL_SKU_COLORS);
      if (colorValues.includes(tag)) {
        colorName = tag;
        break;
      }
    }
  }

  // If no color found from known list, try to find any single capitalized word
  if (!colorName) {
    for (const tag of tagList) {
      if (mfrSku && tag === mfrSku) continue;
      if (patternName && tag === patternName) continue;
      if (tag.startsWith('Background Color')) continue;
      if (tag === 'Wallcovering' || tag === 'Architectural' || tag === 'Koroseal') continue;
      if (tag === 'Type II' || tag === 'Commercial Grade') continue;
      if (skuPatterns.some(p => p.test(tag))) continue;

      // Single word that looks like a color name (capitalized)
      if (tag.split(' ').length === 1 && tag.length > 3 && /^[A-Z]/.test(tag)) {
        colorName = tag;
        break;
      }
    }
  }

  console.log(`  Tags parsed: SKU="${mfrSku}", Pattern="${patternName}", Color="${colorName}"`);
  return { mfrSku, patternName, colorName };
}

/**
 * Parse existing title for pattern, color, SKU, vendor, and artist name
 *
 * Title formats:
 * - "Pattern - Color Wallcovering | Vendor Name (DWWC-123) | Brand"
 *   → Pattern="Pattern", Color="Color", Vendor="Vendor Name"
 * - "Arco Malta (Ra21-03) | Type 2 Vinyl | Architectural Wallcoverings"
 *   → Pattern="Arco", Color="Malta", MFR SKU="RA21-03"
 * - "Along The River (MB00002) | Mandy Budan | Architectural Wallcoverings"
 *   → Artwork name="Along The River", Artist="Mandy Budan"
 *
 * IMPORTANT: DWWC- numbers are INTERNAL SKUs, not manufacturer SKUs!
 * Real MFR SKUs come from tags or have formats like RA21-03, AL21-15, etc.
 */
function parseExistingTitle(title: string): {
  pattern: string | null,
  color: string | null,
  sku: string | null,
  artist: string | null,
  vendor: string | null,
  isArtwork: boolean
} {
  if (!title) return { pattern: null, color: null, sku: null, artist: null, vendor: null, isArtwork: false };

  // Known vendor names to detect (separate from artist names)
  const knownVendors = ['Andrew Martin', 'Koroseal', 'Arte-International', 'Arte', 'Cowtan & Tout',
    'Colefax & Fowler', 'Phillip Jeffries', 'Thibaut', 'York', 'Brewster', 'Graham & Brown',
    'Schumacher', 'Kravet', 'JF Fabrics', 'MDC', 'Wolf-Gordon', 'Carnegie', 'Maharam'];

  // Artist names (for artwork collections - different handling)
  const artistNames = ['Mandy Budan', 'Sarah Rowland'];

  let artist: string | null = null;
  let vendor: string | null = null;
  let isArtwork = false;

  // Split title on "|" to separate components
  const segments = title.split('|').map(s => s.trim());

  // First segment is the product name (before any |)
  let productPart = segments[0] || '';

  // Check other segments for vendor/artist names
  for (let i = 1; i < segments.length; i++) {
    const seg = segments[i];

    // Check for artist names
    for (const artistName of artistNames) {
      if (seg.includes(artistName)) {
        artist = artistName;
        isArtwork = true;
        break;
      }
    }

    // Check for vendor names
    for (const vendorName of knownVendors) {
      if (seg.includes(vendorName)) {
        vendor = vendorName;
        break;
      }
    }
  }

  // Extract SKU from parentheses - but ONLY if it's a REAL MFR SKU format
  // REAL MFR SKUs: RA21-03, AL21-15, MB00002, TAK-DA01-01, C921-10, 2R15-21
  // INTERNAL SKUs (skip): DWWC-500560, DWK-30865
  let sku: string | null = null;
  const allSkuMatches = title.match(/\(([^)]+)\)/g);
  if (allSkuMatches) {
    for (const match of allSkuMatches) {
      const skuCandidate = match.slice(1, -1); // Remove parentheses
      // Skip DWWC- and DWK- internal SKUs
      if (skuCandidate.toUpperCase().startsWith('DWWC-') || skuCandidate.toUpperCase().startsWith('DWK-')) {
        continue;
      }
      // Accept real MFR SKU patterns
      if (/^[A-Z]{1,4}[-]?[A-Z0-9]{2,6}[-]?[A-Z0-9]{0,4}$/i.test(skuCandidate)) {
        sku = skuCandidate.toUpperCase();
        break;
      }
    }
  }

  // Also detect artwork by SKU prefix
  if (!artist && sku) {
    if (sku.startsWith('MB0') || sku.match(/^MB\d/)) {
      artist = 'Mandy Budan';
      isArtwork = true;
    } else if (sku.startsWith('SRD')) {
      artist = 'Sarah Rowland';
      isArtwork = true;
    }
  }

  // Get the product name part (before parentheses and before |)
  let productName = productPart.split('(')[0].trim();

  // Remove trailing "Wallcovering" or "Wallpaper" if present
  productName = productName.replace(/\s+(Wallcovering|Wallpaper)$/i, '').trim();

  // Check if it looks like garbage data
  const badPatterns = ['Type 2', 'Type II', 'Vinyl', 'Sample', 'Architectural', 'Designer Wallcoverings', 'Commercial', 'PVC'];
  const containsBadPattern = badPatterns.some(bad => productName.toLowerCase().includes(bad.toLowerCase()));

  if (containsBadPattern) {
    return { pattern: null, color: null, sku, artist, vendor, isArtwork };
  }

  // For artwork/artist collections, the WHOLE thing is the artwork name - don't split!
  if (isArtwork) {
    console.log(`  ✓ ARTWORK DETECTED: "${productName}" by ${artist} (SKU: ${sku})`);
    return { pattern: productName, color: null, sku, artist, vendor, isArtwork: true };
  }

  // Parse "Pattern - Color" format (common for many vendors)
  // e.g., "Pear Tree - Plaster" → Pattern="Pear Tree", Color="Plaster"
  if (productName.includes(' - ')) {
    const dashParts = productName.split(' - ');
    if (dashParts.length >= 2) {
      const pattern = dashParts[0].trim();
      const color = dashParts[dashParts.length - 1].trim();
      if (pattern.length >= 2 && color.length >= 2) {
        console.log(`  ✓ DASH FORMAT: Pattern="${pattern}", Color="${color}", Vendor="${vendor}"`);
        return { pattern, color, sku, artist: null, vendor, isArtwork: false };
      }
    }
  }

  // Fallback: Split into words - typical format is "Pattern Color" or "Pattern Pattern Color"
  const words = productName.split(/\s+/).filter(w => w.length > 0);

  // Validate starts with uppercase - including Unicode letters
  const startsWithUppercase = (str: string) => {
    const firstChar = str.charAt(0);
    return firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
  };

  if (words.length >= 2) {
    // Last word is usually the color
    const color = words[words.length - 1];
    // Everything before is the pattern
    const pattern = words.slice(0, -1).join(' ');

    if (pattern.length >= 3 && color.length >= 2 &&
        startsWithUppercase(pattern) && startsWithUppercase(color)) {
      return { pattern, color, sku, artist: null, vendor, isArtwork: false };
    }
  } else if (words.length === 1 && words[0].length > 2 && startsWithUppercase(words[0])) {
    // Single word pattern (e.g., "Damier" from "Damier Wallcovering | Innovationsusa")
    // Pattern name is often already the first word in the old title
    console.log(`  ✓ SINGLE-WORD PATTERN: "${words[0]}" (color unknown)`);
    return { pattern: words[0], color: null, sku, artist: null, vendor, isArtwork: false };
  }

  return { pattern: null, color: null, sku, artist, vendor, isArtwork };
}

/**
 * Extract complete manufacturer data from product
 * Priority: 1. EXISTING TITLE (if it already has valid pattern/color - preserve it!)
 *           2. Handle (reliable for live site products)
 *           3. Tags (has mfr# per user)
 *           4. SKU mappings (fallback)
 */
async function extractManufacturerData(product: any, options: { scrapeWebsite?: boolean } = {}): Promise<ManufacturerData> {
  const handle = product.handle || '';
  const tags = product.tags || '';
  const existingTitle = product.title || '';

  console.log(`  Existing title: ${existingTitle}`);
  console.log(`  Parsing handle: ${handle}`);
  console.log(`  Tags: ${tags.substring(0, 100)}...`);

  // PRIORITY 0: Check existing title FIRST - if it already has good data, PRESERVE IT!
  const fromTitle = parseExistingTitle(existingTitle);
  if (fromTitle.isArtwork && fromTitle.pattern) {
    console.log(`  ✓ ARTWORK: "${fromTitle.pattern}" by ${fromTitle.artist} (SKU: ${fromTitle.sku})`);
  } else if (fromTitle.pattern && fromTitle.color) {
    console.log(`  ✓ PRESERVING existing title data: Pattern="${fromTitle.pattern}", Color="${fromTitle.color}", SKU="${fromTitle.sku}", Vendor="${fromTitle.vendor}"`);
  }

  // PRIORITY 1: Extract from TAGS (user confirmed: "the mfr number is in the tags on shopify!!")
  // For vendors like Andrew Martin, the REAL MFR SKU is in tags, not DWWC internal SKU from title
  const fromTags = extractDataFromTags(tags);

  // PRIORITY 2: Extract SKU from handle (reliable for live site products)
  // Handle format: dwk-30865-koroseal-koroseal-type-2-vinyl-2r15-21-rememberance-lite-canfu
  const mfrSkuFromHandle = extractRealMfrSku(product);

  // SKU priority: Tags FIRST (real MFR SKU), then title (only if not DWWC), then handle
  // CRITICAL: Skip DWWC- internal SKUs from title
  let mfrSku = fromTags.mfrSku || mfrSkuFromHandle;
  if (!mfrSku && fromTitle.sku && !fromTitle.sku.startsWith('DWWC')) {
    mfrSku = fromTitle.sku;
  }

  // Parse handle for pattern and color (after SKU in handle)
  const fromHandle = parseHandleForPatternAndColor(handle, mfrSku);

  // Combine all sources: EXISTING TITLE > handle > tags > mappings
  // CRITICAL: Preserve existing good data - don't overwrite!
  let patternName = fromTitle.pattern || fromHandle.pattern || fromTags.patternName || getPatternFromSkuOrHandle(mfrSku, handle);
  let colorName = fromTitle.color || fromHandle.color || fromTags.colorName || getColorFromSkuOrHandle(mfrSku, handle);

  // If we have an SKU but no color, try to get from mapping
  if (mfrSku && !colorName) {
    colorName = KOROSEAL_SKU_COLORS[mfrSku.toUpperCase()] || null;
  }

  // If we have SKU but no pattern, try to get from mapping
  if (mfrSku && !patternName) {
    for (const [prefix, collection] of Object.entries(KOROSEAL_SKU_COLLECTIONS)) {
      if (mfrSku.toUpperCase().startsWith(prefix)) {
        patternName = KOROSEAL_PATTERN_NAMES[collection] || collection;
        break;
      }
    }
  }

  // Get artist info from title parsing
  const artist = fromTitle.artist || null;
  const isArtwork = fromTitle.isArtwork || false;

  // For artworks, don't try to find color - the pattern IS the artwork name
  if (isArtwork && !colorName) {
    colorName = null; // Don't force a color for artworks
  }

  let data: ManufacturerData = {
    mfrSku,
    patternName,
    colorName,
    artist,
    isArtwork
  };

  console.log(`  FINAL: SKU="${mfrSku}", Pattern="${patternName}", Color="${colorName}", Artist="${artist}", Artwork=${isArtwork}`);

  // Optionally scrape website for additional data
  if (options.scrapeWebsite && mfrSku) {
    const scraped = await scrapeKorosealData(mfrSku);
    data = { ...data, ...scraped };
  }

  return data;
}

/**
 * Build tags array from color analysis AND manufacturer data
 * Per user requirement: pattern name, color, and real SKU must be in tags
 * IMPORTANT: Preserves existing manufacturer number tags from original product
 */
function buildTags(colorAnalysis: any, mfrData: ManufacturerData, vendor: string, existingTags: string[] = []): string[] {
  const tags = new Set<string>();

  // FIRST: Preserve existing manufacturer number tags (SKUs, part numbers)
  // Patterns like: DWWC-505040-SAMPLE, AL21-15, C921-10, TAK-DA01-01, etc.
  const mfrNumberPattern = /^[A-Z0-9]{2,}[-][A-Z0-9]+/i;
  existingTags.forEach(tag => {
    const trimmed = tag.trim();
    if (mfrNumberPattern.test(trimmed)) {
      tags.add(trimmed);
    }
  });

  // Add manufacturer pattern name (REQUIRED per user)
  if (mfrData.patternName) {
    tags.add(mfrData.patternName);
  }

  // Add manufacturer color name (REQUIRED per user)
  if (mfrData.colorName) {
    tags.add(mfrData.colorName);
  }

  // Add real manufacturer SKU (REQUIRED per user)
  if (mfrData.mfrSku) {
    tags.add(mfrData.mfrSku);
  }

  // Add AI-analyzed colors from interior designer skill
  if (colorAnalysis) {
    if (colorAnalysis.backgroundColor) {
      tags.add(`Background Color ${colorAnalysis.backgroundColor}`);
    }
    colorAnalysis.colorTags?.forEach((c: string) => tags.add(c));
    colorAnalysis.styleTags?.forEach((s: string) => tags.add(s));
    colorAnalysis.applicationTags?.forEach((a: string) => tags.add(a));
    colorAnalysis.materialTags?.forEach((m: string) => tags.add(m));

    // NOTE: Type II and Commercial Grade tags removed per user request
    // These should only be added from actual product specs, not AI detection
  }

  // Always add base tags
  tags.add('Wallcovering');
  tags.add('Architectural');

  // Add actual vendor name (not hardcoded)
  if (vendor) {
    tags.add(vendor);
  }

  return Array.from(tags);
}

/**
 * Metafield push interfaces and function
 */
interface MetafieldInput {
  key: string;
  namespace: string;
  ownerId: string;
  type: string;
  value: string;
}

interface MetafieldPushResult {
  success: boolean;
  updatedFields: string[];
  errors: string[];
}

/**
 * Push AI-generated data to Shopify metafields using GraphQL
 * Uses metafieldsSet mutation for efficient bulk updates
 */
async function pushMetafieldsToShopify(
  productId: string | number,
  mfrData: any,
  colorAnalysis: any,
  description: string
): Promise<MetafieldPushResult> {
  const result: MetafieldPushResult = {
    success: false,
    updatedFields: [],
    errors: []
  };

  // Build metafield inputs
  const metafields: MetafieldInput[] = [];
  const ownerId = `gid://shopify/Product/${productId}`;

  // dwc.ai_generated_colors - list of color tags (max 2 unique colors)
  const uniqueColors = [...new Set(colorAnalysis?.colorTags || [])].slice(0, 2);
  if (uniqueColors.length > 0) {
    metafields.push({
      key: 'ai_generated_colors',
      namespace: 'dwc',
      ownerId,
      type: 'list.single_line_text_field',
      value: JSON.stringify(uniqueColors)
    });
  }

  // dwc.ai_generated_tags - combined style/application/material tags
  const allTags: string[] = [
    ...(colorAnalysis?.styleTags || []),
    ...(colorAnalysis?.applicationTags || []),
    ...(colorAnalysis?.materialTags || [])
  ];
  if (allTags.length > 0) {
    metafields.push({
      key: 'ai_generated_tags',
      namespace: 'dwc',
      ownerId,
      type: 'list.single_line_text_field',
      value: JSON.stringify(allTags)
    });
  }

  // dwc.ai_generated_description
  if (description) {
    // Strip HTML tags for clean metafield value
    const cleanDescription = description.replace(/<[^>]*>/g, '').trim();
    if (cleanDescription) {
      metafields.push({
        key: 'ai_generated_description',
        namespace: 'dwc',
        ownerId,
        type: 'multi_line_text_field',
        value: cleanDescription
      });
    }
  }

  // dwc.background_color
  if (colorAnalysis?.backgroundColor) {
    metafields.push({
      key: 'background_color',
      namespace: 'dwc',
      ownerId,
      type: 'single_line_text_field',
      value: colorAnalysis.backgroundColor
    });
  }

  // dwc.color_analysis - full JSON for debugging/advanced use
  if (colorAnalysis) {
    metafields.push({
      key: 'color_analysis',
      namespace: 'dwc',
      ownerId,
      type: 'json',
      value: JSON.stringify(colorAnalysis)
    });
  }

  // dwc.manufacturer_sku
  if (mfrData?.mfrSku) {
    metafields.push({
      key: 'manufacturer_sku',
      namespace: 'dwc',
      ownerId,
      type: 'single_line_text_field',
      value: mfrData.mfrSku
    });
  }

  // dwc.pattern_name
  if (mfrData?.patternName) {
    metafields.push({
      key: 'pattern_name',
      namespace: 'dwc',
      ownerId,
      type: 'single_line_text_field',
      value: mfrData.patternName
    });
  }

  // dwc.color (color name)
  if (mfrData?.colorName) {
    metafields.push({
      key: 'color',
      namespace: 'dwc',
      ownerId,
      type: 'single_line_text_field',
      value: mfrData.colorName
    });
  }

  // dwc.real_vendor (actual vendor)
  if (mfrData?.vendor || processState.vendor) {
    metafields.push({
      key: 'real_vendor',
      namespace: 'dwc',
      ownerId,
      type: 'single_line_text_field',
      value: mfrData?.vendor || processState.vendor
    });
  }

  if (metafields.length === 0) {
    result.success = true;
    return result;
  }

  // Execute GraphQL mutation
  const mutation = `
    mutation MetafieldsSet($metafields: [MetafieldsSetInput!]!) {
      metafieldsSet(metafields: $metafields) {
        metafields {
          id
          key
          namespace
        }
        userErrors {
          field
          message
        }
      }
    }
  `;

  try {
    const graphqlUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`;

    const response = await fetch(graphqlUrl, {
      method: 'POST',
      headers: {
        'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        query: mutation,
        variables: { metafields }
      })
    });

    if (!response.ok) {
      const errorText = await response.text();
      result.errors.push(`HTTP ${response.status}: ${errorText}`);
      return result;
    }

    const data = await response.json() as any;

    // Check for GraphQL errors
    if (data.errors && data.errors.length > 0) {
      result.errors = data.errors.map((e: any) => e.message);
      return result;
    }

    // Check for user errors
    const userErrors = data.data?.metafieldsSet?.userErrors || [];
    if (userErrors.length > 0) {
      result.errors = userErrors.map((e: any) => `${e.field}: ${e.message}`);
      return result;
    }

    // Track updated fields
    const updatedMetafields = data.data?.metafieldsSet?.metafields || [];
    result.updatedFields = updatedMetafields.map((m: any) => `${m.namespace}.${m.key}`);
    result.success = true;

    console.log(`  📦 Metafields updated: ${result.updatedFields.length} fields`);

  } catch (e: any) {
    result.errors.push(e.message);
    console.error('  ⚠️ Metafield push error:', e.message);
  }

  return result;
}

/**
 * Update Shopify product images from manufacturer URLs
 */
async function updateProductImages(productId: number, imageUrls: string[]): Promise<boolean> {
  if (!imageUrls || imageUrls.length === 0) {
    console.log(`  No images to update for product ${productId}`);
    return false;
  }

  try {
    // First, delete existing images
    const getUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/products/${productId}/images.json`;
    const existingRes = await fetch(getUrl, {
      headers: {
        'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
        'Content-Type': 'application/json'
      }
    });

    if (existingRes.ok) {
      const existingData: any = await existingRes.json();
      // Delete each existing image
      for (const img of existingData.images || []) {
        const deleteUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/products/${productId}/images/${img.id}.json`;
        await fetch(deleteUrl, {
          method: 'DELETE',
          headers: {
            'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken
          }
        });
        await new Promise(r => setTimeout(r, 200)); // Rate limit
      }
    }

    // Add new images from manufacturer
    for (let i = 0; i < imageUrls.length && i < 5; i++) { // Max 5 images
      const imageUrl = imageUrls[i];
      console.log(`  Adding image ${i + 1}: ${imageUrl.substring(0, 60)}...`);

      const addUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/products/${productId}/images.json`;
      const addRes = await fetch(addUrl, {
        method: 'POST',
        headers: {
          'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          image: {
            src: imageUrl,
            position: i + 1
          }
        })
      });

      if (!addRes.ok) {
        console.log(`  Failed to add image: ${await addRes.text()}`);
      }

      await new Promise(r => setTimeout(r, 500)); // Rate limit
    }

    return true;
  } catch (e: any) {
    console.log(`  Image update error: ${e.message}`);
    return false;
  }
}

/**
 * Update a single product using real manufacturer data AND images
 */
async function updateProduct(product: any, options: { syncImages?: boolean } = {}): Promise<any> {
  // Check for user overrides for this product
  const productId = String(product.id);
  const userOverrides = processState.overrides[productId];
  if (userOverrides && Object.keys(userOverrides).length > 0) {
    console.log(`  User overrides found for product ${productId}:`, Object.keys(userOverrides));
  }

  // Extract REAL manufacturer data (not Shopify internal SKUs)
  // Also scrape website if we need images or more data
  const mfrData = await extractManufacturerData(product, { scrapeWebsite: options.syncImages });

  // Apply user overrides to mfrData if provided
  if (userOverrides?.pattern) {
    console.log(`  Applying pattern override: "${userOverrides.pattern}"`);
    mfrData.patternName = userOverrides.pattern;
  }
  if (userOverrides?.color) {
    console.log(`  Applying color override: "${userOverrides.color}"`);
    mfrData.colorName = userOverrides.color;
  }
  if (userOverrides?.mfrSku) {
    console.log(`  Applying SKU override: "${userOverrides.mfrSku}"`);
    mfrData.mfrSku = userOverrides.mfrSku;
  }

  console.log(`  Extracted: Pattern="${mfrData.patternName}", Color="${mfrData.colorName}", SKU="${mfrData.mfrSku}"`);

  // If syncImages is enabled and we have a real SKU, fetch images from manufacturer
  let mfrImages: string[] = [];
  if (options.syncImages && mfrData.mfrSku) {
    console.log(`  Fetching manufacturer images for ${mfrData.mfrSku}...`);
    mfrImages = await scrapeKorosealImages(mfrData.mfrSku, product.handle || '');
    console.log(`  Found ${mfrImages.length} images from manufacturer`);
  }

  // Analyze colors with AI if image available (use manufacturer image if we have one)
  let colorAnalysis = null;
  const imageToAnalyze = mfrImages.length > 0 ? mfrImages[0] : (product.images?.[0]?.src || null);
  if (imageToAnalyze) {
    colorAnalysis = await analyzeColorsWithAI(imageToAnalyze, product.title);
  }

  // Format new title using REAL manufacturer data (or override if provided)
  // For artworks: "Artwork Name (SKU) | Artist Name | Suffix"
  // For regular: "Pattern Name Color (Real mfr#) | Suffix"
  const fallbackVendorName = product.vendor || 'Wallcovering';
  let newTitle = formatTitle(
    mfrData.patternName || fallbackVendorName,
    mfrData.colorName || '',
    mfrData.mfrSku,
    { artist: mfrData.artist, isArtwork: mfrData.isArtwork }
  );

  // Apply title override if user provided one
  if (userOverrides?.title) {
    console.log(`  Applying title override: "${userOverrides.title}"`);
    newTitle = userOverrides.title;
  }

  // Build rich description with AI content, specs, and manufacturer link
  const currentSpecs = getCurrentUpdateSpecs();

  // Extract AI description and source URL from product metafields
  const metafields = product.metafields || [];
  const getMetafield = (key: string) => {
    const mf = metafields.find((m: any) => m.key === key);
    return mf?.value || null;
  };
  const aiDescription = getMetafield('ai_generated_description');
  const sourceUrl = getMetafield('source_url');

  // Start with AI description if available, otherwise use default SEO text
  let newDescription = '';

  if (aiDescription) {
    // Use the AI-generated description (already HTML formatted)
    newDescription = aiDescription + '\n\n';
  } else {
    newDescription = currentSpecs.seoDescription + '\n\n';
  }

  // Add manufacturer specs if available
  const specsToShow = [];
  if (mfrData.material) specsToShow.push(`<li><strong>Material:</strong> ${mfrData.material}</li>`);
  if (mfrData.backing) specsToShow.push(`<li><strong>Backing:</strong> ${mfrData.backing}</li>`);
  if (mfrData.fireRating) specsToShow.push(`<li><strong>Fire Rating:</strong> ${mfrData.fireRating}</li>`);
  if (mfrData.mfrSku) specsToShow.push(`<li><strong>Manufacturer SKU:</strong> ${mfrData.mfrSku}</li>`);

  if (specsToShow.length > 0) {
    newDescription += '<h3>Specifications</h3>\n<ul>\n' + specsToShow.join('\n') + '\n</ul>\n\n';
  }

  // Add manufacturer link if available
  if (sourceUrl) {
    newDescription += `<p><a href="${sourceUrl}" target="_blank" rel="noopener">View on Manufacturer Website →</a></p>\n\n`;
  }

  newDescription += '<p><strong>Resources:</strong> Spec Sheet | Installation Guide</p>';

  // Apply description override if user provided one
  if (userOverrides?.description) {
    console.log(`  Applying description override`);
    newDescription = userOverrides.description;
  }

  // Build tags - includes pattern name, color, real SKU, and actual vendor name
  // IMPORTANT: Pass existing tags to preserve manufacturer numbers
  const existingTags = (product.tags || '').split(',').map((t: string) => t.trim()).filter((t: string) => t);
  const newTags = buildTags(colorAnalysis, mfrData, product.vendor || '', existingTags);

  // Prepare update payload
  const updateData = {
    product: {
      id: product.id,
      title: newTitle,
      body_html: newDescription,
      product_type: currentSpecs.productType,
      tags: newTags.join(', '),
      variants: product.variants?.map((v: any) => ({
        id: v.id,
        price: currentSpecs.samplePrice.toString()
      }))
    }
  };

  // Make API call to Shopify
  const url = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/products/${product.id}.json`;

  const response = await fetch(url, {
    method: 'PUT',
    headers: {
      'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(updateData)
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Shopify API error: ${response.status} - ${error}`);
  }

  // Update images if enabled and we have manufacturer images
  let imagesUpdated = false;
  if (options.syncImages && mfrImages.length > 0) {
    console.log(`  Syncing ${mfrImages.length} images to Shopify...`);
    imagesUpdated = await updateProductImages(product.id, mfrImages);
  }

  // Push AI data to Shopify metafields (non-blocking - errors don't fail the update)
  let metafieldResult: MetafieldPushResult = { success: false, updatedFields: [], errors: [] };
  try {
    metafieldResult = await pushMetafieldsToShopify(
      product.id,
      mfrData,
      colorAnalysis,
      newDescription
    );
    if (!metafieldResult.success && metafieldResult.errors.length > 0) {
      console.warn(`  ⚠️ Metafield warnings: ${metafieldResult.errors.join(', ')}`);
    }
  } catch (e: any) {
    console.warn(`  ⚠️ Metafield push skipped: ${e.message}`);
  }

  // Log this update to the database for tracking
  const oldTags = (product.tags || '').split(',').map((t: string) => t.trim()).filter((t: string) => t);
  await logUpdateToDatabase(
    product,
    mfrData,
    colorAnalysis,
    product.title,
    newTitle,
    oldTags,
    newTags,
    imagesUpdated,
    mfrImages.length,
    metafieldResult
  );

  return {
    success: true,
    productId: product.id,
    oldTitle: product.title,
    newTitle,
    mfrData,
    colorAnalysis,
    tags: newTags,
    imagesUpdated,
    imageCount: mfrImages.length,
    metafieldsUpdated: metafieldResult.success,
    metafieldsCount: metafieldResult.updatedFields.length,
    metafieldErrors: metafieldResult.errors,
    metafieldResult  // Full result for UI display
  };
}

// Dashboard HTML
function getDashboardHTML(): string {
  const theme = 'dark';
  const themeStyles = getThemeStyles(theme);

  return `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Architectural Update Agent | DW-Agents</title>
  ${themeStyles}
  <style>
    :root {
      --bg-primary: #1a1a2e;
      --bg-secondary: #16213e;
      --text-primary: #eaeaea;
      --text-secondary: #a0a0a0;
      --accent: #00d9ff;
      --border: #2a2a4a;
      --surface: #1e1e3f;
      --surface-alt: #252550;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); min-height: 100vh; }
    .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
    .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
    .header h1 { color: var(--accent); font-size: 24px; }
    .status-badge { padding: 6px 16px; border-radius: 20px; font-size: 12px; font-weight: 600; }
    .status-idle { background: var(--bg-secondary); color: var(--text-secondary); }
    .status-running { background: #27ae60; color: white; }
    .status-paused { background: #f39c12; color: #1a1a2e; }
    .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
    @media (max-width: 900px) { .grid { grid-template-columns: 1fr; } }
    .card { background: var(--bg-secondary); border-radius: 12px; padding: 20px; border: 1px solid var(--border); }
    .card h2 { color: var(--accent); margin-bottom: 16px; font-size: 16px; border-bottom: 1px solid var(--border); padding-bottom: 10px; }
    .form-group { margin-bottom: 16px; }
    label { display: block; margin-bottom: 6px; color: var(--text-secondary); font-size: 14px; }
    select, input { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-primary); color: var(--text-primary); font-size: 16px; }
    select:focus, input:focus { outline: none; border-color: var(--accent); }
    .btn-group { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; }
    .btn { padding: 12px 24px; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
    .btn:disabled { opacity: 0.5; cursor: not-allowed; }
    .btn-primary { background: var(--accent); color: var(--bg-primary); }
    .btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
    .btn-warning { background: #f39c12; color: #1a1a2e; }
    .btn-danger { background: #e74c3c; color: white; }
    .btn-secondary { background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border); }
    .progress-bar { height: 24px; background: var(--bg-primary); border-radius: 12px; overflow: hidden; margin: 15px 0; }
    .progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent), #44a08d); transition: width 0.3s; display: flex; align-items: center; justify-content: center; font-weight: 600; color: var(--bg-primary); font-size: 12px; }
    .stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-top: 15px; }
    .stat { background: var(--bg-primary); padding: 15px; border-radius: 8px; text-align: center; }
    .stat-value { font-size: 24px; font-weight: 700; color: var(--accent); }
    .stat-label { font-size: 11px; color: var(--text-secondary); margin-top: 4px; text-transform: uppercase; }
    .products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; max-height: 70vh; overflow-y: auto; margin-top: 15px; padding: 10px 5px; }
    .sku-card { background: var(--bg-secondary); border-radius: 12px; padding: 15px; border: 2px solid var(--border); transition: all 0.3s ease; position: relative; }
    .sku-card.pending { border-color: var(--border); opacity: 0.7; }
    .sku-card.updating { border-color: #f39c12; box-shadow: 0 0 10px rgba(243, 156, 18, 0.3); animation: pulse 1.5s infinite; }
    .sku-card.completed { border-color: #10b981; background: linear-gradient(135deg, #ecfdf5, #d1fae5); box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); }
    .sku-card.previously-updated { border-color: #3b82f6; background: linear-gradient(135deg, #eff6ff, #dbeafe); box-shadow: 0 0 8px rgba(59, 130, 246, 0.3); }
    .sku-card.error { border-color: #e74c3c; box-shadow: 0 0 10px rgba(231, 76, 60, 0.2); }
    .sku-card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
    .sku-card img { width: 60px; height: 60px; object-fit: cover; border-radius: 8px; border: 1px solid var(--border); }
    .sku-card-info { flex: 1; min-width: 0; }
    .sku-card-status { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 10px; font-weight: 600; text-transform: uppercase; margin-bottom: 8px; }
    .sku-card-status.pending { background: #95a5a6; color: white; }
    .sku-card-status.updating { background: #f39c12; color: white; }
    .sku-card-status.completed { background: #10b981; color: white; }
    .sku-card-status.previously-updated { background: #3b82f6; color: white; }
    .sku-card-status.error { background: #e74c3c; color: white; }
    .sku-card-details { margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.1); border-radius: 8px; font-size: 10px; display: none; }
    .sku-card.completed .sku-card-details { display: block; }
    .sku-card-details .detail-row { display: flex; justify-content: space-between; margin-bottom: 4px; }
    .sku-card-details .detail-label { color: var(--text-secondary); }
    .sku-card-details .detail-value { color: #10b981; font-weight: 500; }
    .sku-card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
    .sku-card-tag { background: #10b981; color: white; padding: 2px 6px; border-radius: 4px; font-size: 9px; }
    .sku-card-tag.old { background: #95a5a6; text-decoration: line-through; }
    .sku-card-refresh { position: absolute; top: 10px; right: 10px; background: #3498db; color: white; border: none; padding: 4px 8px; border-radius: 4px; font-size: 10px; cursor: pointer; opacity: 0; transition: opacity 0.2s; }
    .sku-card:hover .sku-card-refresh { opacity: 1; }
    .sku-card-refresh:hover { background: #2980b9; }
    .sku-card-title { font-size: 12px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.3; }
    .sku-card-sku { font-size: 10px; color: var(--text-secondary); font-family: monospace; margin-bottom: 8px; }
    .sku-card-old-title { font-size: 11px; color: var(--text-secondary); margin-bottom: 6px; padding: 6px; background: var(--bg-primary); border-radius: 4px; border-left: 3px solid #95a5a6; }
    .sku-card-new-title { font-size: 11px; color: var(--text-primary); font-weight: 500; padding: 6px; background: var(--bg-primary); border-radius: 4px; border-left: 3px solid var(--accent); margin-top: 6px; }
    .sku-card-timestamp { font-size: 9px; color: var(--text-secondary); margin-top: 8px; text-align: right; }
    .sku-card-error { font-size: 10px; color: #e74c3c; margin-top: 8px; padding: 6px; background: rgba(231, 76, 60, 0.1); border-radius: 4px; }
    .sku-card-images-info { font-size: 9px; color: var(--accent); margin-top: 4px; }
    .sku-card-metafields-info { font-size: 9px; color: #10b981; margin-top: 4px; }
    .sku-card-metafields-info.warning { color: #f59e0b; }
    .sku-card-metafields-info.error { color: #ef4444; }
    @keyframes pulse { 0%, 100% { opacity: 1; box-shadow: 0 0 10px rgba(243, 156, 18, 0.3); } 50% { opacity: 0.9; box-shadow: 0 0 20px rgba(243, 156, 18, 0.5); } }
    .specs-box { background: var(--bg-primary); padding: 15px; border-radius: 8px; font-size: 12px; margin-top: 15px; }
    .specs-box code { color: var(--accent); background: var(--border); padding: 2px 6px; border-radius: 4px; }
    .log-box { background: #0d0d1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 11px; max-height: 200px; overflow-y: auto; margin-top: 15px; }
    .log-entry { margin-bottom: 4px; }
    .log-entry.error { color: #e74c3c; }
    .log-entry.success { color: var(--accent); }
    .log-entry.info { color: #f39c12; }
    .history-toggle { cursor: pointer; display: flex; align-items: center; gap: 8px; }
    .history-toggle:hover { color: var(--accent); }
    .history-content { display: none; margin-top: 15px; }
    .history-content.open { display: block; }
    .history-item { background: var(--bg-primary); padding: 16px; border-radius: 12px; margin-bottom: 20px; font-size: 12px; border: 1px solid var(--border); }
    .history-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 12px; border-bottom: 1px solid var(--border); flex-wrap: wrap; gap: 10px; }
    .history-item-stats { display: flex; gap: 20px; flex-wrap: wrap; }
    .history-item-stat { text-align: center; }
    .history-item-stat-value { font-size: 18px; font-weight: 700; color: var(--accent); }
    .history-item-stat-label { font-size: 10px; color: var(--text-secondary); text-transform: uppercase; }
    .history-products-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 12px; }
    @media (max-width: 1400px) { .history-products-grid { grid-template-columns: repeat(3, 1fr); } }
    @media (max-width: 1000px) { .history-products-grid { grid-template-columns: repeat(2, 1fr); } }
    @media (max-width: 600px) { .history-products-grid { grid-template-columns: 1fr; } }
    .history-product-card { background: var(--bg-secondary); border-radius: 8px; padding: 12px; border: 1px solid var(--border); font-size: 11px; }
    .history-product-card.completed { border-color: #10b981; background: linear-gradient(135deg, #ecfdf5, #d1fae5); }
    .history-product-card.error { border-color: #e74c3c; background: linear-gradient(135deg, #fef2f2, #fecaca); }
    .history-product-card-title { font-weight: 600; color: var(--text-primary); margin-bottom: 6px; word-break: break-word; }
    .history-product-card-sku { color: var(--accent); font-family: monospace; margin-bottom: 4px; }
    .history-product-card-details { color: var(--text-secondary); font-size: 10px; }
    .history-product-card-old { color: #ef4444; font-size: 10px; margin-top: 6px; padding: 4px 6px; background: rgba(239, 68, 68, 0.1); border-radius: 4px; }
    .history-product-card-new { color: #10b981; font-size: 10px; margin-top: 4px; padding: 4px 6px; background: rgba(16, 185, 129, 0.1); border-radius: 4px; }
    .history-toggle-products { cursor: pointer; color: var(--accent); font-size: 11px; margin-top: 10px; display: inline-block; }
    .history-toggle-products:hover { text-decoration: underline; }
    /* Approval Modal - White Background Theme */
    .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 10000; overflow-y: auto; }
    .modal-overlay.open { display: flex; justify-content: center; padding: 20px; align-items: flex-start; }
    .modal-content { background: #ffffff; border-radius: 16px; padding: 30px; width: 100%; max-width: 1200px; margin: 20px auto; box-shadow: 0 20px 60px rgba(0,0,0,0.3); box-sizing: border-box; overflow-x: hidden; }
    .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #e5e7eb; }
    .modal-header h2 { color: #1f2937; font-size: 24px; font-weight: 700; }
    .modal-close { background: #f3f4f6; border: none; color: #6b7280; font-size: 28px; cursor: pointer; padding: 5px 15px; border-radius: 8px; }
    .modal-close:hover { background: #e5e7eb; color: #1f2937; }
    .select-all-bar { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; background: #f8fafc; padding: 16px 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid #e5e7eb; }
    .select-all-bar label { display: flex; align-items: center; gap: 10px; cursor: pointer; font-weight: 600; color: #1f2937; }
    .select-all-bar input[type="checkbox"] { width: 22px; height: 22px; accent-color: #10b981; cursor: pointer; }
    .selected-count { color: #10b981; font-weight: 700; font-size: 16px; }
    .approval-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; max-height: 60vh; overflow-y: auto; overflow-x: hidden; padding: 20px; background: #f9fafb; border-radius: 12px; box-sizing: border-box; }
    .approval-item { background: #ffffff; border-radius: 12px; padding: 15px; border: 2px solid #e5e7eb; transition: all 0.2s; position: relative; cursor: pointer; box-shadow: 0 2px 8px rgba(0,0,0,0.05); display: flex; gap: 15px; box-sizing: border-box; overflow: visible; }
    .approval-item:hover { border-color: #10b981; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
    .approval-item.selected { border-color: #10b981; background: #ecfdf5; }
    .approval-item input[type="checkbox"] { position: absolute; top: 12px; left: 12px; width: 20px; height: 20px; accent-color: #10b981; cursor: pointer; z-index: 10; }
    .approval-item img { width: 100px; height: 100px; object-fit: cover; border-radius: 8px; border: 1px solid #e5e7eb; background: #f3f4f6; flex-shrink: 0; }
    .approval-item .info-panel { flex: 1; text-align: left; min-width: 0; }
    .approval-item .before-after { display: flex; flex-direction: column; gap: 8px; }
    .approval-item .section { padding: 8px; border-radius: 6px; }
    .approval-item .before { background: #fef2f2; border-left: 3px solid #ef4444; }
    .approval-item .after { background: #ecfdf5; border-left: 3px solid #10b981; }
    .approval-item .section-label { font-size: 10px; font-weight: 700; text-transform: uppercase; color: #6b7280; margin-bottom: 4px; }
    .approval-item .before .section-label { color: #ef4444; }
    .approval-item .after .section-label { color: #10b981; }
    .approval-item .title { font-size: 12px; color: #1f2937; font-weight: 600; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.3; }
    .approval-item .details { font-size: 11px; color: #6b7280; }
    .approval-item .details span { display: inline-block; margin-right: 8px; }
    .approval-item .sku { font-size: 11px; color: #10b981; font-weight: 600; }
    .approval-item.previously-updated { border-color: #3b82f6; background: linear-gradient(135deg, #eff6ff, #dbeafe); }
    .approval-item .prev-updated-badge { display: flex; align-items: center; gap: 6px; background: #3b82f6; color: white; padding: 4px 10px; border-radius: 6px; font-size: 10px; font-weight: 600; margin-bottom: 8px; }
    .approval-item .prev-updated-badge svg { width: 12px; height: 12px; }
    .modal-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 24px; padding-top: 20px; border-top: 2px solid #e5e7eb; flex-wrap: wrap; gap: 15px; }
    .batch-sizes { display: flex; gap: 10px; flex-wrap: wrap; }
    .batch-btn { padding: 10px 20px; border: 2px solid #e5e7eb; background: #ffffff; color: #374151; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }
    .batch-btn:hover { border-color: #10b981; color: #10b981; }
    .batch-btn.active { background: #10b981; color: #ffffff; border-color: #10b981; }
    .modal-footer .btn { padding: 12px 28px; font-size: 15px; font-weight: 600; }
    .modal-footer .btn-primary { background: #10b981; color: white; border: none; border-radius: 8px; cursor: pointer; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); transition: all 0.2s; }
    .modal-footer .btn-primary:hover { background: #059669; box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4); transform: translateY(-1px); }
    .modal-footer .btn-primary:active { transform: translateY(0); box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3); }
    .modal-footer .btn-primary:disabled { background: #d1d5db; color: #9ca3af; cursor: not-allowed; box-shadow: none; transform: none; }
    .modal-footer .btn-secondary { background: #f3f4f6; color: #374151; border: 2px solid #e5e7eb; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
    .modal-footer .btn-secondary:hover { background: #e5e7eb; }
    .modal-footer .btn-load-more { background: #3b82f6; color: white; border: none; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
    .modal-footer .btn-load-more:hover { background: #2563eb; }
    .modal-footer .btn-load-more:disabled { background: #9ca3af; cursor: wait; }
    /* Expanded Detail View */
    .approval-item .expand-btn { position: absolute; top: 12px; right: 12px; background: #f3f4f6; border: none; color: #6b7280; font-size: 14px; cursor: pointer; padding: 4px 10px; border-radius: 6px; z-index: 10; }
    .approval-item .expand-btn:hover { background: #e5e7eb; color: #1f2937; }
    .approval-item.expanded { grid-column: 1 / -1; overflow: visible; margin-left: 0; margin-right: 0; }
    .approval-item .detail-panel { display: none; width: 100%; margin-top: 15px; margin-left: 0; margin-right: 0; padding-top: 15px; padding-left: 0; padding-right: 0; border-top: 1px solid #e5e7eb; box-sizing: border-box; overflow: visible; }
    .approval-item.expanded .detail-panel { display: block; }
    .detail-panel .detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; width: 100%; box-sizing: border-box; }
    .detail-panel .detail-section { background: #f9fafb; border-radius: 8px; padding: 15px; min-width: 0; overflow-wrap: break-word; }
    .detail-panel .detail-section.current { border-left: 4px solid #ef4444; margin-left: 0; }
    .detail-panel .detail-section.proposed { border-left: 4px solid #10b981; margin-left: 0; }
    .detail-panel .detail-section h4 { font-size: 12px; text-transform: uppercase; color: #6b7280; margin-bottom: 10px; font-weight: 700; }
    .detail-panel .detail-section.current h4 { color: #ef4444; }
    .detail-panel .detail-section.proposed h4 { color: #10b981; }
    .detail-panel .detail-row { margin-bottom: 10px; }
    .detail-panel .detail-row label { display: block; font-size: 11px; color: #6b7280; margin-bottom: 4px; font-weight: 600; }
    .detail-panel .detail-row .value { font-size: 13px; color: #1f2937; padding: 8px; background: #ffffff; border-radius: 4px; border: 1px solid #e5e7eb; word-break: break-word; }
    .detail-panel .detail-row input, .detail-panel .detail-row textarea { width: 100%; padding: 8px; font-size: 13px; border: 2px solid #e5e7eb; border-radius: 6px; background: #ffffff; color: #1f2937; }
    .detail-panel .detail-row input:focus, .detail-panel .detail-row textarea:focus { outline: none; border-color: #10b981; }
    .detail-panel .detail-row textarea { min-height: 80px; resize: vertical; }
    .detail-panel .override-indicator { display: inline-block; background: #fbbf24; color: #1f2937; font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px; margin-left: 8px; }
    .detail-panel .reset-btn { background: #f3f4f6; border: 1px solid #e5e7eb; color: #6b7280; font-size: 11px; padding: 4px 8px; border-radius: 4px; cursor: pointer; margin-left: 8px; }
    .detail-panel .reset-btn:hover { background: #e5e7eb; }
    @media (max-width: 800px) { .detail-panel .detail-grid { grid-template-columns: 1fr; } }
    /* Make detail panel more visible */
    .approval-item { min-height: 200px; }
    .approval-item .detail-panel { max-height: none; }
    .detail-panel .detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
    @media (max-width: 1000px) { 
      .detail-panel .detail-grid { grid-template-columns: 1fr; }
      .approval-item.expanded { min-height: auto; }
    }

    .approval-item.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 20000; background: #ffffff; overflow-y: auto; margin: 0; border-radius: 0; max-width: 100%; }
    .approval-item.fullscreen .detail-panel { display: block !important; }
    .fullscreen-btn { position: absolute; top: 12px; right: 120px; background: #3b82f6; border: none; color: white; font-size: 12px; cursor: pointer; padding: 4px 10px; border-radius: 6px; z-index: 10; }
    .fullscreen-btn:hover { background: #2563eb; }

  
    /* Make detail panel more prominent and visible */
    .approval-item { min-height: 180px; }
    .approval-item .detail-panel { 
      border: 2px dashed #e5e7eb;
      background: #fafbfc;
      margin-top: 20px;
    }
    .approval-item.expanded .detail-panel { 
      border-color: #10b981;
      background: #f0fdf4;
    }
    .detail-panel .detail-section { 
      box-shadow: 0 2px 8px rgba(0,0,0,0.05);
    }
    /* Make expand button more prominent */
    .approval-item .expand-btn {
      background: #10b981 !important;
      color: white !important;
      font-weight: 600;
    }
    .approval-item .expand-btn:hover {
      background: #059669 !important;
    }

  </style>
</head>
<body>
  ${getUniversalHeader('Architectural Update Agent', theme)}

  <div class="container">
    <div class="header">
      <h1>Architectural Wallcoverings Updater</h1>
      <span id="statusBadge" class="status-badge status-idle">IDLE</span>
    </div>

    <div class="grid">
      <!-- Left Column: Configuration -->
      <div>
        <div class="card">
          <h2>Configuration</h2>
          <div class="form-group" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid var(--accent);">
            <label>Update Skill</label>
            <div style="display: flex; gap: 10px; margin-top: 8px;">
              <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: var(--bg-primary); border: 2px solid var(--accent); border-radius: 8px; flex: 1; justify-content: center;" id="skillArchLabel">
                <input type="radio" name="skillType" value="architectural" checked onchange="setSkill('architectural')" style="accent-color: var(--accent);">
                <span style="font-weight: 600;">Architectural</span>
              </label>
              <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: var(--bg-primary); border: 2px solid var(--border); border-radius: 8px; flex: 1; justify-content: center;" id="skillResLabel">
                <input type="radio" name="skillType" value="residential" onchange="setSkill('residential')" style="accent-color: #10b981;">
                <span style="font-weight: 600;">Residential</span>
              </label>
            </div>
            <small id="skillDescription" style="color: var(--text-secondary); margin-top: 8px; display: block;">
              Commercial/specification-grade materials for architects and designers
            </small>
          </div>
          <div class="form-group">
            <label style="display: flex; justify-content: space-between; align-items: center;">
              <span>Select Vendor</span>
              <button onclick="refreshCache()" style="font-size: 12px; padding: 4px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; color: var(--text-secondary);">↻ Refresh</button>
            </label>
            <select id="vendorSelect">
              <option value="">-- Loading vendors... --</option>
            </select>
          </div>
          <div class="form-group">
            <label>Or Select Collection</label>
            <select id="collectionSelect">
              <option value="">-- Loading collections... --</option>
            </select>
          </div>
          <div class="form-group" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
            <label>Or Paste Product URL</label>
            <div style="display: flex; gap: 8px;">
              <input type="text" id="productUrl" placeholder="https://designerwallcoverings.com/... or Shopify admin URL" style="flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text-primary);">
              <button class="btn btn-primary" onclick="updateSingleProduct()" style="white-space: nowrap;">Update Product</button>
            </div>
            <small style="color: var(--text-secondary); margin-top: 4px; display: block;">
              Paste a product URL to update just that one product
            </small>
          </div>
          <div class="form-group" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
            <label>Batch Size</label>
            <select id="batchSize">
              <option value="1">1 product</option>
              <option value="2">2 products</option>
              <option value="5">5 products</option>
              <option value="10" selected>10 products</option>
              <option value="20">20 products</option>
              <option value="30">30 products</option>
              <option value="50">50 products</option>
              <option value="100">100 products</option>
              <option value="250">250 products</option>
              <option value="500">500 products</option>
              <option value="all">All products</option>
            </select>
          </div>

          <div class="form-group" style="margin-top: 15px;">
            <label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
              <input type="checkbox" id="syncImages" checked style="width: 18px; height: 18px; accent-color: var(--accent);">
              <span>Sync images from manufacturer website</span>
            </label>
            <small style="color: var(--text-secondary); margin-left: 28px; display: block; margin-top: 4px;">
              Scrapes and updates product images from manufacturer website
            </small>
          </div>

          <div class="specs-box">
            <strong>Update Specifications:</strong><br><br>
            <strong>Title:</strong> <code>Pattern Name Color (mfr#) | Architectural Wallcoverings</code><br>
            <strong>No MFR#:</strong> <code>Pattern Name Color | Architectural Wallcoverings</code><br>
            <strong>Category:</strong> <code>Building Materials</code><br>
            <strong>Type:</strong> <code>Architectural Wallcoverings</code><br>
            <strong>Sample:</strong> <code>$4.25</code><br>
            <strong>Tags:</strong> <code>Pattern Name + Color + MFR SKU + AI color analysis</code><br>
            <strong>Images:</strong> <code>Synced from manufacturer website</code>
          </div>

          <div class="btn-group">
            <button id="btnPreview" class="btn btn-secondary" onclick="showPreviewModal()">Preview & Approve</button>
            <button id="btnStart" class="btn btn-primary" onclick="startProcess()">Start Update</button>
            <button id="btnPause" class="btn btn-warning" onclick="pauseProcess()" disabled>Pause</button>
            <button id="btnResume" class="btn btn-primary" onclick="resumeProcess()" disabled>Resume</button>
            <button id="btnStop" class="btn btn-danger" onclick="stopProcess()" disabled>Stop</button>
          </div>
        </div>
      </div>

      <!-- Right Column: Live Progress -->
      <div>
        <!-- Live Status Card -->
        <div class="card" style="margin-bottom: 20px;">
          <h2 style="display: flex; align-items: center; gap: 10px;">
            <span id="liveIndicator" style="width: 12px; height: 12px; background: #10b981; border-radius: 50%; animation: pulse 2s infinite;"></span>
            Live Status
          </h2>
          <div id="liveStatus" style="padding: 15px; background: var(--surface-alt, #f1f5f9); border-radius: 8px; font-family: monospace; font-size: 13px; max-height: 200px; overflow-y: auto;">
            <div style="color: var(--text-secondary);">Initializing...</div>
          </div>
        </div>

        <!-- Progress Card -->
        <div class="card">
          <h2>Progress</h2>
          <div class="progress-bar">
            <div id="progressFill" class="progress-fill" style="width: 0%;">0%</div>
          </div>
          <div class="stats">
            <div class="stat">
              <div id="statProcessed" class="stat-value">0</div>
              <div class="stat-label">Processed</div>
            </div>
            <div class="stat">
              <div id="statTotal" class="stat-value">0</div>
              <div class="stat-label">Total</div>
            </div>
            <div class="stat">
              <div id="statErrors" class="stat-value">0</div>
              <div class="stat-label">Errors</div>
            </div>
            <div class="stat">
              <div id="statElapsed" class="stat-value">0:00</div>
              <div class="stat-label">Elapsed</div>
            </div>
          </div>

          <div id="productsPreview" class="products-grid"></div>
          <div id="log" class="log-box"></div>
        </div>
      </div>
    </div>

    <!-- History - Full Width -->
    <div class="card" style="margin-top: 20px;">
      <h2 class="history-toggle" onclick="toggleHistory()">
        <span id="historyArrow">&#9654;</span> Process History
      </h2>
      <div id="historyContent" class="history-content"></div>
    </div>
  </div>

  <!-- Approval Modal -->
  <div id="approvalModal" class="modal-overlay">
    <div class="modal-content">
      <div class="modal-header">
        <h2 id="modalTitle">Preview Products for Approval</h2>
        <button class="modal-close" onclick="closeModal()">&times;</button>
      </div>
      <div class="select-all-bar">
        <label>
          <input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
          <span>Select All</span>
        </label>
        <div class="batch-sizes">
          <button class="batch-btn" onclick="selectBatch(50)">50</button>
          <button class="batch-btn" onclick="selectBatch(100)">100</button>
          <button class="batch-btn" onclick="selectBatch(250)">250</button>
          <button class="batch-btn" onclick="selectBatch(500)">500</button>
        </div>
        <button class="batch-btn" onclick="unselectAlreadyRun()" style="background: #ef4444; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-weight: 600; cursor: pointer;" title="Unselect products that were already processed">Unselect Already Run</button>
        <span class="selected-count"><span id="selectedCount">0</span> / <span id="totalCount">0</span> selected</span>
      </div>
      <div id="approvalGrid" class="approval-grid"></div>
      <div class="modal-footer">
        <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
        <button id="loadMoreBtn" class="btn btn-load-more" onclick="loadMoreProducts()" style="display: none;">Load +5 More</button>
        <button id="approveBtn" class="btn btn-primary" onclick="approveAndStart()">Approve & Start Update</button>
      </div>
    </div>
  </div>

  <script>
    let pollInterval = null;
    let startTime = null;
    let previewProducts = [];
    let selectedProductIds = new Set();
    let productOverrides = {}; // Store user overrides: { productId: { title: '', pattern: '', color: '', mfrSku: '', description: '' } }
    let currentOffset = 0;
    let totalProducts = 0;
    let currentFilterType = '';
    let currentFilterValue = '';

    // Toggle expand/collapse detail panel
    function toggleExpand(productId) {
      const item = document.querySelector('.approval-item[data-id="' + productId + '"]');
      const btn = item?.querySelector('.expand-btn');
      if (item) {
        if (item.classList.contains('expanded')) {
          item.classList.remove('expanded');
          if (btn) btn.textContent = 'Details ▼';
        } else {
          // Collapse all others first
          document.querySelectorAll('.approval-item.expanded').forEach(function(el) {
            el.classList.remove('expanded');
            const otherBtn = el.querySelector('.expand-btn');
            if (otherBtn) otherBtn.textContent = 'Details ▼';
          });
          item.classList.add('expanded');
          if (btn) btn.textContent = 'Details ▲';
        }
      }
    }
    function toggleFullscreen(productId) {
      const item = document.querySelector('.approval-item[data-id="' + productId + '"]');
      if (item) {
        if (item.classList.contains('fullscreen')) {
          item.classList.remove('fullscreen');
          document.body.style.overflow = '';
        } else {
          // Remove fullscreen from all other items
          document.querySelectorAll('.approval-item.fullscreen').forEach(function(el) {
            el.classList.remove('fullscreen');
          });
          item.classList.add('fullscreen');
          document.body.style.overflow = 'hidden';
        }
      }
    }


    // Set override for a product field
    function setOverride(productId, field, value) {
      if (!productOverrides[productId]) {
        productOverrides[productId] = {};
      }
      const product = previewProducts.find(p => p.id == productId);
      const originalValue = getOriginalValue(product, field);

      if (value !== originalValue) {
        productOverrides[productId][field] = value;
        const indicator = document.getElementById(field + 'Override_' + productId);
        if (indicator) indicator.innerHTML = '<span class="override-indicator">MODIFIED</span>';
      } else {
        delete productOverrides[productId][field];
        const indicator = document.getElementById(field + 'Override_' + productId);
        if (indicator) indicator.innerHTML = '';
        // Clean up empty override objects
        if (Object.keys(productOverrides[productId]).length === 0) {
          delete productOverrides[productId];
        }
      }
      log('Override set for product ' + productId + ': ' + field + ' = ' + value.substring(0, 50), 'info');
    }

    // Get original proposed value for comparison
    function getOriginalValue(product, field) {
      if (!product) return '';
      switch(field) {
        case 'title': return product.preview?.newTitle || '';
        case 'pattern': return product.preview?.pattern || '';
        case 'color': return product.preview?.color || '';
        case 'mfrSku': return product.preview?.mfrSku || product.variants?.[0]?.sku || '';
        case 'description': return product.preview?.newDescription || 'Architectural Wallcoverings - Premium wallcovering solution for commercial and hospitality applications.';
        case 'tags': return product.tags || '';
        case 'handle': return product.handle || '';
        case 'artist': return product.preview?.artist || '';
        case 'tags': return product.tags || '';
                default: return '';
      }
    }

    async function setSkill(skill) {
      try {
        const res = await fetch('/api/set-skill', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ skill })
        });
        const data = await res.json();
        if (data.success) {
          // Update UI
          const archLabel = document.getElementById('skillArchLabel');
          const resLabel = document.getElementById('skillResLabel');
          const desc = document.getElementById('skillDescription');

          if (skill === 'architectural') {
            archLabel.style.borderColor = 'var(--accent)';
            resLabel.style.borderColor = 'var(--border)';
            desc.textContent = 'Commercial/specification-grade materials for architects and designers';
          } else {
            archLabel.style.borderColor = 'var(--border)';
            resLabel.style.borderColor = '#10b981';
            desc.textContent = 'Premium designer materials for residential and boutique interiors';
          }
          log('Skill set to: ' + skill.charAt(0).toUpperCase() + skill.slice(1), 'success');
        }
      } catch (e) {
        log('Error setting skill: ' + e.message, 'error');
      }
    }

    window.onload = function() {
      // Show initial status
      var liveEl = document.getElementById('liveStatus');
      if (liveEl) liveEl.innerHTML = '<div style="color:#f59e0b;">> Loading vendors and collections...</div>';

      loadVendors();
      loadCollections();
      loadHistory();
      pollStatus();
      
      // Cleanup on page unload
      window.addEventListener('beforeunload', function() {
        if (pollInterval) {
          clearInterval(pollInterval);
          pollInterval = null;
        }
      });

      // Clear other dropdown when one is selected
      document.getElementById('vendorSelect').addEventListener('change', function() {
        if (this.value) document.getElementById('collectionSelect').value = '';
      });
      document.getElementById('collectionSelect').addEventListener('change', function() {
        if (this.value) document.getElementById('vendorSelect').value = '';
      });
    };

    async function loadVendors() {
      const select = document.getElementById('vendorSelect');
      select.innerHTML = '<option value="">-- Loading vendors... --</option>';
      try {
        const res = await fetch('/api/vendors', { credentials: 'include' });
        if (!res.ok) throw new Error('HTTP ' + res.status);
        const data = await res.json();
        if (data.error) throw new Error(data.error);
        select.innerHTML = '<option value="">-- Select a vendor (' + (data.vendorCount || data.vendors?.length || 0) + ' available) --</option>';
        (data.vendors || []).forEach(v => {
          select.innerHTML += '<option value="' + v.name + '">' + v.name + ' (' + v.count + ')</option>';
        });
        log('Loaded ' + (data.vendorCount || data.vendors?.length) + ' vendors', 'success');
      } catch (e) {
        select.innerHTML = '<option value="">-- Error loading vendors --</option>';
        log('Failed to load vendors: ' + e.message, 'error');
        console.error('Vendors error:', e);
      }
    }

    async function loadCollections() {
      const select = document.getElementById('collectionSelect');
      select.innerHTML = '<option value="">-- Loading collections... --</option>';
      try {
        const res = await fetch('/api/collections', { credentials: 'include' });
        if (!res.ok) throw new Error('HTTP ' + res.status);
        const data = await res.json();
        if (data.error) throw new Error(data.error);
        select.innerHTML = '<option value="">-- Select a collection (' + (data.collections?.length || 0) + ' available) --</option>';
        (data.collections || []).forEach(c => {
          select.innerHTML += '<option value="' + c.id + '">' + c.title + ' (' + c.count + ')</option>';
        });
        log('Loaded ' + (data.collections?.length || 0) + ' collections', 'success');
      } catch (e) {
        select.innerHTML = '<option value="">-- Error loading collections --</option>';
        log('Failed to load collections: ' + e.message, 'error');
        console.error('Collections error:', e);
      }
    }

    async function loadHistory() {
      try {
        const res = await fetch('/api/history');
        const data = await res.json();
        const container = document.getElementById('historyContent');
        if (!data.history || data.history.length === 0) {
          container.innerHTML = '<p style="color:var(--text-secondary);">No history yet.</p>';
          return;
        }
        container.innerHTML = data.history.map((h, idx) => {
          const statusColor = h.status === 'completed' ? '#10b981' : h.status === 'stopped' ? '#f59e0b' : '#6b7280';
          const statusLabel = h.status === 'completed' ? 'Completed' : h.status === 'stopped' ? 'Stopped' : 'Unknown';

          // Build products grid if products data exists
          let productsHtml = '';
          if (h.products && h.products.length > 0) {
            const productCards = h.products.map(p => {
              const cardClass = p.status === 'completed' ? 'completed' : p.status === 'error' ? 'error' : '';
              const oldTitle = p.oldTitle ? '<div class="history-product-card-old">Old: ' + (p.oldTitle || '').substring(0, 60) + (p.oldTitle && p.oldTitle.length > 60 ? '...' : '') + '</div>' : '';
              const newTitle = p.newTitle ? '<div class="history-product-card-new">New: ' + (p.newTitle || '').substring(0, 60) + (p.newTitle && p.newTitle.length > 60 ? '...' : '') + '</div>' : '';
              const details = [];
              if (p.pattern) details.push('Pattern: ' + p.pattern);
              if (p.color) details.push('Color: ' + p.color);
              if (p.mfrSku) details.push('MFR: ' + p.mfrSku);
              if (p.imagesUpdated) details.push('Images: ' + (p.imageCount || 0) + ' synced');
              const detailsHtml = details.length > 0 ? '<div class="history-product-card-details">' + details.join(' | ') + '</div>' : '';
              const errorHtml = p.error ? '<div style="color:#e74c3c;font-size:10px;margin-top:4px;">Error: ' + p.error.substring(0, 50) + '</div>' : '';

              return '<div class="history-product-card ' + cardClass + '">' +
                '<div class="history-product-card-sku">SKU: ' + (p.sku || 'N/A') + '</div>' +
                oldTitle +
                newTitle +
                detailsHtml +
                errorHtml +
                '</div>';
            }).join('');

            productsHtml = '<div class="history-toggle-products" onclick="toggleHistoryProducts(' + idx + ')">' +
              '<span id="historyProductsArrow' + idx + '">&#9654;</span> Show ' + h.products.length + ' Products' +
              '</div>' +
              '<div id="historyProducts' + idx + '" class="history-products-grid" style="display:none;">' +
              productCards +
              '</div>';
          }

          return '<div class="history-item">' +
            '<div class="history-item-header">' +
            '<div>' +
            '<span style="color:var(--text-secondary);font-size:13px;">' + new Date(h.date).toLocaleString() + '</span>' +
            '<span style="color:var(--accent);font-weight:600;font-size:16px;margin-left:12px;">' + (h.vendor || 'Unknown Vendor') + '</span>' +
            '<span style="background:' + statusColor + ';color:white;padding:2px 8px;border-radius:4px;font-size:10px;margin-left:10px;">' + statusLabel + '</span>' +
            '</div>' +
            '<div class="history-item-stats">' +
            '<div class="history-item-stat"><div class="history-item-stat-value">' + h.processed + '</div><div class="history-item-stat-label">Processed</div></div>' +
            '<div class="history-item-stat"><div class="history-item-stat-value">' + h.total + '</div><div class="history-item-stat-label">Total</div></div>' +
            '<div class="history-item-stat"><div class="history-item-stat-value" style="color:' + (h.errors > 0 ? '#e74c3c' : 'var(--accent)') + ';">' + h.errors + '</div><div class="history-item-stat-label">Errors</div></div>' +
            (h.syncImages !== undefined ? '<div class="history-item-stat"><div class="history-item-stat-value" style="font-size:14px;">' + (h.syncImages ? 'Yes' : 'No') + '</div><div class="history-item-stat-label">Images</div></div>' : '') +
            '</div>' +
            '</div>' +
            productsHtml +
            '</div>';
        }).join('');
      } catch (e) { console.error('Error loading history:', e); }
    }

    function toggleHistoryProducts(idx) {
      const grid = document.getElementById('historyProducts' + idx);
      const arrow = document.getElementById('historyProductsArrow' + idx);
      if (grid && arrow) {
        const isHidden = grid.style.display === 'none';
        grid.style.display = isHidden ? 'grid' : 'none';
        arrow.innerHTML = isHidden ? '&#9660;' : '&#9654;';
      }
    }

    function toggleHistory() {
      const content = document.getElementById('historyContent');
      const arrow = document.getElementById('historyArrow');
      content.classList.toggle('open');
      arrow.innerHTML = content.classList.contains('open') ? '&#9660;' : '&#9654;';
    }

    async function showPreviewModal() {
      const vendor = document.getElementById('vendorSelect').value;
      const collection = document.getElementById('collectionSelect').value;
      if (!vendor && !collection) { alert('Select a vendor or collection first'); return; }
      const batchSize = document.getElementById('batchSize').value;
      const filterType = vendor ? 'vendor' : 'collection';
      const filterValue = vendor || collection;

      // Store current filter for load more
      currentFilterType = filterType;
      currentFilterValue = filterValue;
      currentOffset = 0;
      previewProducts = [];
      selectedProductIds = new Set();

      log('Loading preview for ' + filterType + ': ' + filterValue + '...', 'info');

      try {
        const res = await fetch('/api/preview?' + filterType + '=' + encodeURIComponent(filterValue) + '&limit=' + batchSize + '&offset=0');
        const data = await res.json();
        previewProducts = data.products || [];
        totalProducts = data.total || 0;
        currentOffset = previewProducts.length;
        selectedProductIds = new Set(previewProducts.map(p => p.id));

        document.getElementById('modalTitle').textContent = 'Preview: ' + filterValue + ' (' + data.total + ' total)';
        updateLoadMoreButton();
        document.getElementById('totalCount').textContent = previewProducts.length;
        document.getElementById('selectedCount').textContent = selectedProductIds.size;
        document.getElementById('selectAll').checked = true;

        const grid = document.getElementById('approvalGrid');
        grid.innerHTML = previewProducts.map(function(p) {
          const img = p.images?.[0]?.src || p.image || 'https://via.placeholder.com/120?text=No+Image';
          const oldSku = p.variants?.[0]?.sku || 'N/A';
          const newTitle = p.preview?.newTitle || 'Processing...';
          const pattern = p.preview?.pattern || '—';
          const color = p.preview?.color || '—';
          const mfrSku = p.preview?.mfrSku || oldSku;
          const oldDesc = (p.body_html || p.description || '').replace(/<[^>]*>/g, '').substring(0, 200) || 'No description';
          const newDesc = p.preview?.newDescription || 'Architectural Wallcoverings - Premium wallcovering solution for commercial and hospitality applications.';
          const prevUpdatedClass = p.previouslyUpdated ? ' previously-updated' : '';
          const prevUpdatedBadge = p.previouslyUpdated && p.previouslyUpdatedAt
            ? '<div class="prev-updated-badge"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>Updated: ' + new Date(p.previouslyUpdatedAt).toLocaleString() + '</div>'
            : '';
          return '<div class="approval-item selected' + prevUpdatedClass + '" data-id="' + p.id + '" onclick="toggleProduct(' + p.id + ')">' +
            '<input type="checkbox" checked onclick="event.stopPropagation(); toggleProduct(' + p.id + ')">' +
            '<button class="expand-btn" onclick="event.stopPropagation(); toggleExpand(' + p.id + ')">Details ▲</button>' +
            '<button class="fullscreen-btn" onclick="event.stopPropagation(); toggleFullscreen(' + p.id + ')" style="position: absolute; top: 12px; right: 120px; background: #3b82f6; border: none; color: white; font-size: 12px; cursor: pointer; padding: 4px 10px; border-radius: 6px; z-index: 10;">⛶ Fullscreen</button>' +
            '<img src="' + img + '" alt="" onerror="this.src=&#39;https://via.placeholder.com/120?text=No+Image&#39;">' +
            '<div class="info-panel">' +
              prevUpdatedBadge +
              '<div class="before-after">' +
                '<div class="section before">' +
                  '<div class="section-label">CURRENT</div>' +
                  '<div class="title">' + (p.title || 'Untitled') + '</div>' +
                  '<div class="details"><span>SKU: ' + oldSku + '</span></div>' +
                '</div>' +
                '<div class="section after">' +
                  '<div class="section-label">PROPOSED</div>' +
                  '<div class="title">' + newTitle + '</div>' +
                  '<div class="details">' +
                    '<span>Pattern: <b>' + pattern + '</b></span>' +
                    '<span>Color: <b>' + color + '</b></span>' +
                  '</div>' +
                  '<div class="sku">MFR#: ' + mfrSku + '</div>' +
                '</div>' +
              '</div>' +
            '</div>' +
            '<div class="detail-panel">' +
              '<div class="detail-grid">' +
                '<div class="detail-section current">' +
                  '<h4>Current Data</h4>' +
                  '<div class="detail-row"><label>Title</label><div class="value">' + (p.title || 'Untitled') + '</div></div>' +
                  '<div class="detail-row"><label>Description</label><div class="value">' + oldDesc + '</div></div>' +
                  '<div class="detail-row"><label>SKU</label><div class="value">' + oldSku + '</div></div>' +
                  '<div class="detail-row"><label>Tags</label><div class="value" style="font-size:11px;">' + (p.tags || 'None') + '</div></div>' +
                  '<div class="detail-row"><label>Shopify Link</label><div class="value"><a href="' + (p.preview?.shopifyLink || (p.handle ? 'https://designerwallcoverings.com/products/' + p.handle : '#')) + '" target="_blank" style="color: #10b981; text-decoration: underline; word-break: break-all;">View Product</a></div></div>' +
                '</div>' +
                '<div class="detail-section proposed">' +
                  '<h4>Proposed Updates <span style="font-size:10px;color:#6b7280;">(editable)</span></h4>' +
                  '<div class="detail-row"><label>Title <span id="titleOverride_' + p.id + '"></span></label><input type="text" id="editTitle_' + p.id + '" value="' + newTitle.replace(/"/g, '&quot;') + '" onchange="setOverride(' + p.id + ', &#39;title&#39;, this.value)" onclick="event.stopPropagation()"></div>' +
                  '<div class="detail-row"><label>Pattern <span id="patternOverride_' + p.id + '"></span></label><input type="text" id="editPattern_' + p.id + '" value="' + pattern.replace(/"/g, '&quot;') + '" onchange="setOverride(' + p.id + ', &#39;pattern&#39;, this.value)" onclick="event.stopPropagation()"></div>' +
                  '<div class="detail-row"><label>Color <span id="colorOverride_' + p.id + '"></span></label><input type="text" id="editColor_' + p.id + '" value="' + color.replace(/"/g, '&quot;') + '" onchange="setOverride(' + p.id + ', &#39;color&#39;, this.value)" onclick="event.stopPropagation()"></div>' +
                  '<div class="detail-row"><label>MFR SKU <span id="skuOverride_' + p.id + '"></span></label><input type="text" id="editSku_' + p.id + '" value="' + mfrSku.replace(/"/g, '&quot;') + '" onchange="setOverride(' + p.id + ', &#39;mfrSku&#39;, this.value)" onclick="event.stopPropagation()"></div>' +
                  '<div class="detail-row"><label>Description</label><textarea id="editDesc_' + p.id + '" onchange="setOverride(' + p.id + ', &#39;description&#39;, this.value)" onclick="event.stopPropagation()">' + newDesc + '</textarea></div>' +
                  '<div class="detail-row"><label>Shopify Link</label><div class="value"><a href="' + (p.preview?.shopifyLink || (p.handle ? 'https://designerwallcoverings.com/products/' + p.handle : '#')) + '" target="_blank" style="color: #10b981; text-decoration: underline; word-break: break-all;">View Product</a></div></div>' +
                  '<div class="detail-row"><label>Tags <span id="tagsOverride_' + p.id + '"></span></label><input type="text" id="editTags_' + p.id + '" value="' + (p.tags || '').replace(/"/g, '&quot;') + '" onchange="setOverride(' + p.id + ', &#39;tags&#39;, this.value)" onclick="event.stopPropagation()" style="width: 100%;"></div>' +
                  '<div class="detail-row"><label>Handle <span id="handleOverride_' + p.id + '"></span></label><input type="text" id="editHandle_' + p.id + '" value="' + (p.handle || '').replace(/"/g, '&quot;') + '" onchange="setOverride(' + p.id + ', &#39;handle&#39;, this.value)" onclick="event.stopPropagation()"></div>' +
                  '<div class="detail-row"><label>Images</label><div class="value">' + (p.images?.length || 0) + ' image(s) - <a href="#" onclick="event.stopPropagation(); alert(&#39;Images will be synced from manufacturer website if enabled&#39;)" style="color: #10b981;">Info</a></div></div>' +
                  '<div class="detail-row" id="artistRow_' + p.id + '" style="display: ' + (p.preview?.isArtwork ? 'block' : 'none') + ';"><label>Artist <span id="artistOverride_' + p.id + '"></span></label><input type="text" id="editArtist_' + p.id + '" value="' + (p.preview?.artist || '').replace(/"/g, '&quot;') + '" onchange="setOverride(' + p.id + ', &#39;artist&#39;, this.value)" onclick="event.stopPropagation()"></div>' +
                '</div>' +
                '<div class="detail-section ai-data" style="background: linear-gradient(135deg, #eff6ff, #dbeafe); border: 1px solid #3b82f6; border-radius: 8px; margin-top: 12px;">' +
                  '<h4 style="color: #1d4ed8; margin-bottom: 10px;">🤖 AI Generated Data (from Shopify Metafields)</h4>' +
                  '<div class="detail-row"><label style="color: #1e40af;">Manufacturer SKU</label><div class="value" style="font-family: monospace; font-weight: bold; font-size: 14px; color: #1d4ed8; cursor: pointer;" onclick="navigator.clipboard.writeText(&#39;' + (p.preview?.manufacturerSku || '') + '&#39;); this.style.background=&#39;#bbf7d0&#39;; setTimeout(() => this.style.background=&#39;&#39;, 500);">' + (p.preview?.manufacturerSku || '—') + ' 📋</div></div>' +
                  '<div class="detail-row"><label style="color: #1e40af;">AI Colors</label><div class="value" style="cursor: pointer;" onclick="document.getElementById(&#39;editColor_' + p.id + '&#39;).value = &#39;' + (p.preview?.aiColors?.split(',')?.[0]?.trim() || '') + '&#39;; setOverride(' + p.id + ', &#39;color&#39;, &#39;' + (p.preview?.aiColors?.split(',')?.[0]?.trim() || '') + '&#39;); this.style.background=&#39;#bbf7d0&#39;; setTimeout(() => this.style.background=&#39;&#39;, 500);">' + (p.preview?.aiColors || '—') + ' <span style="color:#10b981;">← click to use</span></div></div>' +
                  '<div class="detail-row"><label style="color: #1e40af;">AI Tags</label><div class="value" style="font-size: 10px; max-height: 60px; overflow-y: auto;">' + (p.preview?.aiTags || '—') + '</div></div>' +
                  '<div class="detail-row"><label style="color: #1e40af;">AI Description</label><div class="value" style="font-size: 11px; max-height: 100px; overflow-y: auto;">' + (p.preview?.aiDescription?.replace(/<[^>]*>/g, ' ').substring(0, 300) || '—') + '...</div></div>' +
                  '<div class="detail-row"><label style="color: #1e40af;">Source URL</label><div class="value">' + (p.preview?.sourceUrl ? '<a href="' + p.preview.sourceUrl + '" target="_blank" style="color: #3b82f6; text-decoration: underline;">View on Manufacturer Site ↗</a>' : '—') + '</div></div>' +
                  '<div class="detail-row"><label style="color: #1e40af;">All Metafields (' + (p.preview?.allMetafields?.length || 0) + ')</label><div class="value" style="font-size: 10px; max-height: 150px; overflow-y: auto; font-family: monospace; background: #f8fafc; padding: 8px; border-radius: 4px;">' + (p.preview?.allMetafieldsHtml || '<em>No metafields</em>') + '</div></div>' +
                '</div>' +
              '</div>' +
            '</div>' +
          '</div>';
        }).join('');

        document.getElementById('approvalModal').classList.add('open');
        log('Loaded ' + previewProducts.length + ' products for approval', 'success');
      } catch (e) {
        log('Preview failed: ' + e.message, 'error');
      }
    }

    function closeModal() {
      document.getElementById('approvalModal').classList.remove('open');
    }

    function updateLoadMoreButton() {
      const btn = document.getElementById('loadMoreBtn');
      const remaining = totalProducts - currentOffset;
      if (remaining > 0) {
        btn.style.display = 'inline-block';
        btn.textContent = 'Load +5 More (' + remaining + ' remaining)';
      } else {
        btn.style.display = 'none';
      }
    }

    async function loadMoreProducts() {
      const loadMoreBtn = document.getElementById('loadMoreBtn');
      loadMoreBtn.disabled = true;
      loadMoreBtn.textContent = 'Loading...';

      try {
        const res = await fetch('/api/preview?' + currentFilterType + '=' + encodeURIComponent(currentFilterValue) + '&limit=5&offset=' + currentOffset);
        const data = await res.json();
        const newProducts = data.products || [];

        if (newProducts.length > 0) {
          // Add new products to array
          newProducts.forEach(p => {
            if (!previewProducts.find(existing => existing.id === p.id)) {
              previewProducts.push(p);
              selectedProductIds.add(p.id);
            }
          });
          currentOffset += newProducts.length;

          // Append to grid
          const grid = document.getElementById('approvalGrid');
          newProducts.forEach(function(p) {
            const img = p.images?.[0]?.src || p.image || 'https://via.placeholder.com/120?text=No+Image';
            const oldSku = p.variants?.[0]?.sku || 'N/A';
            const newTitle = p.preview?.newTitle || 'Processing...';
            const pattern = p.preview?.pattern || '—';
            const color = p.preview?.color || '—';
            const mfrSku = p.preview?.mfrSku || oldSku;
            const prevUpdatedClass = p.previouslyUpdated ? ' previously-updated' : '';
            const prevUpdatedBadge = p.previouslyUpdated && p.previouslyUpdatedAt
              ? '<div class="prev-updated-badge"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>Updated: ' + new Date(p.previouslyUpdatedAt).toLocaleString() + '</div>'
              : '';
            const html = '<div class="approval-item selected' + prevUpdatedClass + '" data-id="' + p.id + '" onclick="toggleProduct(' + p.id + ')">' +
              '<input type="checkbox" checked onclick="event.stopPropagation(); toggleProduct(' + p.id + ')">' +
              '<img src="' + img + '" alt="" onerror="this.src=&#39;https://via.placeholder.com/120?text=No+Image&#39;">' +
              '<div class="info-panel">' +
                prevUpdatedBadge +
                '<div class="before-after">' +
                  '<div class="section before">' +
                    '<div class="section-label">BEFORE</div>' +
                    '<div class="title">' + (p.title || 'Untitled') + '</div>' +
                    '<div class="details"><span>SKU: ' + oldSku + '</span></div>' +
                  '</div>' +
                  '<div class="section after">' +
                    '<div class="section-label">AFTER</div>' +
                    '<div class="title">' + newTitle + '</div>' +
                    '<div class="details">' +
                      '<span>Pattern: <b>' + pattern + '</b></span>' +
                      '<span>Color: <b>' + color + '</b></span>' +
                    '</div>' +
                    '<div class="sku">MFR#: ' + mfrSku + '</div>' +
                  '</div>' +
                '</div>' +
              '</div>' +
            '</div>';
            grid.insertAdjacentHTML('beforeend', html);
          });

          document.getElementById('totalCount').textContent = previewProducts.length;
          document.getElementById('selectedCount').textContent = selectedProductIds.size;
          log('Loaded ' + newProducts.length + ' more products (total: ' + previewProducts.length + ')', 'success');
        }

        updateLoadMoreButton();
      } catch (e) {
        log('Failed to load more: ' + e.message, 'error');
      } finally {
        loadMoreBtn.disabled = false;
      }
    }

    function toggleProduct(id) {
      const item = document.querySelector('.approval-item[data-id="' + id + '"]');
      const checkbox = item?.querySelector('input[type="checkbox"]');
      if (selectedProductIds.has(id)) {
        selectedProductIds.delete(id);
        item?.classList.remove('selected');
        if (checkbox) checkbox.checked = false;
      } else {
        selectedProductIds.add(id);
        item?.classList.add('selected');
        if (checkbox) checkbox.checked = true;
      }
      updateSelectedCount();
    }

    function toggleSelectAll() {
      const selectAll = document.getElementById('selectAll').checked;
      document.querySelectorAll('.approval-item').forEach(item => {
        const id = parseInt(item.dataset.id);
        const checkbox = item.querySelector('input[type="checkbox"]');
        if (selectAll) {
          selectedProductIds.add(id);
          item.classList.add('selected');
          if (checkbox) checkbox.checked = true;
        } else {
          selectedProductIds.delete(id);
          item.classList.remove('selected');
          if (checkbox) checkbox.checked = false;
        }
      });
      updateSelectedCount();
    }

    async function selectBatch(count) {
      // If we need more products than currently loaded, fetch them first
      if (count > previewProducts.length && currentOffset < totalProducts) {
        const needed = Math.min(count - previewProducts.length, totalProducts - currentOffset);
        log('Loading ' + needed + ' more products to reach ' + count + '...', 'info');

        try {
          const res = await fetch('/api/preview?' + currentFilterType + '=' + encodeURIComponent(currentFilterValue) + '&limit=' + needed + '&offset=' + currentOffset);
          const data = await res.json();
          const newProducts = data.products || [];

          if (newProducts.length > 0) {
            const grid = document.getElementById('approvalGrid');
            newProducts.forEach(function(p) {
              if (!previewProducts.find(existing => existing.id === p.id)) {
                previewProducts.push(p);
                const img = p.images?.[0]?.src || p.image || 'https://via.placeholder.com/120?text=No+Image';
                const oldSku = p.variants?.[0]?.sku || 'N/A';
                const newTitle = p.preview?.newTitle || 'Processing...';
                const pattern = p.preview?.pattern || '—';
                const color = p.preview?.color || '—';
                const mfrSku = p.preview?.mfrSku || oldSku;
                const prevUpdatedClass = p.previouslyUpdated ? ' previously-updated' : '';
                const prevUpdatedBadge = p.previouslyUpdated && p.previouslyUpdatedAt
                  ? '<div class="prev-updated-badge"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg>Updated: ' + new Date(p.previouslyUpdatedAt).toLocaleString() + '</div>'
                  : '';
                const html = '<div class="approval-item" data-id="' + p.id + '" onclick="toggleProduct(' + p.id + ')">' +
                  '<input type="checkbox" onclick="event.stopPropagation(); toggleProduct(' + p.id + ')">' +
                  '<img src="' + img + '" alt="" onerror="this.src=&#39;https://via.placeholder.com/120?text=No+Image&#39;">' +
                  '<div class="info-panel">' +
                    prevUpdatedBadge +
                    '<div class="before-after">' +
                      '<div class="section before">' +
                        '<div class="section-label">BEFORE</div>' +
                        '<div class="title">' + (p.title || 'Untitled') + '</div>' +
                        '<div class="details"><span>SKU: ' + oldSku + '</span></div>' +
                      '</div>' +
                      '<div class="section after">' +
                        '<div class="section-label">AFTER</div>' +
                        '<div class="title">' + newTitle + '</div>' +
                        '<div class="details">' +
                          '<span>Pattern: <b>' + pattern + '</b></span>' +
                          '<span>Color: <b>' + color + '</b></span>' +
                        '</div>' +
                        '<div class="sku">MFR#: ' + mfrSku + '</div>' +
                      '</div>' +
                    '</div>' +
                  '</div>' +
                '</div>';
                grid.insertAdjacentHTML('beforeend', html);
              }
            });
            currentOffset += newProducts.length;
            document.getElementById('totalCount').textContent = previewProducts.length;
            updateLoadMoreButton();
            log('Loaded ' + newProducts.length + ' more products', 'success');
          }
        } catch (e) {
          log('Failed to load more products: ' + e.message, 'error');
        }
      }

      // Now select the requested count
      selectedProductIds.clear();
      document.querySelectorAll('.approval-item').forEach((item, i) => {
        const id = parseInt(item.dataset.id);
        const checkbox = item.querySelector('input[type="checkbox"]');
        if (i < count) {
          selectedProductIds.add(id);
          item.classList.add('selected');
          if (checkbox) checkbox.checked = true;
        } else {
          item.classList.remove('selected');
          if (checkbox) checkbox.checked = false;
        }
      });
      document.getElementById('selectAll').checked = selectedProductIds.size === previewProducts.length;
      updateSelectedCount();
    }

    // Unselect products that were already processed (from history)
    async function unselectAlreadyRun() {
      try {
        log('Fetching process history to find already processed products...', 'info');
        const res = await fetch('/api/history');
        const data = await res.json();

        // Collect all product IDs that were processed successfully
        const processedIds = new Set();
        (data.history || []).forEach(entry => {
          if (entry.products && Array.isArray(entry.products)) {
            entry.products.forEach(p => {
              if (p.id && p.status === 'completed') {
                processedIds.add(String(p.id));
              }
            });
          }
        });

        if (processedIds.size === 0) {
          log('No processed products found in history', 'info');
          return;
        }

        log('Found ' + processedIds.size + ' already processed products', 'info');

        // Unselect products that are in the processedIds set
        let unselectedCount = 0;
        document.querySelectorAll('.approval-item').forEach(item => {
          const id = item.dataset.id;
          if (processedIds.has(id)) {
            selectedProductIds.delete(parseInt(id));
            item.classList.remove('selected');
            const checkbox = item.querySelector('input[type="checkbox"]');
            if (checkbox) checkbox.checked = false;
            unselectedCount++;
          }
        });

        document.getElementById('selectAll').checked = false;
        updateSelectedCount();
        log('Unselected ' + unselectedCount + ' already processed products', 'success');
      } catch (e) {
        log('Failed to unselect already run products: ' + e.message, 'error');
      }
    }

    function updateSelectedCount() {
      const selectedCountEl = document.getElementById('selectedCount');
      const approveBtn = document.getElementById('approveBtn');
      
      if (selectedCountEl) {
        selectedCountEl.textContent = selectedProductIds.size;
      }
      
      const selectAllEl = document.getElementById('selectAll');
      if (selectAllEl) {
        selectAllEl.checked = selectedProductIds.size === previewProducts.length;
      }
      
      if (approveBtn) {
        approveBtn.disabled = selectedProductIds.size === 0;
        if (selectedProductIds.size > 0) {
          approveBtn.style.opacity = '1';
          approveBtn.style.cursor = 'pointer';
        } else {
          approveBtn.style.opacity = '0.5';
          approveBtn.style.cursor = 'not-allowed';
        }
      }
    }

    async function approveAndStart() {
      if (selectedProductIds.size === 0) { 
        alert('Please select at least one product to update'); 
        return; 
      }
      
      // Disable button and show loading state
      const approveBtn = document.getElementById('approveBtn');
      if (approveBtn) {
        approveBtn.disabled = true;
        approveBtn.textContent = 'Starting Update...';
        approveBtn.style.opacity = '0.7';
      }
      
      closeModal();

      const vendor = document.getElementById('vendorSelect').value;
      const collection = document.getElementById('collectionSelect').value;
      const syncImages = document.getElementById('syncImages').checked;
      const selectedProducts = previewProducts.filter(p => selectedProductIds.has(p.id));

      log('Starting approved update for ' + selectedProducts.length + ' products...', 'info');
      updateButtons('running');
      startTime = Date.now();

      // Show selected products in display cards
      const container = document.getElementById('productsPreview');
      container.innerHTML = selectedProducts.map(p => {
        const sku = p.variants?.[0]?.sku || 'N/A';
        const img = p.images?.[0]?.src || p.image || 'https://via.placeholder.com/60?text=No+Image';
        const prevUpdated = p.previouslyUpdated ? 'previously-updated' : 'pending';
        const prevStatus = p.previouslyUpdated ? 'Previously Updated' : 'Pending';
        return '<div class="sku-card ' + prevUpdated + '" data-id="' + p.id + '" data-sku="' + sku + '">' +
          '<button class="sku-card-refresh" onclick="refreshSingleSku(event, &#39;' + p.id + '&#39;, &#39;' + sku + '&#39;)">↻ Refresh</button>' +
          '<div class="sku-card-header">' +
          '<img src="' + img + '" alt="" onerror="this.src=&#39;https://via.placeholder.com/60?text=No+Image&#39;">' +
          '<div class="sku-card-info">' +
          '<span class="sku-card-status ' + prevUpdated + '">' + prevStatus + '</span>' +
          '<div class="sku-card-title">' + (p.title || 'Untitled').substring(0, 60) + '</div>' +
          '<div class="sku-card-sku">SKU: ' + sku + '</div>' +
          '</div>' +
          '</div>' +
          '<div class="sku-card-old-title">Old: ' + (p.title || 'Untitled').substring(0, 80) + '</div>' +
          '<div class="sku-card-new-title" style="display:none;"></div>' +
          '<div class="sku-card-images-info" style="display:none;"></div>' +
          '<div class="sku-card-metafields-info" style="display:none;"></div>' +
          '<div class="sku-card-details"></div>' +
          '<div class="sku-card-error" style="display:none;"></div>' +
          '<div class="sku-card-timestamp"></div>' +
          '</div>';
      }).join('');

      const statTotalEl = document.getElementById('statTotal');
      if (statTotalEl) statTotalEl.textContent = selectedProducts.length;

      try {
        // Only send product IDs and overrides to avoid 413 Payload Too Large error
        const overridesCount = Object.keys(productOverrides).length;
        if (overridesCount > 0) {
          log('Sending ' + overridesCount + ' user overrides with request', 'info');
        }
        const res = await fetch('/api/start-approved', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ vendor, collection, syncImages, productIds: Array.from(selectedProductIds), overrides: productOverrides })
        });
        if (!res.ok) {
          throw new Error('HTTP ' + res.status + ': ' + res.statusText);
        }
        const data = await res.json();
        if (data.success) {
          log('Approved process started with ' + selectedProducts.length + ' products!', 'success');
          startPolling();
        } else {
          log('Failed: ' + (data.error || 'Unknown error'), 'error');
          updateButtons('idle');
          // Re-enable approve button on error
          const approveBtn = document.getElementById('approveBtn');
          if (approveBtn) {
            approveBtn.disabled = false;
            approveBtn.textContent = 'Approve & Start Update';
            approveBtn.style.opacity = '1';
          }
        }
      } catch (e) {
        log('Error starting process: ' + (e.message || 'Unknown error'), 'error');
        updateButtons('idle');
        console.error('Start approved error:', e);
        // Re-enable approve button on error
        const approveBtn = document.getElementById('approveBtn');
        if (approveBtn) {
          approveBtn.disabled = false;
          approveBtn.textContent = 'Approve & Start Update';
          approveBtn.style.opacity = '1';
        }
      }
    }

    async function startProcess() {
      const vendor = document.getElementById('vendorSelect').value;
      const collection = document.getElementById('collectionSelect').value;
      if (!vendor && !collection) { alert('Select a vendor or collection first'); return; }
      const batchSize = document.getElementById('batchSize').value;
      const syncImages = document.getElementById('syncImages').checked;
      const filterType = vendor ? 'vendor' : 'collection';
      const filterValue = vendor || collection;

      log('Starting update for ' + filterType + ': ' + filterValue + (syncImages ? ' (with image sync)' : '') + '...', 'info');
      updateButtons('running');
      startTime = Date.now();

      try {
        const res = await fetch('/api/start', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ vendor, collection, batchSize, syncImages })
        });
        if (!res.ok) {
          throw new Error('HTTP ' + res.status + ': ' + res.statusText);
        }
        const data = await res.json();
        if (data.success) {
          log('Process started!', 'success');
          startPolling();
        } else {
          log('Failed: ' + (data.error || 'Unknown error'), 'error');
          updateButtons('idle');
        }
      } catch (e) {
        log('Error starting process: ' + (e.message || 'Unknown error'), 'error');
        updateButtons('idle');
        console.error('Start process error:', e);
      }
    }

    async function refreshCache() {
      log('Refreshing vendor/collection cache...', 'info');
      try {
        const res = await fetch('/api/refresh-cache', { method: 'POST' });
        if (!res.ok) {
          throw new Error('HTTP ' + res.status + ': ' + res.statusText);
        }
        const data = await res.json();
        if (data.success) {
          log('Cache refresh started, reloading in 5s...', 'info');
          setTimeout(() => {
            loadVendors();
            loadCollections();
          }, 5000);
        } else {
          log('Refresh failed: ' + (data.error || 'Unknown error'), 'error');
        }
      } catch (e) {
        log('Refresh failed: ' + (e.message || 'Unknown error'), 'error');
        console.error('Refresh cache error:', e);
      }
    }

    async function updateSingleProduct() {
      const urlInput = document.getElementById('productUrl');
      if (!urlInput) return;
      
      const url = urlInput.value.trim();
      if (!url) { 
        alert('Please paste a product URL'); 
        return; 
      }

      const syncImagesEl = document.getElementById('syncImages');
      const syncImages = syncImagesEl ? syncImagesEl.checked : false;
      
      log('Updating single product from URL: ' + url, 'info');
      
      // Disable button during request
      const btn = document.querySelector('button[onclick="updateSingleProduct()"]');
      const originalText = btn ? btn.textContent : '';
      if (btn) {
        btn.disabled = true;
        btn.textContent = 'Updating...';
      }

      try {
        const res = await fetch('/api/update-single', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ url, syncImages })
        });
        
        if (!res.ok) {
          throw new Error('HTTP ' + res.status + ': ' + res.statusText);
        }
        
        const data = await res.json();
        if (data.success) {
          log('Product update started: ' + data.title + ' (running in background)', 'success');
          urlInput.value = '';
          
          // Show the product in display cards
          const container = document.getElementById('productsPreview');
          if (container) {
            const sku = 'N/A'; // Will be updated by polling
            const img = 'https://via.placeholder.com/60?text=Loading';
            container.innerHTML = '<div class="sku-card pending" data-id="' + data.id + '" data-sku="' + sku + '">' +
              '<div class="sku-card-header">' +
              '<img src="' + img + '" alt="" onerror="this.src=&#39;https://via.placeholder.com/60?text=No+Image&#39;">' +
              '<div class="sku-card-info">' +
              '<span class="sku-card-status pending">Pending</span>' +
              '<div class="sku-card-title">' + (data.title || 'Loading...').substring(0, 60) + '</div>' +
              '<div class="sku-card-sku">SKU: ' + sku + '</div>' +
              '</div>' +
              '</div>' +
              '<div class="sku-card-old-title">Old: ' + (data.title || 'Loading...').substring(0, 80) + '</div>' +
              '<div class="sku-card-new-title" style="display:none;"></div>' +
              '<div class="sku-card-images-info" style="display:none;"></div>' +
              '<div class="sku-card-metafields-info" style="display:none;"></div>' +
              '<div class="sku-card-error" style="display:none;"></div>' +
              '<div class="sku-card-timestamp"></div>' +
              '</div>';

            const statTotalEl = document.getElementById('statTotal');
            if (statTotalEl) statTotalEl.textContent = '1';
            startPolling();
          }
          
          loadHistory();
        } else {
          log('Failed: ' + (data.error || 'Unknown error'), 'error');
        }
      } catch (e) {
        log('Error: ' + (e.message || 'Unknown error'), 'error');
      } finally {
        // Re-enable button
        if (btn) {
          btn.disabled = false;
          btn.textContent = originalText || 'Update Product';
        }
      }
    }

    async function pauseProcess() {
      try {
        const res = await fetch('/api/pause', { method: 'POST' });
        if (!res.ok) throw new Error('Pause failed');
        log('Paused', 'info');
        updateButtons('paused');
      } catch (e) {
        log('Failed to pause: ' + (e.message || 'Unknown error'), 'error');
      }
    }

    async function resumeProcess() {
      try {
        const res = await fetch('/api/resume', { method: 'POST' });
        if (!res.ok) throw new Error('Resume failed');
        log('Resumed', 'info');
        updateButtons('running');
        startPolling();
      } catch (e) {
        log('Failed to resume: ' + (e.message || 'Unknown error'), 'error');
      }
    }

    async function stopProcess() {
      if (!confirm('Stop the process?')) return;
      try {
        const res = await fetch('/api/stop', { method: 'POST' });
        if (!res.ok) throw new Error('Stop failed');
        log('Stopped', 'info');
        updateButtons('idle');
        stopPolling();
        loadHistory();
      } catch (e) {
        log('Failed to stop: ' + (e.message || 'Unknown error'), 'error');
      }
    }

    function startPolling() {
      if (pollInterval) clearInterval(pollInterval);
      pollInterval = setInterval(pollStatus, 2000);
    }

    function stopPolling() {
      if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
    }

    async function pollStatus() {
      try {
        const res = await fetch('/api/status');
        if (!res.ok) {
          console.error('Status fetch failed:', res.status);
          return;
        }
        const data = await res.json();

        const badge = document.getElementById('statusBadge');
        if (!badge) return; // Safety check
        
        if (data.running && !data.paused) {
          badge.className = 'status-badge status-running';
          badge.textContent = 'RUNNING';
          updateButtons('running');
          if (!pollInterval) startPolling();
        } else if (data.paused) {
          badge.className = 'status-badge status-paused';
          badge.textContent = 'PAUSED';
          updateButtons('paused');
        } else {
          badge.className = 'status-badge status-idle';
          badge.textContent = 'IDLE';
          if (data.processed > 0 && !data.running) {
            updateButtons('idle');
            stopPolling();
            loadHistory();
          }
        }

        const statProcessed = document.getElementById('statProcessed');
        const statTotal = document.getElementById('statTotal');
        const statErrors = document.getElementById('statErrors');
        const progressFill = document.getElementById('progressFill');
        
        if (statProcessed) statProcessed.textContent = data.processed || 0;
        if (statTotal) statTotal.textContent = data.total || 0;
        if (statErrors) statErrors.textContent = data.errors || 0;

        const pct = data.total > 0 ? Math.round((data.processed / data.total) * 100) : 0;
        if (progressFill) {
          progressFill.style.width = pct + '%';
          progressFill.textContent = pct + '%';
        }

        if (startTime) {
          const elapsed = Math.floor((Date.now() - startTime) / 1000);
          document.getElementById('statElapsed').textContent = Math.floor(elapsed / 60) + ':' + String(elapsed % 60).padStart(2, '0');
        }

        // Update display cards with product update status
        if (data.productUpdates && data.products) {
          data.products.forEach(function(p) {
            const productId = String(p.id);
            const update = data.productUpdates[productId];
            const cardEl = document.querySelector('.sku-card[data-id="' + productId + '"]');
            
            if (cardEl && update) {
              // Update card status class
              cardEl.className = 'sku-card ' + update.status;
              
              // Update status badge
              const statusEl = cardEl.querySelector('.sku-card-status');
              if (statusEl) {
                statusEl.className = 'sku-card-status ' + update.status;
                statusEl.textContent = update.status.charAt(0).toUpperCase() + update.status.slice(1);
              }
              
              // Update new title if available
              const newTitleEl = cardEl.querySelector('.sku-card-new-title');
              if (newTitleEl && update.newTitle) {
                newTitleEl.style.display = 'block';
                newTitleEl.textContent = 'New: ' + update.newTitle.substring(0, 80);
              }
              
              // Update timestamp
              const timestampEl = cardEl.querySelector('.sku-card-timestamp');
              if (timestampEl && update.timestamp) {
                const date = new Date(update.timestamp);
                timestampEl.textContent = date.toLocaleTimeString();
              }
              
              // Show error if present
              const errorEl = cardEl.querySelector('.sku-card-error');
              if (errorEl) {
                if (update.error) {
                  errorEl.style.display = 'block';
                  errorEl.textContent = 'Error: ' + update.error;
                } else {
                  errorEl.style.display = 'none';
                }
              }
              
              // Show images info if available
              const imagesInfoEl = cardEl.querySelector('.sku-card-images-info');
              if (imagesInfoEl) {
                if (update.imagesUpdated && update.imageCount) {
                  imagesInfoEl.style.display = 'block';
                  imagesInfoEl.textContent = '✓ ' + update.imageCount + ' images synced';
                } else {
                  imagesInfoEl.style.display = 'none';
                }
              }

              // Show metafields info if available
              const metafieldsInfoEl = cardEl.querySelector('.sku-card-metafields-info');
              if (metafieldsInfoEl) {
                if (update.metafieldResult) {
                  metafieldsInfoEl.style.display = 'block';
                  if (update.metafieldResult.success && update.metafieldResult.updatedFields) {
                    metafieldsInfoEl.className = 'sku-card-metafields-info';
                    metafieldsInfoEl.textContent = '✓ ' + update.metafieldResult.updatedFields.length + ' metafields synced';
                  } else if (update.metafieldResult.errors && update.metafieldResult.errors.length > 0) {
                    metafieldsInfoEl.className = 'sku-card-metafields-info warning';
                    metafieldsInfoEl.textContent = '⚠ Metafield warnings: ' + update.metafieldResult.errors.length;
                  } else {
                    metafieldsInfoEl.style.display = 'none';
                  }
                } else {
                  metafieldsInfoEl.style.display = 'none';
                }
              }

              // Update full details card for completed items
              if (update.status === 'completed') {
                updateCardDetails(cardEl, update);
              }

              // Scroll card into view if it's currently being updated
              if (update.status === 'updating') {
                cardEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
              }
            } else if (cardEl && !update) {
              // Card exists but no update data - might be a new product, initialize as pending
              const statusEl = cardEl.querySelector('.sku-card-status');
              if (statusEl && !statusEl.classList.contains('pending')) {
                cardEl.className = 'sku-card pending';
                statusEl.className = 'sku-card-status pending';
                statusEl.textContent = 'Pending';
              }
            }
          });
        }
      } catch (e) {
        console.error('Poll status error:', e);
        // Don't stop polling on error, just log it
      }
    }

    function updateButtons(state) {
      const btns = {
        start: document.getElementById('btnStart'),
        pause: document.getElementById('btnPause'),
        resume: document.getElementById('btnResume'),
        stop: document.getElementById('btnStop'),
        preview: document.getElementById('btnPreview')
      };
      const selects = [document.getElementById('vendorSelect'), document.getElementById('collectionSelect'), document.getElementById('batchSize')];

      // Safety checks
      if (!btns.start || !btns.pause || !btns.resume || !btns.stop || !btns.preview) {
        console.error('Button elements not found');
        return;
      }

      if (state === 'running') {
        btns.start.disabled = btns.preview.disabled = true;
        btns.pause.disabled = btns.stop.disabled = false;
        btns.resume.disabled = true;
        selects.forEach(s => { if (s) s.disabled = true; });
      } else if (state === 'paused') {
        btns.start.disabled = btns.pause.disabled = btns.preview.disabled = true;
        btns.resume.disabled = btns.stop.disabled = false;
        selects.forEach(s => { if (s) s.disabled = true; });
      } else {
        btns.start.disabled = btns.preview.disabled = false;
        btns.pause.disabled = btns.resume.disabled = btns.stop.disabled = true;
        selects.forEach(s => { if (s) s.disabled = false; });
      }
    }

    function log(msg, type = '') {
      const el = document.getElementById('log');
      const entry = document.createElement('div');
      entry.className = 'log-entry ' + type;
      entry.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
      el.appendChild(entry);
      el.scrollTop = el.scrollHeight;
      // Also update live status
      updateLiveStatus(msg, type);
    }

    function updateLiveStatus(msg, type = '') {
      const el = document.getElementById('liveStatus');
      if (!el) return;
      const color = type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#f59e0b';
      const entry = document.createElement('div');
      entry.style.color = color;
      entry.style.marginBottom = '4px';
      entry.textContent = '> ' + msg;
      el.appendChild(entry);
      el.scrollTop = el.scrollHeight;
      // Keep only last 20 entries
      while (el.children.length > 20) el.removeChild(el.firstChild);
    }

    async function refreshSingleSku(event, productId, sku) {
      event.stopPropagation();
      const cardEl = document.querySelector('.sku-card[data-id="' + productId + '"]');
      if (!cardEl) return;

      // Set card to updating state
      cardEl.className = 'sku-card updating';
      const statusEl = cardEl.querySelector('.sku-card-status');
      if (statusEl) {
        statusEl.className = 'sku-card-status updating';
        statusEl.textContent = 'Refreshing...';
      }

      log('Refreshing SKU: ' + sku, 'info');

      try {
        const syncImages = document.getElementById('syncImages')?.checked || false;
        const res = await fetch('/api/refresh-sku', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ productId, sku, syncImages })
        });

        const data = await res.json();
        if (data.success) {
          log('Refreshed: ' + (data.newTitle || sku), 'success');
          // Update card with new data
          cardEl.className = 'sku-card completed';
          if (statusEl) {
            statusEl.className = 'sku-card-status completed';
            statusEl.textContent = 'Completed';
          }
          const newTitleEl = cardEl.querySelector('.sku-card-new-title');
          if (newTitleEl && data.newTitle) {
            newTitleEl.style.display = 'block';
            newTitleEl.textContent = 'New: ' + data.newTitle.substring(0, 80);
          }
          // Update details section
          updateCardDetails(cardEl, data);
        } else {
          throw new Error(data.error || 'Refresh failed');
        }
      } catch (e) {
        log('Refresh error: ' + e.message, 'error');
        cardEl.className = 'sku-card error';
        if (statusEl) {
          statusEl.className = 'sku-card-status error';
          statusEl.textContent = 'Error';
        }
        const errorEl = cardEl.querySelector('.sku-card-error');
        if (errorEl) {
          errorEl.style.display = 'block';
          errorEl.textContent = 'Error: ' + e.message;
        }
      }
    }

    function updateCardDetails(cardEl, update) {
      const detailsEl = cardEl.querySelector('.sku-card-details');
      if (!detailsEl) {
        console.error('No details element found in card');
        return;
      }
      console.log('Updating card details with:', JSON.stringify(update, null, 2).substring(0, 500));

      let html = '';
      if (update.pattern) {
        html += '<div class="detail-row"><span class="detail-label">Pattern:</span><span class="detail-value">' + update.pattern + '</span></div>';
      }
      if (update.color) {
        html += '<div class="detail-row"><span class="detail-label">Color:</span><span class="detail-value">' + update.color + '</span></div>';
      }
      if (update.mfrSku) {
        html += '<div class="detail-row"><span class="detail-label">MFR#:</span><span class="detail-value">' + update.mfrSku + '</span></div>';
      }
      if (update.productType) {
        html += '<div class="detail-row"><span class="detail-label">Type:</span><span class="detail-value">' + update.productType + '</span></div>';
      }
      if (update.category) {
        html += '<div class="detail-row"><span class="detail-label">Category:</span><span class="detail-value">' + update.category + '</span></div>';
      }
      if (update.imagesUpdated && update.imageCount) {
        html += '<div class="detail-row"><span class="detail-label">Images:</span><span class="detail-value">✓ ' + update.imageCount + ' synced</span></div>';
      }
      if (update.tags && update.tags.length > 0) {
        html += '<div class="sku-card-tags">';
        update.tags.slice(0, 8).forEach(function(tag) {
          html += '<span class="sku-card-tag">' + tag + '</span>';
        });
        if (update.tags.length > 8) html += '<span class="sku-card-tag">+' + (update.tags.length - 8) + ' more</span>';
        html += '</div>';
      }
      if (update.colorAnalysis) {
        if (update.colorAnalysis.backgroundColor) {
          html += '<div class="detail-row"><span class="detail-label">BG Color:</span><span class="detail-value">' + update.colorAnalysis.backgroundColor + '</span></div>';
        }
      }
      // Show metafield update results
      if (update.metafieldResult) {
        if (update.metafieldResult.success && update.metafieldResult.updatedFields && update.metafieldResult.updatedFields.length > 0) {
          html += '<div class="detail-row"><span class="detail-label">Metafields:</span><span class="detail-value" style="color:#10b981;">✓ ' + update.metafieldResult.updatedFields.length + ' synced (' + update.metafieldResult.updatedFields.join(', ') + ')</span></div>';
        }
        if (update.metafieldResult.errors && update.metafieldResult.errors.length > 0) {
          html += '<div class="detail-row"><span class="detail-label">MF Warnings:</span><span class="detail-value" style="color:#f59e0b;">' + update.metafieldResult.errors.join('; ') + '</span></div>';
        }
      }

      detailsEl.innerHTML = html;
      detailsEl.style.display = html ? 'block' : 'none';
    }
  </script>
</body>
</html>
  `;
}

// API Routes
app.get('/', (req, res) => {
  res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
  res.set('Pragma', 'no-cache');
  res.set('Expires', '0');
  res.send(getDashboardHTML());
});

// Cache for vendors (refreshes every 5 minutes)
let vendorCache: { data: any; timestamp: number } | null = null;
const VENDOR_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

app.get('/api/vendors', async (req, res) => {
  try {
    // Return cached data if fresh
    if (vendorCache && (Date.now() - vendorCache.timestamp) < VENDOR_CACHE_TTL) {
      console.log('[Vendors] Returning cached data');
      return res.json(vendorCache.data);
    }

    console.log('[Vendors] Using GraphQL to fetch all vendors...');
    const graphqlUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`;

    // GraphQL query to get all vendor names (fast - single call)
    const vendorQuery = `{
      shop {
        productVendors(first: 250) {
          edges { node }
          pageInfo { hasNextPage endCursor }
        }
      }
    }`;

    let allVendors: string[] = [];
    let hasNextPage = true;
    let cursor: string | null = null;

    while (hasNextPage) {
      const query = cursor
        ? `{ shop { productVendors(first: 250, after: "${cursor}") { edges { node } pageInfo { hasNextPage endCursor } } } }`
        : vendorQuery;

      const response = await fetch(graphqlUrl, {
        method: 'POST',
        headers: {
          'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ query })
      });

      const data: any = await response.json();
      const vendorData = data?.data?.shop?.productVendors;

      if (vendorData?.edges) {
        allVendors.push(...vendorData.edges.map((e: any) => e.node));
      }

      hasNextPage = vendorData?.pageInfo?.hasNextPage || false;
      cursor = vendorData?.pageInfo?.endCursor || null;
    }

    // Get product count for each vendor using GraphQL productsCount
    const vendorCounts: { name: string; count: number }[] = [];

    // Batch vendor count queries (5 at a time to avoid rate limits)
    for (let i = 0; i < allVendors.length; i += 5) {
      const batch = allVendors.slice(i, i + 5);
      const countPromises = batch.map(async (vendor) => {
        const countQuery = `{
          productsCount(query: "vendor:'${vendor.replace(/'/g, "\\'")}'") {
            count
          }
        }`;
        const countRes = await fetch(graphqlUrl, {
          method: 'POST',
          headers: {
            'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ query: countQuery })
        });
        const countData: any = await countRes.json();
        return { name: vendor, count: countData?.data?.productsCount?.count || 0 };
      });

      const results = await Promise.all(countPromises);
      vendorCounts.push(...results);

      if (i % 50 === 0 && i > 0) {
        console.log(`[Vendors] Counted ${i}/${allVendors.length} vendors...`);
      }
    }

    // Sort alphabetically
    vendorCounts.sort((a, b) => a.name.localeCompare(b.name));
    const totalProducts = vendorCounts.reduce((sum, v) => sum + v.count, 0);

    console.log(`[Vendors] Complete! ${vendorCounts.length} vendors, ${totalProducts} total products`);

    // Save to cache
    const responseData = { vendors: vendorCounts, totalProducts, vendorCount: vendorCounts.length };
    vendorCache = { data: responseData, timestamp: Date.now() };

    res.json(responseData);
  } catch (e: any) {
    console.error('[Vendors] Error:', e.message);
    res.json({ vendors: [], error: e.message });
  }
});

// Cache for collections (refreshes every 5 minutes)
let collectionsCache: { data: any; timestamp: number } | null = null;
const COLLECTIONS_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

// Refresh cache endpoint
app.post('/api/refresh-cache', async (req, res) => {
  console.log('[Cache] Manual refresh requested...');
  vendorCache = null;
  collectionsCache = null;
  // Trigger preload in background
  preloadCache();
  res.json({ success: true, message: 'Cache refresh started' });
});

// Set skill endpoint (Architectural vs Residential)
app.post('/api/set-skill', (req, res) => {
  const { skill } = req.body;
  if (skill === 'architectural' || skill === 'residential') {
    currentSkill = skill;
    const specs = getCurrentUpdateSpecs();
    console.log(`[Skill] Changed to: ${skill} - Suffix: "${specs.suffix}", Type: "${specs.productType}"`);
    res.json({ success: true, skill, specs });
  } else {
    res.status(400).json({ success: false, error: 'Invalid skill type. Use "architectural" or "residential"' });
  }
});

// Get current skill
app.get('/api/skill', (req, res) => {
  const specs = getCurrentUpdateSpecs();
  res.json({ skill: currentSkill, specs });
});

// Refresh single SKU
app.post('/api/refresh-sku', async (req, res) => {
  const { productId, sku, syncImages } = req.body;
  if (!productId) {
    return res.status(400).json({ success: false, error: 'Product ID is required' });
  }

  try {
    console.log(`[Refresh SKU] Refreshing product ${productId} (SKU: ${sku})`);

    // Fetch the product from Shopify
    const productUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/products/${productId}.json`;
    const productRes = await fetch(productUrl, {
      headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken }
    });

    if (!productRes.ok) {
      throw new Error(`Failed to fetch product: ${productRes.status}`);
    }

    const productData = await productRes.json();
    const product = productData.product;

    // Update the product
    const result = await updateProduct(product, { syncImages: syncImages || false });

    // Save to updated SKUs
    if (sku && sku !== 'N/A') {
      saveUpdatedSku({
        sku,
        productId: String(productId),
        title: result.newTitle,
        pattern: result.mfrData?.patternName || null,
        color: result.mfrData?.colorName || null,
        mfrSku: result.mfrData?.mfrSku || null,
        updatedAt: new Date().toISOString(),
        vendor: product.vendor || null
      });
    }

    res.json({
      success: true,
      newTitle: result.newTitle,
      pattern: result.mfrData?.patternName,
      color: result.mfrData?.colorName,
      mfrSku: result.mfrData?.mfrSku,
      artist: result.mfrData?.artist,
      productType: getCurrentUpdateSpecs().productType,
      category: getCurrentUpdateSpecs().category,
      tags: result.tags,
      imagesUpdated: result.imagesUpdated,
      imageCount: result.imageCount,
      colorAnalysis: result.colorAnalysis
    });
  } catch (e: any) {
    console.error(`[Refresh SKU] Error:`, e.message);
    res.status(500).json({ success: false, error: e.message });
  }
});

app.get('/api/collections', async (req, res) => {
  try {
    // Return cached data if fresh
    if (collectionsCache && (Date.now() - collectionsCache.timestamp) < COLLECTIONS_CACHE_TTL) {
      console.log('[Collections] Returning cached data');
      return res.json(collectionsCache.data);
    }

    console.log('[Collections] Using GraphQL to fetch all collections...');
    const graphqlUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`;

    // GraphQL query to get all collections with product counts
    const collectionsQuery = `{
      collections(first: 250) {
        edges {
          node {
            id
            title
            productsCount {
              count
            }
          }
        }
      }
    }`;

    const response = await fetch(graphqlUrl, {
      method: 'POST',
      headers: {
        'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ query: collectionsQuery })
    });

    const data: any = await response.json();

    if (data.errors) {
      console.error('[Collections] GraphQL errors:', data.errors);
      return res.json({ collections: [], error: data.errors[0]?.message });
    }

    const collections = data.data?.collections?.edges?.map((edge: any) => {
      const node = edge.node;
      const numericId = node.id.replace('gid://shopify/Collection/', '');
      return {
        id: numericId,
        title: node.title,
        count: node.productsCount?.count || 0
      };
    }) || [];

    collections.sort((a: any, b: any) => a.title.localeCompare(b.title));
    console.log(`[Collections] Found ${collections.length} collections`);

    // Save to cache
    const responseData = { collections };
    collectionsCache = { data: responseData, timestamp: Date.now() };

    res.json(responseData);
  } catch (e: any) {
    console.error('[Collections] Error:', e.message);
    res.json({ collections: [], error: e.message });
  }
});

app.get('/api/preview', async (req, res) => {
  try {
    const { vendor, collection, limit, offset } = req.query as { vendor?: string; collection?: string; limit: string; offset?: string };
    const limitNum = limit === 'all' ? 250 : parseInt(limit) || 10;
    const offsetNum = parseInt(offset || '0') || 0;

    // Use GraphQL to fetch products WITH metafields
    const graphqlUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`;
    const fetchLimit = Math.min(offsetNum + limitNum, 250);

    let queryFilter = '';
    if (collection) {
      queryFilter = `collection_id:${collection}`;
    } else if (vendor) {
      queryFilter = `vendor:'${vendor.replace(/'/g, "\\'")}'`;
    } else {
      return res.json({ products: [], total: 0, error: 'No vendor or collection specified' });
    }

    const productQuery = `{
      products(first: ${fetchLimit}, query: "${queryFilter}") {
        edges {
          node {
            id
            title
            handle
            tags
            images(first: 10) { edges { node { url altText } } }
            variants(first: 1) { edges { node { price sku } } }
            metafields(first: 25) {
              edges {
                node {
                  namespace
                  key
                  value
                }
              }
            }
          }
        }
      }
      productsCount(query: "${queryFilter}") { count }
    }`;

    const response = await fetch(graphqlUrl, {
      method: 'POST',
      headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: productQuery })
    });
    const graphqlData: any = await response.json();

    if (graphqlData.errors) {
      console.error('[Preview] GraphQL errors:', graphqlData.errors);
      return res.json({ products: [], total: 0, error: graphqlData.errors[0]?.message || 'GraphQL error' });
    }

    // Transform GraphQL response to match expected format
    const data = {
      products: graphqlData.data?.products?.edges?.map((e: any) => {
        const node = e.node;
        return {
          id: node.id?.replace('gid://shopify/Product/', ''),
          title: node.title,
          handle: node.handle,
          tags: node.tags?.join(', ') || '',
          images: node.images?.edges?.map((img: any) => ({ src: img.node.url, alt: img.node.altText })) || [],
          variants: node.variants?.edges?.map((v: any) => ({ price: v.node.price, sku: v.node.sku })) || [],
          metafields: node.metafields?.edges?.map((m: any) => m.node) || []
        };
      }) || [],
      totalCount: graphqlData.data?.productsCount?.count || 0
    };

    // Generate preview data for each product (what it will look like after update)
    const products = data.products?.map((p: any) => {
      // Extract manufacturer data to preview the new title
      const handle = p.handle || '';
      const tags = p.tags || '';
      const existingTitle = p.title || '';

      // PRIORITY 0: Check existing title FIRST - if it already has good data, PRESERVE IT!
      const fromTitle = parseExistingTitle(existingTitle);

      // Quick extraction for preview (same logic as full extraction but sync)
      // SKU priority: Tags FIRST (real MFR SKU), then handle, then title (only if not DWWC internal)
      const fromTags = extractDataFromTags(tags);
      const mfrSkuFromHandle = extractRealMfrSku(p);

      // CRITICAL: Skip DWWC- internal SKUs from title - get REAL MFR SKU from tags
      let mfrSku = fromTags.mfrSku || mfrSkuFromHandle;
      if (!mfrSku && fromTitle.sku && !fromTitle.sku.startsWith('DWWC')) {
        mfrSku = fromTitle.sku;
      }
      const fromHandle = parseHandleForPatternAndColor(handle, mfrSku);

      // CRITICAL: Preserve existing good data - check title FIRST!
      const pattern = fromTitle.pattern || fromHandle.pattern || fromTags.patternName || getPatternFromSkuOrHandle(mfrSku, handle) || 'Unknown';
      // For artworks, don't force a color
      const color = fromTitle.isArtwork ? null : (fromTitle.color || fromHandle.color || fromTags.colorName || getColorFromSkuOrHandle(mfrSku, handle) || null);
      const artist = fromTitle.artist || null;
      const isArtwork = fromTitle.isArtwork || false;

      // Log if we're preserving existing title data
      if (fromTitle.isArtwork && fromTitle.pattern) {
        console.log(`  ✓ ARTWORK: "${existingTitle}" → "${fromTitle.pattern}" by ${fromTitle.artist}`);
      } else if (fromTitle.pattern && fromTitle.color) {
        console.log(`  ✓ PRESERVING: "${existingTitle}" → Pattern="${fromTitle.pattern}", Color="${fromTitle.color}"`);
      }

      // Generate preview title (with artist info for artworks)
      const newTitle = formatTitle(pattern, color, mfrSku, { artist, isArtwork });

      // Check if this SKU was previously updated
      const variantSku = p.variants?.[0]?.sku;
      const previouslyUpdatedRecord = variantSku ? isSkuPreviouslyUpdated(variantSku) : null;

      // Extract AI-generated metafield data
      const metafields = p.metafields || [];
      const getMetafield = (key: string) => {
        const mf = metafields.find((m: any) => m.key === key);
        return mf?.value || null;
      };
      const aiMfrSku = getMetafield('manufacturer_sku');
      const aiDescription = getMetafield('ai_generated_description');
      const aiColors = getMetafield('ai_generated_colors');
      const aiTags = getMetafield('ai_generated_tags');
      const sourceUrl = getMetafield('source_url');

      return {
        id: p.id,
        title: p.title,
        handle: p.handle,
        tags: p.tags,
        image: p.images?.[0]?.src || null,
        variants: p.variants,
        images: p.images,
        metafields: p.metafields,
        previouslyUpdated: !!previouslyUpdatedRecord,
        previouslyUpdatedAt: previouslyUpdatedRecord?.updatedAt || null,
        // Preview data showing what the product will look like after update
        preview: {
          newTitle,
          pattern,
          color: color || (isArtwork ? '(Artwork)' : '(none)'),
          mfrSku: mfrSku || 'N/A',
          artist,
          isArtwork,
          shopifyLink: p.handle ? `https://designerwallcoverings.com/products/${p.handle}` : null,
          // AI-generated metafield data from Shopify
          manufacturerSku: aiMfrSku || mfrSku || 'N/A',
          aiDescription: aiDescription,
          aiColors: aiColors,
          aiTags: aiTags,
          sourceUrl: sourceUrl,
          // Generate preview description with AI content, specs, and MFR link
          newDescription: (() => {
            let desc = aiDescription || 'Architectural Wallcoverings - Premium wallcovering solution for commercial and hospitality applications.';
            desc += '\n\n';
            // Add specs
            const specs = [];
            if (aiMfrSku || mfrSku) specs.push(`<li><strong>Manufacturer SKU:</strong> ${aiMfrSku || mfrSku}</li>`);
            if (specs.length > 0) {
              desc += '<h3>Specifications</h3>\n<ul>\n' + specs.join('\n') + '\n</ul>\n\n';
            }
            // Add manufacturer link
            if (sourceUrl) {
              desc += `<p><a href="${sourceUrl}" target="_blank" rel="noopener">View on Manufacturer Website →</a></p>\n\n`;
            }
            desc += '<p><strong>Resources:</strong> Spec Sheet | Installation Guide</p>';
            return desc;
          })(),
          // All metafields for display
          allMetafields: metafields.filter((m: any) => m.value && m.value.trim() !== ''),
          allMetafieldsHtml: metafields
            .filter((m: any) => m.value && m.value.trim() !== '')
            .map((m: any) => `<div style="margin-bottom: 4px; padding: 2px 0; border-bottom: 1px solid #e2e8f0;"><strong style="color: #3b82f6;">${m.namespace}.${m.key}:</strong> <span style="color: #374151;">${(m.value || '').substring(0, 100)}${m.value?.length > 100 ? '...' : ''}</span></div>`)
            .join('')
        }
      };
    }) || [];

    // DEDUPLICATE products by SKU - remove duplicates that have same variant SKU
    const seenSkus = new Set<string>();
    const deduplicatedProducts = products.filter((p: any) => {
      const sku = p.variants?.[0]?.sku;
      if (!sku) return true; // Keep products without SKU
      if (seenSkus.has(sku)) {
        console.log(`  [Dedup] Removing duplicate SKU: ${sku} (${p.title.substring(0, 40)}...)`);
        return false;
      }
      seenSkus.add(sku);
      return true;
    });

    console.log(`  [Preview] Loaded ${products.length} products, ${deduplicatedProducts.length} after deduplication`);

    // Apply offset - slice to get only the requested portion
    const slicedProducts = deduplicatedProducts.slice(offsetNum, offsetNum + limitNum);
    console.log(`  [Preview] Offset=${offsetNum}, Limit=${limitNum}, Returning ${slicedProducts.length} products`);

    // Total count from GraphQL response
    const totalCount = data.totalCount || deduplicatedProducts.length;

    // Only update processState if this is the initial load (offset=0)
    if (offsetNum === 0) {
      processState.products = slicedProducts;
      processState.totalProducts = totalCount;
      // Initialize product updates tracking
      processState.productUpdates.clear();
    }

    // Add new products to tracking (for load more)
    slicedProducts.forEach((p: any) => {
      if (!processState.productUpdates.has(String(p.id))) {
        processState.productUpdates.set(String(p.id), {
          status: 'pending',
          oldTitle: p.title,
          newTitle: p.preview?.newTitle,
          sku: p.variants?.[0]?.sku || 'N/A'
        });
      }
    });

    res.json({ products: slicedProducts, total: totalCount });
  } catch (e: any) {
    res.json({ products: [], total: 0, error: e.message });
  }
});

app.get('/api/status', (req, res) => {
  // Convert Map to object for JSON serialization
  const productUpdatesObj: any = {};
  processState.productUpdates.forEach((value, key) => {
    productUpdatesObj[key] = {
      ...value,
      timestamp: value.timestamp?.toISOString()
    };
  });

  res.json({
    running: processState.running,
    paused: processState.paused,
    vendor: processState.vendor,
    processed: processState.processed,
    total: processState.totalProducts,
    errors: processState.errors.length,
    products: processState.products,
    productUpdates: productUpdatesObj
  });
});

app.post('/api/start', async (req, res) => {
  if (processState.running) {
    return res.json({ success: false, error: 'Already running' });
  }

  const { vendor, batchSize, syncImages } = req.body;
  if (!vendor) return res.json({ success: false, error: 'No vendor' });

  const limitNum = batchSize === 'all' ? 250 : parseInt(batchSize) || 10;

  // Fetch products using GraphQL
  console.log(`[Start] Fetching ${limitNum} products for vendor: ${vendor}`);
  const graphqlUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`;

  const productQuery = `{
    products(first: ${Math.min(limitNum, 250)}, query: "vendor:'${vendor.replace(/'/g, "\\'")}'") {
      edges {
        node {
          id
          title
          handle
          tags
          images(first: 10) { edges { node { url altText } } }
          variants(first: 1) { edges { node { price sku } } }
        }
      }
    }
    productsCount(query: "vendor:'${vendor.replace(/'/g, "\\'")}'") { count }
  }`;

  try {
    const response = await fetch(graphqlUrl, {
      method: 'POST',
      headers: {
        'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ query: productQuery })
    });

    const data: any = await response.json();

    if (data.errors) {
      console.error('[Start] GraphQL errors:', data.errors);
      return res.json({ success: false, error: data.errors[0]?.message || 'GraphQL error' });
    }

    // Transform GraphQL response to REST-like format for compatibility
    const products = data.data?.products?.edges?.map((edge: any) => {
      const node = edge.node;
      const numericId = node.id.replace('gid://shopify/Product/', '');
      return {
        id: parseInt(numericId),
        title: node.title,
        handle: node.handle,
        tags: node.tags?.join(', ') || '',
        images: node.images?.edges?.map((ie: any) => ({ src: ie.node.url, alt: ie.node.altText })) || [],
        variants: node.variants?.edges?.map((ve: any) => ({ price: ve.node.price, sku: ve.node.sku })) || []
      };
    }) || [];

    const totalCount = data.data?.productsCount?.count || products.length;

    console.log(`[Start] Found ${products.length} products (total: ${totalCount})`);

    processState.running = true;
    processState.paused = false;
    processState.vendor = vendor;
    processState.batchSize = limitNum;
    processState.currentIndex = 0;
    processState.processed = 0;
    processState.errors = [];
    processState.startTime = new Date();
    processState.logs = [];
    processState.syncImages = syncImages !== false;
    processState.products = products;
    processState.totalProducts = totalCount;
    // Initialize product updates tracking
    processState.productUpdates.clear();
    products.forEach((p: any) => {
      processState.productUpdates.set(String(p.id), {
        status: 'pending',
        oldTitle: p.title,
        sku: p.variants?.[0]?.sku || 'N/A'
      });
    });

    runUpdateProcess();
    res.json({ success: true, productsLoaded: products.length, totalAvailable: totalCount });
  } catch (e: any) {
    console.error('[Start] Error fetching products:', e.message);
    res.json({ success: false, error: e.message });
  }
});

// Start with pre-approved products (from approval modal)
// Update single product by URL
app.post('/api/update-single', async (req, res) => {
  const { url, syncImages } = req.body;
  if (!url) return res.json({ success: false, error: 'No URL provided' });

  // Check if already running
  if (processState.running) {
    return res.json({ success: false, error: 'Update process already running. Please stop it first.' });
  }

  console.log(`[Single] Updating product from URL: ${url}`);

  try {
    // Extract product ID or handle from URL
    let productId: string | null = null;
    let handle: string | null = null;

    // Shopify admin URL: /admin/products/12345 or /products/12345
    const adminMatch = url.match(/(?:admin\/)?products\/(\d+)/);
    if (adminMatch) productId = adminMatch[1];

    // Frontend URL: /products/handle-name or designerwallcoverings.com/products/handle
    // Only match if we didn't already find a numeric ID
    if (!productId) {
      const handleMatch = url.match(/\/products\/([^\/\?]+)/);
      if (handleMatch) handle = handleMatch[1];
    }

    // Search URL: /search?q=something - extract query and search for product
    let searchQuery: string | null = null;
    if (!productId && !handle) {
      const searchMatch = url.match(/[?&]q=([^&]+)/);
      if (searchMatch) {
        searchQuery = decodeURIComponent(searchMatch[1]).replace(/\+/g, ' ');
        console.log('[Single] Detected search URL, query: ' + searchQuery);
      }
    }

    if (!productId && !handle && !searchQuery) {
      return res.json({ success: false, error: 'Could not extract product ID, handle, or search query from URL. Use a product URL like /products/handle-name' });
    }

    const graphqlUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`;
    let product: any = null;

    if (searchQuery) {
      // Search for product by title
      const escapedQuery = searchQuery.replace(/"/g, '\\"');
      const query = '{ products(first: 1, query: "title:*' + escapedQuery.split(' ')[0] + '*") { edges { node { id title handle vendor tags images(first:10) { edges { node { url } } } variants(first:1) { edges { node { price sku } } } } } } }';
      const response = await fetch(graphqlUrl, {
        method: 'POST',
        headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' },
        body: JSON.stringify({ query })
      });
      const data: any = await response.json();
      if (data.errors) {
        return res.json({ success: false, error: data.errors[0]?.message || 'GraphQL error' });
      }
      product = data.data?.products?.edges?.[0]?.node;
      if (!product) {
        return res.json({ success: false, error: 'No product found matching: ' + searchQuery });
      }
    } else if (productId) {
      // Fetch by ID
      const query = `{ product(id: "gid://shopify/Product/${productId}") { id title handle vendor tags images(first:10) { edges { node { url } } } variants(first:1) { edges { node { price sku } } } } }`;
      const response = await fetch(graphqlUrl, {
        method: 'POST',
        headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' },
        body: JSON.stringify({ query })
      });
      const data: any = await response.json();
      if (data.errors) {
        return res.json({ success: false, error: data.errors[0]?.message || 'GraphQL error' });
      }
      product = data.data?.product;
    } else if (handle) {
      // Fetch by handle - escape handle for GraphQL
      const escapedHandle = handle.replace(/"/g, '\\"');
      const query = `{ productByHandle(handle: "${escapedHandle}") { id title handle vendor tags images(first:10) { edges { node { url } } } variants(first:1) { edges { node { price sku } } } } }`;
      const response = await fetch(graphqlUrl, {
        method: 'POST',
        headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' },
        body: JSON.stringify({ query })
      });
      const data: any = await response.json();
      if (data.errors) {
        return res.json({ success: false, error: data.errors[0]?.message || 'GraphQL error' });
      }
      product = data.data?.productByHandle;
    }

    if (!product) {
      return res.json({ success: false, error: 'Product not found' });
    }

    // Transform and update
    const numericId = product.id.replace('gid://shopify/Product/', '');
    const productData = {
      id: parseInt(numericId),
      title: product.title,
      handle: product.handle,
      vendor: product.vendor,
      tags: product.tags?.join(', ') || '',
      images: product.images?.edges?.map((e: any) => ({ src: e.node.url })) || [],
      variants: product.variants?.edges?.map((e: any) => ({ price: e.node.price, sku: e.node.sku })) || []
    };

    // Initialize state for single product update
    processState.products = [productData];
    processState.totalProducts = 1;
    processState.processed = 0;
    processState.errors = [];
    processState.running = true;
    processState.paused = false;
    processState.vendor = product.vendor || 'Single Product';
    processState.batchSize = 1;
    processState.currentIndex = 0;
    processState.startTime = new Date();
    processState.syncImages = syncImages !== false;
    
    // Initialize product updates tracking
    processState.productUpdates.clear();
    processState.productUpdates.set(String(productData.id), {
      status: 'pending',
      oldTitle: productData.title,
      sku: productData.variants?.[0]?.sku || 'N/A'
    });

    // Return immediately and run update in background
    res.json({ success: true, title: product.title, id: numericId, message: 'Update started in background' });
    
    // Run update process in background (don't await)
    runUpdateProcess().catch((e: any) => {
      console.error('[Single] Update process error:', e.message);
    });
  } catch (e: any) {
    console.error('[Single] Error:', e.message);
    res.json({ success: false, error: e.message });
  }
});

app.post('/api/start-approved', async (req, res) => {
  if (processState.running) {
    return res.json({ success: false, error: 'Already running' });
  }

  const { vendor, collection, syncImages, productIds, products, overrides } = req.body;

  // Log if overrides received
  if (overrides && Object.keys(overrides).length > 0) {
    console.log(`[Start-Approved] Received ${Object.keys(overrides).length} user overrides`);
  }

  // Support both new (productIds only) and legacy (full products) formats
  let productsToProcess: any[] = [];

  if (productIds && productIds.length > 0) {
    // New format: fetch products by IDs in batches to avoid timeout
    console.log(`[Start-Approved] Fetching ${productIds.length} products by ID...`);

    const batchSize = 50;
    for (let i = 0; i < productIds.length; i += batchSize) {
      const batchIds = productIds.slice(i, i + batchSize);
      const idFilter = batchIds.map((id: any) => `id:${id}`).join(' OR ');

      const query = `{
        products(first: ${batchSize}, query: "${idFilter}") {
          edges {
            node {
              id
              title
              handle
              vendor
              productType
              tags
              images(first: 5) { edges { node { src altText } } }
              variants(first: 10) { edges { node { id sku price } } }
            }
          }
        }
      }`;

      try {
        const response = await fetch(`https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`, {
          method: 'POST',
          headers: {
            'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ query })
        });

        const data: any = await response.json();
        const fetchedProducts = data?.data?.products?.edges?.map((e: any) => {
          const node = e.node;
          return {
            id: node.id.replace('gid://shopify/Product/', ''),
            title: node.title,
            handle: node.handle,
            vendor: node.vendor,
            product_type: node.productType,
            tags: node.tags?.join(', ') || '',
            images: node.images?.edges?.map((ie: any) => ({ src: ie.node.src, alt: ie.node.altText })) || [],
            variants: node.variants?.edges?.map((ve: any) => ({
              id: ve.node.id.replace('gid://shopify/ProductVariant/', ''),
              sku: ve.node.sku,
              price: ve.node.price
            })) || []
          };
        }) || [];

        productsToProcess.push(...fetchedProducts);
        console.log(`[Start-Approved] Fetched batch ${Math.floor(i/batchSize) + 1}: ${fetchedProducts.length} products`);
      } catch (e: any) {
        console.error(`[Start-Approved] Error fetching batch: ${e.message}`);
      }

      // Small delay between batches
      if (i + batchSize < productIds.length) {
        await new Promise(r => setTimeout(r, 200));
      }
    }

    console.log(`[Start-Approved] Total fetched: ${productsToProcess.length} products`);
  } else if (products && products.length > 0) {
    // Legacy format: use provided products directly
    productsToProcess = products;
  }

  if (productsToProcess.length === 0) {
    return res.json({ success: false, error: 'No products found or selected' });
  }

  console.log(`[Start-Approved] Starting with ${productsToProcess.length} pre-approved products`);

  processState.running = true;
  processState.paused = false;
  processState.vendor = vendor || collection || 'Approved Selection';
  processState.batchSize = productsToProcess.length;
  processState.currentIndex = 0;
  processState.processed = 0;
  processState.errors = [];
  processState.startTime = new Date();
  processState.logs = [];
  processState.syncImages = syncImages !== false;
  processState.overrides = overrides || {};  // Store user overrides
  processState.products = productsToProcess;
  processState.totalProducts = productsToProcess.length;
  // Initialize product updates tracking
  processState.productUpdates.clear();
  productsToProcess.forEach((p: any) => {
    processState.productUpdates.set(String(p.id), {
      status: 'pending',
      oldTitle: p.title,
      sku: p.variants?.[0]?.sku || 'N/A'
    });
  });

  runUpdateProcess();
  res.json({ success: true, productsLoaded: productsToProcess.length });
});

app.post('/api/pause', (req, res) => {
  processState.paused = true;
  res.json({ success: true });
});

app.post('/api/resume', (req, res) => {
  processState.paused = false;
  res.json({ success: true });
});

app.post('/api/stop', (req, res) => {
  processState.running = false;
  processState.paused = false;
  // Serialize productUpdates Map to array for history
  const productsData = Array.from(processState.productUpdates.entries()).map(([id, data]) => ({
    id,
    ...data
  }));
  saveHistoryEntry({
    date: new Date().toISOString(),
    vendor: processState.vendor,
    processed: processState.processed,
    total: processState.totalProducts,
    errors: processState.errors.length,
    status: 'stopped',
    products: productsData
  });
  res.json({ success: true });
});

app.get('/api/history', (req, res) => {
  res.json({ history: loadHistory() });
});

// AI Chat endpoint
app.post('/api/chat', async (req, res) => {
  try {
    const { message } = req.body;
    if (!message) {
      return res.json({ response: 'Please provide a message.' });
    }

    const response = await anthropic.messages.create({
      model: 'claude-opus-4-8',
      max_tokens: 500,
      system: `You are the Architectural Update Agent AI assistant. You help with:
- Updating commercial wallcovering products to Architectural Wallcoverings branding
- Title format: "Pattern Name Color (MFR#) | Architectural Wallcoverings"
- Color analysis using AI vision
- Syncing images from manufacturer websites
- Tracking updates in PostgreSQL database

Current status: ${processState.running ? 'Running' : 'Idle'}
Vendor: ${processState.vendor || 'None selected'}
Processed: ${processState.processed}/${processState.totalProducts}

Be helpful and concise.`,
      messages: [{ role: 'user', content: message }]
    });

    const textContent = response.content.find(c => c.type === 'text');
    res.json({ response: textContent?.text || 'No response generated.' });
  } catch (e: any) {
    console.error('Chat error:', e.message);
    res.json({ response: `Sorry, I encountered an error: ${e.message}` });
  }
});

// Database endpoints for tracking
app.get('/api/db/updates', async (req, res) => {
  try {
    const { vendor, limit = 100 } = req.query;
    const result = await pool.query(`
      SELECT * FROM shopify_product_updates
      WHERE ($1::text IS NULL OR vendor_name = $1)
      ORDER BY created_at DESC LIMIT $2
    `, [vendor || null, Number(limit)]);
    res.json({ updates: result.rows, count: result.rows.length });
  } catch (e: any) {
    res.status(500).json({ error: e.message });
  }
});

app.get('/api/db/batches', async (req, res) => {
  try {
    const result = await pool.query(`
      SELECT * FROM shopify_update_batches ORDER BY started_at DESC LIMIT 50
    `);
    res.json({ batches: result.rows });
  } catch (e: any) {
    res.status(500).json({ error: e.message });
  }
});

app.get('/api/db/check/:mfrSku', async (req, res) => {
  try {
    const { mfrSku } = req.params;
    const result = await pool.query(
      'SELECT id, shopify_product_id, new_title, updated_at FROM shopify_product_updates WHERE mfr_sku = $1',
      [mfrSku]
    );
    res.json({
      exists: result.rows.length > 0,
      mfrSku,
      record: result.rows[0] || null
    });
  } catch (e: any) {
    res.status(500).json({ error: e.message });
  }
});

app.get('/api/db/stats', async (req, res) => {
  try {
    const result = await pool.query(`
      SELECT
        COUNT(*) as total_updates,
        COUNT(DISTINCT vendor_name) as vendors_updated,
        COUNT(DISTINCT mfr_sku) as unique_skus,
        MAX(created_at) as last_update
      FROM shopify_product_updates
    `);
    res.json(result.rows[0]);
  } catch (e: any) {
    res.status(500).json({ error: e.message });
  }
});

// Helper function to log update to database
async function logUpdateToDatabase(
  product: any,
  mfrData: any,
  colorAnalysis: any,
  oldTitle: string,
  newTitle: string,
  oldTags: string[],
  newTags: string[],
  imagesUpdated: boolean,
  imageCount: number,
  metafieldResult?: MetafieldPushResult
) {
  try {
    await pool.query(`
      INSERT INTO shopify_product_updates
      (shopify_product_id, shopify_handle, mfr_sku, vendor_name,
       pattern_name, color_name, old_title, new_title,
       old_tags, new_tags, manufacturer_data, color_analysis,
       images_synced, image_count, batch_id, update_status,
       metafields_updated, metafields_count, metafield_errors)
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, 'success', $16, $17, $18)
      ON CONFLICT (shopify_product_id) DO UPDATE SET
        new_title = EXCLUDED.new_title,
        new_tags = EXCLUDED.new_tags,
        manufacturer_data = EXCLUDED.manufacturer_data,
        color_analysis = EXCLUDED.color_analysis,
        images_synced = EXCLUDED.images_synced,
        image_count = EXCLUDED.image_count,
        batch_id = EXCLUDED.batch_id,
        metafields_updated = EXCLUDED.metafields_updated,
        metafields_count = EXCLUDED.metafields_count,
        metafield_errors = EXCLUDED.metafield_errors,
        updated_at = NOW()
    `, [
      product.id,
      product.handle || '',
      mfrData?.mfrSku || null,
      processState.vendor,
      mfrData?.patternName || null,
      mfrData?.colorName || null,
      oldTitle,
      newTitle,
      oldTags,
      newTags,
      JSON.stringify(mfrData || {}),
      JSON.stringify(colorAnalysis || {}),
      imagesUpdated,
      imageCount,
      currentBatchId,
      metafieldResult?.success || false,
      metafieldResult?.updatedFields?.length || 0,
      JSON.stringify(metafieldResult?.errors || [])
    ]);
  } catch (e: any) {
    console.error('Database logging error:', e.message);
  }
}

// Helper function to start batch tracking
async function startBatchTracking(vendor: string, batchSize: number, totalProducts: number, syncImages: boolean) {
  currentBatchId = crypto.randomUUID();
  try {
    await pool.query(`
      INSERT INTO shopify_update_batches (batch_id, vendor_name, batch_size, sync_images, total_products, status)
      VALUES ($1, $2, $3, $4, $5, 'running')
    `, [currentBatchId, vendor, batchSize, syncImages, totalProducts]);
  } catch (e: any) {
    console.error('Batch tracking start error:', e.message);
  }
  return currentBatchId;
}

// Helper function to update batch progress
async function updateBatchProgress(processed: number, successful: number, failed: number) {
  if (!currentBatchId) return;
  try {
    await pool.query(`
      UPDATE shopify_update_batches
      SET processed = $1, successful = $2, failed = $3
      WHERE batch_id = $4
    `, [processed, successful, failed, currentBatchId]);
  } catch (e: any) {
    console.error('Batch progress update error:', e.message);
  }
}

// Helper function to complete batch tracking
async function completeBatchTracking(status: string, summary: any) {
  if (!currentBatchId) return;
  try {
    await pool.query(`
      UPDATE shopify_update_batches
      SET status = $1, completed_at = NOW(), summary = $2,
          processed = $3, successful = $4, failed = $5
      WHERE batch_id = $6
    `, [status, JSON.stringify(summary), summary.processed, summary.successful, summary.failed, currentBatchId]);
  } catch (e: any) {
    console.error('Batch tracking complete error:', e.message);
  }
  currentBatchId = null;
}

// Background update process
async function runUpdateProcess() {
  console.log(`[Architectural Update] Starting for vendor: ${processState.vendor} (syncImages: ${processState.syncImages})`);
  logActivity('info', `Starting update for ${processState.vendor}${processState.syncImages ? ' (with image sync)' : ''}`);

  // Start batch tracking in database
  await startBatchTracking(
    processState.vendor || 'Unknown',
    processState.batchSize,
    processState.products.length,
    processState.syncImages
  );

  let successCount = 0;
  let failCount = 0;

  while (processState.running && processState.currentIndex < processState.products.length) {
    if (processState.paused) {
      await new Promise(r => setTimeout(r, 1000));
      continue;
    }

    const product = processState.products[processState.currentIndex];
    if (!product) break;

    const productId = String(product.id);
    const sku = product.variants?.[0]?.sku || 'N/A';

    // Mark product as updating
    processState.productUpdates.set(productId, {
      status: 'updating',
      oldTitle: product.title,
      sku: sku,
      timestamp: new Date()
    });

    try {
      logActivity('info', `Updating: ${product.title.substring(0, 40)}...`);
      const result = await updateProduct(product, { syncImages: processState.syncImages });

      // Log update result with image info
      let logMsg = `Updated: ${result.newTitle.substring(0, 50)}`;
      if (result.imagesUpdated) {
        logMsg += ` (${result.imageCount} images synced)`;
      }
      logActivity('update', logMsg);

      // Mark product as completed with FULL update details
      const oldTags = (product.tags || '').split(',').map((t: string) => t.trim()).filter((t: string) => t);
      processState.productUpdates.set(productId, {
        status: 'completed',
        oldTitle: product.title,
        newTitle: result.newTitle,
        sku: sku,
        timestamp: new Date(),
        imagesUpdated: result.imagesUpdated,
        imageCount: result.imageCount,
        // Extended details for full display
        pattern: result.mfrData?.patternName || null,
        color: result.mfrData?.colorName || null,
        mfrSku: result.mfrData?.mfrSku || null,
        tags: result.tags || [],
        oldTags: oldTags,
        productType: getCurrentUpdateSpecs().productType,
        category: getCurrentUpdateSpecs().category,
        colorAnalysis: result.colorAnalysis || null,
        mfrData: result.mfrData || null,
        metafieldResult: result.metafieldResult || null
      });

      // Save to persistent updated SKUs store (for tracking across sessions)
      if (sku && sku !== 'N/A') {
        saveUpdatedSku({
          sku,
          productId,
          title: result.newTitle,
          pattern: result.mfrData?.patternName || null,
          color: result.mfrData?.colorName || null,
          mfrSku: result.mfrData?.mfrSku || null,
          updatedAt: new Date().toISOString(),
          vendor: processState.vendor || product.vendor || null
        });
      }

      stats.totalUpdated++;
      processState.processed++;
      processState.currentIndex++;
      successCount++;

      // Update batch progress in database
      await updateBatchProgress(processState.processed, successCount, failCount);

      // Rate limiting (longer if syncing images)
      const delay = processState.syncImages ? 2000 : 1000;
      await new Promise(r => setTimeout(r, delay));
    } catch (e: any) {
      console.error(`[Architectural Update] Error:`, e.message);
      logActivity('error', `Error on ${product.id}: ${e.message}`);
      
      // Mark product as error
      processState.productUpdates.set(productId, {
        status: 'error',
        oldTitle: product.title,
        sku: sku,
        timestamp: new Date(),
        error: e.message
      });

      processState.errors.push({ id: product.id, error: e.message });
      stats.totalErrors++;
      processState.currentIndex++;
      failCount++;

      // Update batch progress in database
      await updateBatchProgress(processState.processed, successCount, failCount);
    }
  }

  if (processState.running) {
    processState.running = false;
    stats.lastVendor = processState.vendor;
    stats.lastRunTime = new Date();
    if (processState.vendor && !stats.vendorsProcessed.includes(processState.vendor)) {
      stats.vendorsProcessed.push(processState.vendor);
    }

    // Serialize productUpdates Map to array for history
    const productsData = Array.from(processState.productUpdates.entries()).map(([id, data]) => ({
      id,
      ...data
    }));
    saveHistoryEntry({
      date: new Date().toISOString(),
      vendor: processState.vendor,
      processed: processState.processed,
      total: processState.totalProducts,
      errors: processState.errors.length,
      status: 'completed',
      syncImages: processState.syncImages,
      products: productsData
    });

    logActivity('complete', `Completed: ${processState.processed} updated, ${processState.errors.length} errors`);
    console.log(`[Architectural Update] Completed. Updated: ${processState.processed}, Errors: ${processState.errors.length}`);

    // Complete batch tracking in database
    await completeBatchTracking('completed', {
      processed: processState.processed,
      successful: successCount,
      failed: failCount,
      vendor: processState.vendor,
      syncImages: processState.syncImages
    });
  } else {
    // Batch was stopped early
    await completeBatchTracking('stopped', {
      processed: processState.processed,
      successful: successCount,
      failed: failCount,
      vendor: processState.vendor,
      syncImages: processState.syncImages
    });
  }
}

// Pre-load cache function
async function preloadCache() {
  console.log('[Startup] Pre-loading vendor and collection cache...');
  const graphqlUrl = `https://${SHOPIFY_CONFIG.shop}/admin/api/${SHOPIFY_CONFIG.apiVersion}/graphql.json`;

  try {
    // Load vendors
    const vendorQuery = `{ shop { productVendors(first: 250) { edges { node } } } }`;
    const vendorRes = await fetch(graphqlUrl, {
      method: 'POST',
      headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: vendorQuery })
    });
    const vendorData: any = await vendorRes.json();
    const allVendors = vendorData?.data?.shop?.productVendors?.edges?.map((e: any) => e.node) || [];

    // Get counts in batches of 10
    const vendorCounts: { name: string; count: number }[] = [];
    for (let i = 0; i < allVendors.length; i += 10) {
      const batch = allVendors.slice(i, i + 10);
      const results = await Promise.all(batch.map(async (vendor: string) => {
        const countQuery = `{ productsCount(query: "vendor:'${vendor.replace(/'/g, "\\'")}'") { count } }`;
        const res = await fetch(graphqlUrl, {
          method: 'POST',
          headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' },
          body: JSON.stringify({ query: countQuery })
        });
        const data: any = await res.json();
        return { name: vendor, count: data?.data?.productsCount?.count || 0 };
      }));
      vendorCounts.push(...results);
      if (i % 50 === 0) console.log(`[Startup] Loaded ${Math.min(i + 10, allVendors.length)}/${allVendors.length} vendors...`);
    }
    vendorCounts.sort((a, b) => a.name.localeCompare(b.name));
    const totalProducts = vendorCounts.reduce((sum, v) => sum + v.count, 0);
    vendorCache = { data: { vendors: vendorCounts, totalProducts, vendorCount: vendorCounts.length }, timestamp: Date.now() };
    console.log(`[Startup] Vendors cached: ${vendorCounts.length} vendors, ${totalProducts} products`);

    // Load collections
    const collectionsQuery = `{ collections(first: 250) { edges { node { id title productsCount { count } } } } }`;
    const colRes = await fetch(graphqlUrl, {
      method: 'POST',
      headers: { 'X-Shopify-Access-Token': SHOPIFY_CONFIG.accessToken, 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: collectionsQuery })
    });
    const colData: any = await colRes.json();
    const collections = colData.data?.collections?.edges?.map((edge: any) => ({
      id: edge.node.id.replace('gid://shopify/Collection/', ''),
      title: edge.node.title,
      count: edge.node.productsCount?.count || 0
    })) || [];
    collections.sort((a: any, b: any) => a.title.localeCompare(b.title));
    collectionsCache = { data: { collections }, timestamp: Date.now() };
    console.log(`[Startup] Collections cached: ${collections.length} collections`);
  } catch (e: any) {
    console.error('[Startup] Cache preload error:', e.message);
  }
}

// Start server
app.listen(PORT, '0.0.0.0', () => {
  console.log(`Architectural Update Agent running at http://45.61.58.125:${PORT}`);
  console.log('Auth: admin / <password> (via shared-global-auth)');
  // Pre-load cache in background
  preloadCache();
});