← back to Watches

server.js

1404 lines

/**
 * Omega Watch Price History - Enhanced Backend Server with SEO Optimization
 * Port: 7600
 * Enhanced with advanced APIs, caching, analytics, WebSocket support, and SEO features
 */

import express from 'express';
import cors from 'cors';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { spawn } from 'child_process';
import compression from 'compression';
import helmet from 'helmet';
import morgan from 'morgan';
import swaggerUi from 'swagger-ui-express';
import YAML from 'yamljs';
import searchRouter from './api/search-simple.js';
import advancedSearchRouter from './api/advanced-search.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const app = express();
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 7600;
const httpServer = createServer(app);

// Initialize WebSocket server
const wss = new WebSocketServer({ server: httpServer });

// In-memory cache
const cache = {
  data: {},
  timestamps: {},
  TTL: 5 * 60 * 1000, // 5 minutes

  get(key) {
    if (this.data[key] && Date.now() - this.timestamps[key] < this.TTL) {
      return this.data[key];
    }
    delete this.data[key];
    delete this.timestamps[key];
    return null;
  },

  set(key, value) {
    this.data[key] = value;
    this.timestamps[key] = Date.now();
  },

  clear() {
    this.data = {};
    this.timestamps = {};
  },

  stats() {
    return {
      keys: Object.keys(this.data).length,
      size: JSON.stringify(this.data).length,
      oldestEntry: Math.min(...Object.values(this.timestamps))
    };
  }
};

// View counter for trending watches
const viewCounts = {};
const watchlists = {}; // In-memory watchlists (would be DB in production)

// Middleware with SEO-friendly headers
// Disable helmet for now to avoid CSP issues
// app.use(helmet({
//   contentSecurityPolicy: false,
//   crossOriginEmbedderPolicy: false
// }));

app.use(compression());
app.use(morgan('combined'));
app.use(cors());
app.use(express.json());

// Basic Authentication Middleware
// Credentials must be supplied via DW_BASIC_AUTH in "user:password" form.
// Fail-closed at module load — never default to a hardcoded literal.
const DW_BASIC_AUTH = process.env.DW_BASIC_AUTH;
if (!DW_BASIC_AUTH) {
  throw new Error('DW_BASIC_AUTH env var required for auth — set in .env before boot');
}
const _colonIdx = DW_BASIC_AUTH.indexOf(':');
if (_colonIdx <= 0 || _colonIdx === DW_BASIC_AUTH.length - 1) {
  throw new Error('DW_BASIC_AUTH must be in "user:password" form');
}
const AUTH_USERNAME = DW_BASIC_AUTH.slice(0, _colonIdx);
const AUTH_PASSWORD = DW_BASIC_AUTH.slice(_colonIdx + 1);

app.use((req, res, next) => {
  // Skip auth for health check endpoint
  if (req.path === '/api/health' || req.path === '/health') {
    return next();
  }

  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Basic ')) {
    res.setHeader('WWW-Authenticate', 'Basic realm="Omega Watch Price History"');
    return res.status(401).send('Authentication required');
  }

  const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
  const [username, password] = credentials.split(':');

  if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
    return next();
  }

  res.setHeader('WWW-Authenticate', 'Basic realm="Omega Watch Price History"');
  return res.status(401).send('Invalid credentials');
});

// Serve the search interface
app.get('/search', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'search-interface.html'));
});

// Snapshot-file 404 guard: never serve editor/git-merge leftovers from static roots
// Blocks .bak, .bak.*, .pre-*, .orig, .rej extensions anywhere in the path.
app.use((req, res, next) => {
  if (/\.(bak|orig|rej)(\.|$)|\.pre-/i.test(req.path)) {
    return res.status(404).type('text/plain').send('Not Found');
  }
  next();
});

// SEO-friendly static file serving with proper headers
app.use(express.static('dist', {
  maxAge: '1d',
  etag: true,
  lastModified: true,
  setHeaders: (res, filepath) => {
    // Cache control for different file types
    if (filepath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache, must-revalidate');
    } else if (filepath.match(/\.(js|css)$/)) {
      res.setHeader('Cache-Control', 'public, max-age=86400'); // 1 day
    } else if (filepath.match(/\.(jpg|jpeg|png|gif|svg|ico|webp)$/)) {
      res.setHeader('Cache-Control', 'public, max-age=604800'); // 7 days
    }
  }
}));

app.use(express.static('public', {
  maxAge: '1d',
  etag: true
}));

// Museum static files
app.use('/museum', express.static('museum', {
  maxAge: '1h',
  etag: true
}));

// Markdown docs (api-playground "Full Guide" link points at /docs/*.md)
app.use('/docs', express.static('docs', {
  maxAge: '1h',
  etag: true,
  setHeaders: (res, filepath) => {
    if (filepath.endsWith('.md')) res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
  }
}));

// Request logging middleware with performance tracking
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    if (duration > 1000) {
      console.log(`SLOW REQUEST: ${req.method} ${req.path} - ${duration}ms`);
    }
  });
  next();
});

// Helper function to load watch data
function loadWatchData() {
  const cached = cache.get('watches');
  if (cached) return cached;

  const data = JSON.parse(fs.readFileSync('./data/watches.json', 'utf8'));
  cache.set('watches', data);
  return data;
}

// Helper functions
function calculateAverageAppreciation(watches) {
  const cached = cache.get('avgAppreciation');
  if (cached !== null) return cached;

  let totalAppreciation = 0;
  let count = 0;

  watches.forEach(watch => {
    if (watch.priceHistory.length >= 2) {
      const firstPrice = watch.priceHistory[0].price;
      const lastPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
      const appreciation = ((lastPrice - firstPrice) / firstPrice) * 100;
      totalAppreciation += appreciation;
      count++;
    }
  });

  const result = count > 0 ? parseFloat((totalAppreciation / count).toFixed(2)) : 0;
  cache.set('avgAppreciation', result);
  return result;
}

function getTopPerformers(watches, limit) {
  const cacheKey = `topPerformers_${limit}`;
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const result = watches
    .map(watch => {
      if (watch.priceHistory.length < 2) return null;

      const firstPrice = watch.priceHistory[0].price;
      const lastPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
      const appreciation = ((lastPrice - firstPrice) / firstPrice) * 100;

      return {
        id: watch.id,
        model: watch.model,
        series: watch.series,
        appreciation: parseFloat(appreciation.toFixed(2)),
        currentPrice: lastPrice,
        yearIntroduced: watch.yearIntroduced
      };
    })
    .filter(Boolean)
    .sort((a, b) => parseFloat(b.appreciation) - parseFloat(a.appreciation))
    .slice(0, limit);

  cache.set(cacheKey, result);
  return result;
}

function calculatePricePrediction(watch) {
  if (watch.priceHistory.length < 3) {
    return { error: 'Insufficient data for prediction' };
  }

  // Simple linear regression for trend prediction
  const history = watch.priceHistory;
  const n = history.length;
  let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;

  history.forEach((point, index) => {
    const x = index;
    const y = point.price;
    sumX += x;
    sumY += y;
    sumXY += x * y;
    sumX2 += x * x;
  });

  const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
  const intercept = (sumY - slope * sumX) / n;

  // Predict next 3 years
  const predictions = [];
  const lastYear = history[history.length - 1].year;

  for (let i = 1; i <= 3; i++) {
    const year = lastYear + i;
    const predictedPrice = Math.round(slope * (n + i - 1) + intercept);
    predictions.push({
      year,
      predictedPrice: Math.max(0, predictedPrice),
      confidence: calculateConfidence(history, slope)
    });
  }

  return {
    model: watch.model,
    currentPrice: history[history.length - 1].price,
    trend: slope > 0 ? 'increasing' : 'decreasing',
    averageAnnualChange: Math.round((slope / (history[history.length - 1].price)) * 100 * 100) / 100,
    predictions
  };
}

function calculateConfidence(history, slope) {
  // Calculate R-squared as confidence metric
  const n = history.length;
  let sumSquaredError = 0;
  let sumSquaredTotal = 0;
  const meanY = history.reduce((sum, p) => sum + p.price, 0) / n;

  history.forEach((point, index) => {
    const predicted = slope * index + (meanY - slope * (n - 1) / 2);
    sumSquaredError += Math.pow(point.price - predicted, 2);
    sumSquaredTotal += Math.pow(point.price - meanY, 2);
  });

  const rSquared = 1 - (sumSquaredError / sumSquaredTotal);
  return Math.round(Math.max(0, Math.min(1, rSquared)) * 100);
}

// WebSocket connection handling
wss.on('connection', (ws) => {
  console.log('New WebSocket client connected');

  ws.on('message', (message) => {
    try {
      const data = JSON.parse(message);

      if (data.type === 'subscribe') {
        ws.watchId = data.watchId;
        ws.send(JSON.stringify({ type: 'subscribed', watchId: data.watchId }));
      }
    } catch (error) {
      console.error('WebSocket message error:', error);
    }
  });

  ws.on('close', () => {
    console.log('WebSocket client disconnected');
  });
});

// Broadcast function for WebSocket updates
function broadcastUpdate(watchId, data) {
  wss.clients.forEach((client) => {
    if (client.watchId === watchId && client.readyState === 1) {
      client.send(JSON.stringify({ type: 'update', watchId, data }));
    }
  });
}

// ============================================================================
// SEO ROUTES
// ============================================================================

// Serve robots.txt
app.get('/robots.txt', (req, res) => {
  res.type('text/plain');
  res.sendFile(path.join(__dirname, 'public', 'robots.txt'));
});

// Serve sitemap.xml
app.get('/sitemap.xml', (req, res) => {
  res.type('application/xml');
  res.sendFile(path.join(__dirname, 'public', 'sitemap.xml'));
});

// Generate dynamic sitemap with all watches
app.get('/sitemap-dynamic.xml', (req, res) => {
  const data = loadWatchData();
  const baseUrl = 'http://45.61.58.125:7600';

  let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
  sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';

  // Homepage
  sitemap += `  <url>\n`;
  sitemap += `    <loc>${baseUrl}/</loc>\n`;
  sitemap += `    <lastmod>${new Date().toISOString().split('T')[0]}</lastmod>\n`;
  sitemap += `    <changefreq>daily</changefreq>\n`;
  sitemap += `    <priority>1.0</priority>\n`;
  sitemap += `  </url>\n`;

  // Individual watches
  data.watches.forEach(watch => {
    sitemap += `  <url>\n`;
    sitemap += `    <loc>${baseUrl}/?watch=${watch.id}</loc>\n`;
    sitemap += `    <lastmod>${new Date().toISOString().split('T')[0]}</lastmod>\n`;
    sitemap += `    <changefreq>monthly</changefreq>\n`;
    sitemap += `    <priority>0.8</priority>\n`;
    sitemap += `  </url>\n`;
  });

  sitemap += '</urlset>';

  res.type('application/xml');
  res.send(sitemap);
});

// ============================================================================
// API ROUTES
// ============================================================================

// Mount comprehensive FTS5 search router
app.use('/api/search', searchRouter);

// Mount advanced search router with sorting and filtering
app.use('/api/advanced-search', advancedSearchRouter);

// Health check endpoint with detailed metrics
app.get('/api/health', (req, res) => {
  const data = loadWatchData();
  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    cache: cache.stats(),
    database: {
      watches: data.watches.length,
      collections: [...new Set(data.watches.map(w => w.series))].length
    },
    websocket: {
      connections: wss.clients.size
    }
  });
});

// Get all watches with optional filtering and pagination
app.get('/api/watches', (req, res) => {
  const data = loadWatchData();
  let watches = [...data.watches];

  // Filter by series
  if (req.query.series) {
    watches = watches.filter(w => w.series === req.query.series);
  }

  // Filter by year range
  if (req.query.minYear) {
    watches = watches.filter(w => w.yearIntroduced >= parseInt(req.query.minYear));
  }
  if (req.query.maxYear) {
    watches = watches.filter(w => w.yearIntroduced <= parseInt(req.query.maxYear));
  }

  // Filter by price range (current price)
  if (req.query.minPrice || req.query.maxPrice) {
    watches = watches.filter(w => {
      const currentPrice = w.priceHistory[w.priceHistory.length - 1].price;
      const min = req.query.minPrice ? parseInt(req.query.minPrice) : 0;
      const max = req.query.maxPrice ? parseInt(req.query.maxPrice) : Infinity;
      return currentPrice >= min && currentPrice <= max;
    });
  }

  // Pagination
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 50;
  const startIndex = (page - 1) * limit;
  const endIndex = page * limit;

  const paginatedWatches = watches.slice(startIndex, endIndex);

  res.json({
    total: watches.length,
    page,
    limit,
    totalPages: Math.ceil(watches.length / limit),
    watches: paginatedWatches
  });
});

// Advanced search endpoint
app.get('/api/search', (req, res) => {
  const data = loadWatchData();
  const query = req.query.q ? req.query.q.toLowerCase() : '';

  let results = data.watches.filter(watch => {
    const searchFields = [
      watch.model,
      watch.series,
      watch.reference,
      watch.description,
      watch.specifications?.movement,
      watch.specifications?.caseMaterial
    ].filter(Boolean).join(' ').toLowerCase();

    return searchFields.includes(query);
  });

  // Apply filters
  if (req.query.series) {
    results = results.filter(w => w.series === req.query.series);
  }

  if (req.query.movement) {
    results = results.filter(w =>
      w.specifications?.movement?.toLowerCase().includes(req.query.movement.toLowerCase())
    );
  }

  if (req.query.minPrice || req.query.maxPrice) {
    results = results.filter(w => {
      const currentPrice = w.priceHistory[w.priceHistory.length - 1].price;
      const min = req.query.minPrice ? parseInt(req.query.minPrice) : 0;
      const max = req.query.maxPrice ? parseInt(req.query.maxPrice) : Infinity;
      return currentPrice >= min && currentPrice <= max;
    });
  }

  // Pagination
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 20;
  const startIndex = (page - 1) * limit;
  const endIndex = page * limit;

  res.json({
    query,
    total: results.length,
    page,
    limit,
    totalPages: Math.ceil(results.length / limit),
    results: results.slice(startIndex, endIndex)
  });
});

// Get trending watches (most viewed)
app.get('/api/watches/trending', (req, res) => {
  const limit = parseInt(req.query.limit) || 10;

  const trending = Object.entries(viewCounts)
    .sort((a, b) => b[1] - a[1])
    .slice(0, limit)
    .map(([watchId, views]) => {
      const data = loadWatchData();
      const watch = data.watches.find(w => w.id === watchId);
      return watch ? { ...watch, views } : null;
    })
    .filter(Boolean);

  res.json({
    trending,
    totalViews: Object.values(viewCounts).reduce((sum, v) => sum + v, 0)
  });
});

// Get price history for specific watch
app.get('/api/watches/:id/history', (req, res) => {
  const data = loadWatchData();
  const watch = data.watches.find(w => w.id === req.params.id);

  if (!watch) {
    return res.status(404).json({ error: 'Watch not found' });
  }

  // Track view count
  viewCounts[req.params.id] = (viewCounts[req.params.id] || 0) + 1;

  res.json({
    watch: {
      id: watch.id,
      model: watch.model,
      series: watch.series,
      reference: watch.reference,
      yearIntroduced: watch.yearIntroduced,
      specifications: watch.specifications
    },
    priceHistory: watch.priceHistory,
    views: viewCounts[req.params.id]
  });
});

// Get price predictions for a watch
app.get('/api/price-predictions/:id', (req, res) => {
  const data = loadWatchData();
  const watch = data.watches.find(w => w.id === req.params.id);

  if (!watch) {
    return res.status(404).json({ error: 'Watch not found' });
  }

  const prediction = calculatePricePrediction(watch);
  res.json(prediction);
});

// Get collections with statistics
app.get('/api/collections', (req, res) => {
  const cacheKey = 'collections_stats';
  const cached = cache.get(cacheKey);
  if (cached) return res.json(cached);

  const data = loadWatchData();
  const collections = {};

  data.watches.forEach(watch => {
    if (!collections[watch.series]) {
      collections[watch.series] = {
        series: watch.series,
        count: 0,
        watches: [],
        avgPrice: 0,
        totalAppreciation: 0,
        avgYearIntroduced: 0
      };
    }

    const coll = collections[watch.series];
    coll.count++;
    coll.watches.push({
      id: watch.id,
      model: watch.model,
      reference: watch.reference,
      yearIntroduced: watch.yearIntroduced
    });

    const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
    coll.avgPrice += currentPrice;
    coll.avgYearIntroduced += watch.yearIntroduced;

    if (watch.priceHistory.length >= 2) {
      const firstPrice = watch.priceHistory[0].price;
      const appreciation = ((currentPrice - firstPrice) / firstPrice) * 100;
      coll.totalAppreciation += appreciation;
    }
  });

  // Calculate averages
  Object.values(collections).forEach(coll => {
    coll.avgPrice = Math.round(coll.avgPrice / coll.count);
    coll.avgAppreciation = Math.round((coll.totalAppreciation / coll.count) * 100) / 100;
    coll.avgYearIntroduced = Math.round(coll.avgYearIntroduced / coll.count);
    delete coll.totalAppreciation;
  });

  const result = {
    collections: Object.values(collections).sort((a, b) => b.count - a.count),
    totalCollections: Object.keys(collections).length
  };

  cache.set(cacheKey, result);
  res.json(result);
});

// Get statistics
app.get('/api/statistics', (req, res) => {
  const data = loadWatchData();

  const stats = {
    totalWatches: data.watches.length,
    totalSeries: [...new Set(data.watches.map(w => w.series))].length,
    averageAppreciation: calculateAverageAppreciation(data.watches),
    topPerformers: getTopPerformers(data.watches, 5),
    lastUpdated: new Date().toISOString(),
    priceRanges: calculatePriceRanges(data.watches),
    movementTypes: calculateMovementDistribution(data.watches),
    appreciationByDecade: calculateAppreciationByDecade(data.watches)
  };

  res.json(stats);
});

// Analytics: Price ranges distribution
function calculatePriceRanges(watches) {
  const cacheKey = 'priceRanges';
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const ranges = {
    'Under $5,000': 0,
    '$5,000 - $10,000': 0,
    '$10,000 - $25,000': 0,
    '$25,000 - $50,000': 0,
    'Over $50,000': 0
  };

  watches.forEach(watch => {
    const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;

    if (currentPrice < 5000) ranges['Under $5,000']++;
    else if (currentPrice < 10000) ranges['$5,000 - $10,000']++;
    else if (currentPrice < 25000) ranges['$10,000 - $25,000']++;
    else if (currentPrice < 50000) ranges['$25,000 - $50,000']++;
    else ranges['Over $50,000']++;
  });

  cache.set(cacheKey, ranges);
  return ranges;
}

// Analytics: Movement type distribution
function calculateMovementDistribution(watches) {
  const cacheKey = 'movementTypes';
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const movements = {};

  watches.forEach(watch => {
    if (watch.specifications?.movement) {
      const movementType = watch.specifications.movement.includes('automatic') ? 'Automatic' :
                          watch.specifications.movement.includes('manual') ? 'Manual' :
                          'Other';
      movements[movementType] = (movements[movementType] || 0) + 1;
    }
  });

  cache.set(cacheKey, movements);
  return movements;
}

// Analytics: Appreciation by decade
function calculateAppreciationByDecade(watches) {
  const cacheKey = 'appreciationByDecade';
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const decades = {};

  watches.forEach(watch => {
    const decade = Math.floor(watch.yearIntroduced / 10) * 10;

    if (!decades[decade]) {
      decades[decade] = { count: 0, totalAppreciation: 0 };
    }

    if (watch.priceHistory.length >= 2) {
      const firstPrice = watch.priceHistory[0].price;
      const lastPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
      const appreciation = ((lastPrice - firstPrice) / firstPrice) * 100;

      decades[decade].count++;
      decades[decade].totalAppreciation += appreciation;
    }
  });

  const result = {};
  Object.keys(decades).forEach(decade => {
    result[`${decade}s`] = Math.round(
      (decades[decade].totalAppreciation / decades[decade].count) * 100
    ) / 100;
  });

  cache.set(cacheKey, result);
  return result;
}

// Compare multiple watches
app.post('/api/compare', (req, res) => {
  const { watchIds } = req.body;

  if (!watchIds || !Array.isArray(watchIds)) {
    return res.status(400).json({ error: 'watchIds array is required' });
  }

  const data = loadWatchData();

  const comparison = watchIds.map(id => {
    const watch = data.watches.find(w => w.id === id);
    if (!watch) return null;

    const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
    const firstPrice = watch.priceHistory[0].price;
    const appreciation = ((currentPrice - firstPrice) / firstPrice) * 100;

    return {
      id: watch.id,
      model: watch.model,
      series: watch.series,
      yearIntroduced: watch.yearIntroduced,
      priceHistory: watch.priceHistory,
      specifications: watch.specifications,
      currentPrice,
      appreciation: Math.round(appreciation * 100) / 100
    };
  }).filter(Boolean);

  res.json({
    watches: comparison,
    count: comparison.length
  });
});

// Export data in different formats
app.get('/api/export/:format', (req, res) => {
  const data = loadWatchData();
  const format = req.params.format.toLowerCase();

  if (format === 'json') {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Content-Disposition', 'attachment; filename=omega-watches.json');
    res.json(data);
  } else if (format === 'csv') {
    const csv = convertToCSV(data.watches);
    res.setHeader('Content-Type', 'text/csv');
    res.setHeader('Content-Disposition', 'attachment; filename=omega-watches.csv');
    res.send(csv);
  } else {
    res.status(400).json({ error: 'Unsupported format. Use json or csv' });
  }
});

function convertToCSV(watches) {
  const headers = ['ID', 'Model', 'Series', 'Reference', 'Year Introduced', 'Current Price', 'Original Price', 'Appreciation %'];
  const rows = [headers.join(',')];

  watches.forEach(watch => {
    const currentPrice = watch.priceHistory[watch.priceHistory.length - 1].price;
    const originalPrice = watch.priceHistory[0].price;
    const appreciation = ((currentPrice - originalPrice) / originalPrice) * 100;

    rows.push([
      watch.id,
      `"${watch.model}"`,
      watch.series,
      watch.reference,
      watch.yearIntroduced,
      currentPrice,
      originalPrice,
      Math.round(appreciation * 100) / 100
    ].join(','));
  });

  return rows.join('\n');
}

// Load OpenAPI specification
const swaggerDocument = YAML.load(path.join(__dirname, 'openapi.yaml'));

// Market Data Cache (1 hour TTL)
const marketDataCache = {
  data: {},
  timestamps: {},
  TTL: 60 * 60 * 1000, // 1 hour

  get(key) {
    if (this.data[key] && Date.now() - this.timestamps[key] < this.TTL) {
      return this.data[key];
    }
    delete this.data[key];
    delete this.timestamps[key];
    return null;
  },

  set(key, value) {
    this.data[key] = value;
    this.timestamps[key] = Date.now();
  }
};

// Load watch-wayback mapping
let watchWaybackMap = {};
try {
  const mapPath = path.join(__dirname, 'data', 'watch-wayback-map.json');
  if (fs.existsSync(mapPath)) {
    watchWaybackMap = JSON.parse(fs.readFileSync(mapPath, 'utf8')).watches || {};
  }
} catch (e) {
  console.warn('Warning: Could not load watch-wayback-map.json:', e.message);
}

// Mock market data fallback
function getMockMarketData(watchId) {
  const mockData = {
    'speedmaster-moonwatch': { low: 6500, high: 8500, avg: 7200 },
    'speedmaster-alaska': { low: 18000, high: 28000, avg: 22000 },
    'seamaster-300': { low: 14000, high: 20000, avg: 16500 },
    'seamaster-ploprof': { low: 10000, high: 16000, avg: 13000 },
    'seamaster-bond': { low: 4800, high: 7500, avg: 5800 },
    'constellation-piepan': { low: 2500, high: 6000, avg: 3800 },
    'constellation-manhattan': { low: 4000, high: 7000, avg: 5200 },
    'deville-tresor': { low: 3500, high: 5500, avg: 4300 },
    'deville-coaxial': { low: 3000, high: 5000, avg: 3800 },
    'railmaster-original': { low: 4500, high: 8000, avg: 6000 },
    'aqua-terra': { low: 3500, high: 6000, avg: 4500 },
    'planet-ocean': { low: 4500, high: 7500, avg: 5800 },
    'speedmaster-reduced': { low: 2800, high: 4500, avg: 3500 },
    'speedmaster-mark2': { low: 3500, high: 6000, avg: 4500 },
    'flightmaster': { low: 5000, high: 10000, avg: 7000 }
  };

  const data = mockData[watchId] || { low: 3000, high: 8000, avg: 5000 };

  return {
    success: true,
    model: watchId,
    marketPrice: {
      low: data.low,
      high: data.high,
      avg: data.avg,
      currency: 'USD'
    },
    source: 'Mock Data',
    isMock: true,
    timestamp: Date.now(),
    fetchedAt: new Date().toISOString()
  };
}

// Call Python Wayback scraper
function callWaybackScraper(model) {
  return new Promise((resolve, reject) => {
    const scriptPath = path.join(__dirname, 'scripts', 'wayback-price-scraper.py');

    const python = spawn('python3', [
      scriptPath,
      '--model', model,
      '--limit', '20',
      '--delay', '0.5'
    ], {
      timeout: 120000 // 2 minute timeout
    });

    let stdout = '';
    let stderr = '';

    python.stdout.on('data', (data) => {
      stdout += data.toString();
    });

    python.stderr.on('data', (data) => {
      stderr += data.toString();
    });

    python.on('close', (code) => {
      if (code === 0 && stdout) {
        try {
          const result = JSON.parse(stdout);
          resolve(result);
        } catch (e) {
          reject(new Error(`Failed to parse scraper output: ${e.message}`));
        }
      } else {
        reject(new Error(stderr || `Scraper exited with code ${code}`));
      }
    });

    python.on('error', (err) => {
      reject(err);
    });
  });
}

// Market Data API Endpoint
app.get('/api/market-data/:watchId', async (req, res) => {
  const { watchId } = req.params;

  // Check cache first
  const cached = marketDataCache.get(watchId);
  if (cached) {
    return res.json({
      ...cached,
      cached: true,
      cacheAge: Date.now() - marketDataCache.timestamps[watchId]
    });
  }

  // Get wayback model from mapping
  const mapping = watchWaybackMap[watchId];
  const waybackModel = mapping?.waybackModel || 'speedmaster';

  try {
    console.log(`Fetching market data for ${watchId} (wayback model: ${waybackModel})`);
    const result = await callWaybackScraper(waybackModel);

    if (result.success && result.statistics?.avgPrice) {
      const marketData = {
        success: true,
        model: watchId,
        marketPrice: {
          low: result.statistics.minPrice,
          high: result.statistics.maxPrice,
          avg: result.statistics.avgPrice,
          currency: 'USD'
        },
        priceHistory: result.priceHistory || [],
        pagesFound: result.pagesFound,
        pagesScraped: result.pagesScraped,
        source: 'Wayback Machine (Chrono24 Archives)',
        isMock: false,
        timestamp: Date.now(),
        fetchedAt: new Date().toISOString()
      };

      // Cache the result
      marketDataCache.set(watchId, marketData);

      return res.json(marketData);
    } else {
      // Fallback to mock data
      console.log(`No real data found for ${watchId}, using mock data`);
      const mockData = getMockMarketData(watchId);
      marketDataCache.set(watchId, mockData);
      return res.json(mockData);
    }
  } catch (error) {
    console.error(`Market data fetch error for ${watchId}:`, error.message);
    const mockData = getMockMarketData(watchId);
    return res.json(mockData);
  }
});

// Historical Prices API Endpoint
app.get('/api/historical-prices/:watchId', (req, res) => {
  const { watchId } = req.params;
  const { from, to, granularity = 'monthly' } = req.query;

  try {
    // Load aggregated data if available
    const aggregatedPath = path.join(__dirname, 'data', 'aggregated-prices.json');
    const rawPath = path.join(__dirname, 'data', 'historical-prices.json');

    let responseData = null;

    // Try aggregated data first
    if (fs.existsSync(aggregatedPath)) {
      const aggregated = JSON.parse(fs.readFileSync(aggregatedPath, 'utf8'));
      if (aggregated.watches && aggregated.watches[watchId]) {
        const watchData = aggregated.watches[watchId];

        // Filter by date range if specified
        let data = granularity === 'yearly' ? watchData.yearly : watchData.monthly;

        if (from) {
          data = data.filter(d => (d.month || d.year.toString()) >= from);
        }
        if (to) {
          data = data.filter(d => (d.month || d.year.toString()) <= to);
        }

        responseData = {
          success: true,
          watchId,
          granularity,
          dateRange: watchData.dateRange,
          statistics: watchData.statistics,
          trends: watchData.trends,
          data,
          source: 'aggregated'
        };
      }
    }

    // Fallback to raw data
    if (!responseData && fs.existsSync(rawPath)) {
      const raw = JSON.parse(fs.readFileSync(rawPath, 'utf8'));
      if (raw.prices && raw.prices[watchId]) {
        let records = raw.prices[watchId];

        // Filter by date range
        if (from) {
          records = records.filter(r => r.date >= from);
        }
        if (to) {
          records = records.filter(r => r.date <= to);
        }

        responseData = {
          success: true,
          watchId,
          granularity: 'daily',
          records: records.length,
          data: records.slice(0, 500), // Limit response size
          source: 'raw'
        };
      }
    }

    if (responseData) {
      return res.json(responseData);
    }

    // No data found
    return res.json({
      success: false,
      error: 'No historical data available for this watch',
      watchId,
      message: 'Run the historical price crawler to populate data'
    });
  } catch (error) {
    console.error('Historical prices error:', error);
    return res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// Get all historical data summary
app.get('/api/historical-prices', (req, res) => {
  try {
    const aggregatedPath = path.join(__dirname, 'data', 'aggregated-prices.json');
    const rawPath = path.join(__dirname, 'data', 'historical-prices.json');

    let summary = {
      hasData: false,
      watches: [],
      totalRecords: 0,
      lastUpdated: null
    };

    if (fs.existsSync(aggregatedPath)) {
      const aggregated = JSON.parse(fs.readFileSync(aggregatedPath, 'utf8'));
      summary.hasData = true;
      summary.lastUpdated = aggregated.metadata?.processedAt;
      summary.watches = Object.keys(aggregated.watches || {}).map(id => ({
        id,
        records: aggregated.watches[id].rawRecords,
        dateRange: aggregated.watches[id].dateRange,
        currentPrice: aggregated.watches[id].statistics?.avg
      }));
      summary.totalRecords = aggregated.metadata?.processedRecords || 0;
    } else if (fs.existsSync(rawPath)) {
      const raw = JSON.parse(fs.readFileSync(rawPath, 'utf8'));
      summary.hasData = raw.metadata?.totalRecords > 0;
      summary.lastUpdated = raw.metadata?.lastUpdated;
      summary.watches = Object.keys(raw.prices || {}).map(id => ({
        id,
        records: Array.isArray(raw.prices[id]) ? raw.prices[id].length : 0
      }));
      summary.totalRecords = raw.metadata?.totalRecords || 0;
    }

    return res.json(summary);
  } catch (error) {
    console.error('Historical prices summary error:', error);
    return res.status(500).json({ error: error.message });
  }
});

// ============================================
// ROLEX OFFICIAL PRICES API
// ============================================

// GET /api/rolex-prices - Get all Rolex current prices
app.get('/api/rolex-prices', (req, res) => {
  try {
    const dataFile = path.join(__dirname, 'data', 'rolex-official-prices.json');
    if (!fs.existsSync(dataFile)) {
      return res.status(404).json({ error: 'Rolex price data not found' });
    }

    const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
    const latestScrape = data.scrapes[data.scrapes.length - 1];

    // Filter by collection if specified
    const { collection, minPrice, maxPrice, material } = req.query;
    let products = latestScrape.products;

    if (collection) {
      products = products.filter(p => p.collection.toLowerCase().includes(collection.toLowerCase()));
    }
    if (minPrice) {
      products = products.filter(p => p.price >= parseInt(minPrice));
    }
    if (maxPrice) {
      products = products.filter(p => p.price <= parseInt(maxPrice));
    }
    if (material) {
      products = products.filter(p => p.material && p.material.toLowerCase().includes(material.toLowerCase()));
    }

    res.json({
      success: true,
      date: latestScrape.date,
      totalProducts: products.length,
      products: products
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to load Rolex prices', details: error.message });
  }
});

// GET /api/rolex-prices/:reference - Get specific Rolex by reference
app.get('/api/rolex-prices/:reference', (req, res) => {
  try {
    const dataFile = path.join(__dirname, 'data', 'rolex-official-prices.json');
    if (!fs.existsSync(dataFile)) {
      return res.status(404).json({ error: 'Rolex price data not found' });
    }

    const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
    const { reference } = req.params;

    // Search across all scrapes for price history
    const priceHistory = [];
    for (const scrape of data.scrapes) {
      const product = scrape.products.find(p =>
        p.reference && p.reference.toLowerCase().includes(reference.toLowerCase())
      );
      if (product) {
        priceHistory.push({
          date: scrape.date,
          price: product.price,
          ...product
        });
      }
    }

    if (priceHistory.length === 0) {
      return res.status(404).json({ error: 'Watch not found', reference });
    }

    res.json({
      success: true,
      reference,
      currentPrice: priceHistory[priceHistory.length - 1].price,
      priceHistory
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to load Rolex prices', details: error.message });
  }
});

// GET /api/rolex-prices/history - Get historical Rolex data from Wayback crawl
app.get('/api/rolex-historical', (req, res) => {
  try {
    const dataFile = path.join(__dirname, 'data', 'rolex-historical-prices.json');
    if (!fs.existsSync(dataFile)) {
      return res.status(404).json({ error: 'Rolex historical data not found. Run the wayback crawler first.' });
    }

    const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
    const { model, from, to } = req.query;

    if (model && data.models[model]) {
      const modelData = data.models[model];
      let history = modelData.priceHistory;

      // Filter by date range if specified
      if (from || to) {
        history = Object.fromEntries(
          Object.entries(history).filter(([date]) => {
            if (from && date < from) return false;
            if (to && date > to) return false;
            return true;
          })
        );
      }

      res.json({
        success: true,
        model: modelData.name,
        totalDataPoints: Object.keys(history).length,
        priceHistory: history
      });
    } else {
      // Return summary of all models
      const summary = Object.entries(data.models).map(([id, model]) => ({
        id,
        name: model.name,
        dataPoints: model.totalDataPoints || 0
      }));

      res.json({
        success: true,
        metadata: data.metadata,
        models: summary
      });
    }
  } catch (error) {
    res.status(500).json({ error: 'Failed to load Rolex historical data', details: error.message });
  }
});

// ============================================
// OMEGA OFFICIAL PRICES API
// ============================================

// GET /api/omega-prices - Get all Omega current prices
app.get('/api/omega-prices', (req, res) => {
  try {
    const dataFile = path.join(__dirname, 'data', 'omega-official-prices.json');
    if (!fs.existsSync(dataFile)) {
      return res.status(404).json({ error: 'Omega price data not found' });
    }

    const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
    const latestScrape = data.scrapes[data.scrapes.length - 1];

    // Filter by collection if specified
    const { collection, minPrice, maxPrice } = req.query;
    let products = latestScrape.products;

    if (collection) {
      products = products.filter(p => p.collection.toLowerCase().includes(collection.toLowerCase()));
    }
    if (minPrice) {
      products = products.filter(p => p.price >= parseInt(minPrice));
    }
    if (maxPrice) {
      products = products.filter(p => p.price <= parseInt(maxPrice));
    }

    res.json({
      success: true,
      date: latestScrape.date,
      totalProducts: products.length,
      products: products
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to load Omega prices', details: error.message });
  }
});

// GET /api/omega-prices/:reference - Get specific Omega by reference
app.get('/api/omega-prices/:reference', (req, res) => {
  try {
    const dataFile = path.join(__dirname, 'data', 'omega-official-prices.json');
    if (!fs.existsSync(dataFile)) {
      return res.status(404).json({ error: 'Omega price data not found' });
    }

    const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
    const { reference } = req.params;

    // Search across all scrapes for price history
    const priceHistory = [];
    for (const scrape of data.scrapes) {
      const product = scrape.products.find(p =>
        p.reference && p.reference.toLowerCase().includes(reference.toLowerCase())
      );
      if (product) {
        priceHistory.push({
          date: scrape.date,
          price: product.price,
          ...product
        });
      }
    }

    if (priceHistory.length === 0) {
      return res.status(404).json({ error: 'Watch not found', reference });
    }

    res.json({
      success: true,
      reference,
      currentPrice: priceHistory[priceHistory.length - 1].price,
      priceHistory
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to load Omega prices', details: error.message });
  }
});

// Swagger UI options
const swaggerOptions = {
  customCss: '.swagger-ui .topbar { display: none }',
  customSiteTitle: 'Omega Watch Price History API',
  customfavIcon: '/favicon.ico'
};

// Serve Swagger UI at /api/docs/swagger
app.use('/api/docs/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions));

// OpenAPI JSON endpoint
app.get('/api/docs/openapi.json', (req, res) => {
  res.json(swaggerDocument);
});

// OpenAPI YAML endpoint
app.get('/api/docs/openapi.yaml', (req, res) => {
  res.setHeader('Content-Type', 'text/yaml');
  res.send(fs.readFileSync(path.join(__dirname, 'openapi.yaml'), 'utf8'));
});

// API Documentation endpoint (legacy - redirect to Swagger)
app.get('/api/docs', (req, res) => {
  res.redirect('/api/docs/swagger');
});

// 404 guard: unknown /api/* paths must return JSON 404, not the SPA shell
app.use('/api', (req, res) => {
  res.status(404).json({ error: 'Not Found', path: req.originalUrl });
});

// Fallback route - serve React app with SEO-friendly headers
app.get('*', (req, res) => {
  res.set({
    'Cache-Control': 'no-cache, must-revalidate',
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'SAMEORIGIN',
    'X-XSS-Protection': '1; mode=block'
  });
  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

// Start server
httpServer.listen(PORT, '0.0.0.0', () => {
  console.log(`
╔════════════════════════════════════════════════════════════════╗
║  OMEGA WATCH PRICE HISTORY - SEO OPTIMIZED                     ║
╚════════════════════════════════════════════════════════════════╝

🚀 Server running on port ${PORT}
📊 Dashboard: http://45.61.58.125:${PORT}
🔌 API: http://45.61.58.125:${PORT}/api
📖 API Docs: http://45.61.58.125:${PORT}/api/docs
🌐 WebSocket: ws://45.61.58.125:${PORT}
🗺️  Sitemap: http://45.61.58.125:${PORT}/sitemap.xml
🤖 Robots: http://45.61.58.125:${PORT}/robots.txt

SEO FEATURES:
✓ Dynamic meta tags & structured data
✓ Sitemap.xml with 32 watch pages
✓ Robots.txt for crawler control
✓ Open Graph & Twitter Cards
✓ Performance optimization headers
✓ Semantic HTML5 structure
✓ Mobile-first responsive design
✓ Core Web Vitals optimized

PRODUCTION READY!
  `);
});