import express from 'express';
import multer from 'multer';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import sharp from 'sharp';
import { join, dirname, extname, basename, resolve, sep } from 'path';
import { fileURLToPath } from 'url';
import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'crypto';
import { networkInterfaces } from 'os';
import { spawn } from 'child_process';
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, unlinkSync, readdirSync, copyFileSync } from 'fs';
import { CONFIG_PATH, updateRuntimeConfig } from './lib/config-store.mjs';
import { deleteSecret, getSecret, hasSecret, listSecretMetadata, secretStoreStatus, setSecret } from './lib/secret-store.mjs';
import { getRequestTelemetry, recordSecurityBlock, requestContextMiddleware } from './lib/structured-logger.mjs';
import { createPlatformKernel } from './app/kernel/platform-kernel.mjs';
import { getSecurityFoundationStatus } from './app/modules/security/index.mjs';
import { objectSchema } from './app/kernel/input-schema.mjs';
import { getThemeContract } from './app/core/themes/theme-contract.mjs';
import { getThemeService } from './app/modules/themes/index.mjs';
import { configureAuthModule } from './app/modules/auth/index.mjs';
import { registerPublicAuthRoutes } from './app/modules/auth/routes.mjs';
import { registerUsersRoutes } from './app/modules/users/routes.mjs';
import { registerSessionRoutes } from './app/modules/sessions/routes.mjs';
import { configureSettingsModule } from './app/modules/settings/index.mjs';
import { registerSettingsRoutes } from './app/modules/settings/routes.mjs';
import { configureListingsModule } from './app/modules/listings/index.mjs';
import { listingsRepository } from './app/modules/listings/repository.mjs';
import { registerListingsRoutes } from './app/modules/listings/routes.mjs';
import { configureInventoryModule } from './app/modules/inventory/index.mjs';
import { inventoryRepository } from './app/modules/inventory/repository.mjs';
import { registerInventoryRoutes } from './app/modules/inventory/routes.mjs';
import { configureMediaModule } from './app/modules/media/index.mjs';
import { mediaRepository } from './app/modules/media/repository.mjs';
import { registerMediaRoutes } from './app/modules/media/routes.mjs';
import { registerModuleManagerRoutes } from './app/modules/module-manager/routes.mjs';
import { configureDeploymentsModule } from './app/modules/deployments/index.mjs';
import { registerDeploymentRoutes } from './app/modules/deployments/routes.mjs';
import { configureAiModule } from './app/modules/ai/index.mjs';
import { configureFluxStudioModule } from './app/modules/flux-studio/index.mjs';
import { configurePublishingModule } from './app/modules/publishing/index.mjs';
import {
  createUser, getUser, getUserByEmail, getUsers, updateUser, deleteUser,
  createSession, validateSession, destroySession, destroySessionById, destroyUserSessions,
  getUserSessions, getAllActiveSessions,
  createAuthToken, validateAuthToken,
  checkRateLimit, generateCsrfToken,
  auditLog, getAuditLog, searchAuditLog, getAuditLogForUser,
  recordLogin, getLoginHistory,
  isSetupMode, getUserCount,
  verifyPassword, setPassword, validatePasswordStrength, registerFailedLogin, clearFailedLogins, isUserLocked,
  cleanExpiredSessions,
} from './lib/auth.mjs';
import { initMailer, sendMagicLink, sendCredentials, isMailerConfigured, reinitMailer, getMailerConfig, testMailer } from './lib/mailer.mjs';
import {
  createListing, getListing, getListings, updateListing, deleteListing,
  softDeleteListing, restoreListing, getDeletedListings, permanentlyDeleteListing, cleanupTrash, getTrashCount,
  duplicateListing,
  getSettings, updateSettings,
  createTemplate, getTemplate, getTemplates, updateTemplate, deleteTemplate,
  getListingsForExport,
  createPublishLog, updatePublishLog, getPublishLogs, getPublishStatus,
  addToEbayQueue, getEbayQueue, updateEbayQueue, removeFromEbayQueue, clearEbayQueue,
  schedulePublish, getScheduledPublishes, getDueScheduled, updateScheduledPublish, removeScheduledPublish,
  getStorageLocations, getListingsByLocation, getUnassignedListings, bulkUpdateLocation,
  getDashboardWorkflow, getDashboardAnalytics,
  createMobileUploadSession, getMobileUploadSessionById, getMobileUploadSessionByTokenHash,
  appendMobileUploadFiles, updateMobileUploadSession, deleteMobileUploadSession, cleanupMobileUploadSessions,
  createAiModelGeneration, updateAiModelGeneration, createAiModelPhoto, getAiModelPhotosForListing,
  getAiModelGenerationsForListing, getAiModelPhotosForGeneration, getAiModelJob,
  createFluxImage, getFluxImages, paginateFluxImages, getFluxImage, setFluxImageFavorite, deleteFluxImage,
  getImageEditVersions, getImageEditRoot, createImageEditVersion,
  listMediaLibraryItems, setMediaLibraryFavorite, getListingReferencedMediaPaths, removeMediaLibraryRecords, db,
} from './lib/db.mjs';
import * as etsyApi from './lib/etsy-api.mjs';
import * as ebayApi from './lib/ebay-api.mjs';
import { wrapInTemplate, getHtmlTemplates, sanitizeRichHtml } from './lib/html-templates.mjs';
import { prepareForAI, removeBackground, addWatermark } from './lib/images.mjs';
import { generate, generateText, getProviders } from './lib/ai-router.mjs';
import { calculateFees, getAllFees } from './lib/fees.mjs';
import { createZip } from './lib/zip.mjs';
import {
  createBackup, listBackups, getBackupPath, verifyStoredBackup, deleteBackup, stageRestore,
  stageUpdate, getOperationsStatus, cancelPendingOperation, checkForUpdates,
  updateOperationSettings, recordExtensionHeartbeat, getExtensionDiagnostics,
  startAutomaticBackupLoop, operationsPaths,
} from './lib/operations-center.mjs';
import {
  analyzeListingQuality, listQualityCenter, getQualityHistory, compareQualityReports,
} from './lib/quality-center.mjs';
import { getCategories, getCategoriesFlat, matchCategories, getAllPlatformIds } from './lib/categories.mjs';
import { precheckAiModelPhotos, generateAiModelPhotos, fetchOpenRouterImageModels, getOpenRouterImageDiagnostics } from './lib/ai-model-photos.mjs';
import { getLocalImageStatus, getLocalFluxRuntimeInfo, generateLocalImageToken, ensureLocalFluxBootstrap } from './lib/ai-local-images.mjs';
import { renderImageVersion, resolveEditableImage } from './lib/image-editor.mjs';
import { enqueueAiModelJob, getAiModelJobWithRelations, startAiModelJobLoop, stopAiModelJobLoop } from './lib/ai-model-jobs.mjs';
import {
  enqueueFluxPromptJob, getFluxPromptJob, listFluxPromptJobs, paginateFluxPromptJobs, getFluxPromptJobSummary,
  cancelFluxPromptJob, retryFluxPromptJob, clearFluxPromptQueue, archiveFluxPromptJobs, restoreFluxPromptJobs,
  deleteArchivedFluxPromptJobs, startFluxPromptJobLoop, stopFluxPromptJobLoop,
} from './lib/ai-flux-prompt-jobs.mjs';
import {
  listImageBatchProfiles, createImageBatchProfile, updateImageBatchProfile, deleteImageBatchProfile,
  createImageBatchJob, getImageBatchJob, paginateImageBatchJobs, pauseImageBatchJob, resumeImageBatchJob,
  cancelImageBatchJob, retryImageBatchJob, retryImageBatchItem, deleteImageBatchJob, getImageBatchOutputPaths,
  startImageBatchJobLoop, stopImageBatchJobLoop,
} from './lib/image-batch-jobs.mjs';
import {
  enqueuePublishingJob, getPublishingJob, paginatePublishingJobs, listPublishingEvents,
  retryPublishingJob, cancelPublishingJob, deletePublishingJobs, claimExtensionJob, completeExtensionJob,
  configurePublishingProcessors, startPublishingJobLoop, stopPublishingJobLoop, recoverPublishingJobs, migrateLegacyEbayQueue, getPublishCapabilities,
} from './lib/publishing-jobs.mjs';
import {
  getRoleDefinitions, getPermissionCatalog, permissionsForRole, hasPermission, requiredPermissionForApi,
  acquireResourceLock, getResourceLock, listResourceLocks, renewResourceLock, releaseResourceLock,
  validateResourceLock, consumeRateLimit, getRateLimitStats,
} from './lib/security.mjs';
import {
  APP_VERSION, DB_PATH, UPLOADS_DIR, OPERATIONS_DIR, LOG_DIR,
  ensureRuntimeDirectories, verifyRuntimeWritable, runtimePathSummary,
} from './lib/runtime-paths.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = Number(process.env.PORT || 8124);
const modulePackageUpload = multer({
  storage: multer.memoryStorage(),
  fileFilter: (_req, file, cb) => {
    const ext = extname(file.originalname || '').toLowerCase();
    const allowedMime = ['application/json', 'application/octet-stream', 'application/vnd.vendoo.module+json'];
    cb(null, ext === '.vmod' || allowedMime.includes(String(file.mimetype || '').toLowerCase()));
  },
  limits: { files: 1, fileSize: 10 * 1024 * 1024 },
});
ensureRuntimeDirectories();
let shuttingDown = false;

configureAuthModule({
  setupTokenMatches(value) {
    const expectedText = String(process.env.VENDOO_SETUP_TOKEN || '').trim();
    if (!expectedText) return true;
    const provided = Buffer.from(String(value || ''));
    const expected = Buffer.from(expectedText);
    return provided.length === expected.length && timingSafeEqual(provided, expected);
  },
});
configureSettingsModule({ etsyApi, ebayApi });
configureListingsModule({ repository: listingsRepository });
configureInventoryModule({ repository: inventoryRepository });
configureMediaModule({ uploadRoot: UPLOADS_DIR, sharp, repository: mediaRepository });
configureAiModule({
  startModelJobs: startAiModelJobLoop,
  stopModelJobs: stopAiModelJobLoop,
  startBatchJobs: startImageBatchJobLoop,
  stopBatchJobs: stopImageBatchJobLoop,
});
configureFluxStudioModule({ start: startFluxPromptJobLoop, stop: stopFluxPromptJobLoop });
configureDeploymentsModule({ createBackup, checkForUpdates });
configurePublishingModule({
  async start() {
    const migration = migrateLegacyEbayQueue();
    if (migration.migrated) console.log(`Publishing: ${migration.migrated} alte eBay-Queue-Einträge übernommen`);
    recoverPublishingJobs();
    startPublishingJobLoop();
  },
  stop: stopPublishingJobLoop,
});

const platformKernel = createPlatformKernel({ version: APP_VERSION, hasPermission });
await platformKernel.start();

startAutomaticBackupLoop();
configurePublishingProcessors({
  async process(job, listing) {
    const payload = job.payload || {};
    const previous = db.prepare(`SELECT * FROM publishing_jobs WHERE listing_id = ? AND platform = ? AND id != ?
      AND status IN ('published','ended') ORDER BY completed_at DESC, created_at DESC LIMIT 1`).get(job.listing_id, job.platform, job.id);
    const offerId = job.offer_id || payload.offerId || previous?.offer_id || null;
    const externalId = job.external_id || payload.externalId || previous?.external_id || null;

    if (job.platform === 'ebay-de') {
      if (!ebayApi.isConnected()) throw new Error('eBay ist nicht verbunden');
      const sku = listing.sku || `AL-${listing.id}`;
      if (job.action === 'publish') return ebayApi.createAndPublish(listing, sku);
      if (job.action === 'update') {
        if (!offerId) throw new Error('Keine eBay Offer-ID für die Aktualisierung vorhanden');
        const offer = await ebayApi.updateOffer(listing, offerId, sku);
        return { offerId, externalId: offer?.listing?.listingId || externalId, url: externalId ? `https://www.ebay.de/itm/${externalId}` : null, status: 'published', offer };
      }
      if (job.action === 'end') {
        if (!offerId) throw new Error('Keine eBay Offer-ID zum Beenden vorhanden');
        const result = await ebayApi.withdrawOffer(offerId);
        return { offerId, externalId: result?.listingId || externalId, url: externalId ? `https://www.ebay.de/itm/${externalId}` : null, status: 'ended', result };
      }
      if (job.action === 'sync') {
        if (!offerId) throw new Error('Keine eBay Offer-ID für den Statusabgleich vorhanden');
        const offer = await ebayApi.getOffer(offerId);
        const status = String(offer?.status || '').toUpperCase() === 'PUBLISHED' ? 'published' : 'ended';
        const listingId = offer?.listing?.listingId || externalId;
        return { offerId, externalId: listingId, url: listingId ? `https://www.ebay.de/itm/${listingId}` : null, status, offer };
      }
    }

    if (job.platform === 'etsy') {
      if (!etsyApi.isConnected()) throw new Error('Etsy ist nicht verbunden');
      if (job.action === 'publish') {
        const shopId = await etsyApi.getShopId();
        const created = await etsyApi.createEtsyListing(listing, shopId);
        if (listing.photos?.length) {
          for (const photo of listing.photos) {
            const photoPath = join(UPLOADS_DIR, photo);
            if (!existsSync(photoPath)) continue;
            await etsyApi.uploadEtsyImage(shopId, created.listing_id, readFileSync(photoPath), photo);
          }
        }
        return { externalId: String(created.listing_id), url: `https://www.etsy.com/listing/${created.listing_id}`, status: created.state || 'draft', listing: created };
      }
      if (job.action === 'sync') {
        if (!externalId) throw new Error('Keine Etsy Listing-ID für den Statusabgleich vorhanden');
        const item = await etsyApi.getEtsyListing(externalId);
        const status = ['active', 'draft'].includes(item?.state) ? 'published' : 'ended';
        return { externalId: String(externalId), url: `https://www.etsy.com/listing/${externalId}`, status, listing: item };
      }
      throw new Error(`Aktion ${job.action} wird für Etsy derzeit nicht unterstützt`);
    }
    throw new Error(`Keine offizielle API-Verarbeitung für ${job.platform}`);
  },
});


function plainTextToSafeHtml(text) {
  const escaped = String(text || '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
  return escaped.split(/\n{2,}/).map(part => `<p>${part.replace(/\n/g, '<br>')}</p>`).join('');
}

function htmlToPlainText(html) {
  return String(html || '')
    .replace(/<\s*br\s*\/?>/gi, '\n')
    .replace(/<\/(p|div|h[1-6]|li|blockquote|pre)>/gi, '\n')
    .replace(/<[^>]+>/g, '')
    .replace(/&nbsp;/gi, ' ')
    .replace(/&amp;/gi, '&')
    .replace(/&lt;/gi, '<')
    .replace(/&gt;/gi, '>')
    .replace(/&quot;/gi, '"')
    .replace(/&#39;/gi, "'")
    .replace(/\n{3,}/g, '\n\n')
    .trim();
}

function normalizeListingPayload(payload = {}) {
  const normalized = { ...payload };
  if (normalized.description_html !== undefined) {
    normalized.description_html = sanitizeRichHtml(normalized.description_html);
    if (normalized.description === undefined) normalized.description = htmlToPlainText(normalized.description_html);
  }
  if (normalized.description !== undefined) normalized.description = String(normalized.description || '').trim();
  if (normalized.tags !== undefined && !Array.isArray(normalized.tags)) {
    normalized.tags = String(normalized.tags || '').split(',').map(tag => tag.trim()).filter(Boolean);
  }
  if (normalized.photos !== undefined && !Array.isArray(normalized.photos)) normalized.photos = [];
  return normalized;
}


function hashMobileUploadToken(token) {
  return createHash('sha256').update(String(token || '')).digest('hex');
}

function isMobileUploadSessionUsable(session) {
  if (!session || session.status !== 'active') return false;
  const expiresAt = Date.parse(String(session.expires_at || '').replace(' ', 'T') + 'Z');
  return Number.isFinite(expiresAt) && expiresAt > Date.now();
}

function getLanIpv4Addresses() {
  const addresses = [];
  const interfaces = networkInterfaces();
  for (const [name, entries] of Object.entries(interfaces)) {
    for (const entry of entries || []) {
      const family = typeof entry.family === 'string' ? entry.family : (entry.family === 4 ? 'IPv4' : String(entry.family));
      if (family !== 'IPv4' || entry.internal) continue;
      const address = String(entry.address || '').trim();
      if (!address || address.startsWith('169.254.')) continue;
      const isPrivate = /^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(address);
      const lowerName = String(name || '').toLowerCase();
      const isVirtual = /(virtual|vmware|hyper-v|vethernet|wsl|docker|vpn|tailscale|zerotier|loopback|bluetooth)/i.test(lowerName);
      const isPrimaryAdapter = /(ethernet|wlan|wi-fi|wifi|lan)/i.test(lowerName);
      const score = (isPrivate ? 100 : 0) + (isPrimaryAdapter ? 30 : 0) - (isVirtual ? 80 : 0);
      addresses.push({ name, address, isPrivate, isVirtual, score });
    }
  }
  addresses.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
  return addresses;
}

function resolvePublicBaseUrl(req) {
  const configured = String(process.env.PUBLIC_BASE_URL || '').trim().replace(/\/$/, '');
  if (configured) {
    try {
      const configuredUrl = new URL(configured);
      const configuredHost = configuredUrl.hostname.toLowerCase();
      if (!['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(configuredHost)) return configured;
    } catch {}
  }

  const forwardedProto = String(req.get('x-forwarded-proto') || '').split(',')[0].trim();
  const protocol = forwardedProto || req.protocol || 'http';
  const rawHost = String(req.get('x-forwarded-host') || req.get('host') || `localhost:${PORT}`).split(',')[0].trim();
  const hostOnly = rawHost.replace(/^\[/, '').replace(/\](:\d+)?$/, '').replace(/:\d+$/, '').toLowerCase();
  const isLoopback = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(hostOnly);
  if (!isLoopback) return `${protocol}://${rawHost}`.replace(/\/$/, '');

  const lan = getLanIpv4Addresses()[0];
  const portMatch = rawHost.match(/:(\d+)$/);
  const port = portMatch?.[1] || String(PORT);
  if (lan?.address) return `${protocol}://${lan.address}:${port}`;
  return `${protocol}://${rawHost}`.replace(/\/$/, '');
}

function parseTrustProxy(value) {
  const raw = String(value || 'false').trim().toLowerCase();
  if (['false', '0', 'no', 'off', ''].includes(raw)) return false;
  if (['true', 'yes', 'on'].includes(raw)) return 1;
  if (/^\d+$/.test(raw)) return Math.max(0, Number(raw));
  if (['loopback', 'linklocal', 'uniquelocal'].includes(raw)) return raw;
  throw new Error('VENDOO_TRUST_PROXY muss false, true, eine Hop-Anzahl oder loopback/linklocal/uniquelocal sein.');
}
const TRUST_PROXY = parseTrustProxy(process.env.VENDOO_TRUST_PROXY);
if (TRUST_PROXY !== false) app.set('trust proxy', TRUST_PROXY);

const SECURITY_CSP = {
  defaultSrc: ["'self'"],
  scriptSrc: ["'self'", 'https://cdn.jsdelivr.net'],
  scriptSrcAttr: ["'none'"],
  styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
  imgSrc: ["'self'", 'data:', 'blob:'],
  fontSrc: ["'self'", 'data:'],
  connectSrc: ["'self'", 'http://127.0.0.1:*', 'http://localhost:*'],
  objectSrc: ["'none'"],
  frameAncestors: ["'none'"],
  baseUri: ["'self'"],
  formAction: ["'self'"],
  upgradeInsecureRequests: process.env.NODE_ENV === 'production' ? [] : null,
};
for (const [key, value] of Object.entries(SECURITY_CSP)) if (value === null) delete SECURITY_CSP[key];
app.use(helmet({
  contentSecurityPolicy: { directives: SECURITY_CSP },
  crossOriginEmbedderPolicy: false,
  referrerPolicy: { policy: 'same-origin' },
  strictTransportSecurity: process.env.NODE_ENV === 'production' ? { maxAge: 15552000, includeSubDomains: true } : false,
}));
app.use(express.json({ limit: '2mb' }));
app.use(cookieParser());
app.use(requestContextMiddleware);
app.use((req, res, next) => {
  res.setHeader('Cache-Control', req.path.startsWith('/api/') || req.path.startsWith('/auth/') ? 'no-store' : 'private, max-age=0, must-revalidate');
  next();
});

function configuredAllowedOrigins() {
  return new Set(String(process.env.VENDOO_ALLOWED_ORIGINS || '')
    .split(',').map(value => value.trim().replace(/\/$/, '')).filter(Boolean));
}

function configuredExtensionOrigins() {
  return new Set(String(process.env.VENDOO_EXTENSION_ORIGINS || '')
    .split(',').map(value => value.trim().replace(/\/$/, '')).filter(Boolean));
}

function isAllowedOrigin(req, origin) {
  if (!origin) return true;
  const normalized = origin.replace(/\/$/, '');
  if (/^(chrome-extension|moz-extension|safari-web-extension):\/\//i.test(normalized)) {
    const allowAny = /^(1|true|yes)$/i.test(String(process.env.VENDOO_ALLOW_ANY_EXTENSION_ORIGIN || (process.env.NODE_ENV === 'production' ? 'false' : 'true')));
    return allowAny || configuredExtensionOrigins().has(normalized);
  }
  const ownOrigin = `${req.protocol}://${req.get('host')}`.replace(/\/$/, '');
  if (normalized === ownOrigin) return true;
  return configuredAllowedOrigins().has(normalized);
}

function isAllowedHost(hostHeader) {
  const raw = String(hostHeader || '').trim();
  let host = raw;
  if (raw.startsWith('[')) host = raw.slice(1, raw.indexOf(']') > 0 ? raw.indexOf(']') : undefined);
  else if ((raw.match(/:/g) || []).length === 1) host = raw.replace(/:\d+$/, '');
  host = host.toLowerCase();
  if (!host) return false;
  const configured = new Set(String(process.env.VENDOO_ALLOWED_HOSTS || '').split(',').map(value => value.trim().toLowerCase()).filter(Boolean));
  if (configured.has(host)) return true;
  if (['localhost', '127.0.0.1', '::1'].includes(host)) return true;
  if (/^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(host)) return true;
  try {
    const publicHost = new URL(String(process.env.PUBLIC_BASE_URL || '')).hostname.toLowerCase();
    if (publicHost && host === publicHost) return true;
  } catch {}
  return configured.size === 0 && !/^[a-z0-9.-]+$/i.test(host) ? false : configured.has(host);
}

app.use((req, res, next) => {
  if (!isAllowedHost(req.get('host'))) { recordSecurityBlock('host'); return res.status(421).json({ error: 'Host nicht freigegeben. VENDOO_ALLOWED_HOSTS prüfen.', request_id: req.requestId }); }
  const origin = req.get('origin');
  if (origin && !isAllowedOrigin(req, origin)) { recordSecurityBlock('origin'); return res.status(403).json({ error: 'Origin nicht freigegeben.', request_id: req.requestId }); }
  if (origin) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Vary', 'Origin');
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,X-CSRF-Token,X-Vendoo-Request-ID,X-Edit-Lock');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

app.get('/healthz', (_req, res) => {
  res.setHeader('Cache-Control', 'no-store');
  res.json({ ok: true, version: APP_VERSION, uptime_seconds: Math.floor(process.uptime()), time: new Date().toISOString() });
});

app.get('/readyz', async (_req, res) => {
  res.setHeader('Cache-Control', 'no-store');
  const checks = [];
  try {
    const dbCheck = db.prepare('SELECT 1 AS ok').get();
    checks.push({ name: 'database', ok: Number(dbCheck?.ok) === 1, path: DB_PATH });
  } catch (error) {
    checks.push({ name: 'database', ok: false, path: DB_PATH, error: error.message });
  }
  for (const item of verifyRuntimeWritable()) checks.push({ name: 'filesystem', ok: item.writable, ...item });
  try {
    const kernel = await platformKernel.health();
    checks.push({ name: 'platform-kernel', ok: kernel.ok, lifecycle: kernel.lifecycle.state, modules: kernel.modules.length });
  } catch (error) {
    checks.push({ name: 'platform-kernel', ok: false, error: error.message });
  }
  const ok = !shuttingDown && checks.every(item => item.ok !== false);
  res.status(ok ? 200 : 503).json({ ok, shutting_down: shuttingDown, version: APP_VERSION, checks, paths: runtimePathSummary(), time: new Date().toISOString() });
});

// Init mailer — check DB settings first, fallback to .env
function initMailerFromDbOrEnv() {
  try {
    const settings = getSettings();
    if (settings.smtp_pass && !hasSecret('SMTP_PASS')) {
      setSecret('SMTP_PASS', settings.smtp_pass, { source: 'legacy-db-migration' });
      updateSettings({ smtp_pass: '' });
    }
    const smtpSecret = getSecret('SMTP_PASS');
    if (settings.smtp_host && settings.smtp_user && smtpSecret) {
      const ok = reinitMailer({
        host: settings.smtp_host,
        port: settings.smtp_port || '587',
        user: settings.smtp_user,
        pass: smtpSecret,
      });
      if (settings.smtp_from) process.env.SMTP_FROM = settings.smtp_from;
      if (ok) return;
    }
  } catch {}
  initMailer();
}
initMailerFromDbOrEnv();

// --- Auth Middleware ---
function getClientIp(req) {
  return TRUST_PROXY ? String(req.ip || '') : String(req.socket?.remoteAddress || '');
}

function requireAuth(req, res, next) {
  if (isSetupMode()) {
    if (req.originalUrl.startsWith('/api')) return res.status(503).json({ error: 'Vendoo muss zuerst über /login.html eingerichtet werden.', setup_required: true });
    return res.redirect('/login.html');
  }

  const token = req.cookies?.auth_token;
  const session = validateSession(token);
  if (!session) {
    if (req.originalUrl.startsWith('/api')) return res.status(401).json({ error: 'Nicht eingeloggt' });
    return res.redirect('/login.html');
  }
  req.user = { id: session.user_id, email: session.email, name: session.name, role: session.role, avatar_color: session.avatar_color, permissions: [...permissionsForRole(session.role)] };
  next();
}

function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'Nicht eingeloggt' });
    if (!roles.includes(req.user.role)) return res.status(403).json({ error: 'Keine Berechtigung' });
    next();
  };
}

function csrfProtect(req, res, next) {
  if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
  const token = req.headers['x-csrf-token'] || req.body?._csrf;
  const cookie = req.cookies?.csrf_token;
  if (!token || !cookie || token !== cookie) {
    return res.status(403).json({ error: 'Ungültiger CSRF-Token' });
  }
  next();
}

function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'Nicht eingeloggt' });
    if (!hasPermission(req.user.role, permission)) {
      auditLog(req.user.id, req.user.email, 'security.permission_denied', 'route', req.path, JSON.stringify({ permission, method: req.method }), getClientIp(req));
      return res.status(403).json({ error: 'Diese Aktion ist für deine Rolle nicht freigegeben.', permission });
    }
    next();
  };
}

function apiPermissionGuard(req, res, next) {
  const routePath = String(req.originalUrl || req.path || '').split('?')[0];
  const permission = requiredPermissionForApi(req.method, routePath);
  if (!permission || hasPermission(req.user?.role, permission)) return next();
  auditLog(req.user?.id || null, req.user?.email || null, 'security.permission_denied', 'route', routePath, JSON.stringify({ permission, method: req.method }), getClientIp(req));
  return res.status(403).json({ error: 'Keine Berechtigung für diese Aktion.', permission });
}

function apiRateLimit(req, res, next) {
  const mutation = !['GET', 'HEAD', 'OPTIONS'].includes(req.method);
  const limit = mutation ? Number(process.env.VENDOO_MUTATION_RATE_LIMIT || 120) : Number(process.env.VENDOO_READ_RATE_LIMIT || 360);
  const key = `api:${req.user?.id || getClientIp(req)}:${mutation ? 'write' : 'read'}`;
  const result = consumeRateLimit(key, { limit: Math.max(20, limit), windowMs: 60_000 });
  res.setHeader('X-RateLimit-Remaining', String(result.remaining));
  if (!result.allowed) {
    res.setHeader('Retry-After', String(result.retryAfter));
    return res.status(429).json({ error: 'Zu viele Anfragen. Bitte kurz warten.', retry_after: result.retryAfter });
  }
  next();
}

function secureCookieOptions(req) {
  const forwardedHttps = TRUST_PROXY !== false && String(req.get('x-forwarded-proto') || '').split(',')[0].trim() === 'https';
  const secureMode = String(process.env.VENDOO_COOKIE_SECURE || 'auto').trim().toLowerCase();
  const detectedSecure = Boolean(req.secure || forwardedHttps || /^https:\/\//i.test(String(process.env.PUBLIC_BASE_URL || '')));
  const secure = secureMode === 'true' ? true : secureMode === 'false' ? false : detectedSecure;
  const sameSiteValue = String(process.env.VENDOO_COOKIE_SAMESITE || 'lax').trim().toLowerCase();
  const sameSite = ['lax', 'strict', 'none'].includes(sameSiteValue) ? sameSiteValue : 'lax';
  if (sameSite === 'none' && !secure) throw new Error('VENDOO_COOKIE_SAMESITE=none erfordert sichere HTTPS-Cookies.');
  const maxAge = Math.max(1, Number(process.env.VENDOO_SESSION_DAYS || 3)) * 24 * 60 * 60 * 1000;
  return { httpOnly: true, secure, sameSite, maxAge, path: '/' };
}

function csrfCookieOptions(req) {
  return { ...secureCookieOptions(req), httpOnly: false };
}

const SETUP_TOKEN = String(process.env.VENDOO_SETUP_TOKEN || '').trim();

// --- Public routes (no auth) ---
app.use('/login.html', express.static(join(__dirname, 'public', 'login.html')));

registerPublicAuthRoutes({
  app,
  routes: platformKernel.routes,
  deps: {
    setupTokenRequired: Boolean(SETUP_TOKEN), isMailerConfigured, requireAuth, csrfProtect, sendMagicLink,
    auditLog, getClientIp,
    setSessionCookies(req, res, session, csrf) {
      res.cookie('auth_token', session.token, secureCookieOptions(req));
      res.cookie('csrf_token', csrf, csrfCookieOptions(req));
    },
    clearSessionCookies(res) {
      res.clearCookie('auth_token');
      res.clearCookie('csrf_token');
    },
  },
});

// --- Mobile photo upload (public token page, no account login required) ---
app.get('/mobile-upload/:token', (req, res) => {
  const session = getMobileUploadSessionByTokenHash(hashMobileUploadToken(req.params.token));
  if (!isMobileUploadSessionUsable(session)) return res.status(410).sendFile(join(__dirname, 'public', 'mobile-upload-expired.html'));
  res.sendFile(join(__dirname, 'public', 'mobile-upload.html'));
});

app.get('/mobile-upload-api/:token', (req, res) => {
  const session = getMobileUploadSessionByTokenHash(hashMobileUploadToken(req.params.token));
  if (!isMobileUploadSessionUsable(session)) return res.status(410).json({ error: 'Upload-Link ist abgelaufen oder geschlossen.' });
  res.json({
    id: session.id,
    context: session.context,
    listing_id: session.listing_id,
    files: session.files || [],
    remaining: Math.max(0, 10 - (session.files || []).length),
    expires_at: session.expires_at,
  });
});

// --- Protected routes below ---
app.use('/api', requireAuth);
app.use('/api', apiRateLimit);
app.use('/api', apiPermissionGuard);
app.use('/auth/ebay', requireAuth, requirePermission('settings.manage'));
app.use('/auth/etsy', requireAuth, requirePermission('settings.manage'));

// CSRF for state-changing API calls
app.use('/api', csrfProtect);

function moduleOwnerForApiPath(pathname) {
  const path = String(pathname || '').toLowerCase();
  if (path.startsWith('/publishing') || path.startsWith('/publish') || path.startsWith('/ebay') || path.startsWith('/etsy') || path.startsWith('/extension/publishing') || path.startsWith('/schedule')) return 'vendoo.publishing';
  if (path.startsWith('/flux') || path.startsWith('/local-image')) return 'vendoo.flux-studio';
  if (path.startsWith('/quality')) return 'vendoo.quality';
  if (path.startsWith('/ai-model') || path.startsWith('/image-batch') || path === '/generate' || path.startsWith('/providers') || path.startsWith('/prompt-lab') || /^\/listings\/[^/]+\/ai-model/.test(path)) return 'vendoo.ai';
  return null;
}

app.use('/api', (req, res, next) => {
  const owner = moduleOwnerForApiPath(req.path);
  if (!owner || platformKernel.modules.isEnabled(owner)) return next();
  return res.status(503).json({ error: 'Dieses Vendoo-Modul ist deaktiviert.', code: 'MODULE_DISABLED', module: owner, request_id: req.requestId });
});

// Platform kernel contracts (new routes must be deny-by-default and explicitly owned)
platformKernel.routes.register(app, {
  id: 'platform.status.read',
  method: 'GET',
  path: '/api/security/platform',
  owner: 'vendoo.system',
  policy: 'platform.security-admin',
  audit: 'platform.status.read',
  rateLimit: 'administration',
  csrf: false,
  stability: 'internal',
  handler: async (_req, res) => res.json(platformKernel.snapshot()),
});

platformKernel.routes.register(app, {
  id: 'platform.modules.read',
  method: 'GET',
  path: '/api/security/platform/modules',
  owner: 'vendoo.system',
  policy: 'platform.security-admin',
  audit: 'platform.modules.read',
  rateLimit: 'administration',
  csrf: false,
  stability: 'internal',
  handler: async (_req, res) => res.json({ modules: platformKernel.modules.snapshot() }),
});

platformKernel.routes.register(app, {
  id: 'themes.contract.read',
  method: 'GET',
  path: '/api/security/platform/theme-contract',
  owner: 'vendoo.themes',
  policy: 'platform.security-admin',
  audit: 'themes.contract.read',
  rateLimit: 'administration',
  csrf: false,
  stability: 'internal',
  handler: async (_req, res) => res.json(getThemeContract()),
});

platformKernel.routes.register(app, {
  id: 'security.secrets.read', method: 'GET', path: '/api/security/secrets', owner: 'vendoo.security',
  policy: 'security.secrets.manage', audit: 'security.secrets.read', rateLimit: 'administration', csrf: false, stability: 'stable',
  handler: async (_req, res) => res.json({ secrets: listSecretMetadata(), store: secretStoreStatus() }),
});

platformKernel.routes.register(app, {
  id: 'security.secrets.update', method: 'PUT', path: '/api/security/secrets/:name', owner: 'vendoo.security',
  policy: 'security.secrets.manage', audit: 'security.secret.updated', rateLimit: 'administration', csrf: true, stability: 'stable',
  input: objectSchema({ value: { type: 'string', maxLength: 16384, trim: false } }),
  handler: async (req, res) => {
    const current = listSecretMetadata().find(item => item.name === req.params.name);
    if (!current || !current.editable) return res.status(400).json({ error: 'Dieses Secret kann nicht manuell geändert werden.', request_id: req.requestId });
    const metadata = setSecret(req.params.name, req.validatedBody?.value ?? '', { source: 'security-center' });
    auditLog(req.user.id, req.user.email, metadata.configured ? 'security.secret.updated' : 'security.secret.deleted', 'secret', metadata.name, null, getClientIp(req));
    res.json(metadata);
  },
});

platformKernel.routes.register(app, {
  id: 'security.secrets.delete', method: 'DELETE', path: '/api/security/secrets/:name', owner: 'vendoo.security',
  policy: 'security.secrets.manage', audit: 'security.secret.deleted', rateLimit: 'administration', csrf: true, stability: 'stable',
  handler: async (req, res) => {
    const current = listSecretMetadata().find(item => item.name === req.params.name);
    if (!current || !current.editable) return res.status(400).json({ error: 'Dieses Secret kann nicht manuell gelöscht werden.', request_id: req.requestId });
    const metadata = deleteSecret(req.params.name);
    auditLog(req.user.id, req.user.email, 'security.secret.deleted', 'secret', metadata.name, null, getClientIp(req));
    res.json(metadata);
  },
});

platformKernel.routes.register(app, {
  id: 'themes.catalog.read', method: 'GET', path: '/api/themes', owner: 'vendoo.themes',
  policy: 'themes.use', audit: 'themes.catalog.read', rateLimit: 'default', csrf: false, stability: 'stable',
  handler: async (req, res) => res.json(getThemeService().catalog(req.user.id)),
});

platformKernel.routes.register(app, {
  id: 'themes.validate', method: 'POST', path: '/api/themes/validate', owner: 'vendoo.themes',
  policy: 'themes.use', audit: 'themes.validated', rateLimit: 'administration', csrf: true, stability: 'stable',
  handler: async (req, res) => res.json(getThemeService().validate(req.body || {})),
});

platformKernel.routes.register(app, {
  id: 'themes.preference.update', method: 'PUT', path: '/api/themes/preference', owner: 'vendoo.themes',
  policy: 'themes.use', audit: 'themes.preference.updated', rateLimit: 'administration', csrf: true, stability: 'stable',
  handler: async (req, res) => {
    const preference = getThemeService().saveUserPreference(req.user.id, req.body || {});
    auditLog(req.user.id, req.user.email, 'theme.preference.updated', 'user', String(req.user.id), JSON.stringify(preference), getClientIp(req));
    res.json(preference);
  },
});

platformKernel.routes.register(app, {
  id: 'themes.profile.create', method: 'POST', path: '/api/themes', owner: 'vendoo.themes',
  policy: 'themes.manage', audit: 'themes.profile.created', rateLimit: 'administration', csrf: true, stability: 'stable',
  handler: async (req, res) => {
    const theme = getThemeService().create(req.body || {}, req.user.id);
    auditLog(req.user.id, req.user.email, 'theme.created', 'theme', theme.id, theme.name, getClientIp(req));
    res.status(201).json(theme);
  },
});

platformKernel.routes.register(app, {
  id: 'themes.system-default.update', method: 'PUT', path: '/api/themes/system-default', owner: 'vendoo.themes',
  policy: 'themes.manage', audit: 'themes.system-default.updated', rateLimit: 'administration', csrf: true, stability: 'stable',
  handler: async (req, res) => {
    const preference = getThemeService().saveSystemDefault(req.body || {});
    auditLog(req.user.id, req.user.email, 'theme.system_default.updated', 'system', 'theme', JSON.stringify(preference), getClientIp(req));
    res.json(preference);
  },
});



platformKernel.routes.register(app, {
  id: 'themes.profile.update', method: 'PUT', path: '/api/themes/:id', owner: 'vendoo.themes',
  policy: 'themes.manage', audit: 'themes.profile.updated', rateLimit: 'administration', csrf: true, stability: 'stable',
  handler: async (req, res) => {
    const theme = getThemeService().update(req.params.id, req.body || {}, req.user.id);
    auditLog(req.user.id, req.user.email, 'theme.updated', 'theme', theme.id, theme.name, getClientIp(req));
    res.json(theme);
  },
});

platformKernel.routes.register(app, {
  id: 'themes.profile.delete', method: 'DELETE', path: '/api/themes/:id', owner: 'vendoo.themes',
  policy: 'themes.manage', audit: 'themes.profile.deleted', rateLimit: 'administration', csrf: true, stability: 'stable',
  handler: async (req, res) => {
    const result = getThemeService().remove(req.params.id);
    auditLog(req.user.id, req.user.email, 'theme.deleted', 'theme', req.params.id, null, getClientIp(req));
    res.json(result);
  },
});

platformKernel.routes.register(app, {
  id: 'themes.profile.export', method: 'GET', path: '/api/themes/:id/export', owner: 'vendoo.themes',
  policy: 'themes.manage', audit: 'themes.profile.exported', rateLimit: 'administration', csrf: false, stability: 'stable',
  handler: async (req, res) => res.json(getThemeService().exportTheme(req.params.id)),
});

registerUsersRoutes({
  app, routes: platformKernel.routes,
  deps: { auditLog, getClientIp, sendMagicLink, sendCredentials },
});
registerSessionRoutes({
  app, routes: platformKernel.routes,
  deps: { auditLog, getClientIp },
});
registerSettingsRoutes({
  app, routes: platformKernel.routes,
  deps: { auditLog, getClientIp, isMailerConfigured, testMailer },
});
registerMediaRoutes({ app, routes: platformKernel.routes, deps: { auditLog, getClientIp } });
registerListingsRoutes({ app, routes: platformKernel.routes, deps: { auditLog, getClientIp, validateResourceLock } });
registerInventoryRoutes({ app, routes: platformKernel.routes, deps: { auditLog, getClientIp } });
registerModuleManagerRoutes({ app, routes: platformKernel.routes, upload: modulePackageUpload, deps: { auditLog, getClientIp } });
registerDeploymentRoutes({ app, routes: platformKernel.routes, deps: { auditLog, getClientIp } });


// --- Roles, permissions and server security ---
app.get('/api/security/roles', (_req, res) => {
  res.json({ roles: getRoleDefinitions(), permissions: getPermissionCatalog() });
});

app.get('/api/security/status', requirePermission('security.manage'), (req, res) => {
  const bindHost = String(process.env.VENDOO_BIND_HOST || '127.0.0.1').trim() || '127.0.0.1';
  const publicBaseUrl = String(process.env.PUBLIC_BASE_URL || '').trim();
  const remoteBinding = ['0.0.0.0', '::'].includes(bindHost);
  const publicHttps = /^https:\/\//i.test(publicBaseUrl);
  const warnings = [];
  if (remoteBinding && !publicHttps) warnings.push('Vendoo lauscht im Netzwerk, aber PUBLIC_BASE_URL verwendet kein HTTPS. Für öffentlichen Zugriff ist ein HTTPS-Reverse-Proxy zwingend.');
  if (remoteBinding && !String(process.env.VENDOO_ALLOWED_HOSTS || '').trim()) warnings.push('VENDOO_ALLOWED_HOSTS ist nicht gesetzt. Private IP-Adressen bleiben erlaubt, öffentliche Hostnamen nicht.');
  if (TRUST_PROXY && !publicHttps) warnings.push('VENDOO_TRUST_PROXY ist aktiv, obwohl keine öffentliche HTTPS-Basis-URL erkannt wurde.');
  if (String(process.env.VENDOO_ALLOWED_ORIGINS || '').includes('*')) warnings.push('Wildcard-Origin ist unsicher und wird von Vendoo nicht als vertrauenswürdig behandelt.');
  if (process.env.NODE_ENV === 'production' && /^(1|true|yes)$/i.test(String(process.env.VENDOO_ALLOW_ANY_EXTENSION_ORIGIN || 'false'))) warnings.push('Alle Browser-Erweiterungs-Origins sind freigegeben. Für Produktion VENDOO_EXTENSION_ORIGINS auf konkrete Extension-IDs begrenzen.');
  if (/^(0|false|no)$/i.test(String(process.env.SMTP_TLS_REJECT_UNAUTHORIZED || 'true'))) warnings.push('SMTP-Zertifikatsprüfung ist deaktiviert. Das ist nur für kontrollierte Testsysteme zulässig.');
  const foundation = getSecurityFoundationStatus();
  if (foundation.secrets.environment > 0) warnings.push(`${foundation.secrets.environment} Secret(s) stammen noch aus Umgebungsvariablen statt aus dem verschlüsselten Secret-Speicher.`);
  if (foundation.secrets.encrypted === 0) warnings.push('Der verschlüsselte Secret-Speicher enthält noch keine Zugangsdaten. Bestehende Umgebungsvariablen können weiter funktionieren, sind aber weniger zentral verwaltbar.');
  res.json({
    version: APP_VERSION, bind_host: bindHost, port: Number(PORT), trust_proxy: TRUST_PROXY,
    public_base_url: publicBaseUrl || null,
    allowed_hosts: String(process.env.VENDOO_ALLOWED_HOSTS || '').split(',').map(v => v.trim()).filter(Boolean),
    allowed_origins: [...configuredAllowedOrigins()],
    extension_origins: [...configuredExtensionOrigins()],
    session_days: Math.max(1, Number(process.env.VENDOO_SESSION_DAYS || 3)),
    session_idle_minutes: Math.max(10, Number(process.env.VENDOO_SESSION_IDLE_MINUTES || 120)),
    remote_binding: remoteBinding, secure_public_url: publicHttps,
    active_locks: listResourceLocks(), rate_limits: getRateLimitStats(), warnings,
    request_ip: getClientIp(req), request_secure: Boolean(req.secure || (TRUST_PROXY && req.get('x-forwarded-proto') === 'https')),
    csp: { enforced: true, script_inline_allowed: false, style_inline_allowed: true },
    secrets: foundation.secrets, requests: foundation.requests,
  });
});

app.get('/api/locks/:type/:id', (req, res) => {
  const lock = getResourceLock(req.params.type, req.params.id);
  res.json({ lock, owned_by_me: Boolean(lock && Number(lock.user_id) === Number(req.user.id)) });
});

app.post('/api/locks/:type/:id/acquire', (req, res) => {
  try {
    const result = acquireResourceLock({
      resourceType: req.params.type, resourceId: req.params.id, userId: req.user.id,
      clientLabel: req.body?.client_label || req.get('user-agent') || '', ttlSeconds: req.body?.ttl_seconds || 120,
    });
    auditLog(req.user.id, req.user.email, 'lock.acquired', req.params.type, req.params.id, null, getClientIp(req));
    res.status(201).json(result);
  } catch (error) {
    res.status(error.code === 'LOCKED' ? 409 : 400).json({ error: error.message, code: error.code, lock: error.lock || null });
  }
});

app.post('/api/locks/:type/:id/renew', (req, res) => {
  try {
    const lock = renewResourceLock({ resourceType: req.params.type, resourceId: req.params.id, userId: req.user.id, token: req.body?.token, ttlSeconds: req.body?.ttl_seconds || 120 });
    res.json({ lock });
  } catch (error) { res.status(409).json({ error: error.message, code: error.code, lock: error.lock || null }); }
});

app.delete('/api/locks/:type/:id', (req, res) => {
  try {
    const changes = releaseResourceLock({ resourceType: req.params.type, resourceId: req.params.id, userId: req.user.id, token: req.body?.token });
    if (changes) auditLog(req.user.id, req.user.email, 'lock.released', req.params.type, req.params.id, null, getClientIp(req));
    res.json({ ok: true, changes });
  } catch (error) { res.status(409).json({ error: error.message, code: error.code }); }
});

app.get('/api/security/locks', requirePermission('security.manage'), (_req, res) => res.json(listResourceLocks()));
app.delete('/api/security/locks/:type/:id', requirePermission('security.manage'), (req, res) => {
  const changes = releaseResourceLock({ resourceType: req.params.type, resourceId: req.params.id, userId: req.user.id, token: '', force: true });
  auditLog(req.user.id, req.user.email, 'lock.force_released', req.params.type, req.params.id, null, getClientIp(req));
  res.json({ ok: true, changes });
});

// --- Admin: Audit Log ---
app.get('/api/admin/audit', requireRole('admin', 'manager'), (req, res) => {
  const result = searchAuditLog({
    limit: req.query.limit, offset: req.query.offset, userId: req.query.user_id,
    action: req.query.action, from: req.query.from, to: req.query.to, query: req.query.q,
  });
  res.json(result);
});

app.get('/api/admin/audit/export.csv', requireRole('admin', 'manager'), (req, res) => {
  const result = searchAuditLog({ limit: 1000, userId: req.query.user_id, action: req.query.action, from: req.query.from, to: req.query.to, query: req.query.q });
  const csvEscape = value => `"${String(value ?? '').replace(/"/g, '""')}"`;
  const lines = [['Zeit', 'Benutzer', 'Aktion', 'Zieltyp', 'Ziel-ID', 'Details', 'IP'].map(csvEscape).join(';')];
  for (const row of result.rows) lines.push([row.created_at, row.user_email, row.action, row.target_type, row.target_id, row.details, row.ip].map(csvEscape).join(';'));
  res.setHeader('Content-Type', 'text/csv; charset=utf-8');
  res.setHeader('Content-Disposition', `attachment; filename="vendoo-audit-${new Date().toISOString().slice(0, 10)}.csv"`);
  res.send('\uFEFF' + lines.join('\n'));
});

// --- Admin: System Diagnostics ---
function diagnosticItem(id, label, status, detail, meta = {}) {
  return { id, label, status, detail, ...meta };
}

async function probeComfyUi(baseUrl) {
  const normalized = String(baseUrl || 'http://127.0.0.1:8188').replace(/\/$/, '');
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 1800);
  try {
    const response = await fetch(`${normalized}/system_stats`, { signal: controller.signal });
    if (!response.ok) return { ok: false, detail: `HTTP ${response.status}` };
    const payload = await response.json().catch(() => ({}));
    const device = payload?.devices?.[0];
    const deviceName = device?.name || device?.type || 'ComfyUI';
    return { ok: true, detail: `Erreichbar · ${deviceName}` };
  } catch (error) {
    return { ok: false, detail: error?.name === 'AbortError' ? 'Zeitüberschreitung' : (error?.message || 'Nicht erreichbar') };
  } finally {
    clearTimeout(timer);
  }
}

app.get('/api/admin/diagnostics', requireRole('admin'), async (_req, res) => {
  const startedAt = Date.now();
  const checks = [];
  const root = __dirname;
  const settings = getSettings();

  try {
    const result = db.pragma('quick_check', { simple: true });
    checks.push(diagnosticItem('database', 'Datenbankintegrität', result === 'ok' ? 'ok' : 'error', result === 'ok' ? 'SQLite quick_check: ok' : String(result)));
  } catch (error) {
    checks.push(diagnosticItem('database', 'Datenbankintegrität', 'error', error.message));
  }

  const writableTargets = [
    ['uploads', 'Upload-Verzeichnis', UPLOADS_DIR],
    ['db-write', 'Datenbank-Verzeichnis', dirname(DB_PATH)],
  ];
  for (const [id, label, target] of writableTargets) {
    try {
      mkdirSync(target, { recursive: true });
      const probe = join(target, `.vendoo-write-test-${process.pid}-${Date.now()}.tmp`);
      writeFileSync(probe, 'ok');
      unlinkSync(probe);
      checks.push(diagnosticItem(id, label, 'ok', 'Schreibzugriff vorhanden'));
    } catch (error) {
      checks.push(diagnosticItem(id, label, 'error', error.message));
    }
  }

  const requiredAssets = [
    ['ui-html', 'UI-Dokument', join(root, 'public', 'index.html')],
    ['ui-js', 'UI-Logik', join(root, 'public', 'app.js')],
    ['ui-css', 'UI-Styles', join(root, 'public', 'style.css')],
    ['flux-workflow', 'FLUX-Workflow', join(root, String(settings.ai_model_photo_local_workflow_path || 'local-ai/workflows/vendoo_flux_schnell_api.json'))],
  ];
  for (const [id, label, target] of requiredAssets) {
    const present = existsSync(target);
    checks.push(diagnosticItem(id, label, present ? 'ok' : 'error', present ? basename(target) : `Fehlt: ${target}`));
  }

  const extensionTargets = [
    ['Chrome', join(root, 'extensions', 'chrome', 'manifest.json')],
    ['Edge', join(root, 'extensions', 'edge', 'manifest.json')],
    ['Firefox', join(root, 'extensions', 'firefox', 'manifest.json')],
    ['Safari', join(root, 'extensions', 'safari', 'web-extension', 'manifest.json')],
  ];
  const extensionStates = extensionTargets.map(([name, target]) => ({ name, present: existsSync(target) }));
  const extensionMissing = extensionStates.filter(item => !item.present).map(item => item.name);
  checks.push(diagnosticItem('extensions', 'Browser-Extensions', extensionMissing.length ? 'warning' : 'ok', extensionMissing.length ? `Fehlend: ${extensionMissing.join(', ')}` : 'Chrome, Edge, Firefox und Safari vorhanden'));

  const configuredProviders = getProviders().map(provider => {
    const configured = !provider.needsKey || Boolean(process.env[provider.needsKey]);
    return { id: provider.id, configured };
  });
  const activeProvider = String(settings.ai_provider || settings.default_ai_provider || 'nicht festgelegt');
  checks.push(diagnosticItem('ai-provider', 'AI-Konfiguration', configuredProviders.some(item => item.configured) ? 'ok' : 'warning', `Aktiv: ${activeProvider} · verfügbar: ${configuredProviders.filter(item => item.configured).map(item => item.id).join(', ') || 'keiner'}`));

  const comfyUrl = settings.ai_model_photo_local_url || 'http://127.0.0.1:8188';
  const comfy = await probeComfyUi(comfyUrl);
  checks.push(diagnosticItem('comfyui', 'ComfyUI / FLUX', comfy.ok ? 'ok' : 'optional', `${comfy.detail} · ${comfyUrl}`));

  const counts = checks.reduce((acc, check) => { acc[check.status] = (acc[check.status] || 0) + 1; return acc; }, {});
  const hardFailures = checks.filter(check => check.status === 'error').length;
  res.setHeader('Cache-Control', 'no-store');
  res.json({
    ok: hardFailures === 0,
    version: APP_VERSION,
    generated_at: new Date().toISOString(),
    duration_ms: Date.now() - startedAt,
    summary: counts,
    checks,
    runtime: { node: process.version, platform: process.platform, arch: process.arch, uptime_seconds: Math.floor(process.uptime()), memory_mb: Math.round(process.memoryUsage().rss / 1048576) },
  });
});

// --- Admin: System Info ---
app.get('/api/admin/system', requireRole('admin'), (_req, res) => {
  const users = getUsers();
  const listings = getListings();
  const sessions = getAllActiveSessions();

  let dbSize = 'unbekannt';
  try {
    const dbPath = DB_PATH;
    if (existsSync(dbPath)) {
      const bytes = statSync(dbPath).size;
      if (bytes < 1024) dbSize = bytes + ' B';
      else if (bytes < 1024 * 1024) dbSize = (bytes / 1024).toFixed(1) + ' KB';
      else dbSize = (bytes / (1024 * 1024)).toFixed(1) + ' MB';
    }
  } catch {}

  const uptimeSeconds = process.uptime();
  const hours = Math.floor(uptimeSeconds / 3600);
  const minutes = Math.floor((uptimeSeconds % 3600) / 60);
  const uptime = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;

  res.json({
    node_version: process.version,
    uptime,
    uptime_seconds: Math.floor(uptimeSeconds),
    total_users: users.length,
    total_listings: listings.length,
    active_sessions: sessions.length,
    db_size: dbSize,
    platform: process.platform,
    memory_mb: Math.round(process.memoryUsage().rss / (1024 * 1024)),
  });
});

// Static files served for everyone — auth check happens via /api/me in frontend
app.use((req, res, next) => {
  const path = String(req.path || '').toLowerCase();
  if (path === '/' || path.endsWith('.html') || path.endsWith('.js') || path.endsWith('.css')) {
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
    res.setHeader('Pragma', 'no-cache');
    res.setHeader('Expires', '0');
    res.setHeader('Surrogate-Control', 'no-store');
  }
  next();
});
app.use(express.static(join(__dirname, 'public'), { etag: true, maxAge: 0 }));
app.use('/uploads', requireAuth, express.static(UPLOADS_DIR, { etag: true, maxAge: '1h', fallthrough: false }));

function sanitizeUploadPlatform(value) {
  const clean = String(value || 'allgemein').trim().toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
  return clean.slice(0, 48) || 'allgemein';
}

const storage = multer.diskStorage({
  destination: (req, _file, cb) => {
    const platform = sanitizeUploadPlatform(req.body?.platform || req.query?.platform || 'allgemein');
    const dir = join(UPLOADS_DIR, platform);
    mkdirSync(dir, { recursive: true });
    cb(null, dir);
  },
  filename: (_req, file, cb) => {
    const ext = extname(file.originalname).toLowerCase();
    cb(null, `${randomUUID()}${ext}`);
  },
});
const upload = multer({
  storage,
  fileFilter: (_req, file, cb) => {
    const allowed = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.heic', '.heif'];
    const ext = extname(file.originalname).toLowerCase();
    const isImageMime = String(file.mimetype || '').startsWith('image/');
    cb(null, allowed.includes(ext) && isImageMime);
  },
  limits: { fileSize: 20 * 1024 * 1024 },
});


const operationsUpload = multer({
  storage: multer.diskStorage({
    destination: (_req, _file, cb) => { const dir = join(OPERATIONS_DIR, 'staging'); mkdirSync(dir, { recursive: true }); cb(null, dir); },
    filename: (_req, file, cb) => cb(null, `${Date.now()}-${randomUUID()}${extname(file.originalname || '').toLowerCase() || '.bin'}`),
  }),
  fileFilter: (_req, file, cb) => {
    const ext = extname(file.originalname || '').toLowerCase();
    cb(null, ['.zip', '.db', '.sqlite', '.sqlite3'].includes(ext));
  },
  limits: { fileSize: 2 * 1024 * 1024 * 1024 },
});



async function normalizeMobileUploadFile(file) {
  const ext = extname(file.originalname || file.filename || '').toLowerCase();
  if (!['.heic', '.heif'].includes(ext) && !/hei[cf]/i.test(file.mimetype || '')) {
    return file;
  }

  const outputName = `${randomUUID()}.jpg`;
  const outputPath = join(dirname(file.path), outputName);
  try {
    await sharp(file.path).rotate().jpeg({ quality: 90 }).toFile(outputPath);
    try { unlinkSync(file.path); } catch {}
    return {
      ...file,
      filename: outputName,
      path: outputPath,
      mimetype: 'image/jpeg',
      originalname: String(file.originalname || 'foto.heic').replace(/\.(heic|heif)$/i, '.jpg'),
    };
  } catch (error) {
    try { unlinkSync(file.path); } catch {}
    throw new Error('HEIC/HEIF konnte auf diesem System nicht in JPG umgewandelt werden. Bitte am Smartphone „Am kompatibelsten“ wählen oder als JPG/PNG hochladen.');
  }
}


function validateMobileUploadToken(req, res, next) {
  const session = getMobileUploadSessionByTokenHash(hashMobileUploadToken(req.params.token));
  if (!isMobileUploadSessionUsable(session)) return res.status(410).json({ error: 'Upload-Link ist abgelaufen oder geschlossen.' });
  if ((session.files || []).length >= 10) return res.status(400).json({ error: 'Maximal 10 Bilder pro Upload-Sitzung.' });
  req.mobileUploadSession = session;
  next();
}

app.post('/mobile-upload-api/:token/photos', validateMobileUploadToken, upload.array('photos', 10), async (req, res) => {
  const session = req.mobileUploadSession;
  const remaining = Math.max(0, 10 - (session.files || []).length);
  const uploadedFiles = req.files || [];
  const acceptedFiles = uploadedFiles.slice(0, remaining);
  for (const extraFile of uploadedFiles.slice(remaining)) {
    try { unlinkSync(extraFile.path); } catch {}
  }

  try {
    const normalizedFiles = [];
    for (const file of acceptedFiles) normalizedFiles.push(await normalizeMobileUploadFile(file));
    const platform = req.body?.platform || req.query?.platform || 'allgemein';
    const filenames = normalizedFiles.map(file => `${platform}/${file.filename}`);
    const updated = appendMobileUploadFiles(session.id, filenames);
    res.json({
      ok: true,
      files: updated?.files || [],
      added: filenames.length,
      remaining: Math.max(0, 10 - (updated?.files || []).length),
    });
  } catch (error) {
    for (const file of acceptedFiles) {
      try { if (existsSync(file.path)) unlinkSync(file.path); } catch {}
    }
    res.status(400).json({ error: error.message || 'Bilder konnten nicht verarbeitet werden.' });
  }
});

// --- Platforms ---
const platforms = {};
async function loadPlatforms() {
  for (const f of ['vinted', 'ebay-ka', 'ebay-de', 'etsy']) {
    const mod = await import(`./platforms/${f}.mjs`);
    platforms[mod.default.id] = mod.default;
  }
}

// --- Upload ---
app.post('/api/upload', upload.array('photos', 10), (req, res) => {
  const platform = sanitizeUploadPlatform(req.body?.platform || req.query?.platform || 'allgemein');
  res.json({ filenames: req.files.map(f => `${platform}/${f.filename}`) });
});

app.get('/api/image-editor/versions', async (req, res) => {
  try {
    const path = String(req.query?.path || '').trim();
    const resolved = resolveEditableImage(UPLOADS_DIR, path);
    const history = getImageEditVersions(resolved.normalized, req.query?.limit || 50);
    const original = resolveEditableImage(UPLOADS_DIR, history.root_path || resolved.normalized);
    const stats = statSync(original.absolute);
    const metadata = await sharp(original.absolute, { failOn: 'none' }).metadata();
    res.json({
      root_path: history.root_path || resolved.normalized,
      current_path: resolved.normalized,
      original: {
        id: 'original',
        output_path: history.root_path || resolved.normalized,
        root_path: history.root_path || resolved.normalized,
        source_path: null,
        context: 'original',
        operations: {},
        width: metadata.width || null,
        height: metadata.height || null,
        format: metadata.format || null,
        file_size: stats.size,
        created_at: stats.birthtime?.toISOString?.() || stats.mtime?.toISOString?.() || null,
      },
      versions: history.versions,
    });
  } catch (error) {
    res.status(400).json({ error: error.message || 'Bildversionen konnten nicht geladen werden.' });
  }
});

app.post('/api/image-editor/render', async (req, res) => {
  try {
    const result = await renderImageVersion({
      uploadRoot: UPLOADS_DIR,
      sourcePath: req.body?.source_path,
      operations: req.body?.operations,
      context: req.body?.context || 'generator',
      userId: req.user?.id || null,
      rootPath: getImageEditRoot(String(req.body?.source_path || '').replace(/\\/g, '/').replace(/^\/+/, '').trim()) || String(req.body?.source_path || '').replace(/\\/g, '/').replace(/^\/+/, '').trim(),
      recordVersion: createImageEditVersion,
    });
    const history = getImageEditVersions(result.filename, 50);
    res.status(201).json({ ...result, versions: history.versions });
  } catch (error) {
    res.status(400).json({ error: error.message || 'Bildversion konnte nicht erstellt werden.' });
  }
});


app.get('/api/network-info', (_req, res) => {
  const addresses = getLanIpv4Addresses();
  res.json({
    port: Number(PORT),
    addresses,
    preferred_url: addresses[0]?.address ? `http://${addresses[0].address}:${PORT}` : `http://localhost:${PORT}`,
    public_base_url: String(process.env.PUBLIC_BASE_URL || '').trim() || null,
    runtime_config_path: CONFIG_PATH,
  });
});

app.post('/api/mobile-upload-sessions', (req, res) => {
  cleanupMobileUploadSessions();
  const token = randomBytes(32).toString('base64url');
  const tokenHash = hashMobileUploadToken(token);
  const context = ['generator', 'listing'].includes(req.body?.context) ? req.body.context : 'generator';
  const listingId = req.body?.listing_id ? Number(req.body.listing_id) : null;
  if (context === 'listing' && listingId && !getListing(listingId)) return res.status(404).json({ error: 'Artikel nicht gefunden' });
  const expires = new Date(Date.now() + 20 * 60 * 1000);
  const expiresSql = expires.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '');
  const session = createMobileUploadSession({
    token_hash: tokenHash,
    user_id: req.user?.id || null,
    listing_id: listingId,
    context,
    expires_at: expiresSql,
  });
  const baseUrl = resolvePublicBaseUrl(req);
  const url = `${baseUrl}/mobile-upload/${encodeURIComponent(token)}`;
  res.json({
    id: session.id,
    url,
    base_url: baseUrl,
    detected_lan_ip: getLanIpv4Addresses()[0]?.address || null,
    token,
    context,
    listing_id: listingId,
    files: [],
    expires_at: expires.toISOString(),
  });
});

app.get('/api/mobile-upload-sessions/:id', (req, res) => {
  cleanupMobileUploadSessions();
  const session = getMobileUploadSessionById(Number(req.params.id), req.user?.id || null);
  if (!session || !isMobileUploadSessionUsable(session)) return res.status(404).json({ error: 'Upload-Sitzung nicht gefunden oder abgelaufen' });
  res.json({ id: session.id, context: session.context, listing_id: session.listing_id, files: session.files || [], expires_at: session.expires_at, status: session.status });
});

app.delete('/api/mobile-upload-sessions/:id', (req, res) => {
  deleteMobileUploadSession(Number(req.params.id), req.user?.id || null);
  res.json({ ok: true });
});

// --- Image tools ---
app.post('/api/images/remove-bg', async (req, res) => {
  try {
    const { filename } = req.body;
    const newName = await removeBackground(filename);
    res.json({ filename: newName });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post('/api/images/watermark', async (req, res) => {
  try {
    const { filename, text } = req.body;
    const newName = await addWatermark(filename, text);
    res.json({ filename: newName });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// --- AI Model Photos ---
app.get('/api/listings/:id/ai-model-generations', (req, res) => {
  const listing = getListing(req.params.id);
  if (!listing) return res.status(404).json({ error: 'Nicht gefunden' });
  const generations = getAiModelGenerationsForListing(req.params.id).map(item => ({
    ...item,
    photos: getAiModelPhotosForGeneration(item.id),
  }));
  res.json(generations);
});

app.get('/api/ai-model-photos/jobs/:id', (req, res) => {
  const job = getAiModelJobWithRelations(req.params.id);
  if (!job) return res.status(404).json({ error: 'Nicht gefunden' });
  const response = { ...job };
  if (job.generation_id) {
    response.photos = getAiModelPhotosForGeneration(job.generation_id);
  }
  res.json(response);
});

app.post('/api/ai-model-photos/jobs', async (req, res) => {
  try {
    const settings = getSettings();
    if (String(settings.ai_model_photos_enabled || '1') !== '1') {
      return res.status(403).json({ error: 'AI-Model-Fotos sind deaktiviert.' });
    }
    const payload = normalizeListingPayload(req.body || {});
    const precheck = await precheckAiModelPhotos(payload, settings);
    if (!precheck.allowed) return res.status(400).json(precheck);
    const job = enqueueAiModelJob({ ...payload, settings, preset: precheck.preset, variants: precheck.variants, mode: precheck.mode });
    res.json({ queued: true, job_id: job.id, job, precheck });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.get('/api/ai-model-photos/providers/openrouter/diagnostics', async (_req, res) => {
  try {
    const diagnostics = await getOpenRouterImageDiagnostics();
    res.json({
      ...diagnostics,
      has_api_key: !!process.env.OPENROUTER_API_KEY,
      free_only_enabled: String(getSettings().ai_model_photo_openrouter_free_only || '1') !== '0',
    });
  } catch (err) {
    res.status(500).json({ error: err.message, has_api_key: !!process.env.OPENROUTER_API_KEY });
  }
});

app.get('/api/ai-model-photos/providers/openrouter/models', async (req, res) => {
  try {
    const settings = getSettings();
    const freeOnly = req.query.free_only !== undefined
      ? String(req.query.free_only) !== '0'
      : String(settings.ai_model_photo_openrouter_free_only || '1') !== '0';
    const models = await fetchOpenRouterImageModels({ freeOnly });
    res.json({ provider: 'openrouter', free_only: freeOnly, models });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

function getLocalFluxFileInfo(pathname) {
  try {
    if (!existsSync(pathname)) return { exists: false, size_bytes: 0, size_mb: 0, size_gb: 0 };
    const stats = statSync(pathname);
    return {
      exists: true,
      size_bytes: stats.size,
      size_mb: Math.round((stats.size / 1024 / 1024) * 10) / 10,
      size_gb: Math.round((stats.size / 1024 / 1024 / 1024) * 100) / 100,
      modified_at: stats.mtime?.toISOString?.() || null,
    };
  } catch {
    return { exists: false, size_bytes: 0, size_mb: 0, size_gb: 0 };
  }
}

function getLocalFluxDirectoryMetrics(rootPath) {
  const result = { exists: false, file_count: 0, size_bytes: 0, size_mb: 0 };
  if (!existsSync(rootPath)) return result;
  result.exists = true;
  const stack = [rootPath];
  while (stack.length) {
    const current = stack.pop();
    let entries = [];
    try { entries = readdirSync(current, { withFileTypes: true }); } catch { continue; }
    for (const entry of entries) {
      const full = join(current, entry.name);
      if (entry.isDirectory()) stack.push(full);
      else if (entry.isFile()) {
        result.file_count += 1;
        try { result.size_bytes += statSync(full).size; } catch {}
      }
    }
  }
  result.size_mb = Math.round((result.size_bytes / 1024 / 1024) * 10) / 10;
  return result;
}

function stripAnsiSequences(value = '') {
  return String(value || '').replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '');
}

function readLocalFluxLogTail(logPath, maxLines = 40) {
  try {
    if (!existsSync(logPath)) return [];
    return readFileSync(logPath, 'utf-8')
      .split(/\r?\n/)
      .map(stripAnsiSequences)
      .filter(Boolean)
      .slice(-maxLines);
  } catch {
    return [];
  }
}

function readLocalFluxRuntimeState(includeLogs = false) {
  const logDir = LOG_DIR;
  const statePath = join(logDir, 'local-flux-runtime-state.json');
  const stdoutPath = join(logDir, 'local-flux-runtime.log');
  const stderrPath = join(logDir, 'local-flux-runtime-error.log');
  let state = {
    status: 'stopped',
    message: 'FLUX / ComfyUI ist nicht gestartet.',
    pid: null,
    elapsed_seconds: 0,
    updated_at: null,
    error: '',
  };
  if (existsSync(statePath)) {
    try {
      state = { ...state, ...JSON.parse(readFileSync(statePath, 'utf-8').replace(/^\uFEFF/, '')) };
    } catch {
      state = { ...state, status: 'unknown', message: 'FLUX-Runtime-Status konnte nicht gelesen werden.' };
    }
  }
  const updatedAt = Date.parse(state.updated_at || '') || 0;
  const ageSeconds = updatedAt ? Math.max(0, Math.round((Date.now() - updatedAt) / 1000)) : null;
  const stale = state.status === 'starting' && ageSeconds !== null && ageSeconds > 240;
  if (stale) {
    state = {
      ...state,
      status: 'failed',
      message: `FLUX-Startstatus ist seit ${ageSeconds} Sekunden veraltet.`,
      error: state.error || 'Der Startprozess liefert keine Statusaktualisierung mehr.',
    };
  }
  return {
    ...state,
    age_seconds: ageSeconds,
    stale,
    state_path: statePath,
    stdout_log: stdoutPath,
    stderr_log: stderrPath,
    ...(includeLogs ? {
      stdout_tail: readLocalFluxLogTail(stdoutPath, 30),
      stderr_tail: readLocalFluxLogTail(stderrPath, 30),
    } : {}),
  };
}

function findLatestLocalFluxArchive(downloadDir) {
  try {
    const items = readdirSync(downloadDir, { withFileTypes: true })
      .filter(entry => entry.isFile() && /\.7z$/i.test(entry.name))
      .map(entry => ({ name: entry.name, path: join(downloadDir, entry.name), info: getLocalFluxFileInfo(join(downloadDir, entry.name)) }))
      .sort((a, b) => String(b.info.modified_at || '').localeCompare(String(a.info.modified_at || '')));
    return items[0] || null;
  } catch {
    return null;
  }
}

function readLocalFluxInstallState(includeLog = false) {
  const localRoot = join(__dirname, 'local-ai');
  const statePath = join(localRoot, 'install-state.json');
  const logPath = join(LOG_DIR, 'local-flux-install.log');
  const portableRoot = join(localRoot, 'comfyui', 'ComfyUI_windows_portable');
  const comfyMain = join(portableRoot, 'ComfyUI', 'main.py');
  const embeddedPython = join(portableRoot, 'python_embeded', 'python.exe');
  const modelPath = join(portableRoot, 'ComfyUI', 'models', 'checkpoints', 'flux1-schnell-fp8.safetensors');
  const workflowPath = join(localRoot, 'workflows', 'vendoo_flux_schnell_api.json');
  const startScript = join(localRoot, 'start-local-flux.ps1');
  const stopScript = join(localRoot, 'stop-local-flux.ps1');
  const downloadDir = join(localRoot, 'downloads');
  const extractTemp = join(localRoot, 'extract-temp');
  const sevenZipOutputPath = join(LOG_DIR, 'local-flux-7zip-output.log');
  const sevenZipErrorPath = join(LOG_DIR, 'local-flux-7zip-error.log');
  const latestArchive = findLatestLocalFluxArchive(downloadDir);
  const partialFiles = (() => {
    try {
      return readdirSync(downloadDir, { withFileTypes: true })
        .filter(entry => entry.isFile() && /\.part$/i.test(entry.name))
        .map(entry => ({ name: entry.name, ...getLocalFluxFileInfo(join(downloadDir, entry.name)) }));
    } catch { return []; }
  })();

  let state = {
    status: 'not-started',
    phase: 'idle',
    progress: 0,
    message: 'Lokale FLUX-Installation wurde noch nicht gestartet.',
  };
  if (existsSync(statePath)) {
    try {
      state = { ...state, ...JSON.parse(readFileSync(statePath, 'utf-8').replace(/^\uFEFF/, '')) };
    } catch {
      state = { ...state, status: 'unknown', phase: 'unknown', message: 'Installationsstatus konnte nicht gelesen werden.' };
    }
  }

  const known7ZipPaths = [
    process.env.ProgramFiles ? join(process.env.ProgramFiles, '7-Zip', '7z.exe') : null,
    process.env['ProgramFiles(x86)'] ? join(process.env['ProgramFiles(x86)'], '7-Zip', '7z.exe') : null,
    process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'Programs', '7-Zip', '7z.exe') : null,
  ].filter(Boolean);
  const sevenZipPath = known7ZipPaths.find(pathname => existsSync(pathname)) || null;

  const checks = {
    seven_zip: { ok: !!sevenZipPath, path: sevenZipPath },
    archive: { ok: !!latestArchive?.info?.exists, name: latestArchive?.name || null, ...(latestArchive?.info || {}) },
    embedded_python: { ok: existsSync(embeddedPython), path: embeddedPython },
    comfyui: { ok: existsSync(comfyMain), path: comfyMain },
    model: { ok: existsSync(modelPath) && getLocalFluxFileInfo(modelPath).size_bytes >= 1024 * 1024 * 1024, path: modelPath, ...getLocalFluxFileInfo(modelPath) },
    workflow: { ok: existsSync(workflowPath), path: workflowPath },
    start_script: { ok: existsSync(startScript), path: startScript },
    stop_script: { ok: existsSync(stopScript), path: stopScript },
  };

  const completedChecks = Object.values(checks).filter(check => check.ok).length;
  const totalChecks = Object.keys(checks).length;
  const readiness = Math.round((completedChecks / totalChecks) * 100);
  const phaseLabels = {
    idle: 'Noch nicht gestartet',
    preflight: 'Vorprüfung',
    'download-comfyui': 'ComfyUI wird heruntergeladen',
    'dependency-7zip': '7-Zip wird geprüft',
    'install-7zip': '7-Zip wird installiert',
    'extract-comfyui': 'ComfyUI wird entpackt',
    'install-comfyui': 'ComfyUI-Struktur wird installiert',
    configure: 'Lokale Konfiguration wird erstellt',
    'download-model': 'FLUX-Modell wird heruntergeladen',
    autostart: 'Autostart wird eingerichtet',
    start: 'Lokaler Bildserver wird gestartet',
    update: 'ComfyUI wird aktualisiert',
    done: 'Installation abgeschlossen',
    error: 'Installation fehlgeschlagen',
    uninstalled: 'Nicht installiert',
  };

  const extractMetrics = getLocalFluxDirectoryMetrics(extractTemp);
  const now = Date.now();
  const heartbeatAt = Date.parse(state.heartbeat_at || state.updated_at || '') || 0;
  const heartbeatAgeSeconds = heartbeatAt ? Math.max(0, Math.round((now - heartbeatAt) / 1000)) : null;
  const staleThresholdSeconds = state.phase === 'extract-comfyui' ? 15 : 120;
  const stalled = state.status === 'running' && heartbeatAgeSeconds !== null && heartbeatAgeSeconds > staleThresholdSeconds;
  const installationComplete = checks.embedded_python.ok && checks.comfyui.ok && checks.model.ok && checks.workflow.ok && checks.start_script.ok && checks.stop_script.ok;
  let effectiveStatus = stalled ? 'stalled' : state.status;
  let effectivePhase = state.phase;
  let effectiveMessage = stalled
    ? `Keine Statusaktualisierung seit ${heartbeatAgeSeconds} Sekunden. Der Installer oder 7-Zip könnte hängen.`
    : state.message;

  // Der reale Dateistand hat Vorrang vor einer alten, beschädigten oder fehlenden Statusdatei.
  if (installationComplete && effectiveStatus !== 'running') {
    effectiveStatus = 'completed';
    effectivePhase = 'done';
    effectiveMessage = 'Lokales FLUX ist vollständig installiert.';
  } else if (!checks.comfyui.ok && ['completed', 'unknown'].includes(effectiveStatus)) {
    effectiveStatus = 'not-started';
    effectivePhase = 'idle';
    effectiveMessage = 'Lokales FLUX ist noch nicht vollständig installiert.';
  }

  const result = {
    ...state,
    status: effectiveStatus,
    original_status: state.status,
    phase: effectivePhase,
    message: effectiveMessage,
    phase_label: stalled ? 'Keine Statusaktualisierung' : (phaseLabels[effectivePhase] || effectivePhase || 'Unbekannt'),
    checks,
    readiness,
    heartbeat_age_seconds: heartbeatAgeSeconds,
    stale_threshold_seconds: staleThresholdSeconds,
    stalled,
    extract_metrics: {
      file_count: Number(state.extract_files ?? extractMetrics.file_count ?? 0),
      size_mb: Number(state.extract_mb ?? extractMetrics.size_mb ?? 0),
      elapsed_seconds: Number(state.extract_elapsed_seconds ?? 0),
      process_id: state.extract_process_id || null,
      root: extractTemp,
    },
    installer_process_id: state.process_id || null,
    completed_checks: completedChecks,
    total_checks: totalChecks,
    installation_complete: installationComplete,
    can_resume: ['failed', 'stalled'].includes(effectiveStatus) || ((checks.archive.ok || checks.comfyui.ok || checks.embedded_python.ok) && (!checks.comfyui.ok || !checks.model.ok)),
    partial_downloads: partialFiles,
    paths: {
      state_file: statePath,
      log_file: logPath,
      portable_root: portableRoot,
      model_path: modelPath,
      workflow_path: workflowPath,
      extract_root: extractTemp,
      seven_zip_output: sevenZipOutputPath,
      seven_zip_error: sevenZipErrorPath,
    },
  };
  if (includeLog) {
    result.log_tail = readLocalFluxLogTail(logPath, 80);
    result.seven_zip_output_tail = readLocalFluxLogTail(sevenZipOutputPath, 30);
    result.seven_zip_error_tail = readLocalFluxLogTail(sevenZipErrorPath, 30);
  }
  return result;
}

function spawnLocalFluxPowerShell(scriptName, args = []) {
  if (process.platform !== 'win32') {
    throw new Error('Die automatische lokale FLUX-Installation ist aktuell für Windows vorgesehen.');
  }
  const scriptPath = join(__dirname, 'scripts', scriptName);
  if (!existsSync(scriptPath)) throw new Error(`Skript fehlt: ${scriptName}`);
  const child = spawn('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', scriptPath, ...args], {
    cwd: __dirname,
    detached: true,
    stdio: 'ignore',
    windowsHide: true,
  });
  child.unref();
  return child.pid;
}

app.get('/api/local-image/status', async (_req, res) => {
  try {
    const settings = getSettings();
    const runtime = await getLocalImageStatus(settings);
    const install = readLocalFluxInstallState();
    const runtimeState = readLocalFluxRuntimeState(true);
    const engineState = !runtime.enabled
      ? 'disabled'
      : runtime.reachable
        ? 'running'
        : runtimeState.status === 'starting'
          ? 'starting'
          : runtimeState.status === 'failed'
            ? 'failed'
            : install.installation_complete
              ? 'stopped'
              : 'not-installed';
    res.json({
      ...runtime,
      install,
      runtime_state: runtimeState,
      engine_state: engineState,
      engine_label: engineState === 'running'
        ? 'Aktiv und bereit'
        : engineState === 'starting'
          ? 'Wird gestartet'
          : engineState === 'failed'
            ? 'Start fehlgeschlagen'
            : engineState === 'stopped'
              ? 'Installiert, aber gestoppt'
              : engineState === 'disabled'
                ? 'Deaktiviert'
                : 'Nicht installiert',
    });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Lokaler Bildstatus konnte nicht geprüft werden.' });
  }
});

app.get('/api/local-image/install-status', requireRole('admin'), (_req, res) => {
  res.json(readLocalFluxInstallState(true));
});

app.post('/api/local-image/install', requireRole('admin'), (req, res) => {
  try {
    const current = readLocalFluxInstallState();
    if (current.status === 'running') {
      return res.status(409).json({ error: 'Eine lokale FLUX-Installation läuft bereits.', install: current });
    }
    const args = ['-NonInteractive'];
    if (req.body?.install_model !== false) args.push('-InstallModel');
    if (req.body?.enable_autostart !== false) args.push('-EnableAutostart');
    if (req.body?.start_now !== false) args.push('-StartNow');
    if (req.body?.force === true) args.push('-Force');
    const gpu = String(req.body?.gpu || 'auto').toLowerCase();
    if (['auto','nvidia','amd','intel','cpu'].includes(gpu)) args.push('-Gpu', gpu);
    const pid = spawnLocalFluxPowerShell('install-local-flux.ps1', args);
    res.json({ ok: true, pid, message: 'Lokale FLUX-Installation wurde gestartet.' });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Lokale FLUX-Installation konnte nicht gestartet werden.' });
  }
});

app.post('/api/local-image/update', requireRole('admin'), (_req, res) => {
  try {
    const current = readLocalFluxInstallState();
    if (current.status === 'running') {
      return res.status(409).json({ error: 'Eine lokale FLUX-Aktion läuft bereits.', install: current });
    }
    const pid = spawnLocalFluxPowerShell('install-local-flux.ps1', ['-NonInteractive', '-UpdateOnly', '-StartNow']);
    res.json({ ok: true, pid, message: 'Lokales FLUX-Update wurde gestartet.' });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Lokales FLUX-Update konnte nicht gestartet werden.' });
  }
});

app.post('/api/local-image/start', requireRole('admin'), (req, res) => {
  try {
    const settings = getSettings();
    const requested = String(req.body?.profile || settings.flux_runtime_profile || 'auto').toLowerCase();
    const profile = ['auto','fast','balanced','lowvram'].includes(requested) ? requested : 'auto';
    const pid = spawnLocalFluxPowerShell('../local-ai/start-local-flux.ps1', ['-Profile', profile]);
    res.json({ ok: true, pid, message: 'Lokaler FLUX-Server wird gestartet.' });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Lokaler FLUX-Server konnte nicht gestartet werden.' });
  }
});

app.post('/api/local-image/stop', requireRole('admin'), (_req, res) => {
  try {
    const pid = spawnLocalFluxPowerShell('../local-ai/stop-local-flux.ps1', []);
    res.json({ ok: true, pid, message: 'Lokaler FLUX-Server wird beendet.' });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Lokaler FLUX-Server konnte nicht beendet werden.' });
  }
});


app.post('/api/local-image/uninstall', requireRole('admin'), (req, res) => {
  try {
    const current = readLocalFluxInstallState();
    if (current.status === 'running') {
      return res.status(409).json({ error: 'Während einer laufenden Installation kann FLUX nicht deinstalliert werden.', install: current });
    }
    const args = ['-NonInteractive'];
    if (req.body?.remove_downloads === true) args.push('-RemoveDownloads');
    const pid = spawnLocalFluxPowerShell('uninstall-local-flux.ps1', args);

    updateSettings({ ai_model_photo_local_enabled: '0' });
    process.env.LOCAL_IMAGE_ENABLED = 'false';
    updateRuntimeConfig({ LOCAL_IMAGE_ENABLED: 'false' });

    res.json({
      ok: true,
      pid,
      message: req.body?.remove_downloads === true
        ? 'Lokales FLUX wird vollständig einschließlich Downloads deinstalliert.'
        : 'Lokales FLUX wird deinstalliert. Installationsarchive bleiben für eine schnelle Neuinstallation erhalten.',
    });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Lokales FLUX konnte nicht deinstalliert werden.' });
  }
});

app.post('/api/local-image/regenerate-token', requireRole('admin'), (req, res) => {
  try {
    const token = generateLocalImageToken();
    setSecret('LOCAL_IMAGE_TOKEN', token, { source: 'token-regeneration' });
    res.json({ ok: true, token });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Lokaler Token konnte nicht erneuert werden.' });
  }
});



// Media Library routes are owned by vendoo.media.

app.get('/api/image-batch/profiles', (_req, res) => {
  res.json({ items: listImageBatchProfiles() });
});

app.post('/api/image-batch/profiles', (req, res) => {
  try { res.status(201).json(createImageBatchProfile(req.body || {})); }
  catch (error) { res.status(400).json({ error: error.message || 'Produktionsprofil konnte nicht erstellt werden.' }); }
});

app.patch('/api/image-batch/profiles/:id', (req, res) => {
  try { res.json(updateImageBatchProfile(req.params.id, req.body || {})); }
  catch (error) { res.status(400).json({ error: error.message || 'Produktionsprofil konnte nicht gespeichert werden.' }); }
});

app.delete('/api/image-batch/profiles/:id', (req, res) => {
  try { res.json(deleteImageBatchProfile(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message || 'Produktionsprofil konnte nicht gelöscht werden.' }); }
});

app.post('/api/image-batch/jobs', (req, res) => {
  try { res.status(202).json(createImageBatchJob(req.body || {})); }
  catch (error) { res.status(400).json({ error: error.message || 'Bildauftrag konnte nicht erstellt werden.' }); }
});

app.get('/api/image-batch/jobs', (req, res) => {
  try {
    res.json(paginateImageBatchJobs({ page: req.query?.page || 1, pageSize: req.query?.page_size || 10, status: req.query?.status || '' }));
  } catch (error) { res.status(500).json({ error: error.message || 'Bildaufträge konnten nicht geladen werden.' }); }
});

app.get('/api/image-batch/jobs/:id', (req, res) => {
  const job = getImageBatchJob(req.params.id);
  if (!job) return res.status(404).json({ error: 'Bildauftrag wurde nicht gefunden.' });
  res.json(job);
});

app.post('/api/image-batch/jobs/:id/pause', (req, res) => {
  try { res.json(pauseImageBatchJob(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message || 'Bildauftrag konnte nicht pausiert werden.' }); }
});

app.post('/api/image-batch/jobs/:id/resume', (req, res) => {
  try { res.json(resumeImageBatchJob(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message || 'Bildauftrag konnte nicht fortgesetzt werden.' }); }
});

app.post('/api/image-batch/jobs/:id/cancel', (req, res) => {
  try { res.json(cancelImageBatchJob(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message || 'Bildauftrag konnte nicht abgebrochen werden.' }); }
});

app.post('/api/image-batch/jobs/:id/retry', (req, res) => {
  try { res.json(retryImageBatchJob(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message || 'Bildauftrag konnte nicht wiederholt werden.' }); }
});

app.post('/api/image-batch/jobs/:id/items/:itemId/retry', (req, res) => {
  try { res.json(retryImageBatchItem(req.params.id, req.params.itemId)); }
  catch (error) { res.status(400).json({ error: error.message || 'Bildposition konnte nicht wiederholt werden.' }); }
});

app.delete('/api/image-batch/jobs/:id', (req, res) => {
  try { res.json(deleteImageBatchJob(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message || 'Bildauftrag konnte nicht gelöscht werden.' }); }
});

app.get('/api/image-batch/jobs/:id/archive', (req, res) => {
  try {
    const job = getImageBatchJob(req.params.id);
    if (!job) return res.status(404).json({ error: 'Bildauftrag wurde nicht gefunden.' });
    const outputs = getImageBatchOutputPaths(req.params.id);
    if (!outputs.length) return res.status(404).json({ error: 'Dieser Auftrag enthält noch keine fertigen Bilder.' });
    const uploadRoot = resolve(UPLOADS_DIR);
    const files = [];
    for (const [index, relative] of outputs.entries()) {
      const absolute = resolve(uploadRoot, relative);
      if (absolute === uploadRoot || !absolute.startsWith(uploadRoot + sep) || !existsSync(absolute)) continue;
      files.push({ name: `${String(index + 1).padStart(2, '0')}-${basename(relative)}`, data: readFileSync(absolute) });
    }
    if (!files.length) return res.status(404).json({ error: 'Die fertigen Bilddateien wurden nicht gefunden.' });
    const archive = createZip(files);
    res.setHeader('Content-Type', 'application/zip');
    res.setHeader('Content-Disposition', `attachment; filename=vendoo-bildproduktion-${String(job.id).slice(0, 8)}.zip`);
    res.setHeader('Content-Length', archive.length);
    res.send(archive);
  } catch (error) { res.status(500).json({ error: error.message || 'ZIP-Export konnte nicht erstellt werden.' }); }
});

app.post('/api/flux-prompt/jobs', (req, res) => {
  try {
    const settings = getSettings();
    const job = enqueueFluxPromptJob({
      prompt: req.body?.prompt,
      width: req.body?.width,
      height: req.body?.height,
      seed: req.body?.seed,
      steps: req.body?.steps,
      style: req.body?.style,
      profile: req.body?.profile || settings.flux_default_profile || 'balanced',
      promptMode: req.body?.prompt_mode || 'precise',
      assistant: req.body?.assistant !== false && String(settings.flux_prompt_assistant || '1') !== '0',
      variants: req.body?.variants || settings.flux_default_variants || 1,
      seedLock: req.body?.seed_lock === true || String(req.body?.seed_lock || settings.flux_seed_lock || '0') === '1',
    });
    res.status(202).json(job);
  } catch (error) { res.status(400).json({ error: error.message || 'FLUX-Auftrag konnte nicht erstellt werden.' }); }
});

app.get('/api/flux-prompt/jobs', (req, res) => {
  const pageData = paginateFluxPromptJobs({
    page: req.query?.page || 1,
    pageSize: req.query?.page_size || req.query?.limit || 10,
    status: req.query?.status || '',
    archived: String(req.query?.archived || '0') === '1',
  });
  res.json({ ...pageData, summary: getFluxPromptJobSummary() });
});

app.post('/api/flux-prompt/jobs/archive', (req, res) => {
  res.json(archiveFluxPromptJobs(req.body?.ids || []));
});

app.post('/api/flux-prompt/jobs/restore', (req, res) => {
  res.json(restoreFluxPromptJobs(req.body?.ids || []));
});

app.delete('/api/flux-prompt/jobs/archive', (req, res) => {
  res.json(deleteArchivedFluxPromptJobs(req.body?.ids || [], { all: String(req.query?.all || '0') === '1' }));
});

app.get('/api/flux-prompt/jobs/:id', (req, res) => {
  const job = getFluxPromptJob(req.params.id);
  if (!job) return res.status(404).json({ error: 'FLUX-Auftrag nicht gefunden.' });
  res.json(job);
});

app.post('/api/flux-prompt/jobs/:id/cancel', async (req, res) => {
  const job = await cancelFluxPromptJob(req.params.id);
  if (!job) return res.status(404).json({ error: 'FLUX-Auftrag nicht gefunden.' });
  res.json(job);
});

app.post('/api/flux-prompt/jobs/:id/retry', (req, res) => {
  try {
    const job = retryFluxPromptJob(req.params.id);
    if (!job) return res.status(404).json({ error: 'FLUX-Auftrag nicht gefunden.' });
    res.status(202).json(job);
  } catch (error) { res.status(400).json({ error: error.message || 'FLUX-Auftrag konnte nicht wiederholt werden.' }); }
});

app.delete('/api/flux-prompt/jobs/queue', (_req, res) => {
  res.json(clearFluxPromptQueue());
});

app.get('/api/flux-prompt/runtime', async (_req, res) => {
  try {
    const settings = getSettings();
    const runtime = await getLocalFluxRuntimeInfo(settings, { historyLimit: 20 });
    res.json({ ...runtime, vendoo_jobs: getFluxPromptJobSummary() });
  } catch (error) {
    res.status(503).json({ error: error.message || 'ComfyUI-Systemdaten konnten nicht geladen werden.', vendoo_jobs: getFluxPromptJobSummary() });
  }
});

app.get('/api/flux-studio/images', (req, res) => {
  const favoritesOnly = String(req.query?.favorites || '0') === '1';
  if (req.query?.page !== undefined || req.query?.page_size !== undefined) {
    const result = paginateFluxImages({ page: req.query.page, pageSize: req.query.page_size, favoritesOnly });
    return res.json({
      items: result.items.map(item => ({ ...item, public_url: `/uploads/${item.image_path}` })),
      pagination: result.pagination,
    });
  }
  const items = getFluxImages(req.query?.limit || 40, { favoritesOnly }).map(item => ({ ...item, public_url: `/uploads/${item.image_path}` }));
  res.json(items);
});

app.post('/api/flux-prompt/improve', async (req, res) => {
  try {
    const prompt = String(req.body?.prompt || '').trim().slice(0, 4000);
    if (prompt.length < 4) return res.status(400).json({ error: 'Bitte zuerst einen aussagekräftigen Prompt eingeben.' });
    const settings = getSettings();
    const provider = String(req.body?.provider || settings.default_ai || 'openrouter').trim();
    const model = String(req.body?.model || '').trim() || undefined;
    const goal = ['precise','product','creative','cinematic'].includes(req.body?.goal) ? req.body.goal : 'product';
    const preserveSubject = req.body?.preserve_subject !== false;
    const avoidText = req.body?.avoid_text !== false;
    const goalLabels = { precise: 'präzise und technisch eindeutig', product: 'hochwertige Produkt- und Marketplace-Fotografie', creative: 'kreativ, aufmerksamkeitsstark und visuell eigenständig', cinematic: 'cinematisch, atmosphärisch und fotografisch hochwertig' };
    const systemPrompt = `Du bist Vendoo Prompt Director für FLUX-Bildgenerierung. Optimiere einen vorhandenen Bildprompt, ohne die eigentliche Produktidentität oder Nutzerabsicht zu verfälschen. Ziel: ${goalLabels[goal]}. ${preserveSubject ? 'Motiv, Produktmerkmale, Farben, Anzahl und Perspektiven dürfen nicht frei erfunden oder ausgetauscht werden.' : ''} ${avoidText ? 'Vermeide sichtbare Schrift, Logos, Wasserzeichen und erfundene Marken, sofern der Nutzer sie nicht ausdrücklich verlangt.' : ''} Ergänze nur sinnvolle Angaben zu Motiv, Komposition, Kamera, Licht, Materialwirkung, Hintergrund und Qualitätsmerkmalen. Gib ausschließlich valides JSON aus: {"improved_prompt":"...","notes":"..."}. Der verbesserte Prompt soll direkt in FLUX nutzbar sein und maximal 1800 Zeichen enthalten.`;
    const userPrompt = `Originalprompt:
${prompt}

Gewünschtes Profil: ${goal}. Formuliere den verbesserten Prompt bevorzugt in klarem, natürlichem Englisch, behalte aber ausdrücklich verlangte deutsche Texte unverändert bei.`;
    const raw = await generateText(provider, systemPrompt, userPrompt, model);
    let parsed = null;
    try { parsed = JSON.parse(raw); } catch {
      const match = raw.match(/\{[\s\S]*\}/);
      if (match) try { parsed = JSON.parse(match[0]); } catch {}
    }
    const improved = String(parsed?.improved_prompt || raw || '').trim().slice(0, 2000);
    if (!improved) throw new Error('Die AI hat keinen verbesserten Prompt zurückgegeben.');
    res.json({ improved_prompt: improved, notes: String(parsed?.notes || '').trim().slice(0, 500), provider, model: model || null });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Prompt konnte nicht verbessert werden.' });
  }
});

app.post('/api/flux-studio/images', async (req, res) => {
  try {
    const imagePath = String(req.body?.image_path || '').trim();
    if (!/^ai-generated\/[a-z0-9-]+\.(png|jpe?g|webp)$/i.test(imagePath)) {
      return res.status(400).json({ error: 'Ungültiger FLUX-Bildpfad.' });
    }
    const absolute = resolve(UPLOADS_DIR, imagePath);
    const imageRoot = resolve(UPLOADS_DIR, 'ai-generated');
    if (!absolute.startsWith(imageRoot + sep) || !existsSync(absolute)) return res.status(400).json({ error: 'Bilddatei wurde nicht gefunden.' });
    const metadata = await sharp(absolute).metadata();
    const item = createFluxImage({
      prompt: req.body?.prompt || 'Nachbearbeitete FLUX-Ausgabe',
      image_path: imagePath,
      width: metadata.width || req.body?.width,
      height: metadata.height || req.body?.height,
      seed: req.body?.seed,
      steps: req.body?.steps,
      style: req.body?.style,
      edited_from: req.body?.edited_from,
      final_prompt: req.body?.final_prompt,
      profile: req.body?.profile,
      duration_ms: req.body?.duration_ms,
      favorite: req.body?.favorite,
      job_id: req.body?.job_id,
      variant_index: req.body?.variant_index,
      prompt_mode: req.body?.prompt_mode,
      parameters: req.body?.parameters,
    });
    res.status(201).json({ ...item, public_url: `/uploads/${item.image_path}` });
  } catch (error) { res.status(400).json({ error: error.message || 'FLUX-Bild konnte nicht gespeichert werden.' }); }
});

app.post('/api/flux-studio/images/:id/copy-to-generator', (req, res) => {
  try {
    const item = getFluxImage(req.params.id);
    if (!item) return res.status(404).json({ error: 'FLUX-Bild nicht gefunden.' });
    const source = resolve(UPLOADS_DIR, item.image_path);
    const imageRoot = resolve(UPLOADS_DIR, 'ai-generated');
    if (!source.startsWith(imageRoot + sep) || !existsSync(source)) return res.status(404).json({ error: 'FLUX-Bilddatei wurde nicht gefunden.' });
    const targetDir = resolve(UPLOADS_DIR, 'allgemein');
    mkdirSync(targetDir, { recursive: true });
    const extension = extname(source).toLowerCase() || '.png';
    const filename = `${randomUUID()}${extension}`;
    copyFileSync(source, join(targetDir, filename));
    res.status(201).json({ filename: `allgemein/${filename}` });
  } catch (error) { res.status(400).json({ error: error.message || 'FLUX-Bild konnte nicht in den Generator übernommen werden.' }); }
});

app.patch('/api/flux-studio/images/:id/favorite', (req, res) => {
  const item = setFluxImageFavorite(req.params.id, req.body?.favorite !== false);
  if (!item) return res.status(404).json({ error: 'FLUX-Bild nicht gefunden.' });
  res.json({ ...item, public_url: `/uploads/${item.image_path}` });
});

app.delete('/api/flux-studio/images/:id', (req, res) => {
  try {
    const item = deleteFluxImage(req.params.id);
    if (!item) return res.status(404).json({ error: 'FLUX-Bild nicht gefunden.' });
    const absolute = resolve(UPLOADS_DIR, item.image_path);
    const imageRoot = resolve(UPLOADS_DIR, 'ai-generated');
    if (absolute.startsWith(imageRoot + sep)) { try { unlinkSync(absolute); } catch {} }
    res.json({ ok: true, deleted: item.id });
  } catch (error) { res.status(400).json({ error: error.message || 'FLUX-Bild konnte nicht gelöscht werden.' }); }
});

app.post('/api/ai-model-photos/precheck', async (req, res) => {
  try {
    const settings = getSettings();
    const payload = normalizeListingPayload(req.body || {});
    const result = await precheckAiModelPhotos(payload, settings);
    res.json(result);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post('/api/ai-model-photos/generate', async (req, res) => {
  try {
    const settings = getSettings();
    if (String(settings.ai_model_photos_enabled || '1') !== '1') {
      return res.status(403).json({ error: 'AI-Model-Fotos sind deaktiviert.' });
    }
    const payload = normalizeListingPayload(req.body || {});
    const result = await generateAiModelPhotos({ ...payload, settings });
    const generation = createAiModelGeneration({
      listing_id: payload.listing_id || null,
      context: payload.listing_id ? 'listing' : 'generator',
      mode: result.mode || result.precheck.mode || payload.mode || 'model',
      preset: result.precheck.preset,
      variants: result.photos.length,
      source_photos: Array.isArray(payload.photos) ? payload.photos : [],
      generated_photos: result.photos.map(photo => photo.filename),
      prompt_summary: result.prompt_summary,
      provider: result.provider?.provider || null,
      provider_model: result.provider?.model || null,
      moderation_notes: result.photos.map(photo => photo.moderation?.notes).filter(Boolean).join(' | ') || null,
      safety_status: 'safe',
      status: 'completed',
    });
    const photos = result.photos.map(photo => createAiModelPhoto({
      generation_id: generation.id,
      listing_id: payload.listing_id || null,
      variant_index: photo.variant_index,
      mode: photo.mode || result.mode || payload.mode || 'model',
      preset: photo.preset,
      image_path: photo.filename,
      source_photo: photo.source_photo,
      prompt: photo.prompt,
      provider: photo.provider || result.provider?.provider || null,
      provider_model: photo.provider_model || result.provider?.model || null,
      moderation_result: JSON.stringify(photo.moderation || {}),
      safety_status: photo.moderation?.safe === false ? 'blocked' : 'safe',
    }));
    updateAiModelGeneration(generation.id, {
      generated_photos: photos.map(photo => photo.image_path),
      provider: result.provider?.provider || null,
      provider_model: result.provider?.model || null,
    });
    res.json({ ...result, generation_id: generation.id, photos, provider: result.provider || null });
  } catch (err) {
    if (err.code === 'AI_MODEL_PRECHECK_FAILED') {
      return res.status(400).json({ error: err.message, precheck: err.precheck || null });
    }
    res.status(500).json({ error: err.message });
  }
});

app.get('/api/listings/:id/ai-model-photos', (req, res) => {
  try {
    res.json(getAiModelPhotosForListing(parseInt(req.params.id)));
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// --- Generate ---
app.post('/api/generate', async (req, res) => {
  try {
    const { photos, platform: platformId, ai_provider, ai_model, language, variants, template_id, seller_notes } = req.body;
    if (!photos?.length) return res.status(400).json({ error: 'Keine Fotos angegeben' });

    const platform = platforms[platformId];
    if (!platform) return res.status(400).json({ error: `Unbekannte Plattform: ${platformId}` });

    const settings = getSettings();
    const selectedTemplate = template_id ? getTemplate(parseInt(template_id)) : null;
    const notesParts = [
      settings.seller_notes,
      selectedTemplate?.seller_notes,
      typeof seller_notes === 'string' ? seller_notes.slice(0, 1000) : '',
    ].map(value => String(value || '').trim()).filter(Boolean);
    const sellerNotes = [...new Set(notesParts)].join('\n\n');
    const templateTags = Array.isArray(selectedTemplate?.default_tags) ? selectedTemplate.default_tags : [];
    const lang = language || selectedTemplate?.language || settings.default_language || 'de';
    const numVariants = Math.min(parseInt(variants) || 1, 3);
    const mergeTags = (values) => {
      const sourceTags = Array.isArray(values)
        ? values
        : typeof values === 'string'
          ? values.split(',')
          : [];
      return [...new Set([...sourceTags, ...templateTags].map(tag => String(tag).trim()).filter(Boolean))];
    };

    const systemPrompt = platform.buildPrompt(sellerNotes, { language: lang, variants: numVariants });
    const images = await prepareForAI(photos);
    const result = await generate(ai_provider || settings.default_ai, images, systemPrompt, ai_model);

    const meta = {
      brand: result.brand || null,
      category: result.category || null,
      size: result.size || null,
      color: result.color || null,
      condition: result.condition || null,
      suggested_price: result.suggested_price || null,
    };

    const baseData = {
      platform: platformId,
      ai_provider: ai_provider || settings.default_ai,
      photos, seller_notes: sellerNotes, language: lang,
    };

    if (numVariants > 1 && result.variants) {
      const variantsOut = result.variants.map(v => {
        const item = { ...baseData, title: v.title || '', description: v.description || '', tags: mergeTags(v.tags || v.hashtags || []), ...meta, price: meta.suggested_price };
        item.description_html = wrapInTemplate(item, platformId);
        return item;
      });
      const fees = meta.suggested_price ? calculateFees(platformId, meta.suggested_price) : null;
      return res.json({ variants: variantsOut, meta, fees });
    }

    const listing = { ...baseData, title: result.title || '', description: result.description || '', tags: mergeTags(result.tags || result.hashtags || []), ...meta, price: meta.suggested_price };
    const descHtml = wrapInTemplate(listing, platformId);
    const fees = meta.suggested_price ? calculateFees(platformId, meta.suggested_price) : null;
    res.json({ listing, meta, fees, description_html: descHtml });
  } catch (err) {
    console.error('Generate error:', err);
    res.status(500).json({ error: err.message });
  }
});

// --- Fee calculator ---
app.get('/api/fees', (req, res) => {
  const price = parseFloat(req.query.price) || 0;
  const platform = req.query.platform;
  if (platform) return res.json(calculateFees(platform, price));
  res.json(getAllFees(price));
});


// --- Browser extension center ---
const EXTENSION_VERSION = '1.9.0';
const EXTENSION_TARGETS = {
  chrome: { folder: join(__dirname, 'extensions', 'chrome'), package: `vendoo-link-chrome-${EXTENSION_VERSION}.zip` },
  edge: { folder: join(__dirname, 'extensions', 'edge'), package: `vendoo-link-edge-${EXTENSION_VERSION}.zip` },
  firefox: { folder: join(__dirname, 'extensions', 'firefox'), package: `vendoo-link-firefox-${EXTENSION_VERSION}.zip` },
  safari: { folder: join(__dirname, 'extensions', 'safari'), package: `vendoo-link-safari-${EXTENSION_VERSION}.zip` },
};

function openLocalFolder(folderPath) {
  try {
    const command = process.platform === 'win32' ? 'explorer.exe' : process.platform === 'darwin' ? 'open' : 'xdg-open';
    const child = spawn(command, [folderPath], { detached: true, stdio: 'ignore', windowsHide: true });
    child.unref();
    return true;
  } catch { return false; }
}

app.get('/api/extensions/info', (_req, res) => {
  const browsers = {};
  for (const [id, target] of Object.entries(EXTENSION_TARGETS)) {
    browsers[id] = { path: target.folder, available: existsSync(target.folder), package: target.package };
  }
  res.json({ version: EXTENSION_VERSION, platform: process.platform, browsers });
});

app.post('/api/extensions/prepare', (req, res) => {
  const browser = String(req.body?.browser || '').toLowerCase();
  const target = EXTENSION_TARGETS[browser];
  if (!target) return res.status(400).json({ error: 'Unbekannter Browser' });
  if (!existsSync(target.folder)) return res.status(404).json({ error: 'Extension-Ordner fehlt im Paket' });
  res.json({ ok: true, browser, path: target.folder, opened: openLocalFolder(target.folder), version: EXTENSION_VERSION });
});

app.get('/api/extensions/download/:browser', (req, res) => {
  const browser = String(req.params.browser || '').toLowerCase();
  const target = EXTENSION_TARGETS[browser];
  if (!target) return res.status(404).json({ error: 'Unbekannter Browser' });
  const packagePath = join(__dirname, 'extensions', 'packages', target.package);
  if (!existsSync(packagePath)) return res.status(404).json({ error: 'Extension-Paket fehlt' });
  res.download(packagePath, target.package);
});


// --- Article Quality Center ---
app.get('/api/quality-center', (req, res) => {
  try {
    res.json(listQualityCenter({
      query: req.query.q || '',
      status: req.query.status || 'all',
      grade: req.query.grade || 'all',
      page: req.query.page || 1,
      pageSize: req.query.page_size || 10,
    }));
  } catch (error) {
    res.status(500).json({ error: error.message || 'Quality Center konnte nicht geladen werden.' });
  }
});

app.get('/api/quality-center/:id', async (req, res) => {
  try {
    const report = await analyzeListingQuality(req.params.id, { uploadRoot: resolve(UPLOADS_DIR), persist: false });
    if (!report) return res.status(404).json({ error: 'Artikel nicht gefunden.' });
    report.history = getQualityHistory(req.params.id);
    report.comparison = compareQualityReports(req.params.id);
    res.json(report);
  } catch (error) {
    res.status(500).json({ error: error.message || 'Artikelqualität konnte nicht analysiert werden.' });
  }
});

app.post('/api/quality-center/:id/analyze', async (req, res) => {
  try {
    const report = await analyzeListingQuality(req.params.id, { uploadRoot: resolve(UPLOADS_DIR), persist: true });
    if (!report) return res.status(404).json({ error: 'Artikel nicht gefunden.' });
    report.history = getQualityHistory(req.params.id);
    report.comparison = compareQualityReports(req.params.id);
    if (req.user) auditLog(req.user.id, req.user.email, 'quality.analyzed', 'listing', String(req.params.id), `Score ${report.score}`, getClientIp(req));
    res.json(report);
  } catch (error) {
    res.status(500).json({ error: error.message || 'Qualitätsanalyse fehlgeschlagen.' });
  }
});

app.post('/api/quality-center/:id/ai-suggestions', async (req, res) => {
  try {
    const listing = getListing(req.params.id);
    if (!listing) return res.status(404).json({ error: 'Artikel nicht gefunden.' });
    const fields = Array.isArray(req.body?.fields) ? req.body.fields.filter(field => ['title','description','tags'].includes(field)) : ['title','description','tags'];
    if (!fields.length) return res.status(400).json({ error: 'Keine verbesserbaren Felder gewählt.' });
    const settings = getSettings();
    const provider = String(req.body?.provider || settings.default_ai || 'openrouter').trim();
    const model = String(req.body?.model || '').trim() || undefined;
    const mode = ['balanced','concise','sales','seo'].includes(req.body?.mode) ? req.body.mode : 'balanced';
    const fieldContract = {
      title: fields.includes('title'), description: fields.includes('description'), tags: fields.includes('tags'),
    };
    const systemPrompt = `Du bist das Vendoo Artikel Quality Center. Verbessere ausschließlich die ausdrücklich gewählten Felder eines bestehenden Gebrauchtartikel-Angebots. Erfinde keine Marke, Größe, Farbe, technischen Daten, Schäden, Echtheit, Lieferumfang oder Produkteigenschaften. Bestehende Tatsachen müssen erhalten bleiben. Profil: ${mode}. Plattform: ${listing.platform || 'allgemein'}. Gewählte Felder: ${fields.join(', ')}. Gib ausschließlich valides JSON aus: {"title":null oder "...","description":null oder "...","tags":null oder ["..."],"reasons":["..."]}. Nicht gewählte Felder müssen null sein.`;
    const userPrompt = JSON.stringify({
      title: listing.title || '', description: listing.description || '', tags: listing.tags || [], brand: listing.brand || '',
      category: listing.category || '', size: listing.size || '', color: listing.color || '', condition: listing.condition || '',
      seller_notes: listing.seller_notes || '', constraints: fieldContract,
    }, null, 2);
    const raw = await generateText(provider, systemPrompt, userPrompt, model);
    let parsed = null;
    try { parsed = JSON.parse(raw); } catch {
      const match = String(raw || '').match(/\{[\s\S]*\}/);
      if (match) try { parsed = JSON.parse(match[0]); } catch {}
    }
    if (!parsed || typeof parsed !== 'object') throw new Error('Die AI hat keinen gültigen Verbesserungsvorschlag geliefert.');
    const suggestions = {
      title: fields.includes('title') ? String(parsed.title || '').trim().slice(0, 180) : null,
      description: fields.includes('description') ? String(parsed.description || '').trim().slice(0, 6000) : null,
      tags: fields.includes('tags') ? [...new Set((Array.isArray(parsed.tags) ? parsed.tags : []).map(value => String(value).trim()).filter(Boolean))].slice(0, 20) : null,
      reasons: (Array.isArray(parsed.reasons) ? parsed.reasons : []).map(value => String(value).trim()).filter(Boolean).slice(0, 8),
    };
    res.json({ listing_id: listing.id, provider, model: model || null, mode, original: { title: listing.title, description: listing.description, tags: listing.tags }, suggestions });
  } catch (error) {
    res.status(500).json({ error: error.message || 'AI-Verbesserung konnte nicht erstellt werden.' });
  }
});

app.post('/api/quality-center/:id/apply-suggestions', async (req, res) => {
  try {
    const current = getListing(req.params.id);
    if (!current) return res.status(404).json({ error: 'Artikel nicht gefunden.' });
    const patch = {};
    if (req.body?.title !== undefined) patch.title = String(req.body.title || '').trim().slice(0, 180);
    if (req.body?.description !== undefined) {
      patch.description = String(req.body.description || '').trim().slice(0, 6000);
      patch.description_html = sanitizeRichHtml(plainTextToSafeHtml(patch.description));
    }
    if (req.body?.tags !== undefined) patch.tags = Array.isArray(req.body.tags) ? req.body.tags.map(value => String(value).trim()).filter(Boolean).slice(0, 20) : current.tags;
    if (!Object.keys(patch).length) return res.status(400).json({ error: 'Keine bestätigten Änderungen vorhanden.' });
    const updated = updateListing(req.params.id, normalizeListingPayload(patch));
    const report = await analyzeListingQuality(req.params.id, { uploadRoot: resolve(UPLOADS_DIR), persist: true });
    if (req.user) auditLog(req.user.id, req.user.email, 'quality.ai_applied', 'listing', String(req.params.id), Object.keys(patch).join(', '), getClientIp(req));
    res.json({ listing: updated, report });
  } catch (error) {
    res.status(500).json({ error: error.message || 'Verbesserungen konnten nicht übernommen werden.' });
  }
});

// Listings and trash routes are owned by vendoo.listings.

// --- Templates CRUD ---
app.get('/api/templates', (_req, res) => res.json(getTemplates()));
app.post('/api/templates', (req, res) => res.json(createTemplate(req.body)));
app.put('/api/templates/:id', (req, res) => {
  const t = updateTemplate(req.params.id, req.body);
  if (!t) return res.status(404).json({ error: 'Nicht gefunden' });
  res.json(t);
});
app.delete('/api/templates/:id', (req, res) => {
  deleteTemplate(req.params.id);
  res.json({ ok: true });
});

// Inventory routes are owned by vendoo.inventory.

// --- CSV Export ---
app.get('/api/export/csv', (req, res) => {
  const listings = getListingsForExport(req.query.platform, req.query.status);
  const headers = ['ID', 'Plattform', 'Status', 'Titel', 'Beschreibung', 'Tags', 'Marke', 'Kategorie', 'Groesse', 'Farbe', 'Zustand', 'Preis', 'Erstellt'];
  const escape = v => `"${String(v || '').replace(/"/g, '""')}"`;
  const rows = listings.map(l => [
    l.id, l.platform, l.status || 'active', l.title, l.description,
    (l.tags || []).join('; '), l.brand, l.category, l.size, l.color,
    l.condition, l.price, l.created_at,
  ].map(escape).join(','));
  const csv = [headers.join(','), ...rows].join('\n');
  res.setHeader('Content-Type', 'text/csv; charset=utf-8');
  res.setHeader('Content-Disposition', 'attachment; filename=vendoo-export.csv');
  res.send('﻿' + csv);
});

// --- HTML Templates ---
app.get('/api/html-templates', (_req, res) => res.json(getHtmlTemplates()));

app.post('/api/html-wrap', (req, res) => {
  const { listing_id, platform } = req.body;
  const listing = getListing(listing_id);
  if (!listing) return res.status(404).json({ error: 'Nicht gefunden' });
  const html = wrapInTemplate(listing, platform || listing.platform);
  res.json({ html });
});

// Live rendering for the generator and listing editor. No data is persisted here.
app.post('/api/html-render', (req, res) => {
  try {
    const listing = normalizeListingPayload(req.body?.listing || req.body || {});
    const platform = req.body?.platform || listing.platform || 'vinted';
    res.json({ html: wrapInTemplate(listing, platform) });
  } catch (err) {
    res.status(400).json({ error: err.message || 'HTML konnte nicht gerendert werden' });
  }
});

// --- Publishing Hardening 1.29.0 ---
app.get('/api/publishing/capabilities', (_req, res) => {
  res.json({
    ...getPublishCapabilities(),
    connections: {
      ebay: { connected: ebayApi.isConnected(), configured: ebayApi.hasCredentials(), sandbox: process.env.EBAY_SANDBOX === 'true' },
      etsy: { connected: etsyApi.isConnected(), configured: Boolean(process.env.ETSY_API_KEY) },
      extension: { version: '1.9.0', platforms: ['vinted', 'ebay-ka'] },
    },
  });
});

app.get('/api/publishing/jobs', (req, res) => {
  res.json(paginatePublishingJobs({
    page: req.query.page,
    pageSize: req.query.page_size,
    status: req.query.status || '',
    platform: req.query.platform || '',
    q: req.query.q || '',
  }));
});

app.get('/api/publishing/jobs/:id', (req, res) => {
  const job = getPublishingJob(req.params.id);
  if (!job) return res.status(404).json({ error: 'Publishing-Auftrag nicht gefunden' });
  res.json(job);
});

app.post('/api/publishing/jobs', (req, res) => {
  try {
    const listingIds = Array.isArray(req.body?.listing_ids) ? req.body.listing_ids : [req.body?.listing_id];
    const platform = req.body?.platform || null;
    const action = req.body?.action || 'publish';
    const results = [];
    for (const listingId of listingIds.filter(Boolean)) {
      results.push(enqueuePublishingJob({ listingId, platform, action, force: Boolean(req.body?.force), payload: req.body?.payload || {} }));
    }
    res.status(results.some(item => !item.duplicate) ? 201 : 200).json({ jobs: results.map(item => item.job), duplicates: results.filter(item => item.duplicate).length });
  } catch (error) {
    const status = error.code === 'DUPLICATE_PUBLISH' ? 409 : 400;
    res.status(status).json({ error: error.message, code: error.code || 'PUBLISH_JOB_ERROR', existing: error.existing || null });
  }
});

app.post('/api/publishing/jobs/:id/retry', (req, res) => {
  try { res.json(retryPublishingJob(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message }); }
});

app.post('/api/publishing/jobs/:id/cancel', (req, res) => {
  try { res.json(cancelPublishingJob(req.params.id)); }
  catch (error) { res.status(400).json({ error: error.message }); }
});

app.post('/api/publishing/jobs/:id/action', (req, res) => {
  try {
    const source = getPublishingJob(req.params.id);
    if (!source) return res.status(404).json({ error: 'Auftrag nicht gefunden' });
    const action = req.body?.action;
    if (!['sync', 'update', 'end'].includes(action)) return res.status(400).json({ error: 'Ungültige Aktion' });
    const result = enqueuePublishingJob({
      listingId: source.listing_id,
      platform: source.platform,
      action,
      force: true,
      payload: { offerId: source.offer_id, externalId: source.external_id },
    });
    res.status(201).json(result.job);
  } catch (error) { res.status(400).json({ error: error.message }); }
});

app.delete('/api/publishing/jobs', (req, res) => {
  try {
    const ids = Array.isArray(req.body?.ids) ? req.body.ids : String(req.query.ids || '').split(',').filter(Boolean);
    res.json({ deleted: deletePublishingJobs(ids) });
  } catch (error) { res.status(400).json({ error: error.message }); }
});

app.get('/api/publishing/events', (req, res) => {
  res.json(listPublishingEvents({ limit: req.query.limit, level: req.query.level || '', platform: req.query.platform || '' }));
});

app.post('/api/extension/publishing/claim', (req, res) => {
  const job = claimExtensionJob({ platform: req.body?.platform || 'vinted', clientId: req.body?.client_id || 'vendoo-link' });
  if (!job) return res.status(204).end();
  res.json(job);
});

app.post('/api/extension/publishing/:id/complete', (req, res) => {
  try { res.json(completeExtensionJob(req.params.id, req.body || {})); }
  catch (error) { res.status(400).json({ error: error.message }); }
});

// --- Publishing ---
const PUBLISH_URLS = {
  vinted: 'https://www.vinted.de/items/new',
  'ebay-ka': 'https://www.kleinanzeigen.de/p-anzeige-aufgeben.html',
  'ebay-de': 'https://www.ebay.de/sell/create',
};

app.get('/api/publish/info/:id', (req, res) => {
  const listing = getListing(req.params.id);
  if (!listing) return res.status(404).json({ error: 'Nicht gefunden' });
  const logs = getPublishLogs(listing.id);
  const platform = listing.platform;
  const publishUrl = PUBLISH_URLS[platform] || null;
  const canApiPublish = (platform === 'etsy' && etsyApi.isConnected()) || (platform === 'ebay-de' && ebayApi.isConnected());
  res.json({ listing, logs, publishUrl, canApiPublish });
});

app.post('/api/publish/smart-copy', (req, res) => {
  const { listing_id, platform } = req.body;
  const listing = getListing(listing_id);
  if (!listing) return res.status(404).json({ error: 'Nicht gefunden' });
  const targetPlatform = platform || listing.platform;
  const publishUrl = PUBLISH_URLS[targetPlatform] || null;
  createPublishLog(listing_id, targetPlatform, 'copied');
  const tags = Array.isArray(listing.tags) ? listing.tags.join(', ') : listing.tags;
  res.json({
    publishUrl,
    fields: {
      title: listing.title,
      description: listing.description,
      tags,
      price: listing.price || '',
      brand: listing.brand || '',
      category: listing.category || '',
      size: listing.size || '',
      color: listing.color || '',
      condition: listing.condition || '',
    },
    photos: listing.photos,
  });
});

app.post('/api/publish/mark', (req, res) => {
  const { listing_id, platform, status, external_id, external_url } = req.body;
  const target = platform || 'unknown';
  const activeJob = db.prepare(`SELECT id FROM publishing_jobs WHERE listing_id = ? AND platform = ? AND mode = 'extension'
    AND status IN ('awaiting_user','running') ORDER BY created_at LIMIT 1`).get(listing_id, target);
  if (activeJob) {
    const job = completeExtensionJob(activeJob.id, { status: status || 'published', externalId: external_id || null, externalUrl: external_url || null });
    return res.json({ ok: true, job });
  }
  createPublishLog(listing_id, target, status || 'published', external_id || null, external_url || null);
  res.json({ ok: true, job: null });
});

app.get('/api/publish/status', (req, res) => {
  const ids = (req.query.ids || '').split(',').map(Number).filter(Boolean);
  res.json(getPublishStatus(ids));
});

app.post('/api/publish/batch-photos', async (req, res) => {
  const { listing_ids } = req.body;
  if (!listing_ids?.length) return res.status(400).json({ error: 'Keine Artikel' });
  const allPhotos = [];
  for (const id of listing_ids) {
    const listing = getListing(id);
    if (listing?.photos) {
      for (const p of listing.photos) allPhotos.push(p);
    }
  }
  res.json({ photos: allPhotos });
});

app.post('/api/photos/archive', (req, res) => {
  const requested = Array.isArray(req.body?.photos) ? req.body.photos : [];
  const listingIds = Array.isArray(req.body?.listing_ids) ? req.body.listing_ids : [];
  const photos = [...requested];
  for (const id of listingIds) {
    const listing = getListing(Number(id));
    if (listing?.photos?.length) photos.push(...listing.photos);
  }

  const unique = [...new Set(photos.map(photo => String(photo || '').replace(/\\/g, '/')).filter(Boolean))];
  if (unique.length < 2) return res.status(400).json({ error: 'Für ein ZIP werden mindestens zwei Fotos benötigt' });

  const uploadRoot = resolve(UPLOADS_DIR);
  const files = [];
  for (const [index, relative] of unique.entries()) {
    const absolute = resolve(uploadRoot, relative);
    if (absolute !== uploadRoot && !absolute.startsWith(uploadRoot + sep)) continue;
    if (!existsSync(absolute) || !statSync(absolute).isFile()) continue;
    const originalName = basename(relative) || `foto-${index + 1}.jpg`;
    const prefix = String(index + 1).padStart(2, '0');
    files.push({ name: `${prefix}-${originalName}`, data: readFileSync(absolute) });
  }

  if (files.length < 2) return res.status(404).json({ error: 'Nicht genügend Fotodateien gefunden' });
  const archive = createZip(files);
  const date = new Date().toISOString().slice(0, 10);
  res.setHeader('Content-Type', 'application/zip');
  res.setHeader('Content-Disposition', `attachment; filename=vendoo-fotos-${date}.zip`);
  res.setHeader('Content-Length', archive.length);
  res.send(archive);
});

// --- Etsy OAuth ---
app.get('/auth/etsy', (req, res) => {
  try {
    const redirectUri = `http://localhost:${PORT}/auth/etsy/callback`;
    const url = etsyApi.getAuthUrl(redirectUri);
    res.redirect(url);
  } catch (err) {
    res.status(400).send(`Fehler: ${err.message}. Bitte ETSY_API_KEY in Einstellungen setzen.`);
  }
});

app.get('/auth/etsy/callback', async (req, res) => {
  try {
    const { code, state } = req.query;
    const redirectUri = `http://localhost:${PORT}/auth/etsy/callback`;
    await etsyApi.handleCallback(code, state, redirectUri);
    res.send('<html><body><h2>Etsy verbunden!</h2><p>Du kannst dieses Fenster schließen.</p><script>window.opener && window.opener.postMessage("etsy-connected","*"); setTimeout(()=>window.close(),2000);</script></body></html>');
  } catch (err) {
    res.status(500).send(`OAuth Fehler: ${err.message}`);
  }
});

app.get('/api/etsy/status', (_req, res) => {
  res.json({ connected: etsyApi.isConnected(), hasApiKey: !!process.env.ETSY_API_KEY });
});

app.post('/api/publish/etsy', (req, res) => {
  try {
    const result = enqueuePublishingJob({ listingId: req.body?.listing_id, platform: 'etsy', action: 'publish', force: Boolean(req.body?.force) });
    res.status(result.duplicate ? 200 : 202).json({ ok: true, queued: true, duplicate: result.duplicate, job: result.job });
  } catch (error) {
    res.status(error.code === 'DUPLICATE_PUBLISH' ? 409 : 400).json({ error: error.message, code: error.code || 'PUBLISH_JOB_ERROR', existing: error.existing || null });
  }
});

// --- Extension Queue ---
app.get('/api/extension/queue', (req, res) => {
  const platform = req.query.platform || 'vinted';
  const jobRows = db.prepare(`SELECT j.id AS publishing_job_id, j.status AS publishing_job_status, l.*
    FROM publishing_jobs j JOIN listings l ON l.id = j.listing_id
    WHERE j.platform = ? AND j.mode = 'extension' AND j.status = 'awaiting_user'
    ORDER BY j.created_at`).all(platform).map(row => {
      try { row.photos = JSON.parse(row.photos || '[]'); } catch { row.photos = []; }
      try { row.tags = JSON.parse(row.tags || '[]'); } catch { row.tags = []; }
      return row;
    });
  const all = getListings(null, platform, 'active');
  const queuedIds = new Set(jobRows.map(row => row.id));
  const ids = all.map(l => l.id);
  const pubStatus = getPublishStatus(ids);
  const fallback = all.filter(l => {
    if (queuedIds.has(l.id)) return false;
    const ps = pubStatus[l.id];
    return !ps || !ps[platform] || ps[platform] === 'pending' || ps[platform] === 'copied';
  });
  const published = all.filter(l => pubStatus[l.id]?.[platform] === 'published');
  res.json({ queue: [...jobRows, ...fallback], published, total: all.length, hardened_jobs: jobRows.length });
});

app.get('/api/extension/listing/:id/photos', (req, res) => {
  const listing = getListing(req.params.id);
  if (!listing) return res.status(404).json({ error: 'Nicht gefunden' });
  const baseUrl = resolvePublicBaseUrl(req);
  const photoUrls = (listing.photos || []).map(filename => `${baseUrl}/uploads/${String(filename).split('/').map(encodeURIComponent).join('/')}`);
  res.json({ photos: photoUrls, listing });
});

// --- eBay OAuth ---
app.get('/auth/ebay', (req, res) => {
  try {
    const redirectUri = `http://localhost:${PORT}/auth/ebay/callback`;
    const url = ebayApi.getOAuthUrl(redirectUri);
    res.redirect(url);
  } catch (err) {
    res.status(400).send(`Fehler: ${err.message}. Bitte eBay Credentials in Einstellungen setzen.`);
  }
});

app.get('/auth/ebay/callback', async (req, res) => {
  try {
    const { code, state } = req.query;
    const redirectUri = `http://localhost:${PORT}/auth/ebay/callback`;
    await ebayApi.handleCallback(code, state, redirectUri);
    res.send('<html><body><h2>eBay verbunden!</h2><p>Du kannst dieses Fenster schließen.</p><script>window.opener && window.opener.postMessage("ebay-connected","*"); setTimeout(()=>window.close(),2000);</script></body></html>');
  } catch (err) {
    res.status(500).send(`OAuth Fehler: ${err.message}`);
  }
});

app.get('/api/ebay/status', (_req, res) => {
  res.json({
    connected: ebayApi.isConnected(),
    hasCredentials: ebayApi.hasCredentials(),
    sandbox: process.env.EBAY_SANDBOX === 'true',
  });
});

app.get('/api/ebay/policies', async (_req, res) => {
  try {
    if (!ebayApi.isConnected()) return res.status(400).json({ error: 'Nicht mit eBay verbunden' });
    const policies = await ebayApi.getPolicies();
    res.json(policies);
  } catch (err) { res.status(500).json({ error: err.message }); }
});

// --- eBay Queue ---
app.get('/api/ebay/queue', (_req, res) => {
  res.json(getEbayQueue());
});

app.post('/api/ebay/queue', (req, res) => {
  const { listing_ids } = req.body;
  if (!listing_ids?.length) return res.status(400).json({ error: 'Keine Artikel' });
  const added = addToEbayQueue(listing_ids);
  res.json({ added: added.length, queue: getEbayQueue() });
});

app.delete('/api/ebay/queue/:id', (req, res) => {
  removeFromEbayQueue(req.params.id);
  res.json({ ok: true });
});

app.delete('/api/ebay/queue', (req, res) => {
  clearEbayQueue(req.query.status || null);
  res.json({ ok: true });
});

app.post('/api/ebay/publish/:id', (req, res) => {
  try {
    const result = enqueuePublishingJob({ listingId: req.params.id, platform: 'ebay-de', action: 'publish', force: Boolean(req.body?.force) });
    res.status(result.duplicate ? 200 : 202).json({ ok: true, queued: true, duplicate: result.duplicate, job: result.job });
  } catch (error) {
    res.status(error.code === 'DUPLICATE_PUBLISH' ? 409 : 400).json({ error: error.message, code: error.code || 'PUBLISH_JOB_ERROR', existing: error.existing || null });
  }
});

app.post('/api/ebay/queue/process', async (_req, res) => {
  if (!ebayApi.isConnected()) return res.status(400).json({ error: 'Nicht mit eBay verbunden' });

  const pending = getEbayQueue('pending');
  const results = [];

  for (const item of pending) {
    updateEbayQueue(item.id, { status: 'uploading' });
    try {
      const listing = getListing(item.listing_id);
      if (!listing) { updateEbayQueue(item.id, { status: 'failed', error: 'Listing nicht gefunden' }); continue; }

      const sku = listing.sku || `AL-${listing.id}`;
      const result = await ebayApi.createAndPublish(listing, sku);

      updateEbayQueue(item.id, { status: 'published', ebay_item_id: result.listingId, ebay_url: result.url });
      createPublishLog(listing.id, 'ebay-de', 'published', result.listingId, result.url);
      results.push({ id: item.id, listing_id: item.listing_id, status: 'published', url: result.url });
    } catch (err) {
      console.error(`eBay queue item ${item.id} failed:`, err);
      updateEbayQueue(item.id, { status: 'failed', error: err.message, retries: (item.retries || 0) + 1 });
      results.push({ id: item.id, listing_id: item.listing_id, status: 'failed', error: err.message });
    }
  }

  res.json({ processed: results.length, results, queue: getEbayQueue() });
});

app.post('/api/ebay/queue/retry', (_req, res) => {
  const failed = getEbayQueue('failed');
  for (const item of failed) {
    updateEbayQueue(item.id, { status: 'pending', error: null });
  }
  res.json({ retried: failed.length, queue: getEbayQueue() });
});

// --- Categories ---
app.get('/api/categories/:platform', (req, res) => {
  const tree = getCategories(req.params.platform);
  const flat = getCategoriesFlat(req.params.platform);
  res.json({ tree, flat });
});

app.get('/api/categories/:platform/match', (req, res) => {
  const q = req.query.q || '';
  res.json(matchCategories(req.params.platform, q));
});

// --- Studio Flow Dashboard ---
app.get('/api/dashboard/workflow', (req, res) => {
  const requestedLimit = Number.parseInt(req.query.limit, 10);
  const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(requestedLimit, 1), 12) : 3;
  res.json(getDashboardWorkflow(limit));
});

app.get('/api/dashboard/analytics', (req, res) => {
  const days = Number.parseInt(req.query.days, 10) || 30;
  res.json(getDashboardAnalytics(days));
});

// --- Dashboard Stats ---
app.get('/api/dashboard/stats', (_req, res) => {
  const listings = getListings();
  const total = listings.length;
  const active = listings.filter(l => l.status === 'active').length;
  const sold = listings.filter(l => l.status === 'sold').length;
  const reserved = listings.filter(l => l.status === 'reserved').length;

  const totalValue = listings.reduce((s, l) => s + (l.price || 0), 0);
  const soldValue = listings.filter(l => l.status === 'sold').reduce((s, l) => s + (l.price || 0), 0);

  const platforms = {};
  for (const l of listings) {
    platforms[l.platform] = (platforms[l.platform] || 0) + 1;
  }

  const now = new Date();
  const timeline = [];
  for (let i = 6; i >= 0; i--) {
    const d = new Date(now);
    d.setDate(d.getDate() - i);
    const dateStr = d.toISOString().slice(0, 10);
    const count = listings.filter(l => l.created_at && l.created_at.startsWith(dateStr)).length;
    timeline.push({ date: dateStr, label: d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit' }), count });
  }

  const recent = listings.slice(0, 5).map(l => ({
    id: l.id, title: l.title, platform: l.platform,
    price: l.price, sku: l.sku, status: l.status,
    photo: l.photos?.[0] || null,
    created_at: l.created_at,
  }));

  res.json({ total, active, sold, reserved, totalValue, soldValue, platforms, timeline, recent });
});

// --- Operations Center 1.30.0: Backup, Restore, Updates & Extension diagnostics ---
app.get('/api/operations/status', requireRole('admin'), (_req, res) => {
  try { res.json(getOperationsStatus()); }
  catch (error) { res.status(500).json({ error: error.message || 'Operationsstatus konnte nicht geladen werden.' }); }
});

app.get('/api/operations/settings', requireRole('admin'), (_req, res) => {
  try { res.json(getOperationsStatus().settings); }
  catch (error) { res.status(500).json({ error: error.message || 'Betriebseinstellungen konnten nicht geladen werden.' }); }
});

app.put('/api/operations/settings', requireRole('admin'), (req, res) => {
  try {
    const settings = updateOperationSettings(req.body || {});
    if (req.user) auditLog(req.user.id, req.user.email, 'operations.settings.updated', 'system', null, JSON.stringify(settings), getClientIp(req));
    res.json(settings);
  } catch (error) { res.status(400).json({ error: error.message || 'Betriebseinstellungen konnten nicht gespeichert werden.' }); }
});

app.get('/api/operations/backups', requireRole('admin'), (_req, res) => {
  try { res.json({ backups: listBackups() }); }
  catch (error) { res.status(500).json({ error: error.message || 'Backups konnten nicht geladen werden.' }); }
});

app.post('/api/operations/backups', requireRole('admin'), async (req, res) => {
  try {
    const result = await createBackup({
      kind: req.body?.kind || 'manual',
      includeUploads: req.body?.include_uploads !== false,
      includeConfig: req.body?.include_config === true,
      label: req.body?.label || '',
    });
    if (req.user) auditLog(req.user.id, req.user.email, 'backup.created', 'backup', result.id, JSON.stringify({ filename: result.filename, sha256: result.sha256 }), getClientIp(req));
    res.status(201).json(result);
  } catch (error) { res.status(400).json({ error: error.message || 'Backup konnte nicht erstellt werden.' }); }
});

app.get('/api/operations/backups/:id/download', requireRole('admin'), (req, res) => {
  try {
    const row = getBackupPath(req.params.id);
    res.download(row.file_path, row.file_name);
  } catch (error) { res.status(404).json({ error: error.message || 'Backup wurde nicht gefunden.' }); }
});

app.post('/api/operations/backups/:id/verify', requireRole('admin'), (req, res) => {
  try {
    const result = verifyStoredBackup(req.params.id);
    if (req.user) auditLog(req.user.id, req.user.email, 'backup.verified', 'backup', req.params.id, JSON.stringify({ ok: result.ok, errors: result.errors }), getClientIp(req));
    res.json(result);
  } catch (error) { res.status(400).json({ error: error.message || 'Backup konnte nicht geprüft werden.' }); }
});

app.delete('/api/operations/backups/:id', requireRole('admin'), (req, res) => {
  try {
    const result = deleteBackup(req.params.id);
    if (req.user) auditLog(req.user.id, req.user.email, 'backup.deleted', 'backup', req.params.id, null, getClientIp(req));
    res.json(result);
  } catch (error) { res.status(400).json({ error: error.message || 'Backup konnte nicht gelöscht werden.' }); }
});

app.post('/api/operations/restore/stage', requireRole('admin'), operationsUpload.single('backup'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'Keine Backup-ZIP ausgewählt.' });
  try {
    if (extname(req.file.originalname || '').toLowerCase() !== '.zip') throw new Error('Für die sichere Wiederherstellung wird ein Vendoo-ZIP-Backup benötigt.');
    const result = await stageRestore(readFileSync(req.file.path), req.file.originalname);
    if (req.user) auditLog(req.user.id, req.user.email, 'restore.staged', 'system', result.id, JSON.stringify({ safety_backup_id: result.safety_backup_id }), getClientIp(req));
    res.status(202).json({ ...result, message: 'Wiederherstellung geprüft und vorbereitet. Vendoo jetzt beenden und im Setup Menüpunkt 11 ausführen.' });
  } catch (error) { res.status(400).json({ error: error.message || 'Wiederherstellung konnte nicht vorbereitet werden.' }); }
  finally { try { if (req.file?.path && existsSync(req.file.path)) unlinkSync(req.file.path); } catch {} }
});

app.post('/api/operations/updates/check', requireRole('admin'), async (_req, res) => {
  try { res.json(await checkForUpdates()); }
  catch (error) { res.status(400).json({ error: error.message || 'Updateprüfung fehlgeschlagen.' }); }
});

app.post('/api/operations/updates/stage', requireRole('admin'), operationsUpload.single('update'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'Kein Update-ZIP ausgewählt.' });
  try {
    if (extname(req.file.originalname || '').toLowerCase() !== '.zip') throw new Error('Vendoo-Updates müssen als ZIP hochgeladen werden.');
    const result = await stageUpdate(readFileSync(req.file.path), req.file.originalname);
    if (req.user) auditLog(req.user.id, req.user.email, 'update.staged', 'system', result.id, JSON.stringify({ from: result.version_from, to: result.version_to, safety_backup_id: result.safety_backup_id }), getClientIp(req));
    res.status(202).json({ ...result, message: `Update ${result.version_to} geprüft und vorbereitet. Vendoo jetzt beenden und im Setup Menüpunkt 11 ausführen.` });
  } catch (error) { res.status(400).json({ error: error.message || 'Update konnte nicht vorbereitet werden.' }); }
  finally { try { if (req.file?.path && existsSync(req.file.path)) unlinkSync(req.file.path); } catch {} }
});

app.post('/api/operations/pending/cancel', requireRole('admin'), (req, res) => {
  try {
    const result = cancelPendingOperation();
    if (req.user) auditLog(req.user.id, req.user.email, 'operation.cancelled', 'system', result.pending?.id || null, JSON.stringify(result.pending || {}), getClientIp(req));
    res.json(result);
  } catch (error) { res.status(400).json({ error: error.message || 'Ausstehende Operation konnte nicht abgebrochen werden.' }); }
});

app.get('/api/extensions/diagnostics', requireRole('admin'), (_req, res) => {
  try { res.json(getExtensionDiagnostics()); }
  catch (error) { res.status(500).json({ error: error.message || 'Extension-Diagnose konnte nicht geladen werden.' }); }
});

app.post('/api/extensions/heartbeat', (req, res) => {
  try {
    res.json(recordExtensionHeartbeat({
      clientId: req.body?.client_id,
      browser: req.body?.browser,
      version: req.body?.version,
      platform: req.body?.platform,
      capabilities: req.body?.capabilities,
      serverUrl: req.body?.server_url,
      userAgent: req.get('user-agent') || '',
    }));
  } catch (error) { res.status(400).json({ error: error.message || 'Extension-Heartbeat fehlgeschlagen.' }); }
});

// Compatibility route: creates a verified database-only ZIP instead of copying an open SQLite file.
app.get('/api/backup', requireRole('admin'), async (req, res) => {
  try {
    const backup = await createBackup({ kind: 'database', includeUploads: false, includeConfig: false, label: 'datenbank' });
    const row = getBackupPath(backup.id);
    res.download(row.file_path, row.file_name);
  } catch (error) { res.status(500).json({ error: error.message || 'Backup konnte nicht erstellt werden.' }); }
});

app.post('/api/restore', requireRole('admin'), operationsUpload.single('backup'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'Keine Backup-Datei ausgewählt.' });
  try {
    if (extname(req.file.originalname || '').toLowerCase() !== '.zip') throw new Error('Direktes Einspielen offener DB-Dateien ist gesperrt. Bitte ein verifiziertes Vendoo-ZIP-Backup verwenden.');
    const result = await stageRestore(readFileSync(req.file.path), req.file.originalname);
    res.status(202).json({ ok: true, ...result, message: 'Backup geprüft und vorbereitet. Vendoo beenden und Setup-Menüpunkt 11 ausführen.' });
  } catch (error) { res.status(400).json({ error: error.message || 'Backup konnte nicht vorbereitet werden.' }); }
  finally { try { if (req.file?.path && existsSync(req.file.path)) unlinkSync(req.file.path); } catch {} }
});

// --- Scheduling ---
app.get('/api/schedule', (_req, res) => {
  res.json(getScheduledPublishes());
});

app.post('/api/schedule', (req, res) => {
  const { listing_id, platform, scheduled_at } = req.body;
  if (!listing_id || !scheduled_at) return res.status(400).json({ error: 'listing_id und scheduled_at erforderlich' });
  const id = schedulePublish(listing_id, platform || 'ebay-de', scheduled_at);
  res.json({ ok: true, id, scheduled_at });
});

app.delete('/api/schedule/:id', (req, res) => {
  removeScheduledPublish(req.params.id);
  res.json({ ok: true });
});

// --- Price Suggestion ---
app.get('/api/price-suggest', async (req, res) => {
  const { title, category, condition } = req.query;
  if (!title) return res.status(400).json({ error: 'Titel erforderlich' });

  const listings = getListings();
  const words = title.toLowerCase().split(/\s+/).filter(w => w.length > 2);

  const similar = listings
    .filter(l => l.price && l.price > 0)
    .map(l => {
      const lTitle = (l.title || '').toLowerCase();
      const matchCount = words.filter(w => lTitle.includes(w)).length;
      const catMatch = category && l.category && l.category.toLowerCase().includes(category.toLowerCase()) ? 2 : 0;
      return { ...l, score: matchCount + catMatch };
    })
    .filter(s => s.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, 10);

  if (!similar.length) return res.json({ suggestion: null, similar: [] });

  const prices = similar.map(s => s.price);
  const avg = prices.reduce((a, b) => a + b, 0) / prices.length;
  const min = Math.min(...prices);
  const max = Math.max(...prices);

  res.json({
    suggestion: Math.round(avg * 2) / 2,
    min, max, avg: Math.round(avg * 100) / 100,
    count: similar.length,
    similar: similar.slice(0, 5).map(s => ({ title: s.title, price: s.price, platform: s.platform })),
  });
});

// --- Providers & Platforms ---
app.get('/api/providers', (_req, res) => res.json(getProviders()));
app.get('/api/platforms', (_req, res) => {
  res.json(Object.values(platforms).map(p => ({
    id: p.id, name: p.name, fields: p.fields,
    maxTitleLength: p.maxTitleLength, maxTags: p.maxTags,
  })));
});

// --- Scheduler Timer ---
async function processScheduledPublishes() {
  const due = getDueScheduled();
  for (const item of due) {
    updateScheduledPublish(item.id, { status: 'publishing', error: null });
    try {
      const result = enqueuePublishingJob({ listingId: item.listing_id, platform: item.platform, action: 'publish' });
      updateScheduledPublish(item.id, { status: result.duplicate ? 'queued' : 'queued', error: result.duplicate ? 'Bereits in der Publishing-Queue' : null });
      console.log(`Scheduled publish ${item.id} → Publishing Job ${result.job.id}`);
    } catch (error) {
      if (error.code === 'DUPLICATE_PUBLISH') {
        updateScheduledPublish(item.id, { status: 'published', error: null });
      } else {
        updateScheduledPublish(item.id, { status: 'failed', error: error.message });
      }
    }
  }
  if (due.length) console.log(`Scheduler: ${due.length} fällige Veröffentlichungen an die gehärtete Queue übergeben`);
}

const scheduledPublishTimer = setInterval(processScheduledPublishes, 60_000);
scheduledPublishTimer.unref?.();

// --- Trash cleanup ---
function runTrashCleanup() {
  try {
    const result = cleanupTrash(30);
    if (result.changes > 0) console.log(`Trash cleanup: ${result.changes} alte Einträge endgültig gelöscht`);
  } catch (err) { console.error('Trash cleanup error:', err.message); }
}
const trashCleanupTimer = setInterval(runTrashCleanup, 60 * 60 * 1000); // every hour
trashCleanupTimer.unref?.();

// --- Start ---
if (!/^(0|false|no|off)$/i.test(String(process.env.LOCAL_IMAGE_ENABLED || 'true'))) {
  try {
    const settings = getSettings();
    const bootstrap = ensureLocalFluxBootstrap({
      token: process.env.LOCAL_IMAGE_TOKEN || '',
      installPath: process.env.LOCAL_IMAGE_INSTALL_PATH || settings.ai_model_photo_local_install_path || 'local-ai/comfyui',
    });
    console.log(`Lokaler FLUX-Prompt-Workflow geprüft: ${bootstrap.workflow_path}`);
  } catch (error) {
    console.error('Lokale AI-Workflow-Selbstheilung fehlgeschlagen:', error.message || error);
  }
} else {
  console.log('FLUX/ComfyUI ist deaktiviert oder wird als externer Dienst betrieben.');
}
await loadPlatforms();
const BIND_HOST = String(process.env.VENDOO_BIND_HOST || '127.0.0.1').trim() || '127.0.0.1';
const server = app.listen(PORT, BIND_HOST, async () => {
  console.log(`Vendoo ${APP_VERSION} läuft auf http://${BIND_HOST === '0.0.0.0' ? 'localhost' : BIND_HOST}:${PORT}`);
  console.log(`Datenverzeichnis: ${runtimePathSummary().data_root}`);
  if (BIND_HOST === '0.0.0.0' || BIND_HOST === '::') for (const entry of getLanIpv4Addresses()) console.log(`Vendoo im LAN: http://${entry.address}:${PORT} (${entry.name})`);
  processScheduledPublishes();
  runTrashCleanup();
});

function gracefulShutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  console.log(`${signal}: Vendoo wird geordnet beendet ...`);
  clearInterval(scheduledPublishTimer);
  clearInterval(trashCleanupTimer);
  const forceTimer = setTimeout(() => {
    console.error('Shutdown-Zeitlimit erreicht. Prozess wird beendet.');
    process.exit(1);
  }, 10_000);
  server.close(async () => {
    try { await platformKernel.stop(); } catch (error) { console.error('Platform-Kernel-Shutdown fehlgeschlagen:', error.message || error); }
    try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch {}
    try { db.close(); } catch {}
    clearTimeout(forceTimer);
    console.log('Vendoo wurde sauber beendet.');
    process.exit(0);
  });
}
process.once('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.once('SIGINT', () => gracefulShutdown('SIGINT'));
