/**
 * BraceFlow DEV API Server
 * Express server that replaces AWS Lambda functions for local development
 */
import express from 'express';
import cors from 'cors';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import crypto from 'crypto';
import { fileURLToPath } from 'url';
// Note: Using undici for HTTP requests and FormData (native fetch + FormData)
// Do NOT import 'form-data' package - it conflicts with native FormData
import undici from 'undici';
const { fetch: undiciFetch, FormData: UndiciFormData, File: UndiciFile } = undici;

import db from './db/sqlite.js';

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

// Configuration
const PORT = process.env.PORT || 3001;
const BRACE_GENERATOR_URL = process.env.BRACE_GENERATOR_URL || 'http://localhost:8001';
// Use DATA_DIR from env (Docker) or local path (dev)
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data');
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads');
const OUTPUTS_DIR = path.join(DATA_DIR, 'outputs');

// PUBLIC_URL for constructing URLs in responses
// In production, this should be empty to use relative URLs
// In local dev, set to http://localhost:3001
const PUBLIC_URL = process.env.PUBLIC_URL || '';

/**
 * Get base URL for constructing file/API URLs
 * In production: returns empty string (uses relative URLs)
 * In local dev: returns http://localhost:PORT
 */
function getBaseUrl(req) {
  if (PUBLIC_URL) return PUBLIC_URL;
  
  // In local development, return absolute URL with port
  // This is needed for CORS when frontend runs on different port (e.g., 5173)
  if (process.env.NODE_ENV !== 'production' && req) {
    const protocol = req.protocol || 'http';
    const host = req.get('host') || `localhost:${PORT}`;
    return `${protocol}://${host}`;
  }
  
  // In production behind proxy, use relative URLs
  return '';
}

// Ensure directories exist
[DATA_DIR, UPLOADS_DIR, OUTPUTS_DIR].forEach(dir => {
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
});

// Middleware - CORS: Allow ALL origins, methods, headers
app.use(cors({
  origin: '*',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin'],
  credentials: false,
  maxAge: 86400
}));

// Handle preflight requests for all routes
app.options('*', cors());

app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));

// Static file serving for outputs with proper MIME types for 3D files
const customMimeTypes = {
  '.glb': 'model/gltf-binary',
  '.gltf': 'model/gltf+json',
  '.stl': 'application/octet-stream',
  '.obj': 'text/plain',
  '.ply': 'application/octet-stream'
};

app.use('/files', express.static(DATA_DIR, {
  setHeaders: (res, filePath) => {
    const ext = path.extname(filePath).toLowerCase();
    if (customMimeTypes[ext]) {
      res.setHeader('Content-Type', customMimeTypes[ext]);
    }
  }
}));

// File upload configuration
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const caseId = req.params.caseId || req.body.caseId || 'temp';
    const caseDir = path.join(UPLOADS_DIR, caseId);
    if (!fs.existsSync(caseDir)) {
      fs.mkdirSync(caseDir, { recursive: true });
    }
    cb(null, caseDir);
  },
  filename: (req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, `xray${ext}`);
  }
});
const upload = multer({ storage, limits: { fileSize: 50 * 1024 * 1024 } });

// ============================================
// API Routes
// ============================================

/**
 * Health check
 */
app.get('/api/health', (req, res) => {
  res.json({ 
    status: 'ok', 
    service: 'braceflow-api-dev',
    timestamp: new Date().toISOString()
  });
});

/**
 * Create a new case
 * POST /api/cases
 */
app.post('/api/cases', (req, res) => {
  try {
    const { caseType = 'braceflow', notes = null } = req.body;
    
    // Generate case ID
    const yyyymmdd = new Date().toISOString().slice(0, 10).replace(/-/g, '');
    const rand = crypto.randomBytes(4).toString('hex');
    const caseId = `case-${yyyymmdd}-${rand}`;
    
    const result = db.createCase(caseId, caseType, notes);
    
    // Create case directories
    const caseUploadDir = path.join(UPLOADS_DIR, caseId);
    const caseOutputDir = path.join(OUTPUTS_DIR, caseId);
    fs.mkdirSync(caseUploadDir, { recursive: true });
    fs.mkdirSync(caseOutputDir, { recursive: true });
    
    res.status(201).json(result);
  } catch (err) {
    console.error('Create case error:', err);
    res.status(500).json({ message: 'Failed to create case', error: err.message });
  }
});

/**
 * List all cases
 * GET /api/cases
 */
app.get('/api/cases', (req, res) => {
  try {
    const cases = db.listCases();
    res.json(cases);
  } catch (err) {
    console.error('List cases error:', err);
    res.status(500).json({ message: 'Failed to list cases', error: err.message });
  }
});

/**
 * Get case by ID
 * GET /api/cases/:caseId
 */
app.get('/api/cases/:caseId', (req, res) => {
  try {
    const { caseId } = req.params;
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    const baseUrl = getBaseUrl(req);
    
    // Add xray_url if file exists
    const caseUploadDir = path.join(UPLOADS_DIR, caseId);
    if (fs.existsSync(caseUploadDir)) {
      const files = fs.readdirSync(caseUploadDir);
      const xrayFile = files.find(f => f.startsWith('xray'));
      if (xrayFile) {
        caseData.xray_url = `${baseUrl}/files/uploads/${caseId}/${xrayFile}`;
      }
    }
    
    // Add visualization_url if file exists
    const vizPath = path.join(OUTPUTS_DIR, caseId, 'visualization.png');
    if (fs.existsSync(vizPath)) {
      caseData.visualization_url = `${baseUrl}/files/outputs/${caseId}/visualization.png`;
    }
    
    // Add landmarks_json_url if file exists
    const landmarksPath = path.join(OUTPUTS_DIR, caseId, 'landmarks.json');
    if (fs.existsSync(landmarksPath)) {
      caseData.landmarks_json_url = `${baseUrl}/files/outputs/${caseId}/landmarks.json`;
    }
    
    res.json(caseData);
  } catch (err) {
    console.error('Get case error:', err);
    res.status(500).json({ message: 'Failed to get case', error: err.message });
  }
});

/**
 * Get case status
 * GET /api/cases/:caseId/status
 */
app.get('/api/cases/:caseId/status', (req, res) => {
  try {
    const caseData = db.getCase(req.params.caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    res.json(caseData);
  } catch (err) {
    console.error('Get case status error:', err);
    res.status(500).json({ message: 'Failed to get case status', error: err.message });
  }
});

/**
 * Get upload URL (for S3 compatibility - returns direct upload endpoint)
 * POST /api/cases/:caseId/upload-url
 */
app.post('/api/cases/:caseId/upload-url', (req, res) => {
  try {
    const { caseId } = req.params;
    const { filename, contentType } = req.body;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // For DEV, return a URL that points to our direct upload endpoint
    // The frontend will PUT to this URL with the file
    const baseUrl = getBaseUrl(req);
    const uploadUrl = `${baseUrl}/api/cases/${caseId}/upload-direct`;
    const s3Key = `cases/${caseId}/input/${filename || 'ap.jpg'}`;
    
    res.json({
      url: uploadUrl,
      s3Key: s3Key
    });
  } catch (err) {
    console.error('Get upload URL error:', err);
    res.status(500).json({ message: 'Failed to get upload URL', error: err.message });
  }
});

/**
 * Direct upload (PUT) - for presigned URL compatibility
 * PUT /api/cases/:caseId/upload-direct
 */
app.put('/api/cases/:caseId/upload-direct', express.raw({ type: '*/*', limit: '50mb' }), (req, res) => {
  try {
    const { caseId } = req.params;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // Save the raw body as a file
    const caseUploadDir = path.join(UPLOADS_DIR, caseId);
    if (!fs.existsSync(caseUploadDir)) {
      fs.mkdirSync(caseUploadDir, { recursive: true });
    }
    
    // Determine extension from content-type
    const contentType = req.headers['content-type'] || 'image/jpeg';
    const ext = contentType.includes('png') ? '.png' : '.jpg';
    const filename = `xray${ext}`;
    const filePath = path.join(caseUploadDir, filename);
    
    fs.writeFileSync(filePath, req.body);
    
    res.status(200).send('OK');
  } catch (err) {
    console.error('Direct upload error:', err);
    res.status(500).json({ message: 'Failed to upload file', error: err.message });
  }
});

/**
 * Upload X-ray image (multipart form)
 * POST /api/cases/:caseId/upload
 */
app.post('/api/cases/:caseId/upload', upload.single('file'), (req, res) => {
  try {
    const { caseId } = req.params;
    
    if (!req.file) {
      return res.status(400).json({ message: 'No file uploaded' });
    }
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    const filePath = `/files/uploads/${caseId}/${req.file.filename}`;
    
    res.json({
      caseId,
      filename: req.file.filename,
      path: filePath,
      size: req.file.size
    });
  } catch (err) {
    console.error('Upload error:', err);
    res.status(500).json({ message: 'Failed to upload file', error: err.message });
  }
});

/**
 * Detect landmarks (Stage 1)
 * POST /api/cases/:caseId/detect-landmarks
 */
app.post('/api/cases/:caseId/detect-landmarks', async (req, res) => {
  const { caseId } = req.params;
  
  try {
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // Find the X-ray file
    const caseUploadDir = path.join(UPLOADS_DIR, caseId);
    const files = fs.readdirSync(caseUploadDir);
    const xrayFile = files.find(f => f.startsWith('xray'));
    
    if (!xrayFile) {
      return res.status(400).json({ message: 'No X-ray image found. Please upload first.' });
    }
    
    const xrayPath = path.join(caseUploadDir, xrayFile);
    
    // Update status
    db.updateCaseStatus(caseId, 'running', 'LandmarkDetection');
    db.updateStepStatus(caseId, 'LandmarkDetection', 'running');
    
    // Call brace generator for landmark detection only
    // Use undici FormData with File for proper multipart handling
    const formData = new UndiciFormData();
    const fileBuffer = fs.readFileSync(xrayPath);
    // Use File instead of Blob for proper filename handling
    const file = new File([fileBuffer], xrayFile, { type: 'image/jpeg' });
    formData.append('file', file);
    formData.append('case_id', caseId);
    formData.append('detect_only', 'true');
    
    console.log(`Calling brace generator: ${BRACE_GENERATOR_URL}/detect-landmarks`);
    
    const response = await undiciFetch(`${BRACE_GENERATOR_URL}/detect-landmarks`, {
      method: 'POST',
      body: formData
    });
    
    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Brace generator error: ${errorText}`);
    }
    
    const result = await response.json();
    
    const baseUrl = getBaseUrl(req);
    
    // Save visualization to outputs
    if (result.visualization_path) {
      const vizResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${path.basename(result.visualization_path)}`);
      if (vizResponse.ok) {
        const vizBuffer = Buffer.from(await vizResponse.arrayBuffer());
        const vizPath = path.join(OUTPUTS_DIR, caseId, 'visualization.png');
        fs.mkdirSync(path.dirname(vizPath), { recursive: true });
        fs.writeFileSync(vizPath, vizBuffer);
        result.visualization_url = `${baseUrl}/files/outputs/${caseId}/visualization.png`;
      }
    }
    
    // Save JSON to outputs
    const jsonPath = path.join(OUTPUTS_DIR, caseId, 'landmarks.json');
    fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2));
    result.json_url = `${baseUrl}/files/outputs/${caseId}/landmarks.json`;
    
    // Save to database
    db.saveLandmarks(caseId, result);
    db.updateStepStatus(caseId, 'LandmarkDetection', 'done');
    
    res.json({
      caseId,
      status: 'landmarks_detected',
      ...result
    });
    
  } catch (err) {
    console.error('Detect landmarks error:', err);
    db.updateStepStatus(caseId, 'LandmarkDetection', 'failed', err.message);
    db.updateCaseStatus(caseId, 'failed');
    res.status(500).json({ message: 'Failed to detect landmarks', error: err.message });
  }
});

/**
 * Save/update landmarks (manual edit)
 * PUT /api/cases/:caseId/landmarks
 */
app.put('/api/cases/:caseId/landmarks', (req, res) => {
  try {
    const { caseId } = req.params;
    const { landmarks_data } = req.body;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    db.saveLandmarks(caseId, landmarks_data);
    
    // Save updated JSON
    const jsonPath = path.join(OUTPUTS_DIR, caseId, 'landmarks.json');
    fs.writeFileSync(jsonPath, JSON.stringify(landmarks_data, null, 2));
    
    res.json({ caseId, status: 'landmarks_updated' });
  } catch (err) {
    console.error('Update landmarks error:', err);
    res.status(500).json({ message: 'Failed to update landmarks', error: err.message });
  }
});

/**
 * Approve landmarks and move to analysis (Stage 2)
 * POST /api/cases/:caseId/approve-landmarks
 */
app.post('/api/cases/:caseId/approve-landmarks', (req, res) => {
  try {
    const { caseId } = req.params;
    const { updated_landmarks } = req.body;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    db.approveLandmarks(caseId, updated_landmarks);
    db.updateStepStatus(caseId, 'LandmarkApproval', 'done');
    
    res.json({ caseId, status: 'landmarks_approved', next_step: 'SpineAnalysis' });
  } catch (err) {
    console.error('Approve landmarks error:', err);
    res.status(500).json({ message: 'Failed to approve landmarks', error: err.message });
  }
});

/**
 * Recalculate spine analysis from landmarks
 * POST /api/cases/:caseId/recalculate
 */
app.post('/api/cases/:caseId/recalculate', async (req, res) => {
  const { caseId } = req.params;
  
  try {
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    if (!caseData.landmarks_data) {
      return res.status(400).json({ message: 'No landmarks data found' });
    }
    
    db.updateStepStatus(caseId, 'SpineAnalysis', 'running');
    
    // Call brace generator to recalculate
    const response = await fetch(`${BRACE_GENERATOR_URL}/recalculate`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        case_id: caseId,
        landmarks: caseData.landmarks_data
      })
    });
    
    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Recalculation error: ${errorText}`);
    }
    
    const result = await response.json();
    
    // Save analysis data separately (for Stage 2 display)
    db.saveAnalysisData(caseId, result);
    
    // Also update landmarks_data with new Cobb angles so it persists
    const updatedLandmarks = {
      ...caseData.landmarks_data,
      cobb_angles: result.cobb_angles,
      rigo_classification: result.rigo_classification,
      curve_type: result.curve_type
    };
    db.saveLandmarks(caseId, updatedLandmarks);
    
    db.updateStepStatus(caseId, 'SpineAnalysis', 'done');
    
    // Save JSON to outputs
    const caseOutputDir = path.join(OUTPUTS_DIR, caseId);
    fs.mkdirSync(caseOutputDir, { recursive: true });
    const jsonPath = path.join(caseOutputDir, 'analysis.json');
    fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2));
    
    res.json({
      caseId,
      status: 'analysis_complete',
      ...result
    });
    
  } catch (err) {
    console.error('Recalculate error:', err);
    db.updateStepStatus(caseId, 'SpineAnalysis', 'failed', err.message);
    res.status(500).json({ message: 'Failed to recalculate', error: err.message });
  }
});

/**
 * Generate brace (Stage 4)
 * POST /api/cases/:caseId/generate-brace
 * 
 * If body scan is available, uses EXPERIMENT_10 (body-fitted brace)
 * Otherwise, uses EXPERIMENT_3 (X-ray only adaptive brace)
 */
app.post('/api/cases/:caseId/generate-brace', async (req, res) => {
  const { caseId } = req.params;
  
  try {
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // Find the X-ray file
    const caseUploadDir = path.join(UPLOADS_DIR, caseId);
    const files = fs.readdirSync(caseUploadDir);
    const xrayFile = files.find(f => f.startsWith('xray'));
    
    if (!xrayFile) {
      return res.status(400).json({ message: 'No X-ray image found' });
    }
    
    const xrayPath = path.join(caseUploadDir, xrayFile);
    
    // Check if body scan should be used (based on database state, not just file existence)
    // This ensures "skip body scan" works correctly even if a file was uploaded earlier
    const hasBodyScanInDb = caseData.body_scan_path && caseData.body_scan_url;
    const bodyScanFile = hasBodyScanInDb ? files.find(f => f.startsWith('body_scan')) : null;
    const hasBodyScan = !!bodyScanFile;
    const bodyScanPath = hasBodyScan ? path.join(caseUploadDir, bodyScanFile) : null;
    
    db.updateCaseStatus(caseId, 'processing_brace', 'BraceGeneration');
    db.updateStepStatus(caseId, 'BraceGeneration', 'running');
    
    let result;
    
    if (hasBodyScan) {
      // Use EXPERIMENT_10: Body-fitted brace generation
      console.log(`Calling brace generator with body scan: ${BRACE_GENERATOR_URL}/generate-with-body`);
      
      // Use undici FormData with File for proper multipart handling
      const formData = new UndiciFormData();
      
      // Add X-ray file
      const xrayBuffer = fs.readFileSync(xrayPath);
      const xrayFileObj = new File([xrayBuffer], xrayFile, { type: 'image/jpeg' });
      formData.append('xray_file', xrayFileObj);
      
      // Add body scan file
      const bodyScanBuffer = fs.readFileSync(bodyScanPath);
      const bodyScanFileObj = new File([bodyScanBuffer], bodyScanFile, { type: 'application/octet-stream' });
      formData.append('body_scan_file', bodyScanFileObj);
      
      formData.append('case_id', caseId);
      
      // Include landmarks if manually edited
      if (caseData.landmarks_data) {
        formData.append('landmarks_json', JSON.stringify(caseData.landmarks_data));
      }
      
      // Add clearance from body scan metadata if available
      const clearance = caseData.body_scan_metadata?.clearance_mm || 8.0;
      formData.append('clearance_mm', clearance.toString());
      
      const response = await undiciFetch(`${BRACE_GENERATOR_URL}/generate-with-body`, {
        method: 'POST',
        body: formData
      });
      
      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Body-fitted brace generator error: ${errorText}`);
      }
      
      result = await response.json();
      result.generation_mode = 'body_fitted';
      
    } else {
      // Use EXPERIMENT_3: X-ray only adaptive brace
      console.log(`Calling brace generator (X-ray only): ${BRACE_GENERATOR_URL}/analyze/upload`);
      
      // Use undici FormData with File for proper multipart handling
      const formData = new UndiciFormData();
      const xrayBuffer = fs.readFileSync(xrayPath);
      const xrayFileObj = new File([xrayBuffer], xrayFile, { type: 'image/jpeg' });
      formData.append('file', xrayFileObj);
      formData.append('case_id', caseId);
      formData.append('experiment', 'experiment_3');
      
      // Include landmarks if manually edited
      if (caseData.landmarks_data) {
        formData.append('landmarks_json', JSON.stringify(caseData.landmarks_data));
      }
      
      const response = await undiciFetch(`${BRACE_GENERATOR_URL}/analyze/upload`, {
        method: 'POST',
        body: formData
      });
      
      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Brace generator error: ${errorText}`);
      }
      
      result = await response.json();
      result.generation_mode = 'xray_only';
    }
    
    // Download output files from brace generator
    const baseUrl = getBaseUrl(req);
    const outputFiles = {};
    const caseOutputDir = path.join(OUTPUTS_DIR, caseId);
    fs.mkdirSync(caseOutputDir, { recursive: true });
    
    for (const [key, serverPath] of Object.entries(result.outputs || {})) {
      if (!serverPath) continue;
      
      const filename = path.basename(serverPath);
      try {
        const fileResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${filename}`);
        if (fileResponse.ok) {
          const buffer = Buffer.from(await fileResponse.arrayBuffer());
          const localPath = path.join(caseOutputDir, filename);
          fs.writeFileSync(localPath, buffer);
          outputFiles[key] = `${baseUrl}/files/outputs/${caseId}/${filename}`;
        }
      } catch (e) {
        console.error(`Failed to download ${key}:`, e.message);
      }
    }
    
    // Also generate BOTH GLB braces (regular + vase) for the dual viewer
    const rigoType = result.rigo_classification?.type || result.rigo_type || 'A1';
    const cobbAngles = result.cobb_angles || {};
    
    console.log(`Generating both GLB braces: ${rigoType}`);
    
    try {
      const bothFormData = new UndiciFormData();
      bothFormData.append('rigo_type', rigoType);
      bothFormData.append('case_id', caseId);
      bothFormData.append('cobb_pt', String(cobbAngles.PT || 0));
      bothFormData.append('cobb_mt', String(cobbAngles.MT || 0));
      bothFormData.append('cobb_tl', String(cobbAngles.TL || 0));
      
      // Add body scan if available
      if (hasBodyScan && bodyScanPath && fs.existsSync(bodyScanPath)) {
        const bodyScanBuffer = fs.readFileSync(bodyScanPath);
        const bodyScanFileObj = new File([bodyScanBuffer], path.basename(bodyScanPath), { type: 'application/octet-stream' });
        bothFormData.append('body_scan', bodyScanFileObj);
      }
      
      const bothResponse = await undiciFetch(`${BRACE_GENERATOR_URL}/generate-both-braces`, {
        method: 'POST',
        body: bothFormData
      });
      
      if (bothResponse.ok) {
        const bothResult = await bothResponse.json();
        
        // Download both brace files via HTTP (they're in different container)
        const braces = {};
        for (const [braceType, braceData] of Object.entries(bothResult.braces || {})) {
          if (braceData.error) {
            braces[braceType] = { error: braceData.error };
            continue;
          }
          
          const outputs = {};
          
          // Download GLB via HTTP
          if (braceData.outputs?.glb) {
            const glbFilename = path.basename(braceData.outputs.glb);
            const glbName = `${caseId}_${rigoType}_${braceType}.glb`;
            try {
              const glbResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${glbFilename}`);
              if (glbResponse.ok) {
                const buffer = Buffer.from(await glbResponse.arrayBuffer());
                fs.writeFileSync(path.join(caseOutputDir, glbName), buffer);
                outputs.glb = `${baseUrl}/files/outputs/${caseId}/${glbName}`;
              }
            } catch (e) { console.warn(`Failed to download ${braceType} GLB:`, e.message); }
          }
          
          // Download STL via HTTP
          if (braceData.outputs?.stl) {
            const stlFilename = path.basename(braceData.outputs.stl);
            const stlName = `${caseId}_${rigoType}_${braceType}.stl`;
            try {
              const stlResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${stlFilename}`);
              if (stlResponse.ok) {
                const buffer = Buffer.from(await stlResponse.arrayBuffer());
                fs.writeFileSync(path.join(caseOutputDir, stlName), buffer);
                outputs.stl = `${baseUrl}/files/outputs/${caseId}/${stlName}`;
              }
            } catch (e) { console.warn(`Failed to download ${braceType} STL:`, e.message); }
          }
          
          // Download markers JSON via HTTP
          if (braceData.outputs?.json) {
            const jsonFilename = path.basename(braceData.outputs.json);
            const jsonName = `${caseId}_${rigoType}_${braceType}_markers.json`;
            try {
              const jsonResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${jsonFilename}`);
              if (jsonResponse.ok) {
                const buffer = Buffer.from(await jsonResponse.arrayBuffer());
                fs.writeFileSync(path.join(caseOutputDir, jsonName), buffer);
                outputs.json = `${baseUrl}/files/outputs/${caseId}/${jsonName}`;
              }
            } catch (e) { console.warn(`Failed to download ${braceType} markers:`, e.message); }
          }
          
          braces[braceType] = {
            outputs,
            markers: braceData.markers,
            pressureZones: braceData.pressure_zones,
            meshStats: braceData.mesh_stats
          };
        }
        
        // Add both braces to result
        result.braces = braces;
        console.log('Both GLB braces generated and downloaded successfully');
      } else {
        console.warn('Failed to generate both braces:', await bothResponse.text());
      }
    } catch (bothErr) {
      console.warn('Error generating both braces:', bothErr.message);
    }
    
    // Save result to database
    const braceResult = {
      ...result,
      outputs: outputFiles
    };
    
    // If we have both braces, use saveBothBracesResult
    if (result.braces) {
      const rigoType = result.rigo_classification?.type || result.rigo_type || 'A1';
      db.saveBothBracesResult(caseId, {
        braces: result.braces,
        rigoType: rigoType,
        cobbAngles: result.cobb_angles,
        bodyScanUsed: hasBodyScan
      });
    } else {
      db.saveBraceResult(caseId, braceResult);
    }
    db.updateStepStatus(caseId, 'BraceGeneration', 'done');
    
    res.json({
      caseId,
      status: 'brace_generated',
      ...braceResult
    });
    
  } catch (err) {
    console.error('Generate brace error:', err);
    db.updateStepStatus(caseId, 'BraceGeneration', 'failed', err.message);
    db.updateCaseStatus(caseId, 'brace_failed');
    res.status(500).json({ message: 'Failed to generate brace', error: err.message });
  }
});

/**
 * Get brace outputs
 * GET /api/cases/:caseId/brace-outputs
 */
app.get('/api/cases/:caseId/brace-outputs', (req, res) => {
  try {
    const { caseId } = req.params;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    const outputs = caseData.analysis_result?.brace?.outputs || {};
    
    res.json({
      caseId,
      status: caseData.status,
      outputs
    });
  } catch (err) {
    console.error('Get brace outputs error:', err);
    res.status(500).json({ message: 'Failed to get brace outputs', error: err.message });
  }
});

// ==============================================
// GLB BRACE GENERATION WITH MARKERS (NEW)
// ==============================================

/**
 * Get available brace templates
 * GET /api/templates
 */
app.get('/api/templates', async (req, res) => {
  try {
    const response = await fetch(`${BRACE_GENERATOR_URL}/templates`);
    const data = await response.json();
    res.json(data);
  } catch (err) {
    console.error('Get templates error:', err);
    res.status(500).json({ message: 'Failed to get templates', error: err.message });
  }
});

/**
 * Get template markers
 * GET /api/templates/:rigoType/markers
 */
app.get('/api/templates/:rigoType/markers', async (req, res) => {
  try {
    const { rigoType } = req.params;
    const { template_type = 'regular' } = req.query;
    
    const response = await fetch(
      `${BRACE_GENERATOR_URL}/templates/${rigoType}/markers?template_type=${template_type}`
    );
    const data = await response.json();
    
    if (!response.ok) {
      return res.status(response.status).json(data);
    }
    
    res.json(data);
  } catch (err) {
    console.error('Get template markers error:', err);
    res.status(500).json({ message: 'Failed to get markers', error: err.message });
  }
});

/**
 * Get pressure zone information
 * GET /api/pressure-zones/:rigoType
 */
app.get('/api/pressure-zones/:rigoType', async (req, res) => {
  try {
    const { rigoType } = req.params;
    const { template_type = 'regular', cobb_mt = 25, cobb_tl = 15 } = req.query;
    
    const response = await fetch(
      `${BRACE_GENERATOR_URL}/pressure-zones/${rigoType}?template_type=${template_type}&cobb_mt=${cobb_mt}&cobb_tl=${cobb_tl}`
    );
    const data = await response.json();
    
    if (!response.ok) {
      return res.status(response.status).json(data);
    }
    
    res.json(data);
  } catch (err) {
    console.error('Get pressure zones error:', err);
    res.status(500).json({ message: 'Failed to get pressure zones', error: err.message });
  }
});

/**
 * Generate GLB brace with markers
 * POST /api/cases/:caseId/generate-glb
 */
app.post('/api/cases/:caseId/generate-glb', upload.single('body_scan'), async (req, res) => {
  const { caseId } = req.params;
  
  try {
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // Get analysis data for Cobb angles
    // analysis_data may already be an object or a JSON string
    let analysisData = {};
    if (caseData.analysis_data) {
      if (typeof caseData.analysis_data === 'string') {
        try {
          analysisData = JSON.parse(caseData.analysis_data);
        } catch (e) {
          console.warn('Could not parse analysis_data:', e.message);
        }
      } else {
        analysisData = caseData.analysis_data;
      }
    }
    const cobbAngles = analysisData.cobb_angles || {};
    
    // Get Rigo type from analysis or request body
    const rigoType = req.body.rigo_type || analysisData.rigo_type || 'A1';
    const templateType = req.body.template_type || 'regular';
    
    // Create form data for brace generator (use UndiciFormData for proper compatibility)
    const formData = new UndiciFormData();
    
    formData.append('rigo_type', rigoType);
    formData.append('template_type', templateType);
    formData.append('case_id', caseId);
    formData.append('cobb_pt', String(cobbAngles.PT || 0));
    formData.append('cobb_mt', String(cobbAngles.MT || 0));
    formData.append('cobb_tl', String(cobbAngles.TL || 0));
    
    // Add body scan if provided in request or exists in case
    let bodyScanPath = req.file?.path;
    if (!bodyScanPath && caseData.body_scan_path) {
      bodyScanPath = caseData.body_scan_path;
    }
    
    if (bodyScanPath && fs.existsSync(bodyScanPath)) {
      const bodyScanBuffer = fs.readFileSync(bodyScanPath);
      const bodyScanFileObj = new File([bodyScanBuffer], path.basename(bodyScanPath), { type: 'application/octet-stream' });
      formData.append('body_scan', bodyScanFileObj);
    }
    
    // Call brace generator
    const response = await undiciFetch(`${BRACE_GENERATOR_URL}/generate-glb`, {
      method: 'POST',
      body: formData
    });
    
    const result = await response.json();
    
    if (!response.ok) {
      return res.status(response.status).json(result);
    }
    
    // Copy output files to case directory
    const caseOutputDir = path.join(OUTPUTS_DIR, caseId);
    if (!fs.existsSync(caseOutputDir)) {
      fs.mkdirSync(caseOutputDir, { recursive: true });
    }
    
    const outputs = {};
    
    // Copy GLB
    if (result.outputs?.glb && fs.existsSync(result.outputs.glb)) {
      const glbName = `${caseId}_${rigoType}_${templateType}.glb`;
      const glbDest = path.join(caseOutputDir, glbName);
      fs.copyFileSync(result.outputs.glb, glbDest);
      outputs.glb = `/files/outputs/${caseId}/${glbName}`;
    }
    
    // Copy STL
    if (result.outputs?.stl && fs.existsSync(result.outputs.stl)) {
      const stlName = `${caseId}_${rigoType}_${templateType}.stl`;
      const stlDest = path.join(caseOutputDir, stlName);
      fs.copyFileSync(result.outputs.stl, stlDest);
      outputs.stl = `/files/outputs/${caseId}/${stlName}`;
    }
    
    // Copy JSON
    if (result.outputs?.json && fs.existsSync(result.outputs.json)) {
      const jsonName = `${caseId}_${rigoType}_${templateType}_markers.json`;
      const jsonDest = path.join(caseOutputDir, jsonName);
      fs.copyFileSync(result.outputs.json, jsonDest);
      outputs.json = `/files/outputs/${caseId}/${jsonName}`;
    }
    
    res.json({
      caseId,
      rigoType,
      templateType,
      outputs,
      markers: result.markers,
      pressureZones: result.pressure_zones,
      meshStats: result.mesh_stats,
      bodyFitting: result.body_fitting
    });
    
  } catch (err) {
    console.error('Generate GLB error:', err);
    res.status(500).json({ message: 'Failed to generate GLB brace', error: err.message });
  }
});

/**
 * Generate both brace types (regular + vase) for comparison
 * POST /api/cases/:caseId/generate-both-braces
 */
app.post('/api/cases/:caseId/generate-both-braces', upload.single('body_scan'), async (req, res) => {
  const { caseId } = req.params;
  
  try {
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // Get analysis data for Cobb angles
    // analysis_data may already be an object or a JSON string
    let analysisData = {};
    if (caseData.analysis_data) {
      if (typeof caseData.analysis_data === 'string') {
        try {
          analysisData = JSON.parse(caseData.analysis_data);
        } catch (e) {
          console.warn('Could not parse analysis_data:', e.message);
        }
      } else {
        analysisData = caseData.analysis_data;
      }
    }
    const cobbAngles = analysisData.cobb_angles || {};
    
    // Get Rigo type from analysis or request body
    const rigoType = req.body.rigo_type || analysisData.rigo_classification?.type || analysisData.rigo_type || 'A1';
    
    console.log('Generating both braces:', { caseId, rigoType, cobbAngles });
    
    // Create form data for brace generator using undici FormData
    const formData = new UndiciFormData();

    formData.append('rigo_type', rigoType);
    formData.append('case_id', caseId);
    formData.append('cobb_pt', String(cobbAngles.PT || 0));
    formData.append('cobb_mt', String(cobbAngles.MT || 0));
    formData.append('cobb_tl', String(cobbAngles.TL || 0));
    
    // Add body scan if provided in request or exists in case
    let bodyScanPath = req.file?.path;
    if (!bodyScanPath && caseData.body_scan_path) {
      bodyScanPath = caseData.body_scan_path;
    }
    
    if (bodyScanPath && fs.existsSync(bodyScanPath)) {
      const bodyScanBuffer = fs.readFileSync(bodyScanPath);
      const bodyScanFileObj = new File([bodyScanBuffer], path.basename(bodyScanPath), { type: 'application/octet-stream' });
      formData.append('body_scan', bodyScanFileObj);
    }
    
    // Call brace generator using undici fetch
    const response = await undiciFetch(`${BRACE_GENERATOR_URL}/generate-both-braces`, {
      method: 'POST',
      body: formData
    });
    
    const result = await response.json();
    
    if (!response.ok) {
      return res.status(response.status).json(result);
    }
    
    // Copy output files to case directory
    const caseOutputDir = path.join(OUTPUTS_DIR, caseId);
    if (!fs.existsSync(caseOutputDir)) {
      fs.mkdirSync(caseOutputDir, { recursive: true });
    }
    
    const braces = {};
    const baseUrl = getBaseUrl(req);
    
    // Process both brace types - download files via HTTP since containers are separate
    for (const [braceType, braceData] of Object.entries(result.braces || {})) {
      if (braceData.error) {
        braces[braceType] = { error: braceData.error };
        continue;
      }
      
      const outputs = {};
      
      // Download GLB via HTTP
      if (braceData.outputs?.glb) {
        const glbFilename = path.basename(braceData.outputs.glb);
        const glbName = `${caseId}_${rigoType}_${braceType}.glb`;
        try {
          const glbResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${glbFilename}`);
          if (glbResponse.ok) {
            const buffer = Buffer.from(await glbResponse.arrayBuffer());
            fs.writeFileSync(path.join(caseOutputDir, glbName), buffer);
            outputs.glb = `/files/outputs/${caseId}/${glbName}`;
          }
        } catch (e) { console.warn(`Failed to download ${braceType} GLB:`, e.message); }
      }
      
      // Download STL via HTTP
      if (braceData.outputs?.stl) {
        const stlFilename = path.basename(braceData.outputs.stl);
        const stlName = `${caseId}_${rigoType}_${braceType}.stl`;
        try {
          const stlResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${stlFilename}`);
          if (stlResponse.ok) {
            const buffer = Buffer.from(await stlResponse.arrayBuffer());
            fs.writeFileSync(path.join(caseOutputDir, stlName), buffer);
            outputs.stl = `/files/outputs/${caseId}/${stlName}`;
          }
        } catch (e) { console.warn(`Failed to download ${braceType} STL:`, e.message); }
      }
      
      // Download markers JSON via HTTP
      if (braceData.outputs?.json) {
        const jsonFilename = path.basename(braceData.outputs.json);
        const jsonName = `${caseId}_${rigoType}_${braceType}_markers.json`;
        try {
          const jsonResponse = await fetch(`${BRACE_GENERATOR_URL}/download/${caseId}/${jsonFilename}`);
          if (jsonResponse.ok) {
            const buffer = Buffer.from(await jsonResponse.arrayBuffer());
            fs.writeFileSync(path.join(caseOutputDir, jsonName), buffer);
            outputs.json = `/files/outputs/${caseId}/${jsonName}`;
          }
        } catch (e) { console.warn(`Failed to download ${braceType} markers:`, e.message); }
      }
      
      braces[braceType] = {
        outputs,
        markers: braceData.markers,
        pressureZones: braceData.pressure_zones,
        meshStats: braceData.mesh_stats
      };
    }
    
    // Build the response
    const responseData = {
      caseId,
      rigoType,
      cobbAngles,
      bodyScanUsed: bodyScanPath !== null && bodyScanPath !== undefined,
      braces
    };
    
    // Save to database so it persists on refresh
    db.saveBothBracesResult(caseId, responseData);
    db.updateStepStatus(caseId, 'BraceGeneration', 'done');
    
    res.json(responseData);
    
  } catch (err) {
    console.error('Generate both braces error:', err);
    db.updateStepStatus(caseId, 'BraceGeneration', 'failed', err.message);
    res.status(500).json({ message: 'Failed to generate braces', error: err.message });
  }
});

/**
 * Save markers (for brace editor)
 * PUT /api/cases/:caseId/markers
 */
app.put('/api/cases/:caseId/markers', (req, res) => {
  try {
    const { caseId } = req.params;
    const { markers_data } = req.body;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    db.saveMarkers(caseId, markers_data);
    
    res.json({ caseId, status: 'markers_updated' });
  } catch (err) {
    console.error('Update markers error:', err);
    res.status(500).json({ message: 'Failed to update markers', error: err.message });
  }
});

// ==============================================
// BODY SCAN ENDPOINTS (NEW - Stage 3)
// ==============================================

/**
 * Upload body scan (STL/OBJ/PLY)
 * POST /api/cases/:caseId/body-scan
 */
app.post('/api/cases/:caseId/body-scan', upload.single('file'), async (req, res) => {
  const { caseId } = req.params;
  
  try {
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    if (!req.file) {
      return res.status(400).json({ message: 'No file uploaded' });
    }
    
    // Validate file type
    const allowedExtensions = ['.stl', '.obj', '.ply', '.glb', '.gltf'];
    const ext = path.extname(req.file.originalname).toLowerCase();
    if (!allowedExtensions.includes(ext)) {
      // Clean up uploaded file
      fs.unlinkSync(req.file.path);
      return res.status(400).json({ 
        message: `Invalid file type. Allowed: ${allowedExtensions.join(', ')}` 
      });
    }
    
    // Move file to case uploads directory
    const caseUploadDir = path.join(UPLOADS_DIR, caseId);
    fs.mkdirSync(caseUploadDir, { recursive: true });
    
    const destFilename = `body_scan${ext}`;
    const destPath = path.join(caseUploadDir, destFilename);
    
    // Remove existing body scan if any
    const existingFiles = fs.readdirSync(caseUploadDir).filter(f => f.startsWith('body_scan'));
    for (const f of existingFiles) {
      fs.unlinkSync(path.join(caseUploadDir, f));
    }
    
    // Move uploaded file
    fs.renameSync(req.file.path, destPath);
    
    const baseUrl = getBaseUrl(req);
    const scanUrl = `${baseUrl}/files/uploads/${caseId}/${destFilename}`;
    
    // Try to extract body measurements via brace generator
    let metadata = {
      file_format: ext.replace('.', ''),
      filename: destFilename,
      uploaded_at: new Date().toISOString()
    };
    
    try {
      // Call brace generator to extract body measurements
      const formData = new UndiciFormData();
      const fileBuffer = fs.readFileSync(destPath);
      const fileObj = new File([fileBuffer], destFilename, { type: 'application/octet-stream' });
      formData.append('file', fileObj);
      
      const measureResponse = await undiciFetch(`${BRACE_GENERATOR_URL}/extract-body-measurements`, {
        method: 'POST',
        body: formData
      });
      
      if (measureResponse.ok) {
        const measurements = await measureResponse.json();
        metadata = { ...metadata, ...measurements };
      }
    } catch (e) {
      console.log('Could not extract body measurements:', e.message);
      // Continue without measurements
    }
    
    // Save to database
    db.saveBodyScan(caseId, destPath, scanUrl, metadata);
    
    res.json({
      caseId,
      status: 'body_scan_uploaded',
      body_scan: {
        path: destPath,
        url: scanUrl,
        metadata
      }
    });
    
  } catch (err) {
    console.error('Upload body scan error:', err);
    res.status(500).json({ message: 'Failed to upload body scan', error: err.message });
  }
});

/**
 * Get body scan info
 * GET /api/cases/:caseId/body-scan
 * Auto-extracts measurements if missing
 */
app.get('/api/cases/:caseId/body-scan', async (req, res) => {
  try {
    const { caseId } = req.params;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    if (!caseData.body_scan_path) {
      return res.json({
        caseId,
        has_body_scan: false,
        body_scan: null
      });
    }
    
    // Ensure URL is absolute (might be stored as relative in older entries)
    let bodyScanUrl = caseData.body_scan_url;
    if (bodyScanUrl && !bodyScanUrl.startsWith('http')) {
      const baseUrl = getBaseUrl(req);
      bodyScanUrl = baseUrl + bodyScanUrl;
    }
    
    let metadata = caseData.body_scan_metadata || {};
    
    // Auto-extract measurements if missing and file exists
    if (!metadata.total_height_mm && fs.existsSync(caseData.body_scan_path)) {
      try {
        console.log(`Auto-extracting measurements for case ${caseId}`);
        const formData = new UndiciFormData();
        const fileBuffer = fs.readFileSync(caseData.body_scan_path);
        const filename = path.basename(caseData.body_scan_path);
        const fileObj = new File([fileBuffer], filename, { type: 'application/octet-stream' });
        formData.append('file', fileObj);
        
        const measureResponse = await undiciFetch(`${BRACE_GENERATOR_URL}/extract-body-measurements`, {
          method: 'POST',
          body: formData
        });
        
        if (measureResponse.ok) {
          const measurements = await measureResponse.json();
          metadata = { ...metadata, ...measurements };
          // Save updated metadata
          db.saveBodyScan(caseId, caseData.body_scan_path, caseData.body_scan_url, metadata);
          console.log(`Measurements extracted and saved for case ${caseId}`);
        }
      } catch (e) {
        console.log(`Could not auto-extract measurements: ${e.message}`);
      }
    }
    
    res.json({
      caseId,
      has_body_scan: true,
      body_scan: {
        path: caseData.body_scan_path,
        url: bodyScanUrl,
        metadata
      }
    });
    
  } catch (err) {
    console.error('Get body scan error:', err);
    res.status(500).json({ message: 'Failed to get body scan', error: err.message });
  }
});

/**
 * Re-extract body measurements from existing body scan
 * POST /api/cases/:caseId/body-scan/refresh-measurements
 */
app.post('/api/cases/:caseId/body-scan/refresh-measurements', async (req, res) => {
  const { caseId } = req.params;
  
  try {
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    if (!caseData.body_scan_path || !fs.existsSync(caseData.body_scan_path)) {
      return res.status(400).json({ message: 'No body scan found for this case' });
    }
    
    // Extract measurements from existing body scan
    const formData = new UndiciFormData();
    const fileBuffer = fs.readFileSync(caseData.body_scan_path);
    const filename = path.basename(caseData.body_scan_path);
    const fileObj = new File([fileBuffer], filename, { type: 'application/octet-stream' });
    formData.append('file', fileObj);
    
    const measureResponse = await undiciFetch(`${BRACE_GENERATOR_URL}/extract-body-measurements`, {
      method: 'POST',
      body: formData
    });
    
    if (!measureResponse.ok) {
      const errorText = await measureResponse.text();
      return res.status(500).json({ message: 'Failed to extract measurements', error: errorText });
    }
    
    const measurements = await measureResponse.json();
    
    // Merge with existing metadata
    const updatedMetadata = {
      ...(caseData.body_scan_metadata || {}),
      ...measurements
    };
    
    // Update database
    db.saveBodyScan(caseId, caseData.body_scan_path, caseData.body_scan_url, updatedMetadata);
    
    const baseUrl = getBaseUrl(req);
    let bodyScanUrl = caseData.body_scan_url;
    if (bodyScanUrl && !bodyScanUrl.startsWith('http')) {
      bodyScanUrl = baseUrl + bodyScanUrl;
    }
    
    res.json({
      caseId,
      status: 'measurements_updated',
      body_scan: {
        path: caseData.body_scan_path,
        url: bodyScanUrl,
        metadata: updatedMetadata
      }
    });
    
  } catch (err) {
    console.error('Refresh measurements error:', err);
    res.status(500).json({ message: 'Failed to refresh measurements', error: err.message });
  }
});

/**
 * Delete body scan (skip body scan stage)
 * DELETE /api/cases/:caseId/body-scan
 */
app.delete('/api/cases/:caseId/body-scan', (req, res) => {
  try {
    const { caseId } = req.params;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // Delete the file if it exists
    if (caseData.body_scan_path && fs.existsSync(caseData.body_scan_path)) {
      fs.unlinkSync(caseData.body_scan_path);
    }
    
    // Clear from database
    db.clearBodyScan(caseId);
    
    res.json({
      caseId,
      status: 'body_scan_removed',
      message: 'Body scan removed. Will generate brace from X-ray only.'
    });
    
  } catch (err) {
    console.error('Delete body scan error:', err);
    res.status(500).json({ message: 'Failed to delete body scan', error: err.message });
  }
});

/**
 * Skip body scan and proceed to brace generation
 * POST /api/cases/:caseId/skip-body-scan
 */
app.post('/api/cases/:caseId/skip-body-scan', (req, res) => {
  try {
    const { caseId } = req.params;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    // Clear any existing body scan data so we use X-ray only generation
    db.clearBodyScan(caseId);
    
    // Update step status to skip body scan
    db.updateStepStatus(caseId, 'BodyScanUpload', 'done');
    db.updateCaseStatus(caseId, 'analysis_complete', 'BraceGeneration');
    
    res.json({
      caseId,
      status: 'analysis_complete',
      message: 'Skipped body scan. Ready for brace generation.'
    });
    
  } catch (err) {
    console.error('Skip body scan error:', err);
    res.status(500).json({ message: 'Failed to skip body scan', error: err.message });
  }
});

// ==============================================
// END BODY SCAN ENDPOINTS
// ==============================================

/**
 * Delete case
 * DELETE /api/cases/:caseId
 */
app.delete('/api/cases/:caseId', (req, res) => {
  try {
    const { caseId } = req.params;
    
    // Delete from database
    db.deleteCase(caseId);
    
    // Delete files
    const uploadDir = path.join(UPLOADS_DIR, caseId);
    const outputDir = path.join(OUTPUTS_DIR, caseId);
    
    if (fs.existsSync(uploadDir)) {
      fs.rmSync(uploadDir, { recursive: true });
    }
    if (fs.existsSync(outputDir)) {
      fs.rmSync(outputDir, { recursive: true });
    }
    
    res.json({ caseId, deleted: true });
  } catch (err) {
    console.error('Delete case error:', err);
    res.status(500).json({ message: 'Failed to delete case', error: err.message });
  }
});

/**
 * Get download URL for case assets (S3 presigned URL compatibility)
 * GET /api/cases/:caseId/download-url?type=xray|landmarks|measurements
 */
app.get('/api/cases/:caseId/download-url', (req, res) => {
  try {
    const { caseId } = req.params;
    const { type = 'xray', view = 'ap' } = req.query;
    
    const caseData = db.getCase(caseId);
    if (!caseData) {
      return res.status(404).json({ message: 'Case not found' });
    }
    
    const baseUrl = getBaseUrl(req);
    let url = null;
    
    if (type === 'xray') {
      // Find X-ray file
      const uploadDir = path.join(UPLOADS_DIR, caseId);
      if (fs.existsSync(uploadDir)) {
        const files = fs.readdirSync(uploadDir);
        const xrayFile = files.find(f => f.match(/\.(jpg|jpeg|png)$/i));
        if (xrayFile) {
          url = `${baseUrl}/files/uploads/${caseId}/${xrayFile}`;
        }
      }
    } else if (type === 'landmarks') {
      const landmarksPath = path.join(OUTPUTS_DIR, caseId, 'landmarks.json');
      if (fs.existsSync(landmarksPath)) {
        url = `${baseUrl}/files/outputs/${caseId}/landmarks.json`;
      }
    } else if (type === 'visualization') {
      const vizPath = path.join(OUTPUTS_DIR, caseId, 'visualization.png');
      if (fs.existsSync(vizPath)) {
        url = `${baseUrl}/files/outputs/${caseId}/visualization.png`;
      }
    }
    
    if (!url) {
      return res.status(404).json({ message: `${type} file not found for case` });
    }
    
    res.json({ url });
  } catch (err) {
    console.error('Get download URL error:', err);
    res.status(500).json({ message: 'Failed to get download URL', error: err.message });
  }
});

/**
 * Get case assets (files)
 * GET /api/cases/:caseId/assets
 */
app.get('/api/cases/:caseId/assets', (req, res) => {
  try {
    const { caseId } = req.params;
    const baseUrl = getBaseUrl(req);
    
    const assets = {
      uploads: [],
      outputs: []
    };
    
    const uploadDir = path.join(UPLOADS_DIR, caseId);
    const outputDir = path.join(OUTPUTS_DIR, caseId);
    
    if (fs.existsSync(uploadDir)) {
      assets.uploads = fs.readdirSync(uploadDir).map(f => ({
        filename: f,
        url: `${baseUrl}/files/uploads/${caseId}/${f}`
      }));
    }
    
    if (fs.existsSync(outputDir)) {
      assets.outputs = fs.readdirSync(outputDir).map(f => ({
        filename: f,
        url: `${baseUrl}/files/outputs/${caseId}/${f}`
      }));
    }
    
    res.json({ caseId, assets });
  } catch (err) {
    console.error('Get assets error:', err);
    res.status(500).json({ message: 'Failed to get assets', error: err.message });
  }
});

// ============================================
// AUTHENTICATION API
// ============================================

/**
 * Login
 * POST /api/auth/login
 */
app.post('/api/auth/login', (req, res) => {
  try {
    const { username, password } = req.body;
    
    if (!username || !password) {
      return res.status(400).json({ message: 'Username and password are required' });
    }
    
    const user = db.getUserByUsername(username);
    
    if (!user) {
      return res.status(401).json({ message: 'Invalid username or password' });
    }
    
    if (!user.is_active) {
      return res.status(401).json({ message: 'Account is disabled' });
    }
    
    // Simple password check for dev (in production, use bcrypt.compare)
    if (user.password_hash !== password) {
      return res.status(401).json({ message: 'Invalid username or password' });
    }
    
    // Generate session token
    const token = crypto.randomBytes(32).toString('hex');
    const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24 hours
    
    db.createSession(user.id, token, expiresAt);
    db.updateLastLogin(user.id);
    db.logAudit(user.id, 'login', 'user', user.id.toString(), null, req.ip);
    
    res.json({
      token,
      user: {
        id: user.id,
        username: user.username,
        email: user.email,
        full_name: user.full_name,
        role: user.role
      },
      expiresAt
    });
  } catch (err) {
    console.error('Login error:', err);
    res.status(500).json({ message: 'Login failed', error: err.message });
  }
});

/**
 * Logout
 * POST /api/auth/logout
 */
app.post('/api/auth/logout', (req, res) => {
  try {
    const authHeader = req.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
      const token = authHeader.slice(7);
      const session = db.getSessionByToken(token);
      if (session) {
        db.logAudit(session.user_id, 'logout', 'user', session.user_id.toString(), null, req.ip);
        db.deleteSession(token);
      }
    }
    res.json({ message: 'Logged out successfully' });
  } catch (err) {
    console.error('Logout error:', err);
    res.status(500).json({ message: 'Logout failed', error: err.message });
  }
});

/**
 * Get current user
 * GET /api/auth/me
 */
app.get('/api/auth/me', (req, res) => {
  try {
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return res.status(401).json({ message: 'No token provided' });
    }
    
    const token = authHeader.slice(7);
    const session = db.getSessionByToken(token);
    
    if (!session) {
      return res.status(401).json({ message: 'Invalid or expired token' });
    }
    
    if (!session.is_active) {
      return res.status(401).json({ message: 'Account is disabled' });
    }
    
    res.json({
      user: {
        id: session.user_id,
        username: session.username,
        full_name: session.full_name,
        role: session.role
      }
    });
  } catch (err) {
    console.error('Get user error:', err);
    res.status(500).json({ message: 'Failed to get user', error: err.message });
  }
});

// ============================================
// AUTH MIDDLEWARE
// ============================================

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ message: 'Authentication required' });
  }
  
  const token = authHeader.slice(7);
  const session = db.getSessionByToken(token);
  
  if (!session) {
    return res.status(401).json({ message: 'Invalid or expired token' });
  }
  
  if (!session.is_active) {
    return res.status(401).json({ message: 'Account is disabled' });
  }
  
  req.user = {
    id: session.user_id,
    username: session.username,
    role: session.role,
    fullName: session.full_name
  };
  
  next();
}

function adminMiddleware(req, res, next) {
  if (req.user.role !== 'admin') {
    return res.status(403).json({ message: 'Admin access required' });
  }
  next();
}

// ============================================
// ADMIN API - USER MANAGEMENT
// ============================================

/**
 * List all users (admin only)
 * GET /api/admin/users
 */
app.get('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const users = db.listUsers();
    res.json({ users });
  } catch (err) {
    console.error('List users error:', err);
    res.status(500).json({ message: 'Failed to list users', error: err.message });
  }
});

/**
 * Get user by ID (admin only)
 * GET /api/admin/users/:userId
 */
app.get('/api/admin/users/:userId', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const user = db.getUserById(parseInt(req.params.userId));
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    res.json({ user });
  } catch (err) {
    console.error('Get user error:', err);
    res.status(500).json({ message: 'Failed to get user', error: err.message });
  }
});

/**
 * Create user (admin only)
 * POST /api/admin/users
 */
app.post('/api/admin/users', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const { username, password, email, fullName, role = 'user' } = req.body;
    
    if (!username || !password) {
      return res.status(400).json({ message: 'Username and password are required' });
    }
    
    // Check if username exists
    const existing = db.getUserByUsername(username);
    if (existing) {
      return res.status(400).json({ message: 'Username already exists' });
    }
    
    // In production, hash the password with bcrypt
    const user = db.createUser(username, password, email, fullName, role);
    db.logAudit(req.user.id, 'create_user', 'user', user.id.toString(), { username, role }, req.ip);
    
    res.status(201).json({ user });
  } catch (err) {
    console.error('Create user error:', err);
    res.status(500).json({ message: 'Failed to create user', error: err.message });
  }
});

/**
 * Update user (admin only)
 * PUT /api/admin/users/:userId
 */
app.put('/api/admin/users/:userId', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const userId = parseInt(req.params.userId);
    const { email, fullName, role, isActive, password } = req.body;
    
    const user = db.getUserById(userId);
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    const updates = {};
    if (email !== undefined) updates.email = email;
    if (fullName !== undefined) updates.fullName = fullName;
    if (role !== undefined) updates.role = role;
    if (isActive !== undefined) updates.isActive = isActive;
    if (password !== undefined) updates.passwordHash = password; // In production, hash with bcrypt
    
    db.updateUser(userId, updates);
    db.logAudit(req.user.id, 'update_user', 'user', userId.toString(), updates, req.ip);
    
    const updatedUser = db.getUserById(userId);
    res.json({ user: updatedUser });
  } catch (err) {
    console.error('Update user error:', err);
    res.status(500).json({ message: 'Failed to update user', error: err.message });
  }
});

/**
 * Delete user (admin only)
 * DELETE /api/admin/users/:userId
 */
app.delete('/api/admin/users/:userId', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const userId = parseInt(req.params.userId);
    
    // Prevent self-deletion
    if (userId === req.user.id) {
      return res.status(400).json({ message: 'Cannot delete your own account' });
    }
    
    const user = db.getUserById(userId);
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    db.deleteUser(userId);
    db.logAudit(req.user.id, 'delete_user', 'user', userId.toString(), { username: user.username }, req.ip);
    
    res.json({ message: 'User deleted successfully' });
  } catch (err) {
    console.error('Delete user error:', err);
    res.status(500).json({ message: 'Failed to delete user', error: err.message });
  }
});

// ============================================
// ADMIN API - CASES (WITH FILTERS)
// ============================================

/**
 * List all cases with filters (admin only)
 * GET /api/admin/cases
 */
app.get('/api/admin/cases', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const { status, createdBy, search, limit = 50, offset = 0, sortBy, sortOrder } = req.query;
    
    const result = db.listCasesFiltered({
      status,
      createdBy: createdBy ? parseInt(createdBy) : undefined,
      search,
      limit: parseInt(limit),
      offset: parseInt(offset),
      sortBy,
      sortOrder
    });
    
    res.json(result);
  } catch (err) {
    console.error('List cases error:', err);
    res.status(500).json({ message: 'Failed to list cases', error: err.message });
  }
});

// ============================================
// ADMIN API - ANALYTICS
// ============================================

/**
 * Get dashboard analytics (admin only)
 * GET /api/admin/analytics/dashboard
 */
app.get('/api/admin/analytics/dashboard', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const caseStats = db.getCaseStats();
    const userStats = db.getUserStats();
    const rigoDistribution = db.getRigoDistribution();
    const cobbStats = db.getCobbAngleStats();
    const processingStats = db.getProcessingTimeStats();
    const bodyScanStats = db.getBodyScanStats();
    
    res.json({
      cases: caseStats,
      users: userStats,
      rigoDistribution,
      cobbAngles: cobbStats,
      processingTime: processingStats,
      bodyScan: bodyScanStats
    });
  } catch (err) {
    console.error('Get analytics error:', err);
    res.status(500).json({ message: 'Failed to get analytics', error: err.message });
  }
});

/**
 * Get Rigo distribution (admin only)
 * GET /api/admin/analytics/rigo
 */
app.get('/api/admin/analytics/rigo', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const distribution = db.getRigoDistribution();
    res.json({ distribution });
  } catch (err) {
    console.error('Get Rigo distribution error:', err);
    res.status(500).json({ message: 'Failed to get Rigo distribution', error: err.message });
  }
});

/**
 * Get Cobb angle statistics (admin only)
 * GET /api/admin/analytics/cobb-angles
 */
app.get('/api/admin/analytics/cobb-angles', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const stats = db.getCobbAngleStats();
    res.json({ stats });
  } catch (err) {
    console.error('Get Cobb angle stats error:', err);
    res.status(500).json({ message: 'Failed to get Cobb angle stats', error: err.message });
  }
});

/**
 * Get processing time statistics (admin only)
 * GET /api/admin/analytics/processing-time
 */
app.get('/api/admin/analytics/processing-time', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const stats = db.getProcessingTimeStats();
    res.json({ stats });
  } catch (err) {
    console.error('Get processing time stats error:', err);
    res.status(500).json({ message: 'Failed to get processing time stats', error: err.message });
  }
});

// ============================================
// ADMIN API - AUDIT LOG
// ============================================

/**
 * Get audit log (admin only)
 * GET /api/admin/audit-log
 */
app.get('/api/admin/audit-log', authMiddleware, adminMiddleware, (req, res) => {
  try {
    const { userId, action, entityType, limit = 100, offset = 0 } = req.query;
    
    const entries = db.getAuditLog({
      userId: userId ? parseInt(userId) : undefined,
      action,
      entityType,
      limit: parseInt(limit),
      offset: parseInt(offset)
    });
    
    res.json({ entries });
  } catch (err) {
    console.error('Get audit log error:', err);
    res.status(500).json({ message: 'Failed to get audit log', error: err.message });
  }
});

// ============================================
// Start server
// ============================================
app.listen(PORT, () => {
  console.log('');
  console.log('============================================');
  console.log('BraceFlow DEV API Server');
  console.log('============================================');
  console.log(`Port:              ${PORT}`);
  console.log(`Brace Generator:   ${BRACE_GENERATOR_URL}`);
  console.log(`Data Directory:    ${DATA_DIR}`);
  console.log('============================================');
  console.log('');
  console.log('Endpoints:');
  console.log('  GET    /api/health');
  console.log('  POST   /api/cases                    Create case');
  console.log('  GET    /api/cases                    List cases');
  console.log('  GET    /api/cases/:id                Get case');
  console.log('  GET    /api/cases/:id/status         Get case status');
  console.log('  POST   /api/cases/:id/upload         Upload X-ray');
  console.log('  POST   /api/cases/:id/detect-landmarks  Stage 1: Detect');
  console.log('  PUT    /api/cases/:id/landmarks      Update landmarks');
  console.log('  POST   /api/cases/:id/approve-landmarks  Approve landmarks');
  console.log('  POST   /api/cases/:id/recalculate    Recalculate analysis');
  console.log('  POST   /api/cases/:id/generate-brace Stage 3: Generate');
  console.log('  GET    /api/cases/:id/brace-outputs  Get outputs');
  console.log('  PUT    /api/cases/:id/markers        Update markers');
  console.log('  DELETE /api/cases/:id                Delete case');
  console.log('  GET    /api/cases/:id/assets         Get files');
  console.log('');
  console.log('Auth Endpoints:');
  console.log('  POST   /api/auth/login               Login');
  console.log('  POST   /api/auth/logout              Logout');
  console.log('  GET    /api/auth/me                  Get current user');
  console.log('');
  console.log('Admin Endpoints (require admin role):');
  console.log('  GET    /api/admin/users              List users');
  console.log('  POST   /api/admin/users              Create user');
  console.log('  PUT    /api/admin/users/:id          Update user');
  console.log('  DELETE /api/admin/users/:id          Delete user');
  console.log('  GET    /api/admin/cases              List cases (filtered)');
  console.log('  GET    /api/admin/analytics/dashboard  Get dashboard stats');
  console.log('  GET    /api/admin/audit-log          Get audit log');
  console.log('');
});
