import express from 'express';
import { Client } from 'ssh2';
import mysql from 'mysql2/promise';
import multer from 'multer';
import path from 'node:path';
import fs from 'node:fs';
import sharp from 'sharp';
import axios from 'axios';
import net from 'node:net';
import { requirePermission } from '../../shared/auth/middleware.js';

const router = express.Router();
const fsp = fs.promises;

// Phase 6.2: imports, uploads, generate-upc and deletions all require product_import.
// Reads (list-uploads, status checks) remain authenticated-only.
router.use((req, res, next) => {
  if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') return next();
  return requirePermission('product_import')(req, res, next);
});

// Create uploads directory if it doesn't exist
const uploadsDir = path.join('/var/www/inventory/uploads/products');
const reusableUploadsDir = path.join('/var/www/inventory/uploads/reusable');
fs.mkdirSync(uploadsDir, { recursive: true });
fs.mkdirSync(reusableUploadsDir, { recursive: true });

// Create a Map to track image upload times and their scheduled deletion
const imageUploadMap = new Map();

// Connection pooling and cache configuration
const connectionCache = {
  ssh: null,
  dbConnection: null,
  lastUsed: 0,
  isConnecting: false,
  connectionPromise: null,
  // Cache expiration time in milliseconds (5 minutes)
  expirationTime: 5 * 60 * 1000,
  // Cache for query results (key: query string, value: {data, timestamp})
  queryCache: new Map(),
  // Cache duration for different query types in milliseconds
  cacheDuration: {
    'field-options': 30 * 60 * 1000, // 30 minutes for field options
    'product-lines': 10 * 60 * 1000, // 10 minutes for product lines
    'sublines': 10 * 60 * 1000,      // 10 minutes for sublines
    'default': 60 * 1000             // 1 minute default
  }
};

const MIN_IMAGE_DIMENSION = 1000;
const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024;

// Function to schedule image deletion after 24 hours
const scheduleImageDeletion = (filename, filePath) => {
  // Only schedule deletion for images in the products folder
  if (!filePath.includes('/uploads/products/')) {
    console.log(`Skipping deletion for non-product image: ${filename}`);
    return;
  }

  // Delete any existing timeout for this file
  if (imageUploadMap.has(filename)) {
    clearTimeout(imageUploadMap.get(filename).timeoutId);
  }

  // Schedule deletion after 24 hours (24 * 60 * 60 * 1000 ms)
  const timeoutId = setTimeout(() => {
    console.log(`Auto-deleting image after 24 hours: ${filename}`);
    
    // Check if file exists before trying to delete
    if (fs.existsSync(filePath)) {
      try {
        fs.unlinkSync(filePath);
        console.log(`Successfully auto-deleted image: ${filename}`);
      } catch (error) {
        console.error(`Error auto-deleting image ${filename}:`, error);
      }
    } else {
      console.log(`File already deleted: ${filename}`);
    }
    
    // Remove from tracking map
    imageUploadMap.delete(filename);
  }, 24 * 60 * 60 * 1000); // 24 hours
  
  // Store upload time and timeout ID
  imageUploadMap.set(filename, {
    uploadTime: new Date(),
    timeoutId: timeoutId,
    filePath: filePath
  });
};

// Function to clean up scheduled deletions on server restart
const cleanupImagesOnStartup = () => {
  console.log('Checking for images to clean up...');
  
  // Check if uploads directory exists
  if (!fs.existsSync(uploadsDir)) {
    console.log('Uploads directory does not exist');
    return;
  }
  
  // Read all files in the directory
  fs.readdir(uploadsDir, (err, files) => {
    if (err) {
      console.error('Error reading uploads directory:', err);
      return;
    }
    
    const now = new Date();
    let countDeleted = 0;
    
    files.forEach(filename => {
      const filePath = path.join(uploadsDir, filename);
      
      // Get file stats
      try {
        const stats = fs.statSync(filePath);
        const fileCreationTime = stats.birthtime || stats.ctime; // birthtime might not be available on all systems
        const ageMs = now.getTime() - fileCreationTime.getTime();
        
        // If file is older than 24 hours, delete it
        if (ageMs > 24 * 60 * 60 * 1000) {
          fs.unlinkSync(filePath);
          countDeleted++;
          console.log(`Deleted old image on startup: ${filename} (age: ${Math.round(ageMs / (60 * 60 * 1000))} hours)`);
        } else {
          // Schedule deletion for remaining time
          const remainingMs = (24 * 60 * 60 * 1000) - ageMs;
          console.log(`Scheduling deletion for ${filename} in ${Math.round(remainingMs / (60 * 60 * 1000))} hours`);
          
          const timeoutId = setTimeout(() => {
            if (fs.existsSync(filePath)) {
              try {
                fs.unlinkSync(filePath);
                console.log(`Successfully auto-deleted scheduled image: ${filename}`);
              } catch (error) {
                console.error(`Error auto-deleting scheduled image ${filename}:`, error);
              }
            }
            imageUploadMap.delete(filename);
          }, remainingMs);
          
          imageUploadMap.set(filename, {
            uploadTime: fileCreationTime,
            timeoutId: timeoutId,
            filePath: filePath
          });
        }
      } catch (error) {
        console.error(`Error processing file ${filename}:`, error);
      }
    });
    
    console.log(`Cleanup completed: ${countDeleted} old images deleted, ${imageUploadMap.size} images scheduled for deletion`);
  });
};

// Run cleanup on server start
cleanupImagesOnStartup();

const bytesToMegabytes = (bytes) => Number((bytes / (1024 * 1024)).toFixed(2));

const getExtensionForImage = (mimetype, sourceName = '') => {
  const sourceExt = path.extname(sourceName).toLowerCase();
  if (sourceExt) return sourceExt;

  switch (mimetype) {
    case 'image/jpeg': return '.jpg';
    case 'image/png': return '.png';
    case 'image/gif': return '.gif';
    case 'image/webp': return '.webp';
    case 'image/tiff': return '.tif';
    default: return '.jpg';
  }
};

const createUploadFilename = (prefix, extension) => {
  const safePrefix = String(prefix || 'product').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 80) || 'product';
  const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
  return `${safePrefix}-${uniqueSuffix}${extension}`;
};

const buildUploadedImageResponse = (reqFile, processingResult, filePath) => {
  reqFile.size = processingResult.finalSize;

  const effectivePath = processingResult.newFilePath || filePath;
  if (processingResult.newFilePath) {
    reqFile.filename = path.basename(processingResult.newFilePath);
    reqFile.mimetype = 'image/jpeg';
  }

  const baseUrl = 'https://tools.acherryontop.com';
  const imageUrl = `${baseUrl}/uploads/products/${reqFile.filename}`;

  scheduleImageDeletion(reqFile.filename, effectivePath);

  return {
    success: true,
    imageUrl,
    fileName: reqFile.filename,
    mimetype: reqFile.mimetype,
    fullPath: effectivePath,
    notices: processingResult.notices,
    warnings: processingResult.warnings,
    metadata: processingResult.metadata,
    message: 'Image uploaded successfully (will auto-delete after 24 hours)'
  };
};

const isBlockedImageSourceHost = (hostname) => {
  const host = String(hostname || '').toLowerCase();
  if (!host || host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) {
    return true;
  }

  const ipVersion = net.isIP(host);
  if (ipVersion === 4) {
    const [first, second] = host.split('.').map(Number);
    return (
      first === 10 ||
      first === 127 ||
      (first === 169 && second === 254) ||
      (first === 172 && second >= 16 && second <= 31) ||
      (first === 192 && second === 168)
    );
  }

  if (ipVersion === 6) {
    return host === '::1' || host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80');
  }

  return false;
};

const processUploadedImage = async (filePath, mimetype) => {
  const notices = [];
  const legacyWarnings = [];
  const metadata = {};

  const originalBuffer = await fsp.readFile(filePath);
  let baseMetadata = await sharp(originalBuffer, { failOn: 'none' }).metadata();

  metadata.width = baseMetadata.width || 0;
  metadata.height = baseMetadata.height || 0;
  metadata.size = originalBuffer.length;
  metadata.colorSpace = baseMetadata.space || baseMetadata.colourspace || null;

  if (
    baseMetadata.width &&
    baseMetadata.height &&
    (baseMetadata.width < MIN_IMAGE_DIMENSION || baseMetadata.height < MIN_IMAGE_DIMENSION)
  ) {
    const message = `Image is ${baseMetadata.width}x${baseMetadata.height}. Recommended minimum is ${MIN_IMAGE_DIMENSION}x${MIN_IMAGE_DIMENSION}.`;
    notices.push({
      message,
      level: 'warning',
      code: 'dimensions_too_small',
      source: 'server'
    });
    legacyWarnings.push(message);
  }

  const colorSpace = (baseMetadata.space || baseMetadata.colourspace || '').toLowerCase();
  let shouldConvertToRgb = colorSpace === 'cmyk';

  if (shouldConvertToRgb) {
    const message = 'Converted image from CMYK to RGB.';
    notices.push({
      message,
      level: 'info',
      code: 'converted_to_rgb',
      source: 'server'
    });
    legacyWarnings.push(message);
  }

  let format = (baseMetadata.format || '').toLowerCase();
  if (format === 'gif') {
    if (metadata.size > MAX_IMAGE_SIZE_BYTES) {
      const message = `GIF optimization is limited; resulting size is ${bytesToMegabytes(metadata.size)}MB (target 5MB).`;
      notices.push({
        message,
        level: 'warning',
        code: 'gif_size_limit',
        source: 'server'
      });
      legacyWarnings.push(message);
    }
    metadata.convertedToRgb = false;
    metadata.resized = false;
    return { notices, warnings: legacyWarnings, metadata, finalSize: metadata.size };
  }

  // Convert unsupported product image storage formats to JPEG.
  let convertedToJpegFrom = null;
  if (format === 'tiff' || format === 'webp') {
    convertedToJpegFrom = format;
    format = 'jpeg';
    const sourceLabel = convertedToJpegFrom === 'tiff' ? 'TIFF' : 'WebP';
    const message = `Converted from ${sourceLabel} to JPEG.`;
    notices.push({
      message,
      level: 'info',
      code: `converted_from_${convertedToJpegFrom}`,
      source: 'server'
    });
    legacyWarnings.push(message);
  }

  const supportsQuality = ['jpeg', 'jpg', 'webp'].includes(format);
  let targetQuality = supportsQuality ? 90 : undefined;
  let finalQuality = undefined;

  let currentWidth = baseMetadata.width || null;
  let currentHeight = baseMetadata.height || null;

  let resized = false;
  let mutated = false;
  let finalBuffer = originalBuffer;
  let finalInfo = baseMetadata;

  const encode = async ({ width, height, quality }) => {
    let pipeline = sharp(originalBuffer, { failOn: 'none' });

    if (shouldConvertToRgb) {
      pipeline = pipeline.toColorspace('srgb');
    }

    if (width || height) {
      pipeline = pipeline.resize({
        width: width ?? undefined,
        height: height ?? undefined,
        fit: 'inside',
        withoutEnlargement: true,
      });
    }

    switch (format) {
      case 'png':
        pipeline = pipeline.png({
          compressionLevel: 9,
          adaptiveFiltering: true,
          palette: true,
        });
        break;
      case 'webp':
        pipeline = pipeline.webp({ quality: quality ?? 90 });
        break;
      case 'jpeg':
      case 'jpg':
      default:
        pipeline = pipeline.jpeg({ quality: quality ?? 90, mozjpeg: true });
        break;
    }

    return pipeline.toBuffer({ resolveWithObject: true });
  };

  const canResize =
    (currentWidth && currentWidth > MIN_IMAGE_DIMENSION) ||
    (currentHeight && currentHeight > MIN_IMAGE_DIMENSION);

  if (metadata.size > MAX_IMAGE_SIZE_BYTES && (supportsQuality || canResize)) {
    const maxAttempts = 8;

    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      let targetWidth = currentWidth;
      let targetHeight = currentHeight;
      let resizedThisAttempt = false;

      if (currentWidth && currentWidth > MIN_IMAGE_DIMENSION) {
        targetWidth = Math.max(MIN_IMAGE_DIMENSION, Math.round(currentWidth * 0.85));
      }

      if (currentHeight && currentHeight > MIN_IMAGE_DIMENSION) {
        targetHeight = Math.max(MIN_IMAGE_DIMENSION, Math.round(currentHeight * 0.85));
      }

      if (
        (targetWidth && currentWidth && targetWidth < currentWidth) ||
        (targetHeight && currentHeight && targetHeight < currentHeight)
      ) {
        resized = true;
        resizedThisAttempt = true;
        currentWidth = targetWidth;
        currentHeight = targetHeight;
      } else if (!supportsQuality || (targetQuality && targetQuality <= 70)) {
        // Cannot resize further and quality cannot be adjusted
        break;
      }

      const qualityForAttempt = supportsQuality ? targetQuality : undefined;
      const { data, info } = await encode({
        width: currentWidth,
        height: currentHeight,
        quality: qualityForAttempt,
      });

      mutated = true;
      finalBuffer = data;
      finalInfo = info;
      metadata.optimizedSize = data.length;
      if (info.width) metadata.width = info.width;
      if (info.height) metadata.height = info.height;
      if (info.width) currentWidth = info.width;
      if (info.height) currentHeight = info.height;

      if (supportsQuality && qualityForAttempt) {
        finalQuality = qualityForAttempt;
      }

      if (data.length <= MAX_IMAGE_SIZE_BYTES) {
        break;
      }

      if (resizedThisAttempt) {
        continue;
      }

      if (supportsQuality && targetQuality && targetQuality > 70) {
        const nextQuality = Math.max(70, targetQuality - 10);
        if (nextQuality === targetQuality) {
          break;
        }
        targetQuality = nextQuality;
        continue;
      }

      break;
    }

    if (finalBuffer.length > MAX_IMAGE_SIZE_BYTES) {
      const message = `Optimized image remains ${bytesToMegabytes(finalBuffer.length)}MB (target 5MB).`;
      notices.push({
        message,
        level: 'warning',
        code: 'size_over_limit',
        source: 'server'
      });
      legacyWarnings.push(message);
    }
  } else if (shouldConvertToRgb || convertedToJpegFrom) {
    const { data, info } = await encode({ width: currentWidth, height: currentHeight, quality: targetQuality });
    mutated = true;
    finalBuffer = data;
    finalInfo = info;
    metadata.optimizedSize = data.length;
    if (info.width) metadata.width = info.width;
    if (info.height) metadata.height = info.height;
    if (info.width) currentWidth = info.width;
    if (info.height) currentHeight = info.height;
  }

  if (mutated) {
    await fsp.writeFile(filePath, finalBuffer);
    metadata.optimizedSize = finalBuffer.length;
  } else {
    // No transformation occurred; still need to ensure we report original stats
    metadata.optimizedSize = metadata.size;
  }

  // Rename converted source files to .jpg after conversion
  let newFilePath = null;
  if (convertedToJpegFrom) {
    newFilePath = filePath.replace(/\.(tiff?|webp)$/i, '.jpg');
    if (newFilePath !== filePath) {
      await fsp.rename(filePath, newFilePath);
    }
  }

  metadata.convertedToRgb = shouldConvertToRgb && mutated;
  metadata.resized = resized;
  if (finalQuality) {
    metadata.quality = finalQuality;
  }

  if (resized && metadata.width && metadata.height) {
    const message = `Image resized to ${metadata.width}x${metadata.height} during optimization.`;
    notices.push({
      message,
      level: 'info',
      code: 'resized',
      source: 'server'
    });
    legacyWarnings.push(message);
  }

  if (finalQuality && finalQuality < 90) {
    const message = `Image quality adjusted to ${finalQuality} to reduce file size.`;
    notices.push({
      message,
      level: 'info',
      code: 'quality_adjusted',
      source: 'server'
    });
    legacyWarnings.push(message);
  }

  return {
    notices,
    warnings: legacyWarnings,
    metadata,
    finalSize: finalBuffer.length,
    newFilePath,
  };
};

// Configure multer for file uploads
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    console.log(`Saving to: ${uploadsDir}`);
    cb(null, uploadsDir);
  },
  filename: function (req, file, cb) {
    const fileExt = getExtensionForImage(file.mimetype, file.originalname);
    const fileName = createUploadFilename(req.body.upc || 'product', fileExt);
    console.log(`Generated filename: ${fileName} with mimetype: ${file.mimetype}`);
    cb(null, fileName);
  }
});

const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;

// Phase 6.7: exact-match MIME + extension allowlist. Substring-based regex
// matchers (the previous /jpeg|png|.../ approach) accepted MIMEs like
// `application/jpeg-payload` because of partial matches; this rejects them.
const ALLOWED_MIME_TYPES = new Set([
  'image/jpeg',
  'image/png',
  'image/gif',
  'image/webp',
  'image/tiff',
]);
const ALLOWED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.tif', '.tiff']);

const upload = multer({
  storage: storage,
  limits: {
    fileSize: MAX_UPLOAD_BYTES,
    files: 1,
  },
  fileFilter: function (req, file, cb) {
    const ext = path.extname(file.originalname).toLowerCase();
    if (!ALLOWED_MIME_TYPES.has(file.mimetype) || !ALLOWED_EXTENSIONS.has(ext)) {
      return cb(new Error('Only image files are allowed (jpg, png, gif, webp, tiff)'));
    }
    cb(null, true);
  }
});

// Modified function to get a database connection with connection pooling
async function getDbConnection() {
  const now = Date.now();
  
  // Check if we need to refresh the connection due to inactivity
  const needsRefresh = !connectionCache.ssh || 
                       !connectionCache.dbConnection || 
                       (now - connectionCache.lastUsed > connectionCache.expirationTime);
  
  // If connection is still valid, update last used time and return existing connection
  if (!needsRefresh) {
    connectionCache.lastUsed = now;
    return {
      ssh: connectionCache.ssh,
      connection: connectionCache.dbConnection
    };
  }
  
  // If another request is already establishing a connection, wait for that promise
  if (connectionCache.isConnecting && connectionCache.connectionPromise) {
    try {
      await connectionCache.connectionPromise;
      return {
        ssh: connectionCache.ssh,
        connection: connectionCache.dbConnection
      };
    } catch (error) {
      // If that connection attempt failed, we'll try again below
      console.error('Error waiting for existing connection:', error);
    }
  }
  
  // Close existing connections if they exist
  if (connectionCache.dbConnection) {
    try {
      await connectionCache.dbConnection.end();
    } catch (error) {
      console.error('Error closing existing database connection:', error);
    }
  }
  
  if (connectionCache.ssh) {
    try {
      connectionCache.ssh.end();
    } catch (error) {
      console.error('Error closing existing SSH connection:', error);
    }
  }
  
  // Mark that we're establishing a new connection
  connectionCache.isConnecting = true;
  
  // Create a new promise for this connection attempt
  connectionCache.connectionPromise = setupSshTunnel().then(tunnel => {
    const { ssh, stream, dbConfig } = tunnel;
    
    return mysql.createConnection({
      ...dbConfig,
      stream
    }).then(connection => {
      // Store the new connections
      connectionCache.ssh = ssh;
      connectionCache.dbConnection = connection;
      connectionCache.lastUsed = Date.now();
      connectionCache.isConnecting = false;
      
      return {
        ssh,
        connection
      };
    });
  }).catch(error => {
    connectionCache.isConnecting = false;
    throw error;
  });
  
  // Wait for the connection to be established
  return connectionCache.connectionPromise;
}

// Helper function to get cached query results or execute query if not cached
async function getCachedQuery(cacheKey, queryType, queryFn) {
  // Get cache duration based on query type
  const cacheDuration = connectionCache.cacheDuration[queryType] || connectionCache.cacheDuration.default;
  
  // Check if we have a valid cached result
  const cachedResult = connectionCache.queryCache.get(cacheKey);
  const now = Date.now();
  
  if (cachedResult && (now - cachedResult.timestamp < cacheDuration)) {
    console.log(`Cache hit for ${queryType} query: ${cacheKey}`);
    return cachedResult.data;
  }
  
  // No valid cache found, execute the query
  console.log(`Cache miss for ${queryType} query: ${cacheKey}`);
  const result = await queryFn();
  
  // Cache the result
  connectionCache.queryCache.set(cacheKey, {
    data: result,
    timestamp: now
  });
  
  return result;
}

// Helper function to setup SSH tunnel - ONLY USED BY getDbConnection NOW
async function setupSshTunnel() {
  const sshConfig = {
    host: process.env.PROD_SSH_HOST,
    port: process.env.PROD_SSH_PORT || 22,
    username: process.env.PROD_SSH_USER,
    privateKey: process.env.PROD_SSH_KEY_PATH
      ? fs.readFileSync(process.env.PROD_SSH_KEY_PATH)
      : undefined,
    compress: true
  };

  const dbConfig = {
    host: process.env.PROD_DB_HOST || 'localhost',
    user: process.env.PROD_DB_USER,
    password: process.env.PROD_DB_PASSWORD,
    database: process.env.PROD_DB_NAME,
    port: process.env.PROD_DB_PORT || 3306,
    // DATETIME columns store Central wall-clock literals — return them as
    // strings; parse with shared/business-time parseMySql() if instants are
    // ever needed (see docs/TIME.md).
    dateStrings: true
  };

  return new Promise((resolve, reject) => {
    const ssh = new Client();

    ssh.on('error', (err) => {
      console.error('SSH connection error:', err);
      reject(err);
    });

    ssh.on('ready', () => {
      ssh.forwardOut(
        '127.0.0.1',
        0,
        dbConfig.host,
        dbConfig.port,
        (err, stream) => {
          if (err) reject(err);
          resolve({ ssh, stream, dbConfig });
        }
      );
    }).connect(sshConfig);
  });
}

// Image upload endpoint
router.post('/upload-image', upload.single('image'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ error: 'No image file provided' });
    }

    // Log file information
    console.log('File uploaded:', {
      filename: req.file.filename,
      originalname: req.file.originalname,
      mimetype: req.file.mimetype,
      size: req.file.size,
      path: req.file.path
    });

    // Ensure the file exists
    const filePath = path.join(uploadsDir, req.file.filename);
    if (!fs.existsSync(filePath)) {
      return res.status(500).json({ error: 'File was not saved correctly' });
    }

    // Log file access permissions
    fs.access(filePath, fs.constants.R_OK, (err) => {
      if (err) {
        console.error('File permission issue:', err);
      } else {
        console.log('File is readable');
      }
    });

    // Process the image (resize/compress/color-space) before responding
    const processingResult = await processUploadedImage(filePath, req.file.mimetype);
    const responsePayload = buildUploadedImageResponse(req.file, processingResult, filePath);
    
    // Return success response with image URL
    res.status(200).json(responsePayload);
    
  } catch (error) {
    console.error('Error uploading image:', error);
    if (req?.file?.filename) {
      const cleanupPath = path.join(uploadsDir, req.file.filename);
      if (fs.existsSync(cleanupPath)) {
        try {
          fs.unlinkSync(cleanupPath);
        } catch (cleanupError) {
          console.error('Failed to remove file after processing error:', cleanupError);
        }
      }
    }
    res.status(500).json({ error: error.message || 'Failed to upload image' });
  }
});

router.post('/upload-image-url', async (req, res) => {
  let filePath = null;

  try {
    const { imageUrl, upc, supplier_no } = req.body || {};
    if (!imageUrl || typeof imageUrl !== 'string') {
      return res.status(400).json({ error: 'imageUrl is required' });
    }

    let parsedUrl;
    try {
      parsedUrl = new URL(imageUrl.trim());
    } catch {
      return res.status(400).json({ error: 'Invalid image URL' });
    }

    if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
      return res.status(400).json({ error: 'Image URL must use http or https' });
    }

    if (isBlockedImageSourceHost(parsedUrl.hostname)) {
      return res.status(400).json({ error: 'Image URL host is not allowed' });
    }

    const response = await axios.get(parsedUrl.toString(), {
      responseType: 'arraybuffer',
      timeout: 15000,
      maxContentLength: 15 * 1024 * 1024,
      maxBodyLength: 15 * 1024 * 1024,
      headers: {
        Accept: 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
        'User-Agent': 'inventory-image-import/1.0'
      }
    });

    const contentType = String(response.headers['content-type'] || '').split(';')[0].toLowerCase();
    if (!contentType.startsWith('image/')) {
      return res.status(400).json({ error: 'URL did not return an image' });
    }

    const imageBuffer = Buffer.from(response.data);
    if (!imageBuffer.length) {
      return res.status(400).json({ error: 'Downloaded image was empty' });
    }

    const fileExt = getExtensionForImage(contentType, parsedUrl.pathname);
    const filename = createUploadFilename(upc || supplier_no || 'product-url', fileExt);
    filePath = path.join(uploadsDir, filename);
    await fsp.writeFile(filePath, imageBuffer);

    const reqFile = {
      filename,
      originalname: path.basename(parsedUrl.pathname) || filename,
      mimetype: contentType,
      size: imageBuffer.length,
      path: filePath
    };

    console.log('Image URL downloaded:', {
      filename: reqFile.filename,
      originalUrl: parsedUrl.toString(),
      mimetype: reqFile.mimetype,
      size: reqFile.size,
      path: reqFile.path
    });

    const processingResult = await processUploadedImage(filePath, reqFile.mimetype);
    const responsePayload = buildUploadedImageResponse(reqFile, processingResult, filePath);

    res.status(200).json(responsePayload);
  } catch (error) {
    console.error('Error uploading image from URL:', error);
    if (filePath && fs.existsSync(filePath)) {
      try {
        fs.unlinkSync(filePath);
      } catch (cleanupError) {
        console.error('Failed to remove URL-downloaded file after processing error:', cleanupError);
      }
    }
    res.status(500).json({ error: error.message || 'Failed to upload image from URL' });
  }
});

// Image deletion endpoint
router.delete('/delete-image', (req, res) => {
  try {
    const { filename } = req.body;
    
    if (!filename) {
      return res.status(400).json({ error: 'Filename is required' });
    }
    
    const filePath = path.join(uploadsDir, filename);
    
    // Check if file exists
    if (!fs.existsSync(filePath)) {
      return res.status(404).json({ error: 'File not found' });
    }
    
    // Only allow deletion of images in the products folder
    if (!filePath.includes('/uploads/products/')) {
      return res.status(403).json({ 
        error: 'Cannot delete images outside the products folder',
        message: 'This image is in a protected folder and cannot be deleted through this endpoint'
      });
    }
    
    // Delete the file
    fs.unlinkSync(filePath);
    
    // Clear any scheduled deletion for this file
    if (imageUploadMap.has(filename)) {
      clearTimeout(imageUploadMap.get(filename).timeoutId);
      imageUploadMap.delete(filename);
    }
    
    // Return success response
    res.status(200).json({
      success: true,
      message: 'Image deleted successfully'
    });
    
  } catch (error) {
    console.error('Error deleting image:', error);
    res.status(500).json({ error: error.message || 'Failed to delete image' });
  }
});

// Clear all taxonomy caches
router.post('/clear-taxonomy-cache', (req, res) => {
  try {
    // Clear all entries from the query cache
    const cacheSize = connectionCache.queryCache.size;
    connectionCache.queryCache.clear();

    console.log(`Cleared ${cacheSize} entries from taxonomy cache`);

    res.json({
      success: true,
      message: `Cache cleared (${cacheSize} entries removed)`,
      clearedEntries: cacheSize
    });
  } catch (error) {
    console.error('Error clearing taxonomy cache:', error);
    res.status(500).json({ error: 'Failed to clear cache' });
  }
});

// Get all options for import fields
router.get('/field-options', async (req, res) => {
    try {
        // Use cached connection
        const { connection } = await getDbConnection();

        const cacheKey = 'field-options';
        const result = await getCachedQuery(cacheKey, 'field-options', async () => {
            // Fetch companies (type 1)
            const [companies] = await connection.query(`
                SELECT cat_id, name 
                FROM product_categories 
                WHERE type = 1 
                ORDER BY name
            `);

            // Fetch artists (type 40)
            const [artists] = await connection.query(`
                SELECT cat_id, name 
                FROM product_categories 
                WHERE type = 40 
                ORDER BY name
            `);

            // Fetch sizes (type 50)
            const [sizes] = await connection.query(`
                SELECT cat_id, name 
                FROM product_categories 
                WHERE type = 50 
                ORDER BY name
            `);

            // Fetch themes with subthemes
            const [themes] = await connection.query(`
                SELECT t.cat_id, t.name AS display_name, t.type, t.name AS sort_theme, 
                       '' AS sort_subtheme, 1 AS level_order 
                FROM product_categories t 
                WHERE t.type = 20 
                UNION ALL 
                SELECT ts.cat_id, CONCAT(t.name,' - ',ts.name) AS display_name, ts.type,
                       t.name AS sort_theme, ts.name AS sort_subtheme, 2 AS level_order 
                FROM product_categories ts 
                JOIN product_categories t ON ts.master_cat_id = t.cat_id 
                WHERE ts.type = 21 AND t.type = 20 
                ORDER BY sort_theme, sort_subtheme
            `);

            // Fetch categories with all levels
            const [categories] = await connection.query(`
                SELECT s.cat_id, s.name AS display_name, s.type, s.name AS sort_section,
                       '' AS sort_category, '' AS sort_subcategory, '' AS sort_subsubcategory,
                       1 AS level_order 
                FROM product_categories s 
                WHERE s.type = 10 
                UNION ALL 
                SELECT c.cat_id, CONCAT(s.name,' - ',c.name) AS display_name, c.type,
                       s.name AS sort_section, c.name AS sort_category, '' AS sort_subcategory,
                       '' AS sort_subsubcategory, 2 AS level_order 
                FROM product_categories c 
                JOIN product_categories s ON c.master_cat_id = s.cat_id 
                WHERE c.type = 11 AND s.type = 10 
                UNION ALL 
                SELECT sc.cat_id, CONCAT(s.name,' - ',c.name,' - ',sc.name) AS display_name,
                       sc.type, s.name AS sort_section, c.name AS sort_category,
                       sc.name AS sort_subcategory, '' AS sort_subsubcategory, 3 AS level_order 
                FROM product_categories sc 
                JOIN product_categories c ON sc.master_cat_id = c.cat_id 
                JOIN product_categories s ON c.master_cat_id = s.cat_id 
                WHERE sc.type = 12 AND c.type = 11 AND s.type = 10 
                UNION ALL 
                SELECT ssc.cat_id, CONCAT(s.name,' - ',c.name,' - ',sc.name,' - ',ssc.name) AS display_name,
                       ssc.type, s.name AS sort_section, c.name AS sort_category,
                       sc.name AS sort_subcategory, ssc.name AS sort_subsubcategory, 4 AS level_order 
                FROM product_categories ssc 
                JOIN product_categories sc ON ssc.master_cat_id = sc.cat_id 
                JOIN product_categories c ON sc.master_cat_id = c.cat_id 
                JOIN product_categories s ON c.master_cat_id = s.cat_id 
                WHERE ssc.type = 13 AND sc.type = 12 AND c.type = 11 AND s.type = 10 
                ORDER BY sort_section, sort_category, sort_subcategory, sort_subsubcategory
            `);

            // Fetch colors
            const [colors] = await connection.query(`
                SELECT color, name, hex_color 
                FROM product_color_list 
                ORDER BY \`order\`
            `);

            // Fetch suppliers
            const [suppliers] = await connection.query(`
                SELECT supplierid as value, companyname as label 
                FROM suppliers 
                WHERE companyname <> '' 
                ORDER BY companyname
            `);

            // Fetch tax categories
            const [taxCategories] = await connection.query(`
                SELECT CAST(tax_code_id AS CHAR) as value, name as label 
                FROM product_tax_codes 
                ORDER BY tax_code_id = 0 DESC, name
            `);

            // Format and return all options
            return {
                companies: companies.map(c => ({ label: c.name, value: c.cat_id.toString() })),
                artists: artists.map(a => ({ label: a.name, value: a.cat_id.toString() })),
                sizes: sizes.map(s => ({ label: s.name, value: s.cat_id.toString() })),
                themes: themes.map(t => ({ 
                    label: t.display_name, 
                    value: t.cat_id.toString(),
                    type: t.type,
                    level: t.level_order
                })),
                categories: categories.map(c => ({ 
                    label: c.display_name, 
                    value: c.cat_id.toString(),
                    type: c.type,
                    level: c.level_order
                })),
                colors: colors.map(c => ({ 
                    label: c.name, 
                    value: c.color,
                    hexColor: c.hex_color 
                })),
                suppliers: suppliers,
                taxCategories: taxCategories,
                shippingRestrictions: [
                    { label: "None", value: "0" },
                    { label: "US Only", value: "1" },
                    { label: "Limited Quantity", value: "2" },
                    { label: "US/CA Only", value: "3" },
                    { label: "No FedEx 2 Day", value: "4" },
                    { label: "North America Only", value: "5" }
                ]
            };
        });

        // Add debugging to verify category types
        console.log(`Returning ${result.categories.length} categories with types: ${Array.from(new Set(result.categories.map(c => c.type))).join(', ')}`);
        
        res.json(result);
    } catch (error) {
        console.error('Error fetching import field options:', error);
        res.status(500).json({ error: 'Failed to fetch import field options' });
    }
});

// Get product lines for a specific company
router.get('/product-lines/:companyId', async (req, res) => {
    try {
        // Use cached connection
        const { connection } = await getDbConnection();

        const companyId = req.params.companyId;
        const cacheKey = `product-lines-${companyId}`;
        
        const lines = await getCachedQuery(cacheKey, 'product-lines', async () => {
            const [queryResult] = await connection.query(`
                SELECT cat_id as value, name as label 
                FROM product_categories 
                WHERE type = 2 
                AND master_cat_id = ? 
                ORDER BY name
            `, [companyId]);
            
            return queryResult.map(l => ({ label: l.label, value: l.value.toString() }));
        });

        res.json(lines);
    } catch (error) {
        console.error('Error fetching product lines:', error);
        res.status(500).json({ error: 'Failed to fetch product lines' });
    }
});

// Get sublines for a specific product line
router.get('/sublines/:lineId', async (req, res) => {
    try {
        // Use cached connection
        const { connection } = await getDbConnection();

        const lineId = req.params.lineId;
        const cacheKey = `sublines-${lineId}`;
        
        const sublines = await getCachedQuery(cacheKey, 'sublines', async () => {
            const [queryResult] = await connection.query(`
                SELECT cat_id as value, name as label 
                FROM product_categories 
                WHERE type = 3 
                AND master_cat_id = ? 
                ORDER BY name
            `, [lineId]);
            
            return queryResult.map(s => ({ label: s.label, value: s.value.toString() }));
        });

        res.json(sublines);
    } catch (error) {
        console.error('Error fetching sublines:', error);
        res.status(500).json({ error: 'Failed to fetch sublines' });
    }
});

// Add a simple endpoint to check file existence and permissions
router.get('/check-file/:filename', (req, res) => {
  const { filename } = req.params;
  
  // Prevent directory traversal
  if (filename.includes('..') || filename.includes('/')) {
    return res.status(400).json({ error: 'Invalid filename' });
  }
  
  // First check in products directory
  let filePath = path.join(uploadsDir, filename);
  let exists = fs.existsSync(filePath);
  
  // If not found in products, check in reusable directory
  if (!exists) {
    filePath = path.join(reusableUploadsDir, filename);
    exists = fs.existsSync(filePath);
  }
  
  try {
    // Check if file exists
    if (!exists) {
      return res.status(404).json({ 
        error: 'File not found', 
        path: filePath,
        exists: false,
        readable: false 
      });
    }
    
    // Check if file is readable
    fs.accessSync(filePath, fs.constants.R_OK);
    
    // Get file stats
    const stats = fs.statSync(filePath);
    
    return res.json({
      filename,
      path: filePath,
      exists: true,
      readable: true,
      isFile: stats.isFile(),
      isDirectory: stats.isDirectory(),
      size: stats.size,
      created: stats.birthtime,
      modified: stats.mtime,
      permissions: stats.mode.toString(8)
    });
  } catch (error) {
    return res.status(500).json({ 
      error: error.message,
      path: filePath,
      exists: fs.existsSync(filePath),
      readable: false
    });
  }
});

// List all files in uploads directory
router.get('/list-uploads', (req, res) => {
  try {
    const { directory = 'products' } = req.query;
    
    // Determine which directory to list
    let targetDir;
    if (directory === 'reusable') {
      targetDir = reusableUploadsDir;
    } else {
      targetDir = uploadsDir; // default to products
    }
    
    if (!fs.existsSync(targetDir)) {
      return res.status(404).json({ error: 'Uploads directory not found', path: targetDir });
    }
    
    const files = fs.readdirSync(targetDir);
    const fileDetails = files.map(file => {
      const filePath = path.join(targetDir, file);
      try {
        const stats = fs.statSync(filePath);
        return {
          filename: file,
          isFile: stats.isFile(),
          isDirectory: stats.isDirectory(),
          size: stats.size,
          created: stats.birthtime,
          modified: stats.mtime,
          permissions: stats.mode.toString(8)
        };
      } catch (error) {
        return { filename: file, error: error.message };
      }
    });
    
    return res.json({
      directory: targetDir,
      type: directory,
      count: files.length,
      files: fileDetails
    });
  } catch (error) {
    return res.status(500).json({ error: error.message });
  }
});

// Search products from production database
router.get('/search-products', async (req, res) => {
  const { q, pid, company, dateRange } = req.query;

  if (!q && !pid) {
    return res.status(400).json({ error: 'Search term or pid is required' });
  }

  try {
    const { connection } = await getDbConnection();

    // Build WHERE clause with additional filters
    let whereClause;
    if (pid) {
      const pids = String(pid).split(',').map(Number).filter(n => !isNaN(n) && n > 0);
      if (pids.length === 0) {
        connection.release();
        return res.status(400).json({ error: 'Invalid pid parameter' });
      }
      if (pids.length === 1) {
        whereClause = `\n      WHERE p.pid = ${connection.escape(pids[0])}`;
      } else {
        whereClause = `\n      WHERE p.pid IN (${pids.map(p => connection.escape(p)).join(',')})`;
      }
    } else {
      whereClause = `
      WHERE (
        p.description LIKE ? OR
        p.itemnumber LIKE ? OR
        p.upc LIKE ? OR
        pc1.name LIKE ? OR
        s.companyname LIKE ?
      )`;
    }
    
    // Add company filter if provided
    if (company) {
      whereClause += ` AND p.company = ${connection.escape(company)}`;
    }
    
    // Add date range filter if provided
    if (dateRange) {
      let dateCondition;
      const now = new Date();
      
      switch(dateRange) {
        case '1week':
          // Last week: date is after (current date - 7 days)
          const weekAgo = new Date(now);
          weekAgo.setDate(now.getDate() - 7);
          dateCondition = `p.datein >= ${connection.escape(weekAgo.toISOString().slice(0, 10))}`;
          break;
        case '1month':
          // Last month: date is after (current date - 30 days)
          const monthAgo = new Date(now);
          monthAgo.setDate(now.getDate() - 30);
          dateCondition = `p.datein >= ${connection.escape(monthAgo.toISOString().slice(0, 10))}`;
          break;
        case '2months':
          // Last 2 months: date is after (current date - 60 days)
          const twoMonthsAgo = new Date(now);
          twoMonthsAgo.setDate(now.getDate() - 60);
          dateCondition = `p.datein >= ${connection.escape(twoMonthsAgo.toISOString().slice(0, 10))}`;
          break;
        case '3months':
          // Last 3 months: date is after (current date - 90 days)
          const threeMonthsAgo = new Date(now);
          threeMonthsAgo.setDate(now.getDate() - 90);
          dateCondition = `p.datein >= ${connection.escape(threeMonthsAgo.toISOString().slice(0, 10))}`;
          break;
        case '6months':
          // Last 6 months: date is after (current date - 180 days)
          const sixMonthsAgo = new Date(now);
          sixMonthsAgo.setDate(now.getDate() - 180);
          dateCondition = `p.datein >= ${connection.escape(sixMonthsAgo.toISOString().slice(0, 10))}`;
          break;
        case '1year':
          // Last year: date is after (current date - 365 days)
          const yearAgo = new Date(now);
          yearAgo.setDate(now.getDate() - 365);
          dateCondition = `p.datein >= ${connection.escape(yearAgo.toISOString().slice(0, 10))}`;
          break;
        default:
          // If an unrecognized value is provided, don't add a date condition
          dateCondition = null;
      }
      
      if (dateCondition) {
        whereClause += ` AND ${dateCondition}`;
      }
    }
    
    const isPidSearch = !!pid;
    // Special case for wildcard search
    const isWildcardSearch = !isPidSearch && q === '*';
    const searchPattern = isWildcardSearch ? '%' : `%${q}%`;
    const exactPattern = isWildcardSearch ? '%' : q;
    
    // Search for products based on various fields
    const query = `
      SELECT 
        p.pid,
        p.description AS title,
        p.notes AS description,
        p.itemnumber AS sku,
        p.upc AS barcode,
        p.harmonized_tariff_code,
        MIN(pcp.price_each) AS price,
        p.sellingprice AS regular_price,
        CASE
          WHEN sid.supplier_id = 92 THEN
            CASE WHEN COALESCE(sid.notions_cost_each, 0) > 0 THEN sid.notions_cost_each ELSE sid.supplier_cost_each END
          ELSE
            CASE WHEN COALESCE(sid.supplier_cost_each, 0) > 0 THEN sid.supplier_cost_each ELSE sid.notions_cost_each END
        END AS cost_price,
        s.companyname AS vendor,
        sid.supplier_itemnumber AS vendor_reference,
        sid.notions_itemnumber AS notions_reference,
        sid.supplier_id AS supplier,
        sid.notions_case_pack AS case_qty,
        pc1.name AS brand,
        p.company AS brand_id,
        pc2.name AS line,
        p.line AS line_id,
        pc3.name AS subline,
        p.subline AS subline_id,
        pc4.name AS artist,
        p.artist AS artist_id,
        COALESCE(CASE
          WHEN sid.supplier_id = 92 THEN sid.notions_qty_per_unit
          ELSE sid.supplier_qty_per_unit
        END, sid.notions_qty_per_unit) AS moq,
        p.weight,
        p.length,
        p.width,
        p.height,
        p.country_of_origin,
        ci.totalsold AS total_sold,
        p.datein AS first_received,
        pls.date_sold AS date_last_sold,
        IF(p.tax_code IS NULL, '', CAST(p.tax_code AS CHAR)) AS tax_code,
        CAST(p.size_cat AS CHAR) AS size_cat,
        CAST(p.shipping_restrictions AS CHAR) AS shipping_restrictions
      FROM products p
      LEFT JOIN product_current_prices pcp ON p.pid = pcp.pid AND pcp.active = 1
      LEFT JOIN supplier_item_data sid ON p.pid = sid.pid
      LEFT JOIN suppliers s ON sid.supplier_id = s.supplierid
      LEFT JOIN product_categories pc1 ON p.company = pc1.cat_id
      LEFT JOIN product_categories pc2 ON p.line = pc2.cat_id
      LEFT JOIN product_categories pc3 ON p.subline = pc3.cat_id
      LEFT JOIN product_categories pc4 ON p.artist = pc4.cat_id
      LEFT JOIN product_last_sold pls ON p.pid = pls.pid
      LEFT JOIN current_inventory ci ON p.pid = ci.pid
      ${whereClause}
      GROUP BY p.pid
      ${isPidSearch ? '' : isWildcardSearch ? 'ORDER BY p.datein DESC' : `
      ORDER BY
        CASE
          WHEN p.description LIKE ? THEN 1
          WHEN p.itemnumber = ? THEN 2
          WHEN p.upc = ? THEN 3
          WHEN pc1.name LIKE ? THEN 4
          WHEN s.companyname LIKE ? THEN 5
          ELSE 6
        END
      `}
      ${isPidSearch ? '' : 'LIMIT 100'}
    `;

    // Prepare query parameters based on search type
    let queryParams;
    if (isPidSearch) {
      queryParams = [];
    } else if (isWildcardSearch) {
      queryParams = [
        searchPattern, // LIKE for description
        searchPattern, // LIKE for itemnumber
        searchPattern, // LIKE for upc
        searchPattern, // LIKE for brand name
        searchPattern  // LIKE for company name
      ];
    } else {
      queryParams = [
        searchPattern, // LIKE for description
        searchPattern, // LIKE for itemnumber
        searchPattern, // LIKE for upc
        searchPattern, // LIKE for brand name
        searchPattern, // LIKE for company name
        // For ORDER BY clause
        searchPattern, // LIKE for description
        exactPattern, // Exact match for itemnumber
        exactPattern, // Exact match for upc
        searchPattern, // LIKE for brand name
        searchPattern  // LIKE for company name
      ];
    }
    
    const [results] = await connection.query(query, queryParams);
    
    // Debug log to check values
    if (results.length > 0) {
      console.log('Product search result sample fields:', {
        pid: results[0].pid,
        tax_code: results[0].tax_code,
        tax_code_type: typeof results[0].tax_code,
        tax_code_value: `Value: '${results[0].tax_code}'`,
        size_cat: results[0].size_cat,
        shipping_restrictions: results[0].shipping_restrictions,
        supplier: results[0].supplier,
        case_qty: results[0].case_qty,
        moq: results[0].moq
      });
    }
    
    res.json(results);
  } catch (error) {
    console.error('Error searching products:', error);
    res.status(500).json({ error: 'Failed to search products' });
  }
});

// Shared SELECT for product queries (matches search-products fields)
const PRODUCT_SELECT = `
  SELECT
    p.pid,
    p.description AS title,
    p.notes AS description,
    p.itemnumber AS sku,
    p.upc AS barcode,
    p.harmonized_tariff_code,
    MIN(pcp.price_each) AS price,
    p.sellingprice AS regular_price,
    CASE
      WHEN sid.supplier_id = 92 THEN
        CASE WHEN COALESCE(sid.notions_cost_each, 0) > 0 THEN sid.notions_cost_each ELSE sid.supplier_cost_each END
      ELSE
        CASE WHEN COALESCE(sid.supplier_cost_each, 0) > 0 THEN sid.supplier_cost_each ELSE sid.notions_cost_each END
    END AS cost_price,
    s.companyname AS vendor,
    sid.supplier_itemnumber AS vendor_reference,
    sid.notions_itemnumber AS notions_reference,
    sid.supplier_id AS supplier,
    sid.notions_case_pack AS case_qty,
    pc1.name AS brand,
    p.company AS brand_id,
    pc2.name AS line,
    p.line AS line_id,
    pc3.name AS subline,
    p.subline AS subline_id,
    pc4.name AS artist,
    p.artist AS artist_id,
    COALESCE(CASE
      WHEN sid.supplier_id = 92 THEN sid.notions_qty_per_unit
      ELSE sid.supplier_qty_per_unit
    END, sid.notions_qty_per_unit) AS moq,
    p.weight,
    p.length,
    p.width,
    p.height,
    p.country_of_origin,
    ci.totalsold AS total_sold,
    p.datein AS first_received,
    pls.date_sold AS date_last_sold,
    IF(p.tax_code IS NULL, '', CAST(p.tax_code AS CHAR)) AS tax_code,
    CAST(p.size_cat AS CHAR) AS size_cat,
    CAST(p.shipping_restrictions AS CHAR) AS shipping_restrictions,
    IF(DATEDIFF(NOW(), p.date_ol) <= 45 AND p.notnew = 0 AND (si_feed.all IS NULL OR si_feed.all != 2), 1, 0) AS is_new,
    IF(si_feed.all = 2, 1, 0) AS is_preorder
  FROM products p
  LEFT JOIN shop_inventory si_feed ON p.pid = si_feed.pid AND si_feed.store = 0
  LEFT JOIN product_current_prices pcp ON p.pid = pcp.pid AND pcp.active = 1
  LEFT JOIN supplier_item_data sid ON p.pid = sid.pid
  LEFT JOIN suppliers s ON sid.supplier_id = s.supplierid
  LEFT JOIN product_categories pc1 ON p.company = pc1.cat_id
  LEFT JOIN product_categories pc2 ON p.line = pc2.cat_id
  LEFT JOIN product_categories pc3 ON p.subline = pc3.cat_id
  LEFT JOIN product_categories pc4 ON p.artist = pc4.cat_id
  LEFT JOIN product_last_sold pls ON p.pid = pls.pid
  LEFT JOIN current_inventory ci ON p.pid = ci.pid`;

// Load products for a specific line (company + line + optional subline)
router.get('/line-products', async (req, res) => {
  const { company, line, subline } = req.query;
  if (!company || !line) {
    return res.status(400).json({ error: 'company and line are required' });
  }
  try {
    const { connection } = await getDbConnection();
    let where = 'WHERE p.company = ? AND p.line = ?';
    const params = [Number(company), Number(line)];
    if (subline) {
      where += ' AND p.subline = ?';
      params.push(Number(subline));
    }
    const query = `${PRODUCT_SELECT} ${where} GROUP BY p.pid ORDER BY IF(p.date_ol != '0000-00-00 00:00:00', p.date_ol, p.date_created) DESC, p.description`;
    const [results] = await connection.query(query, params);
    res.json(results);
  } catch (error) {
    console.error('Error loading line products:', error);
    res.status(500).json({ error: 'Failed to load line products' });
  }
});

// Load new products (last 45 days by release date, excluding preorders)
router.get('/new-products', async (req, res) => {
  try {
    const { connection } = await getDbConnection();
    const query = `${PRODUCT_SELECT}
      LEFT JOIN shop_inventory si2 ON p.pid = si2.pid AND si2.store = 0
      WHERE DATEDIFF(NOW(), p.date_ol) <= 45
        AND p.notnew = 0
        AND (si2.all IS NULL OR si2.all != 2)
      GROUP BY p.pid
      ORDER BY IF(p.date_ol != '0000-00-00', p.date_ol, p.date_created) DESC`;
    const [results] = await connection.query(query);
    res.json(results);
  } catch (error) {
    console.error('Error loading new products:', error);
    res.status(500).json({ error: 'Failed to load new products' });
  }
});

// Load preorder products
router.get('/preorder-products', async (req, res) => {
  try {
    const { connection } = await getDbConnection();
    const query = `${PRODUCT_SELECT}
      LEFT JOIN shop_inventory si2 ON p.pid = si2.pid AND si2.store = 0
      WHERE si2.all = 2
      GROUP BY p.pid
      ORDER BY IF(p.date_ol != '0000-00-00', p.date_ol, p.date_created) DESC`;
    const [results] = await connection.query(query);
    res.json(results);
  } catch (error) {
    console.error('Error loading preorder products:', error);
    res.status(500).json({ error: 'Failed to load preorder products' });
  }
});

// Load hidden recently-created products from local PG, enriched from MySQL
router.get('/hidden-new-products', async (req, res) => {
  try {
    const pool = req.app.locals.pool;
    const pgResult = await pool.query(
      `SELECT pid FROM products WHERE visible = false AND created_at > NOW() - INTERVAL '90 days' ORDER BY created_at DESC LIMIT 500`
    );
    const pids = pgResult.rows.map(r => r.pid);
    if (pids.length === 0) return res.json([]);

    const { connection } = await getDbConnection();
    const placeholders = pids.map(() => '?').join(',');
    const query = `${PRODUCT_SELECT} WHERE p.pid IN (${placeholders}) GROUP BY p.pid ORDER BY FIELD(p.pid, ${placeholders})`;
    const [results] = await connection.query(query, [...pids, ...pids]);
    res.json(results);
  } catch (error) {
    console.error('Error loading hidden new products:', error);
    res.status(500).json({ error: 'Failed to load hidden new products' });
  }
});

// Load landing page extras (featured lines) for new/preorder pages
router.get('/landing-extras', async (req, res) => {
  const { catId, sid } = req.query;
  if (!catId) {
    return res.status(400).json({ error: 'catId is required' });
  }
  try {
    const { connection } = await getDbConnection();
    const [results] = await connection.query(
      `SELECT extra_id, image, extra_cat_id, path, name, top_text, is_new
       FROM product_category_landing_extras
       WHERE cat_id = ? AND sid = ? AND section_cat_id = 0 AND hidden = 0
       ORDER BY \`order\` DESC, name ASC`,
      [Number(catId), Number(sid) || 0]
    );
    res.json(results);
  } catch (error) {
    console.error('Error loading landing extras:', error);
    res.status(500).json({ error: 'Failed to load landing extras' });
  }
});

// Load products by shop path (resolves category names to IDs)
router.get('/path-products', async (req, res) => {
  res.set('Cache-Control', 'no-store');
  const { path: shopPath } = req.query;
  if (!shopPath) {
    return res.status(400).json({ error: 'path is required' });
  }

  try {
    const { connection } = await getDbConnection();

    // Strip common URL prefixes (full URLs, /shop/, leading slash)
    const cleanPath = String(shopPath)
      .replace(/^https?:\/\/[^/]+/, '')
      .replace(/^\/shop\//, '/')
      .replace(/^\//, '');
    const parts = cleanPath.split('/');
    const filters = {};
    for (let i = 0; i < parts.length - 1; i += 2) {
      filters[parts[i]] = decodeURIComponent(parts[i + 1]).replace(/_/g, ' ');
    }

    if (Object.keys(filters).length === 0) {
      return res.status(400).json({ error: 'No valid filters found in path' });
    }

    // Resolve category names to IDs (order matters: company -> line -> subline)
    const typeMap = { company: 1, line: 2, subline: 3, section: 10, cat: 11, subcat: 12, subsubcat: 13 };
    const resolvedIds = {};
    const resolveOrder = ['company', 'line', 'subline', 'section', 'cat', 'subcat', 'subsubcat'];

    for (const key of resolveOrder) {
      const value = filters[key];
      if (!value) continue;
      const type = typeMap[key];
      if (!type) continue;
      const types = key === 'cat' ? [11, 20] : key === 'subcat' ? [12, 21] : [type];

      // For line/subline, filter by parent (master_cat_id) to disambiguate
      let parentFilter = '';
      const qParams = [value];
      if (key === 'line' && resolvedIds.company != null) {
        parentFilter = ' AND master_cat_id = ?';
        qParams.push(resolvedIds.company);
      } else if (key === 'subline' && resolvedIds.line != null) {
        parentFilter = ' AND master_cat_id = ?';
        qParams.push(resolvedIds.line);
      }

      const [rows] = await connection.query(
        `SELECT cat_id FROM product_categories WHERE LOWER(name) = LOWER(?) AND type IN (${types.join(',')})${parentFilter} LIMIT 1`,
        qParams
      );
      if (rows.length > 0) {
        resolvedIds[key] = rows[0].cat_id;
      } else {
        return res.json([]);
      }
    }

    // Build WHERE using resolved IDs
    const whereParts = [];
    const params = [];
    const directFields = { company: 'p.company', line: 'p.line', subline: 'p.subline' };

    for (const [key, catId] of Object.entries(resolvedIds)) {
      if (directFields[key]) {
        whereParts.push(`${directFields[key]} = ?`);
        params.push(catId);
      } else {
        whereParts.push('EXISTS (SELECT 1 FROM product_category_index pci2 WHERE pci2.pid = p.pid AND pci2.cat_id = ?)');
        params.push(catId);
      }
    }

    if (whereParts.length === 0) {
      return res.status(400).json({ error: 'No valid filters found in path' });
    }

    const query = `${PRODUCT_SELECT} WHERE ${whereParts.join(' AND ')} GROUP BY p.pid ORDER BY IF(p.date_ol != '0000-00-00 00:00:00', p.date_ol, p.date_created) DESC, p.description`;
    const [results] = await connection.query(query, params);
    res.json(results);
  } catch (error) {
    console.error('Error loading path products:', error);
    res.status(500).json({ error: 'Failed to load products by path' });
  }
});

// Get product images for a given PID from production DB
router.get('/product-images/:pid', async (req, res) => {
  const pid = parseInt(req.params.pid, 10);
  if (!pid || pid <= 0) {
    return res.status(400).json({ error: 'Valid PID is required' });
  }

  try {
    const { connection } = await getDbConnection();

    const [rows] = await connection.query(
      'SELECT iid, type, width, height, `order`, hidden FROM product_images WHERE pid = ? ORDER BY `order` DESC, type',
      [pid]
    );

    // Group by iid and build image URLs using the same logic as the PHP codebase
    const typeMap = { 1: 'o', 2: 'l', 3: 't', 4: '100x100', 5: '175x175', 6: '300x300', 7: '600x600', 8: '500x500', 9: '150x150' };
    const padded = String(pid).padStart(10, '0');
    const pathPrefix = `${padded.substring(0, 4)}/${padded.substring(4, 7)}/`;

    const imagesByIid = {};
    for (const row of rows) {
      const typeName = typeMap[row.type];
      if (!typeName) continue;
      if (!imagesByIid[row.iid]) {
        imagesByIid[row.iid] = { iid: row.iid, order: row.order, hidden: !!row.hidden, sizes: {} };
      }
      imagesByIid[row.iid].sizes[typeName] = {
        width: row.width,
        height: row.height,
        url: `https://sbing.com/i/products/${pathPrefix}${pid}-${typeName}-${row.iid}.jpg`,
      };
    }

    const images = Object.values(imagesByIid).sort((a, b) => b.order - a.order);
    res.json(images);
  } catch (error) {
    console.error('Error fetching product images:', error);
    res.status(500).json({ error: 'Failed to fetch product images' });
  }
});

// Batch fetch product images for multiple PIDs
router.get('/product-images-batch', async (req, res) => {
  const { pids } = req.query;
  if (!pids) {
    return res.status(400).json({ error: 'pids query parameter is required' });
  }
  const pidList = String(pids).split(',').map(Number).filter(n => n > 0);
  if (pidList.length === 0) {
    return res.json({});
  }

  try {
    const { connection } = await getDbConnection();
    const placeholders = pidList.map(() => '?').join(',');
    const [rows] = await connection.query(
      `SELECT pid, iid, type, width, height, \`order\`, hidden FROM product_images WHERE pid IN (${placeholders}) ORDER BY \`order\` DESC, type`,
      pidList
    );

    const typeMap = { 1: 'o', 2: 'l', 3: 't', 4: '100x100', 5: '175x175', 6: '300x300', 7: '600x600', 8: '500x500', 9: '150x150' };
    const result = {};
    for (const pid of pidList) {
      result[pid] = {};
    }

    for (const row of rows) {
      const typeName = typeMap[row.type];
      if (!typeName) continue;
      const pid = row.pid;
      if (!result[pid]) result[pid] = {};
      if (!result[pid][row.iid]) {
        result[pid][row.iid] = { iid: row.iid, order: row.order, hidden: !!row.hidden, sizes: {} };
      }
      const padded = String(pid).padStart(10, '0');
      const pathPrefix = `${padded.substring(0, 4)}/${padded.substring(4, 7)}/`;
      result[pid][row.iid].sizes[typeName] = {
        width: row.width,
        height: row.height,
        url: `https://sbing.com/i/products/${pathPrefix}${pid}-${typeName}-${row.iid}.jpg`,
      };
    }

    // Convert each pid's iid map to sorted array
    const output = {};
    for (const pid of pidList) {
      output[pid] = Object.values(result[pid] || {}).sort((a, b) => b.order - a.order);
    }
    res.json(output);
  } catch (error) {
    console.error('Error fetching batch product images:', error);
    res.status(500).json({ error: 'Failed to fetch product images' });
  }
});

const UPC_SUPPLIER_PREFIX_LEADING_DIGIT = '4';
const UPC_MAX_SEQUENCE = 99999;
const UPC_RESERVATION_TTL = 5 * 60 * 1000; // 5 minutes

function buildSupplierPrefix(supplierId) {
  const numericId = Number.parseInt(String(supplierId), 10);
  if (Number.isNaN(numericId) || numericId < 0) {
    return null;
  }

  const padded = String(numericId).padStart(5, '0');
  const prefix = `${UPC_SUPPLIER_PREFIX_LEADING_DIGIT}${padded}`;
  return prefix.length === 6 ? prefix : null;
}

function calculateUpcCheckDigit(upcWithoutCheckDigit) {
  if (!/^\d{11}$/.test(upcWithoutCheckDigit)) {
    throw new Error('UPC body must be 11 numeric characters');
  }

  let sum = 0;
  for (let i = 0; i < upcWithoutCheckDigit.length; i += 1) {
    const digit = Number.parseInt(upcWithoutCheckDigit[i], 10);
    sum += (i % 2 === 0) ? digit * 3 : digit;
  }

  const mod = sum % 10;
  return mod === 0 ? 0 : 10 - mod;
}

const upcReservationCache = new Map();
const upcGenerationLocks = new Map();

function getReservedSequence(prefix) {
  const entry = upcReservationCache.get(prefix);
  if (!entry) {
    return 0;
  }

  if (Date.now() > entry.expiresAt) {
    upcReservationCache.delete(prefix);
    return 0;
  }

  return entry.lastSequence;
}

function setReservedSequence(prefix, sequence) {
  upcReservationCache.set(prefix, {
    lastSequence: sequence,
    expiresAt: Date.now() + UPC_RESERVATION_TTL
  });
}

async function runWithSupplierLock(prefix, task) {
  const previous = upcGenerationLocks.get(prefix) || Promise.resolve();
  const chained = previous.catch(() => {}).then(() => task());
  upcGenerationLocks.set(prefix, chained);

  try {
    return await chained;
  } finally {
    if (upcGenerationLocks.get(prefix) === chained) {
      upcGenerationLocks.delete(prefix);
    }
  }
}

router.post('/generate-upc', async (req, res) => {
  const { supplierId, increment } = req.body || {};

  if (supplierId === undefined || supplierId === null || String(supplierId).trim() === '') {
    return res.status(400).json({ error: 'Supplier ID is required to generate a UPC' });
  }

  const supplierPrefix = buildSupplierPrefix(supplierId);
  if (!supplierPrefix) {
    return res.status(400).json({ error: 'Supplier ID must be a non-negative number with at most 5 digits' });
  }

  const step = Number.parseInt(increment, 10);
  const sequenceIncrement = Number.isNaN(step) || step < 1 ? 1 : step;

  try {
    const result = await runWithSupplierLock(supplierPrefix, async () => {
      const { connection } = await getDbConnection();

      const [rows] = await connection.query(
        `SELECT CAST(SUBSTRING(upc,7,5) AS UNSIGNED) AS num
           FROM products
          WHERE LEFT(upc, 6) = ? AND LENGTH(upc) = 12
       ORDER BY num DESC
          LIMIT 1`,
        [supplierPrefix]
      );

      const lastSequenceFromDb = rows && rows.length > 0 && rows[0].num !== null
        ? Number.parseInt(rows[0].num, 10) || 0
        : 0;

      const cachedSequence = getReservedSequence(supplierPrefix);
      const baselineSequence = Math.max(lastSequenceFromDb, cachedSequence);

      let nextSequence = baselineSequence + sequenceIncrement;
      let candidateUpc = null;
      let attempts = 0;

      while (attempts < 10 && nextSequence <= UPC_MAX_SEQUENCE) {
        const sequencePart = String(nextSequence).padStart(5, '0');
        const upcBody = `${supplierPrefix}${sequencePart}`;
        const checkDigit = calculateUpcCheckDigit(upcBody);
        const fullUpc = `${upcBody}${checkDigit}`;

        const [existing] = await connection.query(
          'SELECT 1 FROM products WHERE upc = ? LIMIT 1',
          [fullUpc]
        );

        if (!existing || existing.length === 0) {
          candidateUpc = { upc: fullUpc, sequence: nextSequence };
          break;
        }

        nextSequence += 1;
        attempts += 1;
      }

      if (!candidateUpc) {
        const reason = nextSequence > UPC_MAX_SEQUENCE
          ? 'UPC range exhausted for this supplier'
          : 'Unable to find an available UPC';
        const error = new Error(reason);
        error.status = 409;
        throw error;
      }

      setReservedSequence(supplierPrefix, candidateUpc.sequence);
      return candidateUpc.upc;
    });

    return res.json({ success: true, upc: result });
  } catch (error) {
    console.error('Error generating UPC:', error);
    const status = error.status && Number.isInteger(error.status) ? error.status : 500;
    const message = status === 500 ? 'Failed to generate UPC' : error.message;
    return res.status(status).json({ error: message, details: status === 500 ? error.message : undefined });
  }
});

// Endpoint to check UPC and generate item number
router.get('/check-upc-and-generate-sku', async (req, res) => {
  const { upc, supplierId } = req.query;
  
  if (!upc || !supplierId) {
    return res.status(400).json({ error: 'UPC and supplier ID are required' });
  }
  
  try {
    const { connection } = await getDbConnection();
    
    // Step 1: Check if the UPC already exists
    const [upcCheck] = await connection.query(
      'SELECT pid, itemnumber FROM products WHERE upc = ? LIMIT 1',
      [upc]
    );
    
    if (upcCheck.length > 0) {
      return res.status(409).json({ 
        error: 'A product with this UPC already exists', 
        existingProductId: upcCheck[0].pid,
        existingItemNumber: upcCheck[0].itemnumber
      });
    }
    
    // Step 2: Generate item number - supplierId-last5DigitsOfUPC minus last digit
    let itemNumber = '';
    const upcStr = String(upc);
    
    // Extract the last 5 digits of the UPC, removing the last digit (checksum)
    // So we get 5 digits from positions: length-6 to length-2
    if (upcStr.length >= 6) {
      const lastFiveMinusOne = upcStr.substring(upcStr.length - 6, upcStr.length - 1);
      itemNumber = `${supplierId}-${lastFiveMinusOne}`;
    } else if (upcStr.length >= 5) {
      // If UPC is shorter, use as many digits as possible
      const digitsToUse = upcStr.substring(0, upcStr.length - 1);
      itemNumber = `${supplierId}-${digitsToUse}`;
    } else {
      // Very short UPC, just use the whole thing
      itemNumber = `${supplierId}-${upcStr}`;
    }
    
    // Step 3: Check if the generated item number exists
    const [itemNumberCheck] = await connection.query(
      'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1',
      [itemNumber]
    );
    
    // Step 4: If the item number exists, modify it to use the last 5 digits of the UPC
    if (itemNumberCheck.length > 0) {
      console.log(`Item number ${itemNumber} already exists, using alternative format`);
      
      if (upcStr.length >= 5) {
        // Use the last 5 digits (including the checksum)
        const lastFive = upcStr.substring(upcStr.length - 5);
        itemNumber = `${supplierId}-${lastFive}`;
        
        // Check again if this new item number also exists
        const [altItemNumberCheck] = await connection.query(
          'SELECT pid FROM products WHERE itemnumber = ? LIMIT 1',
          [itemNumber]
        );
        
        if (altItemNumberCheck.length > 0) {
          // If even the alternative format exists, add a timestamp suffix for uniqueness
          const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp
          itemNumber = `${supplierId}-${timestamp}`;
          console.log(`Alternative item number also exists, using timestamp: ${itemNumber}`);
        }
      } else {
        // For very short UPCs, add a timestamp
        const timestamp = Date.now().toString().substring(8, 13); // Get last 5 digits of timestamp
        itemNumber = `${supplierId}-${timestamp}`;
      }
    }
    
    // Return the generated item number
    res.json({ 
      success: true, 
      itemNumber, 
      upc,
      supplierId
    });
    
  } catch (error) {
    console.error('Error checking UPC and generating item number:', error);
    res.status(500).json({ 
      error: 'Failed to check UPC and generate item number',
      details: error.message
    });
  }
});

// Get product categories for a specific product
router.get('/product-categories/:pid', async (req, res) => {
  try {
    const { pid } = req.params;
    
    if (!pid || isNaN(parseInt(pid))) {
      return res.status(400).json({ error: 'Valid product ID is required' });
    }
    
    // Use the getDbConnection function instead of getPool
    const { connection } = await getDbConnection();
    
    // Query to get categories for a specific product
    const query = `
      SELECT pc.cat_id, pc.name, pc.type, pc.combined_name, pc.master_cat_id
      FROM product_category_index pci
      JOIN product_categories pc ON pci.cat_id = pc.cat_id
      WHERE pci.pid = ?
      ORDER BY pc.type, pc.name
    `;
    
    const [rows] = await connection.query(query, [pid]);
    
    // Add debugging to log category types
    const categoryTypes = rows.map(row => row.type);
    const uniqueTypes = [...new Set(categoryTypes)];
    console.log(`Product ${pid} has ${rows.length} categories with types: ${uniqueTypes.join(', ')}`);
    console.log('Categories:', rows.map(row => ({ id: row.cat_id, name: row.name, type: row.type })));
    
    // Check for parent categories to filter out deals and black friday
    const sectionQuery = `
      SELECT pc.cat_id, pc.name 
      FROM product_categories pc
      WHERE pc.type = 10 AND (LOWER(pc.name) LIKE '%deal%' OR LOWER(pc.name) LIKE '%black friday%')
    `;
    
    const [dealSections] = await connection.query(sectionQuery);
    const dealSectionIds = dealSections.map(section => section.cat_id);
    
    console.log('Filtering out categories from deal sections:', dealSectionIds);
    
    // Filter out categories from deals and black friday sections
    const filteredCategories = rows.filter(category => {
      // Direct check for top-level deal sections
      if (category.type === 10) {
        return !dealSectionIds.some(id => id === category.cat_id);
      }
      
      // For categories (type 11), check if their parent is a deal section
      if (category.type === 11) {
        return !dealSectionIds.some(id => id === category.master_cat_id);
      }
      
      // For subcategories (type 12), get their parent category first
      if (category.type === 12) {
        const parentId = category.master_cat_id;
        // Find the parent category in our rows
        const parentCategory = rows.find(c => c.cat_id === parentId);
        // If parent not found or parent's parent is not a deal section, keep it
        return !parentCategory || !dealSectionIds.some(id => id === parentCategory.master_cat_id);
      }
      
      // For subsubcategories (type 13), check their hierarchy manually
      if (category.type === 13) {
        const parentId = category.master_cat_id;
        // Find the parent subcategory
        const parentSubcategory = rows.find(c => c.cat_id === parentId);
        if (!parentSubcategory) return true;
        
        // Find the grandparent category
        const grandparentId = parentSubcategory.master_cat_id;
        const grandparentCategory = rows.find(c => c.cat_id === grandparentId);
        // If grandparent not found or grandparent's parent is not a deal section, keep it
        return !grandparentCategory || !dealSectionIds.some(id => id === grandparentCategory.master_cat_id);
      }
      
      // Keep all other category types
      return true;
    });
    
    console.log(`Filtered out ${rows.length - filteredCategories.length} deal/black friday categories`);
    
    // Format the response to match the expected format in the frontend
    const categories = filteredCategories.map(category => ({
      value: category.cat_id.toString(),
      label: category.name,
      type: category.type,
      combined_name: category.combined_name
    }));
    
    res.json(categories);
  } catch (error) {
    console.error('Error fetching product categories:', error);
    res.status(500).json({ 
      error: 'Failed to fetch product categories',
      details: error.message 
    });
  }
});

// Build a PIDs-from-query SQL using product_query_filter rows (mirrors productquery.class.php logic)
// NOTE: PRODUCT_SELECT uses aliases: products→p, current_inventory→ci, supplier_item_data→sid
// All filter conditions must use those aliases. Extra JOINs (category index, etc.) are appended.
//
// Constants sourced from registry.class.php / product_category.class.php (ACOT store id confirmed by user):
//   ACOT_STORE=0, SRC_ACOT=10, SRC_PREORDER=11, SRC_NOTIONS=13
//   CAT types: section=10, cat=11, subcat=12, subsubcat=13, theme=20, subtheme=21, digitheme=30
function buildQueryFilterSql(filters) {
  // filterGroups: Map<key, { conditions: string[], isNot: boolean, ororor: boolean }>
  // ororor=true means this group is OR-connected to the previous group (PHP filter_or mechanism)
  const filterGroups = new Map();
  const joinTables = new Map(); // alias -> JOIN clause
  const unsupported = [];

  function addGroup(key, condition, isNot = false, ororor = false) {
    if (!filterGroups.has(key)) filterGroups.set(key, { conditions: [], isNot, ororor });
    filterGroups.get(key).conditions.push(condition);
  }
  function addJoin(alias, clause) {
    if (!joinTables.has(alias)) joinTables.set(alias, clause);
  }
  // INNER JOIN on product_category_index (product must have this category to appear)
  function ciJoin(alias) {
    addJoin(alias, `JOIN product_category_index AS ${alias} ON (p.pid=${alias}.pid)`);
  }

  // Translate operator string to SQL operator
  function toSqlOp(op) {
    const map = {
      equals: '=', notequals: '<>', greater: '>', greater_equals: '>=',
      less: '<', less_equals: '<=', between: ' BETWEEN ',
      contains: ' LIKE ', notcontains: ' NOT LIKE ', begins: ' LIKE ',
      true: '<>0', true1: '=1', false: '=0', isnull: ' IS NULL',
    };
    return map[op] || '=';
  }

  // Proper MySQL string escaping (mirrors mysql_real_escape_string order)
  function strVal(v) {
    return String(v)
      .replace(/\\/g, '\\\\')
      .replace(/\0/g, '\\0')
      .replace(/\n/g, '\\n')
      .replace(/\r/g, '\\r')
      .replace(/'/g, "\\'")
      .replace(/\x1a/g, '\\Z');
  }

  // URL-decode then escape — mirrors PHP: safefor_query(urldecode($row['filter_text1']))
  function decode(v) {
    if (!v) return '';
    try { return decodeURIComponent(String(v)); } catch { return String(v); }
  }

  // Numeric field with BETWEEN support (appends AND t2 when operator is BETWEEN)
  function numFilt(field, sqlOp, t1, t2) {
    if (sqlOp.trim() === 'BETWEEN' && t2) return `${field} BETWEEN ${parseFloat(t1) || 0} AND ${parseFloat(t2) || 0}`;
    return `${field}${sqlOp}${parseFloat(t1) || 0}`;
  }

  // Date/string field with BETWEEN support
  function dateFilt(field, sqlOp, t1, t2) {
    if (sqlOp.trim() === 'BETWEEN' && t2) return `${field} BETWEEN '${strVal(t1)}' AND '${strVal(t2)}'`;
    return `${field}${sqlOp}'${strVal(t1)}'`;
  }

  // Store/source constants (registry.class.php; ACOT store id confirmed by user)
  const ACOT_STORE = 0;
  const SRC_ACOT = 10;
  const SRC_PREORDER = 11;
  const SRC_NOTIONS = 13;

  // Category type constants (product_category.class.php)
  const CAT_THEMES = '20,21';
  const CAT_CATEGORIES_NO_SECTION = '11,12,13';

  for (const row of filters) {
    const sqlOp = toSqlOp(row.filter_operator);
    const isNot = sqlOp === '<>' || row.filter_operator === 'notequals' || row.filter_operator === 'notcontains';
    const t1 = decode(row.filter_text1);
    const t2 = decode(row.filter_text2);
    const filterOr = Boolean(row.filter_or);

    switch (row.filter_type) {

      // ── products table (aliased as p) ──────────────────────────────────────
      case 'company':
        if (t1 && t2) {
          const filt = `(p.company=${parseFloat(t1)||0} AND p.line=${parseFloat(t2)||0})`;
          addGroup('company' + sqlOp, isNot ? `NOT ${filt}` : filt, isNot);
        } else {
          addGroup('company' + sqlOp, `p.company${sqlOp}${parseFloat(t1)||0}`, isNot);
        }
        break;
      case 'line':        addGroup('line' + sqlOp,        numFilt('p.line', sqlOp, t1, t2), isNot); break;
      case 'subline':     addGroup('subline' + sqlOp,     numFilt('p.subline', sqlOp, t1, t2), isNot); break;
      case 'no_company':  addGroup('no_company', 'p.company=0'); break;
      case 'no_line':     addGroup('no_line', 'p.line=0'); break;
      case 'no_subline':  addGroup('no_subline', 'p.subline=0'); break;
      case 'artist':      addGroup('artist' + sqlOp,      numFilt('p.artist', sqlOp, t1, t2), isNot); break;
      case 'size_cat':    addGroup('size_cat' + sqlOp,    numFilt('p.size_cat', sqlOp, t1, t2), isNot); break;
      case 'dimension':   addGroup('dimension' + sqlOp,   numFilt('p.dimension', sqlOp, t1, t2), isNot); break;
      case 'yarn_weight': addGroup('yarn_weight' + sqlOp, numFilt('p.yarn_weight', sqlOp, t1, t2), isNot); break;
      case 'material':    addGroup('material' + sqlOp,    numFilt('p.material', sqlOp, t1, t2), isNot); break;
      case 'weight':      addGroup('weight' + sqlOp,      numFilt('p.weight', sqlOp, t1, t2)); break;
      case 'weight_price_ratio': addGroup('weight' + sqlOp, numFilt('p.weight/p.price_for_sort', sqlOp, t1, t2)); break;
      case 'price_weight_ratio': addGroup('weight' + sqlOp, numFilt('p.price_for_sort/p.weight', sqlOp, t1, t2)); break;
      case 'length':      addGroup('length' + sqlOp,      numFilt('p.length', sqlOp, t1, t2)); break;
      case 'width':       addGroup('width' + sqlOp,       numFilt('p.width', sqlOp, t1, t2)); break;
      case 'height':      addGroup('height' + sqlOp,      numFilt('p.height', sqlOp, t1, t2)); break;
      case 'no_dim':      addGroup('no_dim', 'p.length=0 AND p.width=0 AND p.height=0'); break;
      case 'hide':        addGroup('hide',         `p.hide${sqlOp}`); break;
      case 'hide_in_shop':addGroup('hide_in_shop', `p.hide_in_shop${sqlOp}`); break;
      case 'discontinued':addGroup('discontinued',  `p.discontinued${sqlOp}`); break;
      case 'force_flag':  addGroup('force_flag',   `p.force_flag${sqlOp}`); break;
      case 'exclusive':   addGroup('exclusive',    `p.exclusive${sqlOp}`); break;
      case 'lock_quantity': addGroup('lock_quantity', `p.lock_qty${sqlOp}`); break;
      case 'show_notify': addGroup('show_notify',  `p.show_notify${sqlOp}`); break;
      case 'downloadable':addGroup('downloadable', `p.downloadable${sqlOp}`); break;
      case 'usa_only':    addGroup('usa_only',     `p.usa_only${sqlOp}`); break;
      case 'not_clearance': addGroup('not_clearance', `p.not_clearance${sqlOp}`); break;
      case 'stat_stop':   addGroup('stat_stop',    `p.stat_stop${sqlOp}`); break;
      case 'notnew':      addGroup('notnew',       `p.notnew${sqlOp}`); break;
      case 'not_backinstock': addGroup('not_backinstock', `p.not_backinstock${sqlOp}`); break;
      case 'reorder':     addGroup('reorder',      `p.reorder${sqlOp}${parseFloat(t1)||0}`); break;
      case 'score':            addGroup('score' + sqlOp,            numFilt('p.score', sqlOp, t1, t2)); break;
      case 'sold_view_score':  addGroup('sold_view_score' + sqlOp,  numFilt('p.sold_view_score', sqlOp, t1, t2)); break;
      case 'visibility_score': addGroup('visibility_score' + sqlOp, numFilt('p.visibility_score', sqlOp, t1, t2)); break;
      case 'health_score':     addGroup('health_score' + sqlOp,     `p.health_score${sqlOp}'${strVal(t1)}'`); break;
      case 'tax_code':    addGroup('tax_code', `p.tax_code${sqlOp}${parseFloat(t1)||0}`); break;
      case 'investor':    addGroup('investor' + sqlOp, `p.investorid${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'price':
      case 'default_price':
        addGroup('default_price' + sqlOp, numFilt('p.sellingprice', sqlOp, t1, t2), isNot); break;
      case 'price_for_sort':
        addGroup('price_for_sort' + sqlOp, numFilt('p.price_for_sort', sqlOp, t1, t2)); break;
      case 'salepercent_for_sort':
      case 'salepercent_for_sort__clearance': {
        // PHP divides by 100 if value > 1 (percentages stored as decimals)
        const v1 = (parseFloat(t1)||0) > 1 ? (parseFloat(t1)/100) : (parseFloat(t1)||0);
        const v2 = t2 ? ((parseFloat(t2)||0) > 1 ? (parseFloat(t2)/100) : (parseFloat(t2)||0)) : null;
        const filt = (sqlOp.trim() === 'BETWEEN' && v2 != null)
          ? `p.salepercent_for_sort BETWEEN ${v1} AND ${v2}`
          : `p.salepercent_for_sort${sqlOp}${v1}`;
        addGroup(row.filter_type + sqlOp, filt); break;
      }
      case 'is_clearance':
        addGroup('is_clearance' + sqlOp, `(p.clearance_date != '0000-00-00 00:00:00')${sqlOp}`); break;
      case 'msrp':             addGroup('msrp' + sqlOp,      numFilt('p.msrp', sqlOp, t1, t2)); break;
      case 'default_less_msrp':addGroup('default_less_msrp', '(p.sellingprice < p.msrp)'); break;
      case 'default_more_msrp':addGroup('default_more_msrp', '(p.sellingprice > p.msrp)'); break;
      case 'minimum_advertised_price':
        addGroup('map', numFilt('p.minimum_advertised_price', sqlOp, t1, t2)); break;
      case 'minimum_advertised_price_error':
        addGroup('map_error', row.filter_operator !== 'false'
          ? 'p.minimum_advertised_price > p.sellingprice'
          : 'p.minimum_advertised_price <= p.sellingprice');
        break;
      case 'wholesale_discount': {
        const v1 = (parseFloat(t1)||0) > 1 ? (parseFloat(t1)/100) : (parseFloat(t1)||0);
        const v2 = t2 ? ((parseFloat(t2)||0) > 1 ? (parseFloat(t2)/100) : (parseFloat(t2)||0)) : null;
        const filt = (sqlOp.trim() === 'BETWEEN' && v2 != null)
          ? `p.wholesale_discount BETWEEN ${v1} AND ${v2}`
          : `p.wholesale_discount${sqlOp}${v1}`;
        addGroup('wholesale_discount' + sqlOp, filt); break;
      }
      case 'wholesale_unit_qty': addGroup('wholesale_unit_qty' + sqlOp, numFilt('p.wholesale_unit_qty', sqlOp, t1, t2)); break;
      case 'points_multiplier':  addGroup('points_multiplier' + sqlOp,  numFilt('p.points_multiplier', sqlOp, t1, t2)); break;
      case 'points_bonus':       addGroup('points_bonus' + sqlOp,       numFilt('p.points_bonus', sqlOp, t1, t2)); break;
      case 'points_extra':       addGroup('points_extra', '(p.points_multiplier>.5 OR p.points_bonus>0)'); break;
      case 'auto_pricing_allowed':    addGroup('auto_pricing', 'p.price_lock=0'); break;
      case 'auto_pricing_disallowed': addGroup('auto_pricing', 'p.price_lock=1'); break;
      case 'handling_fee':       addGroup('handling_fee' + sqlOp,       numFilt('p.handling_fee', sqlOp, t1, t2)); break;
      case 'ship_tier':          addGroup('ship_tier' + sqlOp,          numFilt('p.shipping_tier', sqlOp, t1, t2)); break;
      case 'shipping_restrictions': addGroup('ship_tier' + sqlOp,       `p.shipping_restrictions${sqlOp}${parseFloat(t1)||0}`); break;
      case 'min_qty_wanted':     addGroup('min_qty_wanted', numFilt('p.min_qty_wanted', sqlOp, t1, t2)); break;
      case 'qty_bundled':        addGroup('min_qty_wanted', numFilt('p.qty_bundled', sqlOp, t1, t2)); break;
      case 'no_40_percent_promo': addGroup('no_40_percent_promo', `p.no_40_percent_promo${sqlOp}`); break;
      case 'exclude_google_feed': addGroup('exclude_google_feed', `p.exclude_google_feed${sqlOp}`); break;
      case 'store': {
        const bit = Math.pow(2, parseInt(t1) || 0);
        addGroup('store_' + (parseInt(t1)||0), `p.store & ${bit}${sqlOp}${bit}`); break;
      }
      case 'no_store': addGroup('store_' + (parseInt(t1)||0), 'p.store=0'); break;
      case 'location': {
        let filt = `p.aisle${sqlOp}'${strVal(t1)}'`;
        if (t2) filt += ` AND p.rack${sqlOp}'${strVal(t2)}'`;
        addGroup('location' + sqlOp, filt); break;
      }
      case 'name':
        addGroup('name' + sqlOp,
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.description${sqlOp}'%${strVal(t1)}%'`
            : `p.description${sqlOp}'${strVal(t1)}'`, isNot);
        break;
      case 'short_description':
        addGroup('short_description' + sqlOp,
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.description_short${sqlOp}'%${strVal(t1)}%'`
            : `p.description_short${sqlOp}'${strVal(t1)}'`, isNot);
        break;
      case 'description':
        addGroup('description' + sqlOp,
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.notes${sqlOp}'%${strVal(t1)}%'`
            : `p.notes${sqlOp}'${strVal(t1)}'`, isNot);
        break;
      case 'description_char_count':
        addGroup('description_char_count' + sqlOp, `CHAR_LENGTH(p.notes)${sqlOp}${parseFloat(t1)||0}`); break;
      case 'description2':
        addGroup('description2' + sqlOp,
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.notes2${sqlOp}'%${strVal(t1)}%'`
            : `p.notes2${sqlOp}'${strVal(t1)}'`, isNot);
        break;
      case 'keyword':
        addGroup('keyword' + sqlOp,
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.keyword1${sqlOp}'%${strVal(t1)}%'`
            : `p.keyword1${sqlOp}'${strVal(t1)}'`, isNot);
        break;
      case 'notes':
        addGroup('notes' + sqlOp,
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.priv_notes${sqlOp}'%${strVal(t1)}%'`
            : `p.priv_notes${sqlOp}'${strVal(t1)}'`, isNot);
        break;
      case 'price_notes':
        addGroup('price_notes' + sqlOp,
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.price_notes${sqlOp}'%${strVal(t1)}%'`
            : `p.price_notes${sqlOp}'${strVal(t1)}'`);
        break;
      case 'itemnumber':
        addGroup('itemnumber_' + sqlOp, `p.itemnumber${sqlOp}'${strVal(t1)}'`, isNot); break;
      case 'pid':
      case 'pid_auto': {
        // filter_or=true means OR-connect this group to the previous one (PHP OROROR mechanism)
        const key = 'pid' + sqlOp + (filterOr ? 'OROROR' : '');
        addGroup(key, `p.pid${sqlOp}${parseInt(t1) || 0}`, isNot, filterOr);
        break;
      }
      case 'upc':
        addGroup('upc' + sqlOp, `p.upc${sqlOp}'${strVal(t1)}'`, isNot); break;
      case 'size':
        addGroup('size' + sqlOp,
          row.filter_operator === 'begins' ? `p.size LIKE '${strVal(t1)}%'` : `p.size${sqlOp}'${strVal(t1)}'`, isNot);
        break;
      case 'country_of_origin':
        addGroup('country_of_origin',
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `p.country_of_origin${sqlOp}'%${strVal(t1)}%'`
            : `p.country_of_origin${sqlOp}'${strVal(t1)}'`);
        break;
      case 'date_in':       addGroup('date_in' + sqlOp,      dateFilt('DATE(p.datein)', sqlOp, t1, t2)); break;
      case 'date_in_days':
      case 'age':           addGroup('date_in' + sqlOp,      numFilt('DATEDIFF(NOW(),p.datein)', sqlOp, t1, t2)); break;
      case 'date_created':  addGroup('date_created' + sqlOp, dateFilt('p.date_created', sqlOp, t1, t2)); break;
      case 'date_created_days': addGroup('date_created' + sqlOp, numFilt('DATEDIFF(NOW(),p.date_created)', sqlOp, t1, t2)); break;
      case 'date_modified': addGroup('date_modified' + sqlOp, dateFilt('p.stamp', sqlOp, t1, t2)); break;
      case 'date_modified_days': addGroup('date_modified' + sqlOp, numFilt('DATEDIFF(NOW(),p.stamp)', sqlOp, t1, t2)); break;
      case 'date_refill':   addGroup('date_refill' + sqlOp,  dateFilt('p.date_refill', sqlOp, t1, t2)); break;
      case 'date_refill_days': addGroup('date_refill' + sqlOp, numFilt('DATEDIFF(NOW(),p.date_refill)', sqlOp, t1, t2)); break;
      case 'new':
        addGroup('new', `DATEDIFF(NOW(),p.date_ol) <= ${parseInt(t1) || 45}`);
        addGroup('notnew', 'p.notnew=0');
        break;
      case 'new_in':
        addGroup('new2', `p.datein BETWEEN NOW()-INTERVAL ${parseInt(t1) || 30} DAY AND NOW()`);
        addGroup('notnew', 'p.notnew=0');
        break;
      case 'backinstock':
        addGroup('backinstock_1', `p.date_refill BETWEEN NOW()-INTERVAL ${parseInt(t1)||30} DAY AND NOW()`);
        addGroup('backinstock_2', 'p.date_refill > p.datein');
        addGroup('backinstock_3', 'NOT (p.datein BETWEEN NOW()-INTERVAL 30 DAY AND NOW())');
        break;
      case 'arrivals':
        addGroup('arrivals', `(p.date_ol BETWEEN NOW()-INTERVAL ${parseInt(t1)||30} DAY AND NOW() AND p.notnew=0)`);
        addGroup('arrivals', `(p.date_refill BETWEEN NOW()-INTERVAL ${parseInt(t1)||30} DAY AND NOW() AND p.date_refill > p.datein)`);
        break;

      // ── current_inventory (aliased as ci, already LEFT JOINed in PRODUCT_SELECT) ──
      case 'count':        addGroup('count' + sqlOp,        numFilt('ci.available',      sqlOp, t1, t2)); break;
      case 'count_onhand': addGroup('count_onhand' + sqlOp, numFilt('(ci.count-ci.pending)', sqlOp, t1, t2)); break;
      case 'count_shelf':  addGroup('count_shelf' + sqlOp,  numFilt('ci.count_shelf',    sqlOp, t1, t2)); break;
      case 'on_order':     addGroup('on_order' + sqlOp,     numFilt('ci.onorder',        sqlOp, t1, t2)); break;
      case 'on_preorder':  addGroup('on_preorder' + sqlOp,  numFilt('ci.onpreorder',     sqlOp, t1, t2)); break;
      case 'infinite':     addGroup('infinite', `ci.infinite${sqlOp}`); break;
      case 'pending':      addGroup('pending', numFilt('ci.pending', sqlOp, t1, t2)); break;
      case 'date_sold':    addGroup('date_sold' + sqlOp,    numFilt('DATEDIFF(NOW(),ci.lastsolddate)', sqlOp, t1, t2)); break;
      case 'average_cost': addGroup('avg_cost' + sqlOp,     numFilt('ci.avg_cost', sqlOp, t1, t2)); break;
      case 'markup':       addGroup('markup' + sqlOp,       numFilt('(p.sellingprice/ci.avg_cost*100-100)', sqlOp, t1, t2)); break;
      case 'total_sold':   addGroup('total_sold' + sqlOp,   numFilt('ci.totalsold', sqlOp, t1, t2)); break;
      case 'inbaskets':    addGroup('inbaskets', numFilt('ci.baskets', sqlOp, t1, t2)); break;

      // ── supplier_item_data (aliased as sid, already LEFT JOINed in PRODUCT_SELECT) ──
      case 'supplier':
        addGroup('supplier' + sqlOp, `sid.supplier_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'supplier_cost_each':
        addGroup('supplier_cost_each' + sqlOp, numFilt('sid.supplier_cost_each', sqlOp, t1, t2)); break;
      case 'notions_cost_each':
        addGroup('notions_cost_each' + sqlOp, numFilt('sid.notions_cost_each', sqlOp, t1, t2)); break;
      case 'supplier_qty_per_unit':
        addGroup('supplier_qty_per_unit' + sqlOp, numFilt('sid.supplier_qty_per_unit', sqlOp, t1, t2)); break;
      case 'notions_qty_per_unit':
        addGroup('notions_qty_per_unit' + sqlOp, numFilt('sid.notions_qty_per_unit', sqlOp, t1, t2)); break;
      case 'case_pack':
        addGroup('case_pack', `sid.notions_case_pack${sqlOp}${parseFloat(t1)||0}`); break;
      case 'notions_discontinued':
        addGroup('notions_discontinued' + sqlOp, `sid.notions_discontinued${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'missing_any_cost':
        addGroup('missing_any_cost', 'sid.supplier_cost_each=0 OR sid.notions_cost_each=0 OR sid.supplier_cost_each IS NULL OR sid.notions_cost_each IS NULL OR (SELECT COUNT(*) FROM product_inventory WHERE product_inventory.pid=p.pid)=0');
        break;
      case 'notions_itemnumber': {
        // Use a separate LEFT JOIN alias so IS NULL check works correctly
        addJoin('sid_l', 'LEFT JOIN supplier_item_data AS sid_l ON (p.pid=sid_l.pid)');
        let filt = `sid_l.notions_itemnumber${sqlOp}'${strVal(t1)}'`;
        if (sqlOp === '=' && t1 === '') filt += ' OR sid_l.notions_itemnumber IS NULL';
        addGroup('notions_itemnumber' + sqlOp, filt, isNot); break;
      }
      case 'supplier_itemnumber': {
        addJoin('sid_l', 'LEFT JOIN supplier_item_data AS sid_l ON (p.pid=sid_l.pid)');
        let filt = `sid_l.supplier_itemnumber${sqlOp}'${strVal(t1)}'`;
        if (sqlOp === '=' && t1 === '') filt += ' OR sid_l.supplier_itemnumber IS NULL';
        addGroup('supplier_itemnumber' + sqlOp, filt, isNot); break;
      }
      case 'supplier_cost_each_grouped':
        addGroup('supplier_cost_each' + sqlOp, numFilt('sid.supplier_cost_each', sqlOp, t1, t2)); break;

      // ── category index: extra INNER JOINs needed ───────────────────────────
      case 'type':
      case 'cat':
        ciJoin('product_ci_cat');
        addGroup('cat' + sqlOp, `product_ci_cat.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'cat2':
        ciJoin('product_ci_cat2');
        addGroup('cat2' + sqlOp, `product_ci_cat2.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'subtype':
      case 'subcat':
        ciJoin('product_ci_subcat');
        addGroup('subcat' + sqlOp, `product_ci_subcat.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'subsubcat':
        ciJoin('product_ci_subsubcat');
        addGroup('subsubcat' + sqlOp, `product_ci_subsubcat.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'section':
        ciJoin('product_ci_section');
        addGroup('section' + sqlOp, `product_ci_section.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'theme':
        ciJoin('product_ci_theme');
        addGroup('themes' + sqlOp, `product_ci_theme.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'subtheme':
        ciJoin('product_ci_subtheme');
        addGroup('subtheme' + sqlOp, `product_ci_subtheme.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'digitheme':
        ciJoin('product_ci_digitheme');
        addGroup('digithemes' + sqlOp, `product_ci_digitheme.cat_id${sqlOp}${parseFloat(t1)||0}`, isNot); break;
      case 'all_categories':
        if (sqlOp === '=') {
          ciJoin('product_ci_allcats');
          addGroup('allcategories=', `product_ci_allcats.cat_id=${parseFloat(t1)||0}`);
        } else {
          const alias = `not_ci_${parseInt(t1) || 0}`;
          addJoin(alias, `LEFT JOIN product_category_index AS ${alias} ON (${alias}.pid=p.pid AND ${alias}.cat_id=${parseFloat(t1)||0})`);
          addGroup('allcategories<>', `${alias}.cat_id IS NULL`);
        }
        break;
      case 'not_section':
        addGroup('not_section', `(SELECT 1 FROM product_category_index WHERE product_category_index.pid=p.pid AND cat_id=${parseInt(t1)||0}) IS NULL`);
        break;
      case 'all_themes':
        if (sqlOp === '=') {
          ciJoin('product_ci_allthemes');
          addGroup('allthemes=', `product_ci_allthemes.cat_id=${parseFloat(t1)||0}`);
        } else {
          addJoin('product_ci_not_allthemes', `LEFT JOIN product_category_index AS product_ci_not_allthemes ON (p.pid=product_ci_not_allthemes.pid AND product_ci_not_allthemes.cat_id=${parseFloat(t1)||0})`);
          addGroup('allthemes<>', 'product_ci_not_allthemes.pid IS NULL');
        }
        break;
      case 'has_any_category':
        addGroup('has_any_category', '(SELECT COUNT(*) FROM product_category_index WHERE product_category_index.pid=p.pid)>0'); break;
      case 'has_themes':
        addGroup('has_themes', `(SELECT COUNT(*) FROM product_category_index JOIN product_categories ON (product_category_index.cat_id=product_categories.cat_id AND product_categories.type IN (${CAT_THEMES})) WHERE product_category_index.pid=p.pid)>0`); break;
      case 'no_themes':
        addGroup('no_themes', `(SELECT COUNT(*) FROM product_category_index JOIN product_categories ON (product_category_index.cat_id=product_categories.cat_id AND product_categories.type IN (${CAT_THEMES})) WHERE product_category_index.pid=p.pid)=0`); break;
      case 'no_categories':
        addGroup('no_categories', `(SELECT COUNT(*) FROM product_category_index JOIN product_categories ON (product_category_index.cat_id=product_categories.cat_id AND product_categories.type IN (${CAT_CATEGORIES_NO_SECTION})) WHERE product_category_index.pid=p.pid)=0`); break;

      // ── color: extra JOIN product_colors ──────────────────────────────────
      case 'color':
        addJoin('product_colors', 'JOIN product_colors ON (p.pid=product_colors.pid)');
        addGroup('color' + sqlOp, `product_colors.color${sqlOp}${parseInt(t1)||0}`, isNot); break;

      // ── product_inventory: extra JOIN (GROUP BY p.pid covers duplicates) ──
      case 'cost':
        addJoin('product_inventory', 'JOIN product_inventory ON (product_inventory.pid=p.pid)');
        addGroup('cost' + sqlOp, numFilt('product_inventory.costeach', sqlOp, t1, t2)); break;
      case 'original_cost':
        addJoin('product_inventory', 'JOIN product_inventory ON (product_inventory.pid=p.pid)');
        addGroup('orig_cost' + sqlOp, numFilt('product_inventory.orig_costeach', sqlOp, t1, t2)); break;
      case 'total_product_value':
        addGroup('total_product_value' + sqlOp,
          `(SELECT SUM(product_inventory.costeach*product_inventory.count) FROM product_inventory WHERE product_inventory.pid=p.pid)${sqlOp}${parseFloat(t1)||0}`);
        break;

      // ── shop_inventory: extra LEFT JOIN ───────────────────────────────────
      // All shop_* filters share alias 'shop_inv' so they add only one JOIN
      case 'buyable':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${ACOT_STORE} AND shop_inv.buyable=1`); break;
      case 'not_buyable':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${ACOT_STORE} AND shop_inv.buyable=0`); break;
      case 'shop_available':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_store', `shop_inv.store=${ACOT_STORE}`);
        addGroup('shop_inv_avail' + sqlOp, numFilt('shop_inv.available', sqlOp, t1, t2)); break;
      case 'shop_available_local': {
        addJoin('shop_inv_local', `LEFT JOIN shop_inventory AS shop_inv_local ON (p.pid=shop_inv_local.pid AND shop_inv_local.store=${ACOT_STORE})`);
        const filt = sqlOp === '<'
          ? `(shop_inv_local.available_local${sqlOp}${parseFloat(t1)||0} OR shop_inv_local.available_local IS NULL)`
          : numFilt('shop_inv_local.available_local', sqlOp, t1, t2);
        addGroup('shop_avail_local' + sqlOp, filt); break;
      }
      case 'shop_show':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.show=1`); break;
      case 'shop_show_in':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store IN (${strVal(t1)}) AND shop_inv.show=1`); break;
      case 'shop_buyable':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.buyable=1`); break;
      case 'shop_preorder':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.\`all\`=2`); break;
      case 'shop_not_preorder':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.\`all\`!=2`); break;
      case 'shop_inventory_source_acot':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.inventory_source=${SRC_ACOT}`); break;
      case 'shop_inventory_source_acot_or_pre':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.inventory_source IN (${SRC_ACOT},${SRC_PREORDER})`); break;
      case 'shop_inventory_source_notions':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.inventory_source=${SRC_NOTIONS}`); break;
      case 'shop_inventory_source_not_notions':
        addJoin('shop_inv', 'LEFT JOIN shop_inventory AS shop_inv ON (p.pid=shop_inv.pid)');
        addGroup('shop_inv_cond', `shop_inv.store=${parseInt(t1)||0} AND shop_inv.inventory_source!=${SRC_NOTIONS}`); break;
      case 'preorder_item':
        addJoin('shop_inv_pre', `LEFT JOIN shop_inventory AS shop_inv_pre ON (p.pid=shop_inv_pre.pid AND shop_inv_pre.store=${ACOT_STORE})`);
        addGroup('preorder_item', `shop_inv_pre.inventory_source=${SRC_PREORDER}`); break;
      case 'not_preorder_item':
        addJoin('shop_inv_pre', `LEFT JOIN shop_inventory AS shop_inv_pre ON (p.pid=shop_inv_pre.pid AND shop_inv_pre.store=${ACOT_STORE})`);
        addGroup('preorder_item', `shop_inv_pre.inventory_source!=${SRC_PREORDER} OR shop_inv_pre.inventory_source IS NULL`); break;

      // ── product_notions: extra JOIN ────────────────────────────────────────
      case 'notions_use_inventory':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notions_use_inv', `product_notions.use_inventory${sqlOp}`); break;
      case 'notions_inventory':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notions_inv', `product_notions.inventory${sqlOp}${parseInt(t1)||0}`); break;
      case 'notions_sell_qty':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notions_sell_qty', `product_notions.sell_qty${sqlOp}${parseInt(t1)||0}`); break;
      case 'notions_csc':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notions_csc',
          (row.filter_operator === 'contains' || row.filter_operator === 'notcontains')
            ? `product_notions.csc${sqlOp}'%${strVal(t1)}%'`
            : `product_notions.csc${sqlOp}'${strVal(t1)}'`);
        break;
      case 'notions_top_sellers':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notions_top_sellers', 'product_notions.top_seller>0'); break;
      case 'notions_cost_higher_than_selling_price':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notions_cost_vs_sell', '(product_notions.sell_cost*product_notions.sell_qty) > p.sellingprice'); break;
      case 'notions_order_qty_conversion_our':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notion_oqc_our' + sqlOp, numFilt('product_notions.order_qty_conversion_our', sqlOp, t1, t2)); break;
      case 'notions_order_qty_conversion_notions':
        addJoin('product_notions', 'JOIN product_notions ON (p.pid=product_notions.pid)');
        addGroup('notion_oqc_notions' + sqlOp, numFilt('product_notions.order_qty_conversion_notions', sqlOp, t1, t2)); break;

      // ── product_backorders: extra LEFT JOIN ────────────────────────────────
      case 'backorder_qty_any':
        addJoin('product_backorders', 'LEFT JOIN product_backorders ON (product_backorders.pid=p.pid)');
        break; // join only, no WHERE condition
      case 'backorder_qty_notions':
        addJoin('product_backorders', 'LEFT JOIN product_backorders ON (product_backorders.pid=p.pid)');
        addGroup('backorder_qty_notions', `product_backorders.qty${sqlOp}${parseInt(t1)||0} AND product_backorders.supplier=92`); break;

      // ── product_related: extra JOIN ────────────────────────────────────────
      case 'related':
        addJoin('product_related', 'JOIN product_related ON (product_related.to_pid=p.pid)');
        addGroup('related_pid', `product_related.pid${sqlOp}${parseInt(t1)||0}`);
        if (t2) addGroup('related_type', `product_related.type=${parseInt(t2)||0}`);
        break;
      case 'no_relations':
        addJoin('product_related_nr', 'LEFT JOIN product_related AS product_related_nr ON (product_related_nr.pid=p.pid)');
        addGroup('no_relations', 'product_related_nr.to_pid IS NULL'); break;

      // ── receivings / PO: extra INNER JOINs ────────────────────────────────
      case 'receiving_id':
        addJoin('receivings_products', 'JOIN receivings_products ON (p.pid=receivings_products.pid)');
        addGroup('receiving_id' + sqlOp, `receivings_products.receiving_id${sqlOp}'${strVal(t1)}'`, isNot); break;
      case 'po_id':
        addJoin('po_products', 'JOIN po_products ON (p.pid=po_products.pid)');
        addGroup('po_id' + sqlOp, `po_products.po_id${sqlOp}'${strVal(t1)}'`, isNot); break;

      // ── subquery filters ───────────────────────────────────────────────────
      case 'missing_images':
        addGroup('missing_images',
          (row.filter_operator === 'true' || row.filter_operator === 'true1')
            ? '(SELECT COUNT(*) FROM product_images WHERE product_images.pid=p.pid)=0'
            : '(SELECT COUNT(*) FROM product_images WHERE product_images.pid=p.pid)>0');
        break;
      case 'current_price':
        addGroup('current_price' + sqlOp,
          `(SELECT MIN(price_each) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1)${sqlOp}${parseFloat(t1)||0}`);
        break;
      case 'current_price_sale_percent': {
        const v1 = (parseFloat(t1)||0) > 1 ? (parseFloat(t1)/100) : (parseFloat(t1)||0);
        const pe = '(SELECT MIN(price_each) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1)';
        addGroup('current_price_sale_pct' + sqlOp, `(1 - ${pe} / p.sellingprice)${sqlOp}${v1}`); break;
      }
      case 'one_current_price':
        addGroup('one_current_price', '(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1)=1'); break;
      case 'multiple_current_prices':
        addGroup('multiple_current_prices', '(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1)>1'); break;
      case 'current_price_is_missing':
        addGroup('current_price_is_missing', '(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1)=0'); break;
      case 'current_price_not_one_buyable':
        addGroup('current_price_not_one_buyable',
          '(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND qty_buy=1)=0 AND (SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1)>0');
        break;
      case 'current_price_min_buy': {
        const cond = t2
          ? `(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND is_min_qty_buy=1 AND qty_buy${sqlOp}${parseFloat(t1)||0} AND ${parseFloat(t2)||0})>0`
          : `(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND is_min_qty_buy=1 AND qty_buy${sqlOp}${parseFloat(t1)||0})>0`;
        addGroup('current_price_min_buy' + sqlOp, cond); break;
      }
      case 'current_price_each_buy': {
        const cond = t2
          ? `(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND is_min_qty_buy=0 AND qty_buy${sqlOp}${parseFloat(t1)||0} AND ${parseFloat(t2)||0})>0`
          : `(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND is_min_qty_buy=0 AND qty_buy${sqlOp}${parseFloat(t1)||0})>0`;
        addGroup('current_price_each_buy' + sqlOp, cond); break;
      }
      case 'current_price_max_qty': {
        const cond = t2
          ? `(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND qty_limit${sqlOp}${parseFloat(t1)||0} AND ${parseFloat(t2)||0})>0`
          : `(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND qty_limit${sqlOp}${parseFloat(t1)||0})>0`;
        addGroup('current_price_max_qty' + sqlOp, cond); break;
      }
      case 'current_price_is_checkout_offer':
        addGroup('current_price_checkout',
          `(SELECT COUNT(*) FROM product_current_prices WHERE product_current_prices.pid=p.pid AND active=1 AND checkout_offer${sqlOp})>0`);
        break;
      case 'current_price_has_extra':
        addJoin('cp_extras', 'JOIN (SELECT pid,count(*) ct FROM product_current_prices WHERE active=1 GROUP BY pid,qty_buy HAVING ct>1) AS cp_extras ON (cp_extras.pid=p.pid)');
        addGroup('cp_extra', 'cp_extras.ct>1'); break;
      case 'has_video':
        addGroup('has_video', '(SELECT COUNT(*) FROM product_media WHERE product_media.pid=p.pid)>0'); break;
      case 'notions_created':
        addGroup('notions_created',
          sqlOp !== '=0'
            ? '(SELECT COUNT(*) FROM product_notions_created WHERE product_notions_created.pid=p.pid)>0'
            : '(SELECT COUNT(*) FROM product_notions_created WHERE product_notions_created.pid=p.pid)=0');
        break;
      case 'notifications':
        addJoin('pnc', 'JOIN (SELECT pid,COUNT(*) AS notifications_count FROM product_notify GROUP BY pid) pnc ON (pnc.pid=p.pid)');
        addGroup('notifications', numFilt('pnc.notifications_count', sqlOp, t1, t2)); break;
      case 'daily_deal':
        addGroup('daily_deal', '(SELECT deal_id FROM product_daily_deals WHERE deal_date=CURDATE() AND product_daily_deals.pid=p.pid)>0');
        break;

      // ── amazon_price (store id 5, legacy) ─────────────────────────────────
      case 'amazon_price': {
        const AMAZON_STORE = 5;
        addJoin('product_prices', 'LEFT JOIN product_prices ON (p.pid=product_prices.pid)');
        let filt = `product_prices.store=${AMAZON_STORE} AND product_prices.price${sqlOp}${parseFloat(t1)||0}`;
        if (t2) filt += ` AND ${parseFloat(t2)||0}`;
        if ((sqlOp === '<=' || sqlOp === '=') && t1 === '0') filt += ' OR product_prices.price IS NULL';
        addGroup('amazon_price' + sqlOp, filt); break;
      }

      // ── basket (cid must be in filter_text2; skip if not set) ─────────────
      case 'basket':
        if (!t2) { unsupported.push('basket(no-cid)'); break; }
        addJoin('mybasket', 'JOIN mybasket ON (p.pid=mybasket.item)');
        addGroup('basket_cid', `mybasket.cid=${parseInt(t2)||0}`);
        addGroup('basket_sid', `mybasket.sid=0`); // ACOT store
        addGroup('basket_bid', t1 ? `mybasket.bid=${parseInt(t1)||0}` : 'mybasket.bid=0');
        if (t1 === '2') addGroup('basket_qty', 'mybasket.qty>0');
        break;

      // ── hot: recent sales aggregation ─────────────────────────────────────
      case 'hot': {
        const days = parseInt(t1) || 30;
        const alias = `hot_${days}`;
        addJoin(alias, `JOIN (SELECT prod_pid, COUNT(*) hot_ct, SUM(order_items.qty_ordered) hot_sm FROM order_items JOIN _order ON (_order.order_id=order_items.order_id AND _order.order_status>60 AND _order.date_placed BETWEEN NOW()-INTERVAL ${days} DAY AND NOW()) GROUP BY order_items.prod_pid) ${alias} ON (p.pid=${alias}.prod_pid)`);
        break; // join alone filters to products with recent sales; no extra WHERE needed
      }

      // Intentionally skipped (require external services or missing runtime context):
      //   search/search_any (MeiliSearch API), basket without cid (per-session user),
      //   groupby (display aggregation, not a filter)
      default:
        unsupported.push(row.filter_type);
        break;
    }
  }

  // Assemble WHERE: groups are AND-connected by default.
  // A group with ororor=true is OR-connected to the previous group instead (PHP filter_or mechanism).
  let whereClause = '';
  let i = 0;
  for (const [, group] of filterGroups) {
    const wrapped = group.conditions.map(c => `(${c})`);
    const innerJoiner = group.isNot ? ' AND ' : ' OR ';
    const part = `(${wrapped.join(innerJoiner)})`;
    if (i > 0) {
      whereClause += group.ororor ? ` OR ${part}` : ` AND ${part}`;
    } else {
      whereClause += part;
    }
    i++;
  }

  const joinClauses = [...joinTables.values()].join('\n      ');
  return { whereClause, joinClauses, unsupported };
}

// Load products matching a saved product_query by query_id
// Filter types whose v1/v2 values are cat_ids in product_categories
const CAT_ID_FILTER_TYPES = new Set([
  'company', 'line', 'subline', 'artist', 'category', 'theme',
  'size_cat', 'dimension', 'yarn_weight', 'material',
]);

async function resolveFilterLabels(connection, filters) {
  const catIds = new Set();
  const supplierIds = new Set();
  const taxCodeIds = new Set();

  for (const f of filters) {
    const vals = [f.v1, f.v2].filter(v => v && !isNaN(Number(v)));
    if (CAT_ID_FILTER_TYPES.has(f.type)) vals.forEach(v => catIds.add(Number(v)));
    else if (f.type === 'investor') vals.forEach(v => supplierIds.add(Number(v)));
    else if (f.type === 'tax_code') vals.forEach(v => taxCodeIds.add(Number(v)));
  }

  const catLookup = {};
  const supplierLookup = {};
  const taxCodeLookup = {};

  if (catIds.size) {
    const ids = [...catIds];
    const [rows] = await connection.query(
      `SELECT cat_id, name FROM product_categories WHERE cat_id IN (${ids.map(() => '?').join(',')})`, ids
    );
    for (const r of rows) catLookup[r.cat_id] = r.name;
  }
  if (supplierIds.size) {
    const ids = [...supplierIds];
    const [rows] = await connection.query(
      `SELECT supplierid, companyname FROM suppliers WHERE supplierid IN (${ids.map(() => '?').join(',')})`, ids
    );
    for (const r of rows) supplierLookup[r.supplierid] = r.companyname;
  }
  if (taxCodeIds.size) {
    const ids = [...taxCodeIds];
    const [rows] = await connection.query(
      `SELECT tax_code_id, name FROM product_tax_codes WHERE tax_code_id IN (${ids.map(() => '?').join(',')})`, ids
    );
    for (const r of rows) taxCodeLookup[r.tax_code_id] = r.name;
  }

  function labelFor(type, val) {
    if (!val || isNaN(Number(val))) return null;
    const id = Number(val);
    if (CAT_ID_FILTER_TYPES.has(type)) return catLookup[id] ?? null;
    if (type === 'investor') return supplierLookup[id] ?? null;
    if (type === 'tax_code') return taxCodeLookup[id] ?? null;
    return null;
  }

  return filters.map(f => ({
    ...f,
    v1Label: labelFor(f.type, f.v1),
    v2Label: labelFor(f.type, f.v2),
  }));
}

router.get('/query-products', async (req, res) => {
  const { query_id } = req.query;
  if (!query_id || isNaN(parseInt(query_id))) {
    return res.status(400).json({ error: 'Valid query_id is required' });
  }
  const qid = parseInt(query_id);

  try {
    const { connection } = await getDbConnection();

    // Verify query exists
    const [queryRows] = await connection.query(
      'SELECT id, name FROM product_query WHERE id = ?', [qid]
    );
    if (!queryRows.length) {
      return res.status(404).json({ error: `Query ${qid} not found` });
    }

    // Load all filters for this query
    const [filterRows] = await connection.query(
      'SELECT * FROM product_query_filter WHERE query_id = ? ORDER BY id', [qid]
    );

    if (!filterRows.length) {
      return res.json({ results: [], filters: [] });
    }

    const { whereClause, joinClauses, unsupported } = buildQueryFilterSql(filterRows);

    const uniqueUnsupported = [...new Set(unsupported)];
    if (uniqueUnsupported.length) {
      console.warn(`query-products: unsupported filter types for query ${qid}:`, uniqueUnsupported);
    }
    res.setHeader('X-Query-Name', queryRows[0].name || '');

    function tryDecode(v) {
      if (!v) return null;
      try { return decodeURIComponent(String(v)); } catch { return String(v); }
    }
    const rawFilters = filterRows
      .filter(r => !uniqueUnsupported.includes(r.filter_type))
      .map(r => ({
        type: r.filter_type,
        op: r.filter_operator,
        v1: tryDecode(r.filter_text1),
        v2: tryDecode(r.filter_text2),
      }));

    const filters = await resolveFilterLabels(connection, rawFilters);

    // If all filters were unsupported the WHERE clause is empty — return nothing
    // rather than dumping the entire products table.
    if (!whereClause) {
      return res.json({ results: [], filters, unsupported: uniqueUnsupported });
    }

    const sql = `${PRODUCT_SELECT}
      ${joinClauses}
      WHERE ${whereClause}
      GROUP BY p.pid
      ORDER BY p.description
      LIMIT 2000`;

    const [results] = await connection.query(sql);
    res.json({ results, filters, unsupported: uniqueUnsupported });
  } catch (error) {
    console.error('Error loading query products:', error);
    res.status(500).json({ error: 'Failed to load query products', details: error.message });
  }
});

export default router;
