const express = require('express');
const cors = require('cors');
const Database = require('better-sqlite3');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const os = require('os');

const app = express();
const PORT = process.env.PORT || 3000;

const MAX_PROFILES = 5;
const DEFAULT_PROFILE_NAMES = {
  1: 'Profil 1',
  2: 'Profil 2',
  3: 'Profil 3',
  4: 'Profil 4',
  5: 'Profil 5'
};
const PROFILE_SCOPE_COOKIE = 'fb_tracker_scope';
const PROFILE_SCOPE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
const FACEBOOK_TRACKING_PARAM_PREFIXES = ['__cft__', '__tn__', '__eep__', 'mibextid'];
const SEARCH_POST_HIDE_THRESHOLD = 2;
const SEARCH_POST_RETENTION_DAYS = 90;
const MAX_POST_TEXT_LENGTH = 4000;
const MIN_TEXT_HASH_LENGTH = 120;
const MIN_SIMILAR_TEXT_LENGTH = 60;
const MAX_BOOKMARK_LABEL_LENGTH = 120;
const MAX_BOOKMARK_QUERY_LENGTH = 200;
const DEFAULT_BOOKMARK_ID = 'default-search';
const DEFAULT_BOOKMARK_LABEL = 'Gewinnspiel / gewinnen / verlosen';
const DEFAULT_BOOKMARK_QUERY = '';
const DAILY_BOOKMARK_TITLE_MAX_LENGTH = 160;
const DAILY_BOOKMARK_URL_MAX_LENGTH = 800;
const DAILY_BOOKMARK_NOTES_MAX_LENGTH = 800;
const DAILY_BOOKMARK_MARKER_MAX_LENGTH = 120;
const AUTOMATION_TYPE_REQUEST = 'request';
const AUTOMATION_TYPE_EMAIL = 'email';
const AUTOMATION_TYPE_FLOW = 'flow';
const AUTOMATION_MAX_NAME_LENGTH = 160;
const AUTOMATION_MAX_URL_LENGTH = 2000;
const AUTOMATION_MAX_BODY_LENGTH = 12000;
const AUTOMATION_MAX_HEADERS_LENGTH = 6000;
const AUTOMATION_MIN_INTERVAL_MINUTES = 5;
const AUTOMATION_MAX_INTERVAL_MINUTES = 60 * 24 * 14; // 2 Wochen
const AUTOMATION_DEFAULT_INTERVAL_MINUTES = 60;
const AUTOMATION_MAX_JITTER_MINUTES = 120;
const AUTOMATION_MAX_RESPONSE_PREVIEW = 4000;
const AUTOMATION_WORKER_INTERVAL_MS = 30000;
const AUTOMATION_MAX_STEPS = 3;
const AUTOMATION_MAX_EMAIL_TO_LENGTH = 320;
const AUTOMATION_MAX_EMAIL_SUBJECT_LENGTH = 320;
const AUTH_USERNAME = (process.env.AUTH_USERNAME || '').trim();
const AUTH_PASSWORD = (process.env.AUTH_PASSWORD || '').trim();
const AUTH_ENABLED = Boolean(AUTH_USERNAME && AUTH_PASSWORD);
const AUTH_SESSION_COOKIE = 'fb_auth_token';
const AUTH_SESSION_MAX_AGE = 60 * 60 * 24 * 365 * 10; // ~10 Jahre "quasi dauerhaft"
const SPORTS_SCORING_DEFAULTS = {
  enabled: 1,
  threshold: 5,
  auto_hide_enabled: 0,
  weights: {
    scoreline: 3,
    scoreEmoji: 2,
    sportEmoji: 2,
    sportVerb: 1.5,
    sportNoun: 2,
    hashtag: 1.5,
    teamToken: 2,
    competition: 2,
    celebration: 1,
    location: 1
  }
};
const SPORTS_SCORING_TERMS_DEFAULTS = {
  nouns: [
    'auswärtssieg', 'heimsieg', 'derbysieg', 'revanche', 'spiel', 'spieltag', 'match', 'derby', 'finale',
    'cup', 'pokal', 'liga', 'bundesliga', 'oberliga', 'kreisliga', 'bezirksliga', 'meisterschaft',
    'turnier', 'halbzeit', 'tabellenplatz', 'tabelle', 'tor', 'tore', 'treffer', 'stadion', 'arena', 'halle',
    'trainerteam', 'mannschaft', 'fans', 'fanblock', 'jugend', 'u17', 'u19', 'u15'
  ],
  verbs: [
    'gewinnen', 'siegen', 'geholt', 'erkämpfen', 'erkämpft', 'erkämpfen', 'drehen', 'punkten',
    'trifft', 'treffen', 'schießt', 'schiesst', 'schießen', 'schiessen', 'verteidigen', 'stürmen', 'kämpfen'
  ],
  competitions: [
    'bundesliga', 'liga', 'serie a', 'premier league', 'champions league', 'europa league', 'dfb-pokal',
    'cup', 'pokal', 'qualifikation', 'qualirunde', 'halbfinale', 'viertelfinale', 'achtelfinale', 'relegation'
  ],
  celebrations: [
    'sieg', 'siege', 'auswärtssieg', 'heimsieg', 'auswärtsspiel', 'punkte', 'punkte geholt', 'man of the match', 'motm',
    'tabellenführung', 'tabellenplatz', 'tabellendritter', 'tabellenzweiter'
  ],
  locations: [
    'auswärts', 'heimspiel', 'derby', 'arena', 'stadion', 'halle', 'bolle', 'bölle', 'mosel'
  ],
  negatives: [
    'rezept', 'kochen', 'politik', 'wahl', 'bundestag', 'landtag', 'software', 'release', 'update', 'konzert',
    'album', 'tour', 'podcast', 'karriere', 'job', 'stellenangebot', 'bewerbung'
  ]
};
const SIMILARITY_DEFAULTS = {
  text_threshold: 0.85,
  image_distance_threshold: 6
};

const screenshotDir = path.join(__dirname, 'data', 'screenshots');
if (!fs.existsSync(screenshotDir)) {
  fs.mkdirSync(screenshotDir, { recursive: true });
}

const automationConfigPath = path.join(__dirname, 'data', 'automation-config.json');
const defaultAutomationConfig = {
  smtp: {
    host: '',
    port: 587,
    secure: false,
    user: '',
    pass: '',
    from: ''
  }
};

function ensureAutomationConfigFile() {
  try {
    if (!fs.existsSync(automationConfigPath)) {
      const dir = path.dirname(automationConfigPath);
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
      }
      fs.writeFileSync(automationConfigPath, JSON.stringify(defaultAutomationConfig, null, 2), 'utf8');
      console.log('Automation-Config angelegt unter', automationConfigPath);
    }
  } catch (error) {
    console.warn('Konnte Automation-Config nicht erstellen:', error.message);
  }
}

ensureAutomationConfigFile();

function loadAutomationConfig() {
  try {
    const raw = fs.readFileSync(automationConfigPath, 'utf8');
    const parsed = JSON.parse(raw);
    return parsed && typeof parsed === 'object' ? parsed : { ...defaultAutomationConfig };
  } catch (error) {
    return { ...defaultAutomationConfig };
  }
}

const automationConfig = loadAutomationConfig();

let nodemailer = null;
try {
  nodemailer = require('nodemailer');
} catch (error) {
  nodemailer = null;
}

// Middleware - Enhanced CORS for extension
app.use(cors({
  origin: (origin, callback) => {
    callback(null, origin || false);
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: [
    'Content-Type',
    'Authorization',
    'X-AI-Trace-Id',
    'X-AI-Flow-Id',
    'X-AI-Trace-Source'
  ],
  exposedHeaders: ['X-AI-Trace-Id', 'X-AI-Flow-Id', 'Server-Timing'],
  credentials: true
}));
// Allow larger payloads because screenshots from high-res monitors can easily exceed 10 MB
app.use(express.json({ limit: '30mb' }));

// Additional CORS headers for extension compatibility
app.use((req, res, next) => {
  const origin = req.headers.origin;
  const host = req.headers.host;
  const fallbackOrigin = host ? `${isSecureRequest(req) ? 'https' : 'http'}://${host}` : '*';
  res.header('Access-Control-Allow-Origin', origin || fallbackOrigin);
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-AI-Trace-Id, X-AI-Flow-Id, X-AI-Trace-Source');
  res.header('Access-Control-Expose-Headers', 'X-AI-Trace-Id, X-AI-Flow-Id, Server-Timing');
  res.header('Access-Control-Allow-Credentials', 'true');

  if (req.method === 'OPTIONS') {
    res.sendStatus(204);
    return;
  }

  next();
});

// Simple session-based authentication (enabled when AUTH_USERNAME/PASSWORD are set)
app.use(authGuard);

// Assign per-browser profile scopes via cookies
app.use(ensureProfileScope);

// Database setup
const dbPath = path.join(__dirname, 'data', 'tracker.db');
const db = new Database(dbPath);
db.pragma('foreign_keys = ON');

const SSE_RETRY_INTERVAL_MS = 5000;
const SSE_HEARTBEAT_INTERVAL_MS = 30000;
const sseClients = new Map();
let nextSseClientId = 1;

function scheduleAsync(fn) {
  if (typeof setImmediate === 'function') {
    setImmediate(fn);
  } else {
    setTimeout(fn, 0);
  }
}

function removeSseClient(clientId) {
  const client = sseClients.get(clientId);
  if (!client) {
    return;
  }
  sseClients.delete(clientId);
  if (client.heartbeat) {
    clearInterval(client.heartbeat);
  }
}

function addSseClient(res) {
  const clientId = nextSseClientId++;
  const client = {
    id: clientId,
    res,
    heartbeat: setInterval(() => {
      if (res.writableEnded) {
        removeSseClient(clientId);
        return;
      }
      try {
        res.write('event: heartbeat\ndata: {}\n\n');
      } catch (error) {
        removeSseClient(clientId);
      }
    }, SSE_HEARTBEAT_INTERVAL_MS)
  };
  sseClients.set(clientId, client);
  return client;
}

function broadcastSseEvent(payload) {
  if (!payload) {
    return;
  }

  let serialized;
  try {
    serialized = JSON.stringify(payload);
  } catch (error) {
    console.warn('Failed to serialize SSE payload:', error.message);
    return;
  }

  const message = `data: ${serialized}\n\n`;

  for (const [clientId, client] of sseClients.entries()) {
    const target = client && client.res;
    if (!target || target.writableEnded) {
      removeSseClient(clientId);
      continue;
    }
    try {
      target.write(message);
    } catch (error) {
      removeSseClient(clientId);
    }
  }
}

function queuePostBroadcast(postId, options = {}) {
  if (!postId) {
    return;
  }
  scheduleAsync(() => broadcastPostChangeById(postId, options));
}

function ensureColumn(table, column, definition) {
  const info = db.prepare(`PRAGMA table_info(${table})`).all();
  if (!info.some((row) => row.name === column)) {
    db.prepare(`ALTER TABLE ${table} ADD COLUMN ${definition}`).run();
  }
}

ensureColumn('posts', 'post_text', 'post_text TEXT');
ensureColumn('posts', 'post_text_hash', 'post_text_hash TEXT');
ensureColumn('posts', 'content_key', 'content_key TEXT');
ensureColumn('posts', 'first_image_hash', 'first_image_hash TEXT');
ensureColumn('posts', 'first_image_url', 'first_image_url TEXT');

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_posts_content_key
  ON posts(content_key)
`);

const updateContentKeyStmt = db.prepare('UPDATE posts SET content_key = ? WHERE id = ?');
const updatePostTextColumnsStmt = db.prepare('UPDATE posts SET post_text = ?, post_text_hash = ? WHERE id = ?');

const postsMissingKey = db.prepare(`
  SELECT id, url
  FROM posts
  WHERE content_key IS NULL OR content_key = ''
`).all();

for (const entry of postsMissingKey) {
  const normalizedUrl = normalizeFacebookPostUrl(entry.url);
  const key = extractFacebookContentKey(normalizedUrl);
  if (key) {
    updateContentKeyStmt.run(key, entry.id);
  }
}

const postsPermalinks = db.prepare(`
  SELECT id, url, content_key
  FROM posts
  WHERE url LIKE '%/permalink.php%'
`).all();

for (const entry of postsPermalinks) {
  const normalizedUrl = normalizeFacebookPostUrl(entry.url);
  const key = extractFacebookContentKey(normalizedUrl);
  if (key && key !== entry.content_key) {
    updateContentKeyStmt.run(key, entry.id);
  }
}

const postsMissingHash = db.prepare(`
  SELECT id, post_text
  FROM posts
  WHERE post_text IS NOT NULL
    AND TRIM(post_text) <> ''
    AND (post_text_hash IS NULL OR post_text_hash = '')
`).all();

for (const entry of postsMissingHash) {
  const normalizedText = normalizePostText(entry.post_text);
  const hash = computePostTextHash(normalizedText);
  updatePostTextColumnsStmt.run(normalizedText, hash, entry.id);
}

function parseCookies(header) {
  if (!header || typeof header !== 'string') {
    return {};
  }

  return header.split(';').reduce((acc, part) => {
    const index = part.indexOf('=');
    if (index === -1) {
      const key = part.trim();
      if (key) {
        acc[key] = '';
      }
      return acc;
    }

    const key = part.slice(0, index).trim();
    const value = part.slice(index + 1).trim();
    if (key) {
      acc[key] = decodeURIComponent(value);
    }
    return acc;
  }, {});
}

function isSecureRequest(req) {
  if (req.secure) {
    return true;
  }
  const forwardedProto = req.headers['x-forwarded-proto'];
  if (typeof forwardedProto === 'string') {
    return forwardedProto.split(',').map(value => value.trim().toLowerCase()).includes('https');
  }
  return false;
}

const authSessions = new Map();
const AUTH_SESSION_TABLE = 'auth_sessions';

function persistAuthSession(token, username, expiresAt) {
  if (!AUTH_ENABLED) {
    return;
  }
  db.prepare(`
    INSERT INTO ${AUTH_SESSION_TABLE} (token, username, expires_at)
    VALUES (?, ?, ?)
    ON CONFLICT(token) DO UPDATE SET
      username = excluded.username,
      expires_at = excluded.expires_at
  `).run(token, username, expiresAt);
}

function deletePersistedAuthSession(token) {
  if (!AUTH_ENABLED) {
    return;
  }
  db.prepare(`DELETE FROM ${AUTH_SESSION_TABLE} WHERE token = ?`).run(token);
}

function hydrateAuthSessions() {
  if (!AUTH_ENABLED) {
    return;
  }
  const now = Date.now();
  db.prepare(`DELETE FROM ${AUTH_SESSION_TABLE} WHERE expires_at <= ?`).run(now);
  const rows = db.prepare(`
    SELECT token, username, expires_at
    FROM ${AUTH_SESSION_TABLE}
    WHERE expires_at > ?
  `).all(now);

  rows.forEach(row => {
    authSessions.set(row.token, {
      username: row.username,
      expiresAt: row.expires_at
    });
  });
}

function buildAuthCookieValue(token, req) {
  const secure = isSecureRequest(req);
  const attributes = [
    `${AUTH_SESSION_COOKIE}=${encodeURIComponent(token)}`,
    'Path=/',
    `Max-Age=${AUTH_SESSION_MAX_AGE}`,
    'HttpOnly'
  ];

  if (secure) {
    attributes.push('Secure', 'SameSite=None');
  } else {
    attributes.push('SameSite=Lax');
  }

  return attributes.join('; ');
}

function clearAuthCookie(res, req) {
  const secure = isSecureRequest(req);
  const attributes = [
    `${AUTH_SESSION_COOKIE}=`,
    'Path=/',
    'Max-Age=0',
    'HttpOnly'
  ];

  if (secure) {
    attributes.push('Secure', 'SameSite=None');
  } else {
    attributes.push('SameSite=Lax');
  }

  const existing = res.getHeader('Set-Cookie');
  const value = attributes.join('; ');
  if (!existing) {
    res.setHeader('Set-Cookie', value);
  } else if (Array.isArray(existing)) {
    res.setHeader('Set-Cookie', [...existing, value]);
  } else {
    res.setHeader('Set-Cookie', [existing, value]);
  }
}

function createSession(username) {
  const token = crypto.randomBytes(32).toString('hex');
  const expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000;
  authSessions.set(token, { username, expiresAt });
  persistAuthSession(token, username, expiresAt);
  return { token, expiresAt };
}

function getSessionFromRequest(req) {
  const cookies = parseCookies(req.headers.cookie);
  const token = cookies[AUTH_SESSION_COOKIE];
  if (!token) {
    return null;
  }

  const session = authSessions.get(token);
  if (!session) {
    if (AUTH_ENABLED) {
      const row = db.prepare(`
        SELECT username, expires_at
        FROM ${AUTH_SESSION_TABLE}
        WHERE token = ?
      `).get(token);

      if (!row) {
        return null;
      }

      if (row.expires_at <= Date.now()) {
        deletePersistedAuthSession(token);
        return null;
      }

      const hydrated = { username: row.username, expiresAt: row.expires_at };
      authSessions.set(token, hydrated);
      return { token, ...hydrated };
    }
    return null;
  }

  if (session.expiresAt <= Date.now()) {
    authSessions.delete(token);
    deletePersistedAuthSession(token);
    return null;
  }

  // Sliding expiration
  session.expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000;
  authSessions.set(token, session);
  persistAuthSession(token, session.username, session.expiresAt);
  return { token, ...session };
}

function authGuard(req, res, next) {
  if (!AUTH_ENABLED || req.method === 'OPTIONS') {
    next();
    return;
  }

  const publicPaths = ['/api/login', '/api/session', '/health'];
  if (publicPaths.includes(req.path)) {
    next();
    return;
  }

  const session = getSessionFromRequest(req);
  if (!session) {
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }

  req.authUser = session.username;
  next();
}

function buildScopeCookieValue(scopeId, req) {
  const secure = isSecureRequest(req);
  const attributes = [
    `${PROFILE_SCOPE_COOKIE}=${encodeURIComponent(scopeId)}`,
    'Path=/',
    `Max-Age=${PROFILE_SCOPE_MAX_AGE}`
  ];

  if (secure) {
    attributes.push('Secure', 'SameSite=None');
  } else {
    attributes.push('SameSite=Lax');
  }

  return attributes.join('; ');
}

function appendScopeCookie(res, value) {
  const existing = res.getHeader('Set-Cookie');
  if (!existing) {
    res.setHeader('Set-Cookie', value);
  } else if (Array.isArray(existing)) {
    res.setHeader('Set-Cookie', [...existing, value]);
  } else {
    res.setHeader('Set-Cookie', [existing, value]);
  }
}

function ensureProfileScope(req, res, next) {
  const cookies = parseCookies(req.headers.cookie);
  let scopeId = cookies[PROFILE_SCOPE_COOKIE];

  if (!scopeId) {
    scopeId = uuidv4();
  }

  appendScopeCookie(res, buildScopeCookieValue(scopeId, req));

  req.profileScope = scopeId;
  next();
}

function appendCookieHeader(res, value) {
  const existing = res.getHeader('Set-Cookie');
  if (!existing) {
    res.setHeader('Set-Cookie', value);
  } else if (Array.isArray(existing)) {
    res.setHeader('Set-Cookie', [...existing, value]);
  } else {
    res.setHeader('Set-Cookie', [existing, value]);
  }
}

app.post('/api/login', (req, res) => {
  try {
    if (!AUTH_ENABLED) {
      return res.status(400).json({ error: 'Authentication is not configured' });
    }

    const { username, password } = req.body || {};
    if (username !== AUTH_USERNAME || password !== AUTH_PASSWORD) {
      clearAuthCookie(res, req);
      return res.status(401).json({ error: 'Ungültige Zugangsdaten' });
    }

    const session = createSession(username);
    appendCookieHeader(res, buildAuthCookieValue(session.token, req));

    res.json({ authenticated: true, username });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/logout', (req, res) => {
  try {
    const session = getSessionFromRequest(req);
    if (session) {
      authSessions.delete(session.token);
      deletePersistedAuthSession(session.token);
    }
    clearAuthCookie(res, req);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/session', (req, res) => {
  try {
    if (!AUTH_ENABLED) {
      return res.json({ authenticated: true, auth_required: false });
    }
    const session = getSessionFromRequest(req);
    if (!session) {
      return res.status(401).json({ authenticated: false, auth_required: true });
    }
    res.json({ authenticated: true, username: session.username, auth_required: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

function getScopedProfileNumber(scopeId) {
  if (!scopeId) {
    return null;
  }
  const row = db.prepare('SELECT profile_number FROM profile_state_scoped WHERE scope_id = ?').get(scopeId);
  if (!row) {
    return null;
  }
  return sanitizeProfileNumber(row.profile_number);
}

function setScopedProfileNumber(scopeId, profileNumber) {
  if (!scopeId || !profileNumber) {
    return;
  }

  db.prepare(`
    INSERT INTO profile_state_scoped (scope_id, profile_number)
    VALUES (?, ?)
    ON CONFLICT(scope_id) DO UPDATE SET profile_number = excluded.profile_number
  `).run(scopeId, profileNumber);
}

function clampTargetCount(value) {
  const parsed = parseInt(value, 10);
  if (Number.isNaN(parsed)) {
    return 1;
  }
  return Math.min(MAX_PROFILES, Math.max(1, parsed));
}

function validateTargetCount(value) {
  const parsed = parseInt(value, 10);
  if (Number.isNaN(parsed) || parsed < 1 || parsed > MAX_PROFILES) {
    return null;
  }
  return parsed;
}

function sanitizeProfileNumber(value) {
  const parsed = parseInt(value, 10);
  if (Number.isNaN(parsed) || parsed < 1 || parsed > MAX_PROFILES) {
    return null;
  }
  return parsed;
}

function applyProfileVariantTemplates(text, profileNumber) {
  if (typeof text !== 'string' || !text) {
    return text || '';
  }

  const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);
  let result = text.replace(/\{Profil(?:e)?-(\d+)(?:-(\d+))?\?\s*"([\s\S]*?)"(?:\s*:\s*"([\s\S]*?)")?\}/gi, (match, startStr, endStr, ifText, elseText) => {
    if (!normalizedProfileNumber) {
      return '';
    }

    const start = parseInt(startStr, 10);
    const end = endStr ? parseInt(endStr, 10) : start;
    if (Number.isNaN(start) || Number.isNaN(end)) {
      return '';
    }

    const lower = Math.min(start, end);
    const upper = Math.max(start, end);
    if (normalizedProfileNumber < lower || normalizedProfileNumber > upper) {
      return elseText || '';
    }
    return ifText || '';
  });

  result = result.replace(/\{Profil(?:e)?-(\d+)(?:-(\d+))?:([\s\S]*?)\}/gi, (match, startStr, endStr, content) => {
    if (!normalizedProfileNumber) {
      return '';
    }

    const start = parseInt(startStr, 10);
    const end = endStr ? parseInt(endStr, 10) : start;
    if (Number.isNaN(start) || Number.isNaN(end)) {
      return '';
    }

    const lower = Math.min(start, end);
    const upper = Math.max(start, end);
    if (normalizedProfileNumber < lower || normalizedProfileNumber > upper) {
      return '';
    }
    return content;
  });

  return result;
}

function applyRandomNumberTemplates(text) {
  if (typeof text !== 'string' || !text) {
    return text || '';
  }

  return text.replace(/\{ZUFALL-(-?\d+)-(-?\d+)\}/gi, (match, startStr, endStr) => {
    const start = parseInt(startStr, 10);
    const end = parseInt(endStr, 10);
    if (Number.isNaN(start) || Number.isNaN(end)) {
      return match;
    }

    const lower = Math.min(start, end);
    const upper = Math.max(start, end);
    const span = upper - lower + 1;
    if (span <= 0) {
      return String(lower);
    }

    const value = Math.floor(Math.random() * span) + lower;
    return String(value);
  });
}

function normalizeDeadline(value) {
  if (!value && value !== 0) {
    return null;
  }

  if (value instanceof Date) {
    if (!Number.isNaN(value.getTime())) {
      return value.toISOString();
    }
    return null;
  }

  if (typeof value === 'string') {
    const trimmed = value.trim();
    if (!trimmed) {
      return null;
    }

    const parsed = new Date(trimmed);
    if (!Number.isNaN(parsed.getTime())) {
      return parsed.toISOString();
    }
    return null;
  }

  if (typeof value === 'number') {
    const parsed = new Date(value);
    if (!Number.isNaN(parsed.getTime())) {
      return parsed.toISOString();
    }
    return null;
  }

  return null;
}

function getProfileName(profileNumber) {
  return DEFAULT_PROFILE_NAMES[profileNumber] || `Profil ${profileNumber}`;
}

function normalizeCreatorName(value) {
  if (typeof value !== 'string') {
    return null;
  }

  const trimmed = value.trim();
  if (!trimmed) {
    return null;
  }

  return trimmed.slice(0, 160);
}

function normalizePostText(value) {
  if (typeof value !== 'string') {
    return null;
  }

  let text = value.replace(/\s+/g, ' ').trim();
  if (!text) {
    return null;
  }

  if (text.length > MAX_POST_TEXT_LENGTH) {
    text = text.slice(0, MAX_POST_TEXT_LENGTH);
  }

  return text;
}

function computePostTextHash(text) {
  if (!text) {
    return null;
  }
  return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
}

function tokenizeSimilarityText(text) {
  if (!text) {
    return [];
  }
  const tokens = text.toLowerCase().match(/[\p{L}\p{N}]+/gu) || [];
  return tokens.filter(token => token.length > 1);
}

function computeTextSimilarity(a, b) {
  if (!a || !b) {
    return 0;
  }
  const tokensA = tokenizeSimilarityText(a);
  const tokensB = tokenizeSimilarityText(b);
  if (!tokensA.length || !tokensB.length) {
    return 0;
  }
  const setA = new Set(tokensA);
  const setB = new Set(tokensB);
  let intersection = 0;
  for (const token of setA) {
    if (setB.has(token)) {
      intersection += 1;
    }
  }
  const union = setA.size + setB.size - intersection;
  if (union <= 0) {
    return 0;
  }
  return intersection / union;
}

function hammingDistanceHex(a, b) {
  if (!a || !b) {
    return null;
  }
  const left = String(a).toLowerCase().replace(/^0x/, '');
  const right = String(b).toLowerCase().replace(/^0x/, '');
  if (left.length !== right.length) {
    return null;
  }
  let xor;
  try {
    xor = BigInt(`0x${left}`) ^ BigInt(`0x${right}`);
  } catch (error) {
    return null;
  }
  let distance = 0;
  while (xor > 0n) {
    distance += Number(xor & 1n);
    xor >>= 1n;
  }
  return distance;
}

function normalizeBookmarkQuery(value) {
  if (typeof value !== 'string') {
    return null;
  }

  let query = value.trim();
  if (!query) {
    return null;
  }

  query = query.replace(/\s+/g, ' ');
  if (query.length > MAX_BOOKMARK_QUERY_LENGTH) {
    query = query.slice(0, MAX_BOOKMARK_QUERY_LENGTH);
  }
  return query;
}

function normalizeBookmarkLabel(value, fallback = '') {
  const base = typeof value === 'string' ? value.trim() : '';
  let label = base || fallback || '';
  label = label.replace(/\s+/g, ' ');
  if (label.length > MAX_BOOKMARK_LABEL_LENGTH) {
    label = label.slice(0, MAX_BOOKMARK_LABEL_LENGTH);
  }
  return label;
}

function isDefaultBookmarkQuery(value) {
  return typeof value === 'string' && !value.trim();
}

function serializeBookmark(row) {
  if (!row) {
    return null;
  }

  const isDefault = isDefaultBookmarkQuery(row.query);

  return {
    id: row.id,
    label: row.label,
    query: row.query,
    created_at: sqliteTimestampToUTC(row.created_at),
    updated_at: sqliteTimestampToUTC(row.updated_at),
    last_clicked_at: sqliteTimestampToUTC(row.last_clicked_at),
    is_default: isDefault,
    deletable: !isDefault
  };
}

function formatDayKeyFromDate(date = new Date()) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

function normalizeDayKeyValue(rawValue) {
  if (!rawValue && rawValue !== 0) {
    return null;
  }

  if (rawValue instanceof Date) {
    if (!Number.isNaN(rawValue.getTime())) {
      return formatDayKeyFromDate(rawValue);
    }
    return null;
  }

  if (typeof rawValue !== 'string' && typeof rawValue !== 'number') {
    return null;
  }

  const stringValue = String(rawValue).trim();
  if (!stringValue) {
    return null;
  }

  const match = stringValue.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
  if (match) {
    const year = parseInt(match[1], 10);
    const month = parseInt(match[2], 10);
    const day = parseInt(match[3], 10);
    const parsed = new Date(year, month - 1, day);
    if (
      !Number.isNaN(parsed.getTime())
      && parsed.getFullYear() === year
      && parsed.getMonth() === month - 1
      && parsed.getDate() === day
    ) {
      return formatDayKeyFromDate(parsed);
    }
  }

  const parsed = new Date(stringValue);
  if (!Number.isNaN(parsed.getTime())) {
    return formatDayKeyFromDate(parsed);
  }

  return null;
}

function resolveDayKey(dayKey, { defaultToToday = true } = {}) {
  const normalized = normalizeDayKeyValue(dayKey);
  if (normalized) {
    return normalized;
  }
  return defaultToToday ? formatDayKeyFromDate() : null;
}

function dayKeyToDate(dayKey) {
  const normalized = resolveDayKey(dayKey);
  const match = normalized.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!match) {
    return new Date();
  }
  const year = parseInt(match[1], 10);
  const month = parseInt(match[2], 10);
  const day = parseInt(match[3], 10);
  return new Date(year, month - 1, day);
}

function addDays(date, offsetDays = 0) {
  const base = date instanceof Date ? date : new Date();
  const result = new Date(base);
  result.setDate(result.getDate() + offsetDays);
  return result;
}

const DAILY_PLACEHOLDER_PATTERN = /\{\{\s*([^}]+)\s*\}\}/gi;

function resolveDynamicUrlTemplate(template, dayKey) {
  if (typeof template !== 'string') {
    return '';
  }

  const baseDayKey = resolveDayKey(dayKey);
  const baseDate = dayKeyToDate(baseDayKey);

  return template.replace(DAILY_PLACEHOLDER_PATTERN, (_match, contentRaw) => {
    const content = (contentRaw || '').trim();
    if (!content) {
      return '';
    }

    const counterMatch = content.match(/^counter:\s*([+-]?\d+)([+-]\d+)?$/i);
    if (counterMatch) {
      const base = parseInt(counterMatch[1], 10);
      const offset = counterMatch[2] ? parseInt(counterMatch[2], 10) || 0 : 0;
      if (Number.isNaN(base)) {
        return '';
      }
      const date = addDays(baseDate, offset);
      return String(base + date.getDate());
    }

    const placeholderMatch = content.match(/^(date|day|dd|mm|month|yyyy|yy)([+-]\d+)?$/i);
    if (!placeholderMatch) {
      return content;
    }

    const token = String(placeholderMatch[1] || '').toLowerCase();
    const offset = placeholderMatch[2] ? parseInt(placeholderMatch[2], 10) || 0 : 0;
    const date = addDays(baseDate, offset);

    switch (token) {
      case 'date':
        return formatDayKeyFromDate(date);
      case 'day':
        return String(date.getDate());
      case 'dd':
        return String(date.getDate()).padStart(2, '0');
      case 'month':
      case 'mm':
        return String(date.getMonth() + 1).padStart(2, '0');
      case 'yyyy':
        return String(date.getFullYear());
      case 'yy':
        return String(date.getFullYear()).slice(-2);
      default:
        return token;
    }
  });
}

function normalizeDailyBookmarkTitle(value, fallback = '') {
  const source = typeof value === 'string' ? value : fallback;
  let title = (source || '').trim();
  if (!title && fallback) {
    title = String(fallback || '').trim();
  }
  if (!title) {
    title = 'Bookmark';
  }
  title = title.replace(/\s+/g, ' ');
  if (title.length > DAILY_BOOKMARK_TITLE_MAX_LENGTH) {
    title = title.slice(0, DAILY_BOOKMARK_TITLE_MAX_LENGTH);
  }
  return title;
}

function normalizeDailyBookmarkUrlTemplate(value) {
  if (typeof value !== 'string') {
    return null;
  }

  let template = value.trim();
  if (!template) {
    return null;
  }

  if (template.length > DAILY_BOOKMARK_URL_MAX_LENGTH) {
    template = template.slice(0, DAILY_BOOKMARK_URL_MAX_LENGTH);
  }

  return template;
}

function normalizeDailyBookmarkNotes(value) {
  if (typeof value !== 'string') {
    return '';
  }

  let notes = value.trim();
  if (notes.length > DAILY_BOOKMARK_NOTES_MAX_LENGTH) {
    notes = notes.slice(0, DAILY_BOOKMARK_NOTES_MAX_LENGTH);
  }
  return notes;
}

function normalizeDailyBookmarkMarker(value) {
  if (typeof value !== 'string') {
    return '';
  }

  let marker = value.trim();
  marker = marker.replace(/\s+/g, ' ');
  if (marker.length > DAILY_BOOKMARK_MARKER_MAX_LENGTH) {
    marker = marker.slice(0, DAILY_BOOKMARK_MARKER_MAX_LENGTH);
  }
  return marker;
}

function normalizeDailyBookmarkActive(value) {
  if (value === undefined || value === null) {
    return 1;
  }

  if (typeof value === 'string') {
    const trimmed = value.trim().toLowerCase();
    if (trimmed === 'false' || trimmed === '0' || trimmed === 'off') {
      return 0;
    }
    if (trimmed === 'true' || trimmed === '1' || trimmed === 'on') {
      return 1;
    }
  }

  return value ? 1 : 0;
}

function serializeDailyBookmark(row, dayKey) {
  if (!row) {
    return null;
  }

  const resolvedDayKey = resolveDayKey(dayKey);
  const resolvedUrl = resolveDynamicUrlTemplate(row.url_template, resolvedDayKey);

  return {
    id: row.id,
    title: row.title,
    url_template: row.url_template,
    marker: row.marker || '',
    is_active: Number(row.is_active ?? 1) !== 0,
    resolved_url: resolvedUrl,
    notes: row.notes || '',
    created_at: sqliteTimestampToUTC(row.created_at),
    updated_at: sqliteTimestampToUTC(row.updated_at),
    last_completed_at: sqliteTimestampToUTC(row.last_completed_at),
    completed_for_day: !!row.completed_for_day,
    day_key: resolvedDayKey
  };
}

function normalizeFacebookPostUrl(rawValue) {
  if (typeof rawValue !== 'string') {
    return null;
  }

  let value = rawValue.trim();
  if (!value) {
    return null;
  }

  const trackingIndex = value.indexOf('__cft__');
  if (trackingIndex !== -1) {
    value = value.slice(0, trackingIndex);
  }

  value = value.replace(/[?&]$/, '');

  let parsed;
  try {
    parsed = new URL(value);
  } catch (error) {
    try {
      parsed = new URL(value, 'https://www.facebook.com');
    } catch (fallbackError) {
      return null;
    }
  }

  if (!parsed.hostname.toLowerCase().endsWith('facebook.com')) {
    return null;
  }

  parsed.hostname = 'www.facebook.com';
  parsed.protocol = 'https:';
  parsed.port = '';

  const normalizedPathBeforeTrim = parsed.pathname.replace(/\/+$/, '') || '/';
  const lowerPathBeforeTrim = normalizedPathBeforeTrim.toLowerCase();
  const watchId = parsed.searchParams.get('v') || parsed.searchParams.get('video_id');
  if ((lowerPathBeforeTrim === '/watch' || lowerPathBeforeTrim === '/video.php') && watchId) {
    parsed.pathname = `/reel/${watchId}/`;
    parsed.search = '';
  } else {
    const reelMatch = lowerPathBeforeTrim.match(/^\/reel\/([^/]+)$/);
    if (reelMatch) {
      parsed.pathname = `/reel/${reelMatch[1]}/`;
      parsed.search = '';
    }
  }

  const cleanedParams = new URLSearchParams();
  parsed.searchParams.forEach((paramValue, paramKey) => {
    const lowerKey = paramKey.toLowerCase();
    const isSingleUnitParam = lowerKey === 's' && paramValue === 'single_unit';
    if (
      FACEBOOK_TRACKING_PARAM_PREFIXES.some((prefix) => lowerKey.startsWith(prefix))
      || lowerKey === 'set'
      || lowerKey === 'comment_id'
      || lowerKey === 'hoisted_section_header_type'
      || isSingleUnitParam
    ) {
      return;
    }
    cleanedParams.append(paramKey, paramValue);
  });

  const multiPermalinkId = cleanedParams.get('multi_permalinks');
  if (multiPermalinkId) {
    cleanedParams.delete('multi_permalinks');

    const groupMatch = parsed.pathname.match(/^\/groups\/([A-Za-z0-9\.\-_]+)/);
    if (groupMatch && /^[0-9]+$/.test(multiPermalinkId)) {
      parsed.pathname = `/groups/${groupMatch[1]}/posts/${multiPermalinkId}`;
    } else if (groupMatch) {
      parsed.pathname = `/groups/${groupMatch[1]}/permalink/${multiPermalinkId}`;
    }
  }

  const normalizedPath = parsed.pathname.replace(/\/+$/, '').toLowerCase();
  if (normalizedPath.startsWith('/hashtag/') || normalizedPath.startsWith('/watch/hashtag/')) {
    return null;
  }

  const search = cleanedParams.toString();
  const formatted = `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ''}`;
  return formatted.replace(/[?&]$/, '');
}

function extractFacebookContentKey(normalizedUrl) {
  if (!normalizedUrl) {
    return null;
  }

  try {
    const parsed = new URL(normalizedUrl);
    const pathnameRaw = parsed.pathname || '/';
    const pathname = pathnameRaw.replace(/\/+$/, '') || '/';
    const lowerPath = pathname.toLowerCase();
    const params = parsed.searchParams;

    const reelMatch = lowerPath.match(/^\/reel\/([^/]+)/);
    if (reelMatch) {
      return `reel:${reelMatch[1]}`;
    }

    const watchId = params.get('v') || params.get('video_id');
    if ((lowerPath === '/watch' || lowerPath === '/watch/') && watchId) {
      return `video:${watchId}`;
    }
    if (lowerPath === '/video.php' && watchId) {
      return `video:${watchId}`;
    }

    const photoId = params.get('fbid');
    if ((lowerPath === '/photo.php' || lowerPath === '/photo') && photoId) {
      return `photo:${photoId}`;
    }

    const storyFbid = params.get('story_fbid');
    if (lowerPath === '/permalink.php' && storyFbid) {
      return `story:${storyFbid}`;
    }
    if (storyFbid) {
      const ownerId = params.get('id') || params.get('gid') || params.get('group_id') || params.get('page_id') || '';
      return `story:${ownerId}:${storyFbid}`;
    }

    const groupPostMatch = lowerPath.match(/^\/groups\/([^/]+)\/posts\/([^/]+)/);
    if (groupPostMatch) {
      return `group-post:${groupPostMatch[1]}:${groupPostMatch[2]}`;
    }

    const groupPermalinkMatch = lowerPath.match(/^\/groups\/([^/]+)\/permalink\/([^/]+)/);
    if (groupPermalinkMatch) {
      return `group-post:${groupPermalinkMatch[1]}:${groupPermalinkMatch[2]}`;
    }

    const pagePostMatch = lowerPath.match(/^\/([^/]+)\/posts\/([^/]+)/);
    if (pagePostMatch) {
      return `profile-post:${pagePostMatch[1]}:${pagePostMatch[2]}`;
    }

    const pageVideoMatch = lowerPath.match(/^\/([^/]+)\/videos\/([^/]+)/);
    if (pageVideoMatch) {
      return `video:${pageVideoMatch[2]}`;
    }

    const pagePhotoMatch = lowerPath.match(/^\/([^/]+)\/photos\/[^/]+\/([^/]+)/);
    if (pagePhotoMatch) {
      return `photo:${pagePhotoMatch[2]}`;
    }

    if (lowerPath === '/' && storyFbid) {
      const ownerId = params.get('id') || '';
      return `story:${ownerId}:${storyFbid}`;
    }

    if (lowerPath === '/story.php' && storyFbid) {
      const ownerId = params.get('id') || '';
      return `story:${ownerId}:${storyFbid}`;
    }

    const sortedParams = Array.from(params.entries())
      .map(([key, value]) => `${key}=${value}`)
      .sort()
      .join('&');

    return `generic:${lowerPath}?${sortedParams}`;
  } catch (error) {
    return `generic:${normalizedUrl}`;
  }
}

function getRequiredProfiles(targetCount) {
  const count = clampTargetCount(targetCount);
  return Array.from({ length: count }, (_, index) => index + 1);
}

function buildProfileStatuses(requiredProfiles, checks) {
  const validChecks = checks
    .map(check => {
      const profileNumber = sanitizeProfileNumber(check.profile_number);
      if (!profileNumber) {
        return null;
      }
      return {
        ...check,
        profile_number: profileNumber,
        profile_name: getProfileName(profileNumber)
      };
    })
    .filter(Boolean);

  const completedSet = new Set(validChecks.map(check => check.profile_number));
  const checkByProfile = new Map(validChecks.map(check => [check.profile_number, check]));

  const statuses = requiredProfiles.map((profileNumber, index) => {
    const prerequisites = requiredProfiles.slice(0, index);
    const prerequisitesMet = prerequisites.every(num => completedSet.has(num));
    const isChecked = completedSet.has(profileNumber);
    return {
      profile_number: profileNumber,
      profile_name: getProfileName(profileNumber),
      status: isChecked ? 'done' : (prerequisitesMet ? 'available' : 'locked'),
      checked_at: isChecked && checkByProfile.get(profileNumber)
        ? checkByProfile.get(profileNumber).checked_at
        : null
    };
  });

  return {
    statuses,
    completedChecks: validChecks,
    completedSet
  };
}

function buildCompletedProfileSet(rows) {
  const completedSet = new Set();
  rows.forEach((row) => {
    const profileNumber = sanitizeProfileNumber(row.profile_number);
    if (profileNumber) {
      completedSet.add(profileNumber);
    }
  });
  return completedSet;
}

function countUniqueProfileChecks(checks) {
  const uniqueProfiles = new Set();
  checks.forEach((check) => {
    const profileNumber = sanitizeProfileNumber(check.profile_number);
    if (profileNumber) {
      uniqueProfiles.add(profileNumber);
    }
  });
  return uniqueProfiles.size;
}

function shouldEnforceProfileOrder({ post, requiredProfiles, completedSet, ignoreOrder = false }) {
  if (ignoreOrder) {
    return false;
  }
  if (!post || !requiredProfiles.length) {
    return false;
  }
  const isExpired = post.deadline_at ? new Date(post.deadline_at) < new Date() : false;
  if (isExpired) {
    return false;
  }
  const isComplete = requiredProfiles.every(profile => completedSet.has(profile));
  return !isComplete;
}

function recalcCheckedCount(postId) {
  const post = db.prepare('SELECT id, target_count, checked_count FROM posts WHERE id = ?').get(postId);
  if (!post) {
    return null;
  }

  const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ?').all(postId);
  const requiredProfiles = getRequiredProfiles(post.target_count);
  const { statuses } = buildProfileStatuses(requiredProfiles, checks);
  const checkedCount = Math.min(requiredProfiles.length, countUniqueProfileChecks(checks));

  const updates = [];
  const params = [];

  if (post.checked_count !== checkedCount) {
    updates.push('checked_count = ?');
    params.push(checkedCount);
  }

  if (post.target_count !== requiredProfiles.length) {
    updates.push('target_count = ?');
    params.push(requiredProfiles.length);
  }

  if (updates.length) {
    updates.push('last_change = CURRENT_TIMESTAMP');
    params.push(postId);
    db.prepare(`UPDATE posts SET ${updates.join(', ')} WHERE id = ?`).run(...params);
  }

  return checkedCount;
}

// Initialize database tables
db.exec(`
  CREATE TABLE IF NOT EXISTS posts (
    id TEXT PRIMARY KEY,
    url TEXT NOT NULL UNIQUE,
    title TEXT,
    target_count INTEGER NOT NULL,
    checked_count INTEGER DEFAULT 0,
    screenshot_path TEXT,
    created_by_profile INTEGER,
    created_by_name TEXT,
    deadline_at DATETIME,
    post_text TEXT,
    post_text_hash TEXT,
    content_key TEXT,
    first_image_hash TEXT,
    first_image_url TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_change DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS post_urls (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    post_id TEXT NOT NULL,
    url TEXT NOT NULL UNIQUE,
    is_primary INTEGER NOT NULL DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
  );
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_post_urls_post_id
  ON post_urls(post_id);
`);

db.exec(`
  DROP INDEX IF EXISTS idx_post_urls_primary;
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS checks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    post_id TEXT NOT NULL,
    profile_number INTEGER,
    checked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (post_id) REFERENCES posts(id)
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS profile_state (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    profile_number INTEGER NOT NULL
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS profile_state_scoped (
    scope_id TEXT PRIMARY KEY,
    profile_number INTEGER NOT NULL
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS auth_sessions (
    token TEXT PRIMARY KEY,
    username TEXT NOT NULL,
    expires_at INTEGER NOT NULL
  );
`);
hydrateAuthSessions();

db.exec(`
  CREATE TABLE IF NOT EXISTS ai_settings (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    active_credential_id INTEGER,
    prompt_prefix TEXT,
    enabled INTEGER DEFAULT 0,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS ai_credentials (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    provider TEXT NOT NULL,
    api_key TEXT NOT NULL,
    model TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS profile_friends (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    profile_number INTEGER NOT NULL,
    friend_names TEXT NOT NULL,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(profile_number)
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS search_seen_posts (
    url TEXT PRIMARY KEY,
    seen_count INTEGER NOT NULL DEFAULT 1,
    manually_hidden INTEGER NOT NULL DEFAULT 0,
    first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS maintenance_settings (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    search_retention_days INTEGER DEFAULT ${SEARCH_POST_RETENTION_DAYS},
    auto_purge_hidden INTEGER DEFAULT 1,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS moderation_settings (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    sports_scoring_enabled INTEGER DEFAULT ${SPORTS_SCORING_DEFAULTS.enabled},
    sports_score_threshold REAL DEFAULT ${SPORTS_SCORING_DEFAULTS.threshold},
    sports_auto_hide_enabled INTEGER DEFAULT ${SPORTS_SCORING_DEFAULTS.auto_hide_enabled},
    sports_score_weights TEXT,
    sports_terms TEXT,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS similarity_settings (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    text_threshold REAL DEFAULT ${SIMILARITY_DEFAULTS.text_threshold},
    image_distance_threshold INTEGER DEFAULT ${SIMILARITY_DEFAULTS.image_distance_threshold},
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

ensureColumn('moderation_settings', 'sports_terms', 'sports_terms TEXT');
ensureColumn('moderation_settings', 'sports_auto_hide_enabled', 'sports_auto_hide_enabled INTEGER DEFAULT 0');

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_search_seen_posts_last_seen_at
  ON search_seen_posts(last_seen_at);
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS bookmarks (
    id TEXT PRIMARY KEY,
    label TEXT,
    query TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    last_clicked_at DATETIME,
    UNIQUE(query COLLATE NOCASE)
  );
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_bookmarks_last_clicked_at
  ON bookmarks(last_clicked_at);
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_bookmarks_created_at
  ON bookmarks(created_at);
`);

const listBookmarksStmt = db.prepare(`
  SELECT id, label, query, created_at, updated_at, last_clicked_at
  FROM bookmarks
  ORDER BY
    (last_clicked_at IS NULL),
    datetime(COALESCE(last_clicked_at, created_at)) DESC,
    label COLLATE NOCASE
`);

const getBookmarkByIdStmt = db.prepare(`
  SELECT id, label, query, created_at, updated_at, last_clicked_at
  FROM bookmarks
  WHERE id = ?
`);

const findBookmarkByQueryStmt = db.prepare(`
  SELECT id
  FROM bookmarks
  WHERE LOWER(query) = LOWER(?)
`);

const insertBookmarkStmt = db.prepare(`
  INSERT INTO bookmarks (id, label, query)
  VALUES (?, ?, ?)
`);

const ensureDefaultBookmarkStmt = db.prepare(`
  INSERT OR IGNORE INTO bookmarks (id, label, query)
  VALUES (?, ?, ?)
`);

const deleteBookmarkStmt = db.prepare(`
  DELETE FROM bookmarks
  WHERE id = ?
`);

const updateBookmarkLastClickedStmt = db.prepare(`
  UPDATE bookmarks
  SET last_clicked_at = CURRENT_TIMESTAMP,
      updated_at = CURRENT_TIMESTAMP
  WHERE id = ?
`);

ensureDefaultBookmarkStmt.run(DEFAULT_BOOKMARK_ID, DEFAULT_BOOKMARK_LABEL, DEFAULT_BOOKMARK_QUERY);

db.exec(`
  CREATE TABLE IF NOT EXISTS daily_bookmarks (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    url_template TEXT NOT NULL,
    notes TEXT,
    is_active INTEGER NOT NULL DEFAULT 1,
    marker TEXT DEFAULT '',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS daily_bookmark_checks (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    bookmark_id TEXT NOT NULL,
    day_key TEXT NOT NULL,
    completed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (bookmark_id) REFERENCES daily_bookmarks(id) ON DELETE CASCADE,
    UNIQUE(bookmark_id, day_key)
  );
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_daily_bookmark_checks_day
  ON daily_bookmark_checks(day_key);
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_daily_bookmarks_updated
  ON daily_bookmarks(updated_at);
`);

ensureColumn('daily_bookmarks', 'marker', 'marker TEXT DEFAULT \'\'');
ensureColumn('daily_bookmarks', 'is_active', 'is_active INTEGER NOT NULL DEFAULT 1');

const listDailyBookmarksStmt = db.prepare(`
  SELECT
    b.id,
    b.title,
    b.url_template,
    b.notes,
    b.is_active,
    b.marker,
    b.created_at,
    b.updated_at,
    (
      SELECT MAX(completed_at)
      FROM daily_bookmark_checks c
      WHERE c.bookmark_id = b.id
    ) AS last_completed_at,
    EXISTS(
      SELECT 1
      FROM daily_bookmark_checks c
      WHERE c.bookmark_id = b.id
        AND c.day_key = @dayKey
    ) AS completed_for_day
  FROM daily_bookmarks b
  ORDER BY datetime(b.updated_at) DESC, datetime(b.created_at) DESC, b.title COLLATE NOCASE
`);

const getDailyBookmarkStmt = db.prepare(`
  SELECT
    b.id,
    b.title,
    b.url_template,
    b.notes,
    b.is_active,
    b.marker,
    b.created_at,
    b.updated_at,
    (
      SELECT MAX(completed_at)
      FROM daily_bookmark_checks c
      WHERE c.bookmark_id = b.id
    ) AS last_completed_at,
    EXISTS(
      SELECT 1
      FROM daily_bookmark_checks c
      WHERE c.bookmark_id = b.id
        AND c.day_key = @dayKey
    ) AS completed_for_day
  FROM daily_bookmarks b
  WHERE b.id = @bookmarkId
`);

const insertDailyBookmarkStmt = db.prepare(`
  INSERT INTO daily_bookmarks (id, title, url_template, notes, marker, is_active)
  VALUES (@id, @title, @url_template, @notes, @marker, @is_active)
`);

const findDailyBookmarkByUrlStmt = db.prepare(`
  SELECT id
  FROM daily_bookmarks
  WHERE LOWER(url_template) = LOWER(?)
  LIMIT 1
`);

const findOtherDailyBookmarkByUrlStmt = db.prepare(`
  SELECT id
  FROM daily_bookmarks
  WHERE LOWER(url_template) = LOWER(@url)
    AND id <> @id
  LIMIT 1
`);

const updateDailyBookmarkStmt = db.prepare(`
  UPDATE daily_bookmarks
  SET title = @title,
      url_template = @url_template,
      notes = @notes,
      marker = @marker,
      is_active = @is_active,
      updated_at = CURRENT_TIMESTAMP
  WHERE id = @id
`);

const deleteDailyBookmarkStmt = db.prepare(`
  DELETE FROM daily_bookmarks
  WHERE id = ?
`);

const upsertDailyBookmarkCheckStmt = db.prepare(`
  INSERT INTO daily_bookmark_checks (bookmark_id, day_key)
  VALUES (@bookmarkId, @dayKey)
  ON CONFLICT(bookmark_id, day_key) DO NOTHING
`);

const deleteDailyBookmarkCheckStmt = db.prepare(`
  DELETE FROM daily_bookmark_checks
  WHERE bookmark_id = @bookmarkId
    AND day_key = @dayKey
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS automation_requests (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    description TEXT,
    type TEXT NOT NULL DEFAULT 'request',
    method TEXT NOT NULL DEFAULT 'GET',
    url_template TEXT,
    headers_json TEXT,
    body_template TEXT,
    email_to TEXT,
    email_subject_template TEXT,
    email_body_template TEXT,
    steps_json TEXT,
    interval_minutes INTEGER NOT NULL DEFAULT ${AUTOMATION_DEFAULT_INTERVAL_MINUTES},
    jitter_minutes INTEGER DEFAULT 0,
    start_at DATETIME,
    run_until DATETIME,
    active INTEGER NOT NULL DEFAULT 1,
    last_run_at DATETIME,
    last_status TEXT,
    last_status_code INTEGER,
    last_error TEXT,
    next_run_at DATETIME,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

  ensureColumn('automation_requests', 'type', 'type TEXT NOT NULL DEFAULT \'request\'');
  ensureColumn('automation_requests', 'email_to', 'email_to TEXT');
  ensureColumn('automation_requests', 'email_subject_template', 'email_subject_template TEXT');
  ensureColumn('automation_requests', 'email_body_template', 'email_body_template TEXT');
  ensureColumn('automation_requests', 'steps_json', 'steps_json TEXT');
  ensureColumn('automation_requests', 'exclusion_windows_json', 'exclusion_windows_json TEXT');

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_automation_requests_next_run
  ON automation_requests(next_run_at);
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS automation_request_runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    request_id TEXT NOT NULL,
    trigger TEXT DEFAULT 'schedule',
    started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    completed_at DATETIME,
    status TEXT,
    status_code INTEGER,
    error TEXT,
    response_body TEXT,
    duration_ms INTEGER,
    FOREIGN KEY (request_id) REFERENCES automation_requests(id) ON DELETE CASCADE
  );
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_automation_request_runs_request
  ON automation_request_runs(request_id, started_at DESC);
`);

const listAutomationRequestsStmt = db.prepare(`
  SELECT
    id,
    name,
    description,
    type,
    method,
    url_template,
    headers_json,
    body_template,
    email_to,
    email_subject_template,
    email_body_template,
    steps_json,
    interval_minutes,
    jitter_minutes,
    start_at,
    run_until,
    exclusion_windows_json,
    active,
    last_run_at,
    last_status,
    last_status_code,
    last_error,
    next_run_at,
    (
      SELECT COUNT(1)
      FROM automation_request_runs r
      WHERE r.request_id = automation_requests.id
    ) AS runs_count,
    created_at,
    updated_at
  FROM automation_requests
  ORDER BY datetime(updated_at) DESC, datetime(created_at) DESC, name COLLATE NOCASE
`);

const getAutomationRequestStmt = db.prepare(`
  SELECT
    id,
    name,
    description,
    type,
    method,
    url_template,
    headers_json,
    body_template,
    email_to,
    email_subject_template,
    email_body_template,
    steps_json,
    interval_minutes,
    jitter_minutes,
    start_at,
    run_until,
    exclusion_windows_json,
    active,
    last_run_at,
    last_status,
    last_status_code,
    last_error,
    next_run_at,
    created_at,
    updated_at
  FROM automation_requests
  WHERE id = ?
`);

const insertAutomationRequestStmt = db.prepare(`
  INSERT INTO automation_requests (
    id, name, description, type, method, url_template, headers_json, body_template,
    email_to, email_subject_template, email_body_template, steps_json,
    interval_minutes, jitter_minutes, start_at, run_until, exclusion_windows_json, active, last_run_at,
    last_status, last_status_code, last_error, next_run_at
  ) VALUES (
    @id, @name, @description, @type, @method, @url_template, @headers_json, @body_template,
    @email_to, @email_subject_template, @email_body_template, @steps_json,
    @interval_minutes, @jitter_minutes, @start_at, @run_until, @exclusion_windows_json, @active, @last_run_at,
    @last_status, @last_status_code, @last_error, @next_run_at
  )
`);

const updateAutomationRequestStmt = db.prepare(`
  UPDATE automation_requests
  SET name = @name,
      description = @description,
      type = @type,
      method = @method,
      url_template = @url_template,
      headers_json = @headers_json,
      body_template = @body_template,
      email_to = @email_to,
      email_subject_template = @email_subject_template,
      email_body_template = @email_body_template,
      steps_json = @steps_json,
      interval_minutes = @interval_minutes,
      jitter_minutes = @jitter_minutes,
      start_at = @start_at,
      run_until = @run_until,
      exclusion_windows_json = @exclusion_windows_json,
      active = @active,
      last_run_at = @last_run_at,
      last_status = @last_status,
      last_status_code = @last_status_code,
      last_error = @last_error,
      next_run_at = @next_run_at,
      updated_at = CURRENT_TIMESTAMP
  WHERE id = @id
`);

const deleteAutomationRequestStmt = db.prepare('DELETE FROM automation_requests WHERE id = ?');

const listAutomationRunsStmt = db.prepare(`
  SELECT
    id,
    request_id,
    trigger,
    started_at,
    completed_at,
    status,
    status_code,
    error,
    response_body,
    duration_ms
  FROM automation_request_runs
  WHERE request_id = @requestId
  ORDER BY datetime(started_at) DESC
  LIMIT @limit
`);

const insertAutomationRunStmt = db.prepare(`
  INSERT INTO automation_request_runs (
    request_id, trigger, started_at, completed_at, status, status_code, error, response_body, duration_ms
  ) VALUES (
    @request_id, @trigger, @started_at, @completed_at, @status, @status_code, @error, @response_body, @duration_ms
  )
`);

const listDueAutomationRequestsStmt = db.prepare(`
  SELECT *
  FROM automation_requests
  WHERE active = 1
    AND next_run_at IS NOT NULL
    AND datetime(next_run_at) <= datetime(@now)
  ORDER BY datetime(next_run_at) ASC
  LIMIT 10
`);

ensureColumn('posts', 'checked_count', 'checked_count INTEGER DEFAULT 0');
ensureColumn('posts', 'screenshot_path', 'screenshot_path TEXT');
ensureColumn('posts', 'created_by_profile', 'created_by_profile INTEGER');
ensureColumn('posts', 'deadline_at', 'deadline_at DATETIME');
ensureColumn('posts', 'created_by_name', 'created_by_name TEXT');
ensureColumn('posts', 'last_change', 'last_change DATETIME');
ensureColumn('posts', 'is_successful', 'is_successful INTEGER DEFAULT 0');
ensureColumn('ai_settings', 'active_credential_id', 'active_credential_id INTEGER');
ensureColumn('ai_settings', 'prompt_prefix', 'prompt_prefix TEXT');
ensureColumn('ai_settings', 'enabled', 'enabled INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'is_active', 'is_active INTEGER DEFAULT 1');
ensureColumn('ai_credentials', 'priority', 'priority INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'base_url', 'base_url TEXT');
ensureColumn('ai_credentials', 'last_used_at', 'last_used_at DATETIME');
ensureColumn('ai_credentials', 'last_success_at', 'last_success_at DATETIME');
ensureColumn('ai_credentials', 'last_error_message', 'last_error_message TEXT');
ensureColumn('ai_credentials', 'last_error_at', 'last_error_at DATETIME');
ensureColumn('ai_credentials', 'last_status_code', 'last_status_code INTEGER');
ensureColumn('ai_credentials', 'last_rate_limit_remaining', 'last_rate_limit_remaining TEXT');
ensureColumn('ai_credentials', 'rate_limit_reset_at', 'rate_limit_reset_at DATETIME');
ensureColumn('ai_credentials', 'auto_disabled', 'auto_disabled INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'auto_disabled_reason', 'auto_disabled_reason TEXT');
ensureColumn('ai_credentials', 'auto_disabled_until', 'auto_disabled_until DATETIME');
ensureColumn('ai_credentials', 'usage_24h_count', 'usage_24h_count INTEGER DEFAULT 0');
ensureColumn('ai_credentials', 'usage_24h_reset_at', 'usage_24h_reset_at DATETIME');
ensureColumn('search_seen_posts', 'manually_hidden', 'manually_hidden INTEGER NOT NULL DEFAULT 0');
ensureColumn('search_seen_posts', 'sports_auto_hidden', 'sports_auto_hidden INTEGER NOT NULL DEFAULT 0');
db.exec(`
  CREATE TABLE IF NOT EXISTS ai_usage_events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    credential_id INTEGER NOT NULL,
    event_type TEXT NOT NULL,
    status_code INTEGER,
    message TEXT,
    metadata TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (credential_id) REFERENCES ai_credentials(id) ON DELETE CASCADE
  );
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_ai_usage_events_credential_created
  ON ai_usage_events(credential_id, created_at DESC);
`);

db.exec(`
  CREATE TABLE IF NOT EXISTS ai_debug_traces (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    trace_id TEXT NOT NULL UNIQUE,
    flow_id TEXT,
    source TEXT,
    status TEXT NOT NULL DEFAULT 'created',
    origin TEXT,
    request_ip TEXT,
    user_agent TEXT,
    request_meta_json TEXT,
    backend_timing_json TEXT,
    backend_attempts_json TEXT,
    backend_error TEXT,
    response_meta_json TEXT,
    frontend_timing_json TEXT,
    frontend_steps_json TEXT,
    frontend_error TEXT,
    total_duration_ms REAL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_ai_debug_traces_created
  ON ai_debug_traces(created_at DESC);
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_ai_debug_traces_flow
  ON ai_debug_traces(flow_id, created_at DESC);
`);

db.exec(`
  CREATE INDEX IF NOT EXISTS idx_ai_debug_traces_status
  ON ai_debug_traces(status, created_at DESC);
`);
db.prepare(`
  UPDATE posts
  SET last_change = COALESCE(
    last_change,
    (SELECT MAX(checked_at) FROM checks WHERE checks.post_id = posts.id),
    created_at,
    CURRENT_TIMESTAMP
  )
  WHERE last_change IS NULL
`).run();

function touchPost(postId, reason = null) {
  if (!postId) {
    return;
  }
  try {
    db.prepare('UPDATE posts SET last_change = CURRENT_TIMESTAMP WHERE id = ?').run(postId);
  } catch (error) {
    console.warn(`Failed to update last_change for post ${postId}:`, error.message);
  }
  queuePostBroadcast(postId, { reason: reason || 'touch' });
}

function normalizeExistingPostUrls() {
  const rows = db.prepare('SELECT id, url FROM posts').all();
  let updatedCount = 0;

  for (const row of rows) {
    const cleaned = normalizeFacebookPostUrl(row.url);
    if (!cleaned || cleaned === row.url) {
      continue;
    }

    const conflict = db.prepare('SELECT id FROM posts WHERE url = ?').get(cleaned);
    if (conflict && conflict.id !== row.id) {
      console.warn(`Skipping URL normalization for post ${row.id} due to existing post ${conflict.id}`);
      continue;
    }

    try {
      db.prepare('UPDATE posts SET url = ? WHERE id = ?').run(cleaned, row.id);
      const updatedKey = extractFacebookContentKey(cleaned);
      updateContentKeyStmt.run(updatedKey || null, row.id);
      updatedCount += 1;
    } catch (error) {
      if (error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
        console.warn(`Skipping URL normalization for post ${row.id} because the cleaned URL is already used.`);
        continue;
      }
      console.warn(`Failed to normalize URL for post ${row.id}:`, error.message);
    }
  }

  if (updatedCount) {
    console.log(`Normalized URLs for ${updatedCount} stored posts.`);
  }
}

normalizeExistingPostUrls();

function normalizeExistingPostUrlMappings() {
  const rows = db.prepare('SELECT id, url FROM post_urls').all();
  let updated = 0;
  let removed = 0;

  for (const row of rows) {
    const normalized = normalizeFacebookPostUrl(row.url);
    if (!normalized) {
      continue;
    }

    if (normalized === row.url) {
      continue;
    }

    try {
      db.prepare('UPDATE post_urls SET url = ? WHERE id = ?').run(normalized, row.id);
      updated += 1;
    } catch (error) {
      if (error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
        db.prepare('DELETE FROM post_urls WHERE id = ?').run(row.id);
        removed += 1;
      } else {
        console.warn(`Failed to normalize post_urls entry ${row.id}:`, error.message);
      }
    }
  }

  if (updated || removed) {
    console.log(`Normalized post_urls entries: updated ${updated}, removed ${removed}`);
  }
}

normalizeExistingPostUrlMappings();
db.prepare('DELETE FROM post_urls WHERE url IN (SELECT url FROM posts)').run();

function truncateString(value, maxLength) {
  if (typeof value !== 'string') {
    return value;
  }
  if (!Number.isFinite(maxLength) || maxLength <= 0) {
    return value;
  }
  return value.length > maxLength
    ? `${value.slice(0, maxLength - 3)}...`
    : value;
}

function ensureIsoDate(value) {
  if (!value) {
    return null;
  }
  if (value instanceof Date) {
    const time = value.getTime();
    return Number.isNaN(time) ? null : value.toISOString();
  }
  if (typeof value === 'number') {
    return ensureIsoDate(new Date(value));
  }
  const date = new Date(value);
  const time = date.getTime();
  return Number.isNaN(time) ? null : date.toISOString();
}

function parseRetryAfter(value) {
  if (!value && value !== 0) {
    return null;
  }
  if (typeof value === 'number') {
    return value >= 0 ? Math.round(value) : null;
  }
  const numeric = Number(value);
  if (!Number.isNaN(numeric)) {
    return numeric >= 0 ? Math.round(numeric) : null;
  }
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    return null;
  }
  const diffSeconds = Math.round((date.getTime() - Date.now()) / 1000);
  return diffSeconds >= 0 ? diffSeconds : null;
}

function parseRateLimitReset(value) {
  if (!value && value !== 0) {
    return null;
  }

  if (value instanceof Date) {
    return Number.isNaN(value.getTime()) ? null : value;
  }

  if (typeof value === 'number') {
    if (value > 1e12) {
      return new Date(value);
    }
    if (value > 1e9) {
      return new Date(value * 1000);
    }
    return new Date(Date.now() + value * 1000);
  }

  const numeric = Number(value);
  if (!Number.isNaN(numeric)) {
    if (numeric > 1e12) {
      return new Date(numeric);
    }
    if (numeric > 1e9) {
      return new Date(numeric * 1000);
    }
    return new Date(Date.now() + numeric * 1000);
  }

  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? null : date;
}

function extractRateLimitInfo(response, provider) {
  const info = { provider: provider || null };
  if (!response || !response.headers || typeof response.headers.get !== 'function') {
    return info;
  }

  const headersOfInterest = {};
  const captureKeys = new Set([
    'retry-after',
    'x-ratelimit-remaining',
    'x-ratelimit-remaining-requests',
    'x-ratelimit-reset',
    'x-ratelimit-reset-requests',
    'x-ratelimit-limit',
    'x-rate-limit-reset',
    'x-rate-limit-remaining'
  ]);

  try {
    response.headers.forEach((value, key) => {
      const normalizedKey = key.toLowerCase();
      if (captureKeys.has(normalizedKey)) {
        headersOfInterest[normalizedKey] = value;
      }
    });
  } catch (error) {
    console.warn('Failed to iterate rate limit headers:', error.message);
  }

  if (Object.keys(headersOfInterest).length) {
    info.headers = headersOfInterest;
  }

  const retryAfterHeader = response.headers.get('retry-after');
  const retryAfterSeconds = parseRetryAfter(retryAfterHeader);
  if (retryAfterSeconds !== null) {
    info.retryAfterSeconds = retryAfterSeconds;
  }

  const remainingHeader = response.headers.get('x-ratelimit-remaining-requests')
    || response.headers.get('x-ratelimit-remaining')
    || response.headers.get('x-rate-limit-remaining');
  if (remainingHeader !== null && remainingHeader !== undefined) {
    info.rateLimitRemaining = remainingHeader;
  }

  const resetHeader = response.headers.get('x-ratelimit-reset-requests')
    || response.headers.get('x-ratelimit-reset')
    || response.headers.get('x-rate-limit-reset');
  const resetDate = parseRateLimitReset(resetHeader);
  if (resetDate) {
    info.rateLimitResetAt = resetDate.toISOString();
  }

  return info;
}

const RATE_LIMIT_KEYWORDS = ['rate limit', 'ratelimit', 'quota', 'limit', 'too many requests', 'insufficient_quota', 'billing', 'exceeded', 'exceed'];
const AI_COMMENT_RETRY_LIMIT = 5;
const AI_DEBUG_TRACE_ID_MAX_LENGTH = 120;
const AI_DEBUG_FLOW_ID_MAX_LENGTH = 120;
const AI_DEBUG_DEFAULT_LIST_LIMIT = 100;
const AI_DEBUG_MAX_LIST_LIMIT = 500;

function determineAutoDisable(error) {
  if (!error) {
    return null;
  }

  const status = error.status || error.statusCode || null;
  const baseMessage = typeof error.message === 'string' ? error.message : '';
  const errorDetails = error.apiError && typeof error.apiError === 'object'
    ? (error.apiError.error?.message || error.apiError.error || error.apiError.message || '')
    : '';
  const combinedMessage = `${baseMessage} ${errorDetails}`.toLowerCase();

  let isRateLimit = status === 429;
  if (!isRateLimit && status === 403) {
    isRateLimit = RATE_LIMIT_KEYWORDS.some(keyword => combinedMessage.includes(keyword));
  }
  if (!isRateLimit && combinedMessage) {
    isRateLimit = RATE_LIMIT_KEYWORDS.some(keyword => combinedMessage.includes(keyword));
  }

  if (!isRateLimit) {
    return null;
  }

  let retryAfterSeconds = typeof error.retryAfterSeconds === 'number'
    ? error.retryAfterSeconds
    : null;

  if ((!retryAfterSeconds || retryAfterSeconds <= 0) && error.rateLimitResetAt) {
    const resetDate = new Date(error.rateLimitResetAt);
    if (!Number.isNaN(resetDate.getTime())) {
      retryAfterSeconds = Math.round((resetDate.getTime() - Date.now()) / 1000);
    }
  }

  if (!retryAfterSeconds || retryAfterSeconds < 0) {
    retryAfterSeconds = 900; // 15 minutes fallback
  }

  if (retryAfterSeconds < 10) {
    return null;
  }

  const untilDate = new Date(Date.now() + retryAfterSeconds * 1000);
  const reason = status
    ? `Rate limit erreicht (HTTP ${status})`
    : 'Rate limit erreicht';

  return {
    reason,
    seconds: retryAfterSeconds,
    until: untilDate
  };
}

function parseAutomationDate(input) {
  if (!input && input !== 0) {
    return null;
  }
  if (input === 'now') {
    return new Date().toISOString();
  }
  if (input instanceof Date) {
    const time = input.getTime();
    return Number.isNaN(time) ? null : input.toISOString();
  }
  const date = new Date(input);
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
}

function clampAutomationIntervalMinutes(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric) || numeric <= 0) {
    return AUTOMATION_DEFAULT_INTERVAL_MINUTES;
  }
  return Math.max(
    AUTOMATION_MIN_INTERVAL_MINUTES,
    Math.min(AUTOMATION_MAX_INTERVAL_MINUTES, Math.round(numeric))
  );
}

function clampAutomationJitterMinutes(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric) || numeric < 0) {
    return 0;
  }
  return Math.min(AUTOMATION_MAX_JITTER_MINUTES, Math.round(numeric));
}

function toMinutesOfDay(value) {
  if (typeof value !== 'string') return null;
  const trimmed = value.trim();
  const match = /^(\d{1,2}):(\d{2})$/.exec(trimmed);
  if (!match) return null;
  const hours = Number(match[1]);
  const minutes = Number(match[2]);
  if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return null;
  return hours * 60 + minutes;
}

function formatMinutesOfDay(totalMinutes) {
  if (typeof totalMinutes !== 'number' || Number.isNaN(totalMinutes)) return null;
  const minutes = Math.max(0, Math.min(1439, Math.floor(totalMinutes)));
  const h = String(Math.floor(minutes / 60)).padStart(2, '0');
  const m = String(minutes % 60).padStart(2, '0');
  return `${h}:${m}`;
}

function parseExclusionWindows(raw) {
  let source = raw;
  if (typeof source === 'string') {
    try {
      source = JSON.parse(source);
    } catch (error) {
      source = [];
    }
  }
  if (!Array.isArray(source)) {
    return [];
  }

  const windows = [];
  for (const item of source) {
    if (!item || typeof item !== 'object') continue;
    const startStr = typeof item.start === 'string'
      ? item.start
      : (typeof item.from === 'string' ? item.from : '');
    const endStr = typeof item.end === 'string'
      ? item.end
      : (typeof item.to === 'string' ? item.to : '');
    const startMinutes = toMinutesOfDay(startStr);
    const endMinutes = toMinutesOfDay(endStr);
    if (startMinutes === null || endMinutes === null) continue;
    if (startMinutes >= endMinutes) continue;
    windows.push({
      start: formatMinutesOfDay(startMinutes),
      end: formatMinutesOfDay(endMinutes),
      startMinutes,
      endMinutes
    });
  }

  windows.sort((a, b) => a.startMinutes - b.startMinutes);
  return windows;
}

function serializeExclusionWindows(raw, existingJson = null) {
  if (raw === undefined) {
    return existingJson || null;
  }
  const parsed = parseExclusionWindows(raw);
  if (!parsed.length) {
    return null;
  }
  try {
    return JSON.stringify(parsed.map((item) => ({ start: item.start, end: item.end })));
  } catch (error) {
    return null;
  }
}

function normalizeAutomationHeaders(raw) {
  if (!raw) {
    return {};
  }

  let source = raw;
  if (typeof raw === 'string') {
    const trimmed = raw.trim();
    if (!trimmed) {
      return {};
    }
    try {
      source = JSON.parse(trimmed);
    } catch (error) {
      source = trimmed.split('\n').reduce((acc, line) => {
        const idx = line.indexOf(':');
        if (idx === -1) {
          return acc;
        }
        const key = line.slice(0, idx).trim();
        const value = line.slice(idx + 1).trim();
        if (key) {
          acc[key] = value;
        }
        return acc;
      }, {});
    }
  }

  if (!source || typeof source !== 'object') {
    return {};
  }

  const headers = {};
  for (const [key, value] of Object.entries(source)) {
    if (!key) {
      continue;
    }
    const normalizedKey = String(key).trim();
    if (!normalizedKey) {
      continue;
    }
    let normalizedValue;
    if (value === undefined || value === null) {
      normalizedValue = '';
    } else if (typeof value === 'string') {
      normalizedValue = value.trim();
    } else if (typeof value === 'number' || typeof value === 'boolean') {
      normalizedValue = String(value);
    } else {
      try {
        normalizedValue = JSON.stringify(value);
      } catch (error) {
        normalizedValue = '';
      }
    }
    headers[normalizedKey] = truncateString(normalizedValue, 1000);
  }

  return headers;
}

function serializeAutomationHeaders(headers) {
  if (!headers || typeof headers !== 'object' || !Object.keys(headers).length) {
    return null;
  }
  try {
    const serialized = JSON.stringify(headers);
    return serialized.length > AUTOMATION_MAX_HEADERS_LENGTH ? null : serialized;
  } catch (error) {
    return null;
  }
}

function renderAutomationTemplate(template, context = {}) {
  if (typeof template !== 'string') {
    return '';
  }
  const baseDate = context.now instanceof Date ? context.now : new Date();

  return template.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (_, keyRaw) => {
    const key = String(keyRaw || '').trim();
    if (!key) {
      return '';
    }

    if (key === 'uuid') {
      return uuidv4();
    }

    const dateOffsetMatch = key.match(/^date([+-]\d+)?$/);
    if (dateOffsetMatch) {
      const offset = dateOffsetMatch[1] ? parseInt(dateOffsetMatch[1], 10) : 0;
      const shifted = new Date(baseDate);
      shifted.setDate(baseDate.getDate() + (Number.isNaN(offset) ? 0 : offset));
      return shifted.toISOString().slice(0, 10);
    }

    const dayOffsetMatch = key.match(/^day([+-]\d+)?$/);
    if (dayOffsetMatch) {
      const offset = dayOffsetMatch[1] ? parseInt(dayOffsetMatch[1], 10) : 0;
      const shifted = new Date(baseDate);
      shifted.setDate(baseDate.getDate() + (Number.isNaN(offset) ? 0 : offset));
      return String(shifted.getDate()).padStart(2, '0');
    }

    switch (key) {
      case 'today':
      case 'date':
        return baseDate.toISOString().slice(0, 10);
      case 'iso':
      case 'now':
      case 'datetime':
        return baseDate.toISOString();
      case 'timestamp':
        return String(baseDate.getTime());
      case 'year':
        return String(baseDate.getFullYear());
      case 'month':
        return String(baseDate.getMonth() + 1).padStart(2, '0');
      case 'day':
        return String(baseDate.getDate()).padStart(2, '0');
      case 'hour':
        return String(baseDate.getHours()).padStart(2, '0');
      case 'minute':
        return String(baseDate.getMinutes()).padStart(2, '0');
      case 'weekday':
        return baseDate.toLocaleDateString('de-DE', { weekday: 'long' });
      case 'weekday_short':
        return baseDate.toLocaleDateString('de-DE', { weekday: 'short' });
      default:
        return context[key] !== undefined && context[key] !== null
          ? String(context[key])
          : '';
    }
  });
}

function buildAutomationTemplateContext(baseDate = new Date()) {
  const now = baseDate instanceof Date ? baseDate : new Date();
  return {
    now,
    date: now.toISOString().slice(0, 10),
    today: now.toISOString().slice(0, 10),
    iso: now.toISOString(),
    datetime: now.toISOString(),
    timestamp: now.getTime(),
    year: now.getFullYear(),
    month: String(now.getMonth() + 1).padStart(2, '0'),
    day: String(now.getDate()).padStart(2, '0'),
    hour: String(now.getHours()).padStart(2, '0'),
    minute: String(now.getMinutes()).padStart(2, '0'),
    weekday: now.toLocaleDateString('de-DE', { weekday: 'long' }),
    weekday_short: now.toLocaleDateString('de-DE', { weekday: 'short' })
  };
}

function renderAutomationHeaders(headersJson, context) {
  if (!headersJson) {
    return {};
  }
  let parsed = {};
  try {
    parsed = JSON.parse(headersJson);
  } catch (error) {
    parsed = {};
  }
  if (!parsed || typeof parsed !== 'object') {
    return {};
  }
  const rendered = {};
  for (const [key, value] of Object.entries(parsed)) {
    const renderedKey = renderAutomationTemplate(key, context).trim();
    if (!renderedKey) {
      continue;
    }
    const renderedValue = renderAutomationTemplate(
      value === undefined || value === null ? '' : String(value),
      context
    );
    rendered[renderedKey] = renderedValue;
  }
  return rendered;
}

function computeNextAutomationRun(request, options = {}) {
  if (!request) {
    return null;
  }
  const now = options.fromDate instanceof Date ? options.fromDate : new Date();
  const intervalMinutes = clampAutomationIntervalMinutes(
    request.interval_minutes || AUTOMATION_DEFAULT_INTERVAL_MINUTES
  );
  const jitterMinutes = clampAutomationJitterMinutes(request.jitter_minutes || 0);
  const lastRun = request.last_run_at ? new Date(request.last_run_at) : null;
  const startAt = request.start_at ? new Date(request.start_at) : null;
  const exclusionWindows = parseExclusionWindows(request.exclusion_windows_json || request.exclusion_windows || []);

  let base = lastRun
    ? new Date(lastRun.getTime() + intervalMinutes * 60000)
    : (startAt ? new Date(startAt) : new Date(now.getTime() + intervalMinutes * 60000));

  if (base < now) {
    base = new Date(now.getTime() + intervalMinutes * 60000);
  }

  const jitterMs = jitterMinutes > 0
    ? Math.floor(Math.random() * ((jitterMinutes * 60000) + 1))
    : 0;
  let candidate = new Date(base.getTime() + jitterMs);

  const until = request.run_until ? new Date(request.run_until) : null;
  const MAX_SHIFT_ITERATIONS = 50;
  let shifts = 0;

  while (shifts < MAX_SHIFT_ITERATIONS) {
    const minutes = candidate.getHours() * 60 + candidate.getMinutes();
    const hit = exclusionWindows.find((w) => minutes >= w.startMinutes && minutes < w.endMinutes);
    if (!hit) {
      break;
    }
    const nextAllowed = new Date(candidate);
    nextAllowed.setHours(0, 0, 0, 0);
    nextAllowed.setMinutes(hit.endMinutes);
    if (nextAllowed <= candidate) {
      nextAllowed.setDate(nextAllowed.getDate() + 1);
    }
    candidate = nextAllowed;
    shifts += 1;
  }

  if (shifts >= MAX_SHIFT_ITERATIONS) {
    return null;
  }

  if (until && !Number.isNaN(until.getTime()) && candidate > until) {
    return null;
  }

  return candidate.toISOString();
}

function ensureNextRunForRequest(request, options = {}) {
  if (!request || !request.id || !request.active) {
    return null;
  }
  const existingNext = request.next_run_at;
  if (existingNext) {
    return existingNext;
  }
  const next = computeNextAutomationRun(request, options);
  if (!next) {
    return null;
  }
  try {
    updateAutomationRequestStmt.run({
      ...request,
      next_run_at: next
    });
  } catch (error) {
    console.warn(`Failed to persist next_run_at for ${request.id}:`, error.message);
  }
  return next;
}

function serializeAutomationRequest(row) {
  if (!row) {
    return null;
  }
  let headers = {};
  if (row.headers_json) {
    try {
      const parsed = JSON.parse(row.headers_json);
      if (parsed && typeof parsed === 'object') {
        headers = parsed;
      }
    } catch (error) {
      headers = {};
    }
  }
  return {
    id: row.id,
    name: row.name,
    type: row.type || AUTOMATION_TYPE_REQUEST,
    description: row.description || '',
    method: row.method || 'GET',
    url_template: row.url_template,
    headers,
    body_template: row.body_template || '',
    email_to: row.email_to || '',
    email_subject_template: row.email_subject_template || '',
    email_body_template: row.email_body_template || '',
    steps: row.steps_json ? (() => {
      try {
        const parsed = JSON.parse(row.steps_json);
        return Array.isArray(parsed) ? parsed : [];
      } catch (error) {
        return [];
      }
    })() : [],
    runs_count: row.runs_count || 0,
    interval_minutes: clampAutomationIntervalMinutes(row.interval_minutes),
    jitter_minutes: clampAutomationJitterMinutes(row.jitter_minutes || 0),
    start_at: row.start_at ? ensureIsoDate(row.start_at) : null,
    run_until: row.run_until ? ensureIsoDate(row.run_until) : null,
    exclusion_windows: parseExclusionWindows(row.exclusion_windows_json || row.exclusion_windows || []),
    active: row.active ? 1 : 0,
    last_run_at: row.last_run_at ? ensureIsoDate(row.last_run_at) : null,
    last_status: row.last_status || null,
    last_status_code: row.last_status_code || null,
    last_error: row.last_error || null,
    next_run_at: row.next_run_at ? ensureIsoDate(row.next_run_at) : null,
    created_at: row.created_at ? ensureIsoDate(row.created_at) : null,
    updated_at: row.updated_at ? ensureIsoDate(row.updated_at) : null
  };
}

function serializeAutomationRun(row) {
  if (!row) {
    return null;
  }
  return {
    id: row.id,
    request_id: row.request_id,
    trigger: row.trigger || 'schedule',
    started_at: row.started_at ? ensureIsoDate(row.started_at) : null,
    completed_at: row.completed_at ? ensureIsoDate(row.completed_at) : null,
    status: row.status || null,
    status_code: row.status_code || null,
    error: row.error || null,
    response_body: row.response_body || '',
    duration_ms: row.duration_ms || null
  };
}

function normalizeAutomationPayload(payload, existing = {}) {
  const errors = [];
  const normalized = {};

  const rawType = typeof payload.type === 'string'
    ? payload.type.trim().toLowerCase()
    : (existing.type || AUTOMATION_TYPE_REQUEST);
  const type = [AUTOMATION_TYPE_REQUEST, AUTOMATION_TYPE_EMAIL, AUTOMATION_TYPE_FLOW].includes(rawType)
    ? rawType
    : AUTOMATION_TYPE_REQUEST;
  normalized.type = type;

  const nameSource = typeof payload.name === 'string'
    ? payload.name
    : existing.name || '';
  const name = nameSource.trim();
  if (!name) {
    errors.push('Name ist erforderlich');
  } else {
    normalized.name = truncateString(name, AUTOMATION_MAX_NAME_LENGTH);
  }

  const descriptionSource = typeof payload.description === 'string'
    ? payload.description.trim()
    : (existing.description || '');
  normalized.description = truncateString(descriptionSource || '', 800);

  const rawMethod = typeof payload.method === 'string'
    ? payload.method.trim().toUpperCase()
    : (existing.method || 'GET');
  const allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
  if (!allowedMethods.includes(rawMethod)) {
    errors.push('Ungültige HTTP-Methode');
  } else {
    normalized.method = rawMethod;
  }

  const rawUrl = typeof payload.url_template === 'string'
    ? payload.url_template
    : (typeof payload.url === 'string' ? payload.url : existing.url_template || '');
  const urlTemplate = (rawUrl || '').trim();

  if (type === AUTOMATION_TYPE_REQUEST || type === AUTOMATION_TYPE_FLOW) {
    if (!urlTemplate && type === AUTOMATION_TYPE_REQUEST) {
      errors.push('URL-Template fehlt');
    }
    normalized.url_template = urlTemplate
      ? truncateString(urlTemplate, AUTOMATION_MAX_URL_LENGTH)
      : null;
  } else {
    normalized.url_template = null;
  }

  const headersInput = payload.headers
    || payload.headers_json
    || payload.headersText
    || null;
  if (headersInput) {
    const parsedHeaders = normalizeAutomationHeaders(headersInput);
    const serializedHeaders = serializeAutomationHeaders(parsedHeaders);
    if (serializedHeaders === null && Object.keys(parsedHeaders).length) {
      errors.push('Headers sind zu groß oder ungültig');
    }
    normalized.headers_json = serializedHeaders;
  } else {
    normalized.headers_json = existing.headers_json || null;
  }

  if (typeof payload.body_template === 'string') {
    normalized.body_template = truncateString(payload.body_template, AUTOMATION_MAX_BODY_LENGTH);
  } else if (typeof payload.body === 'string') {
    normalized.body_template = truncateString(payload.body, AUTOMATION_MAX_BODY_LENGTH);
  } else {
    normalized.body_template = typeof existing.body_template === 'string'
      ? existing.body_template
      : '';
  }

  if (type === AUTOMATION_TYPE_EMAIL) {
    const toValue = (payload.email_to || payload.to || existing.email_to || '').trim();
    if (!toValue) {
      errors.push('E-Mail Empfänger fehlt');
    } else {
      normalized.email_to = truncateString(toValue, AUTOMATION_MAX_EMAIL_TO_LENGTH);
    }
    const subjectValue = (payload.email_subject_template || payload.subject || existing.email_subject_template || '').trim();
    if (!subjectValue) {
      errors.push('E-Mail Betreff fehlt');
    } else {
      normalized.email_subject_template = truncateString(subjectValue, AUTOMATION_MAX_EMAIL_SUBJECT_LENGTH);
    }
    const bodyValue = typeof payload.email_body_template === 'string'
      ? payload.email_body_template
      : (typeof payload.body_template === 'string' ? payload.body_template : existing.email_body_template || '');
    if (!bodyValue || !String(bodyValue).trim()) {
      errors.push('E-Mail Body fehlt');
    } else {
      normalized.email_body_template = truncateString(String(bodyValue), AUTOMATION_MAX_BODY_LENGTH);
    }
    normalized.steps_json = null;
    normalized.url_template = '';
    normalized.headers_json = null;
    normalized.body_template = null;
  } else if (type === AUTOMATION_TYPE_FLOW) {
    const stepsInput = Array.isArray(payload.steps)
      ? payload.steps
      : (typeof payload.steps_json === 'string' ? (() => {
        try { return JSON.parse(payload.steps_json); } catch (err) { return []; }
      })() : []);

    const steps = [];
    for (const rawStep of stepsInput) {
      if (!rawStep || typeof rawStep !== 'object') continue;
      const stepUrl = typeof rawStep.url === 'string' ? rawStep.url.trim() : '';
      if (!stepUrl) continue;
      const stepMethod = typeof rawStep.method === 'string'
        ? rawStep.method.toUpperCase()
        : 'GET';
      const allowed = allowedMethods.includes(stepMethod) ? stepMethod : 'GET';
      const stepHeaders = normalizeAutomationHeaders(rawStep.headers || rawStep.headers_json || {});
      const stepHeadersSerialized = serializeAutomationHeaders(stepHeaders);
      steps.push({
        method: allowed,
        url: truncateString(stepUrl, AUTOMATION_MAX_URL_LENGTH),
        headers: stepHeadersSerialized ? JSON.parse(stepHeadersSerialized) : {},
        body: typeof rawStep.body === 'string'
          ? truncateString(rawStep.body, AUTOMATION_MAX_BODY_LENGTH)
          : ''
      });
      if (steps.length >= AUTOMATION_MAX_STEPS) break;
    }

    if (!steps.length) {
      errors.push('Mindestens ein Schritt mit URL ist erforderlich');
    } else {
      try {
        const serializedSteps = JSON.stringify(steps);
        normalized.steps_json = serializedSteps;
      } catch (error) {
        errors.push('Schritte konnten nicht gespeichert werden');
      }
    }

    normalized.email_to = null;
    normalized.email_subject_template = null;
    normalized.email_body_template = null;
    normalized.url_template = normalized.url_template || '';
  } else {
    normalized.email_to = null;
    normalized.email_subject_template = null;
    normalized.email_body_template = null;
    normalized.steps_json = null;
  }

  const scheduleType = typeof payload.schedule_type === 'string'
    ? payload.schedule_type
    : payload.interval_type;

  let intervalMinutes;
  if (scheduleType === 'daily') {
    intervalMinutes = 24 * 60;
  } else if (scheduleType === 'hourly') {
    intervalMinutes = 60;
  } else {
    intervalMinutes = payload.interval_minutes
      ?? payload.every_minutes
      ?? existing.interval_minutes
      ?? AUTOMATION_DEFAULT_INTERVAL_MINUTES;
  }
  normalized.interval_minutes = clampAutomationIntervalMinutes(intervalMinutes);
  normalized.jitter_minutes = clampAutomationJitterMinutes(
    payload.jitter_minutes ?? payload.variance_minutes ?? existing.jitter_minutes ?? 0
  );

  normalized.start_at = parseAutomationDate(payload.start_at) || parseAutomationDate(existing.start_at);
  normalized.run_until = parseAutomationDate(payload.run_until) || null;
  normalized.exclusion_windows_json = serializeExclusionWindows(
    payload.exclusion_windows ?? payload.exclusions ?? payload.exclude_windows,
    existing.exclusion_windows_json || null
  );
  normalized.active = payload.active === undefined || payload.active === null
    ? (existing.active ? 1 : 0)
    : (payload.active ? 1 : 0);

  return { data: normalized, errors };
}

function recordAIUsageEvent(credentialId, eventType, options = {}) {
  if (!credentialId || !eventType) {
    return;
  }

  try {
    const { statusCode = null, message = null, metadata = null } = options;
    let metadataJson = null;

    if (metadata && typeof metadata === 'object') {
      try {
        metadataJson = JSON.stringify(metadata);
      } catch (error) {
        metadataJson = null;
      }
    } else if (typeof metadata === 'string') {
      metadataJson = metadata;
    }

    db.prepare(`
      INSERT INTO ai_usage_events (credential_id, event_type, status_code, message, metadata)
      VALUES (?, ?, ?, ?, ?)
    `).run(
      credentialId,
      eventType,
      statusCode !== undefined ? statusCode : null,
      message ? truncateString(message, 512) : null,
      metadataJson
    );
  } catch (error) {
    console.warn('Failed to record AI usage event:', error.message);
  }
}

function safeJsonStringify(value) {
  if (value === undefined) {
    return null;
  }
  try {
    return JSON.stringify(value);
  } catch (error) {
    return null;
  }
}

function safeJsonParse(value, fallback = null) {
  if (!value || typeof value !== 'string') {
    return fallback;
  }
  try {
    return JSON.parse(value);
  } catch (error) {
    return fallback;
  }
}

function sanitizeAIDebugValue(value, maxLength) {
  if (value === undefined || value === null) {
    return null;
  }
  const normalized = String(value).trim();
  if (!normalized) {
    return null;
  }
  if (!Number.isFinite(maxLength) || maxLength <= 0) {
    return normalized;
  }
  return normalized.length > maxLength
    ? normalized.slice(0, maxLength)
    : normalized;
}

function buildAIDebugTraceId(prefix = 'trace') {
  return `${prefix}-${uuidv4()}`;
}

function sanitizeTraceStatus(value, fallback = 'created') {
  const raw = sanitizeAIDebugValue(value, 40);
  if (!raw) {
    return fallback;
  }
  return raw.toLowerCase();
}

function normalizeAIDebugRequestMeta(payload = {}) {
  if (!payload || typeof payload !== 'object') {
    return {};
  }

  const preferredCredential = Number(payload.preferredCredentialId);

  return {
    profileNumber: sanitizeProfileNumber(payload.profileNumber),
    preferredCredentialId: Number.isNaN(preferredCredential) ? null : preferredCredential,
    postTextLength: typeof payload.postText === 'string' ? payload.postText.length : null,
    providerHint: sanitizeAIDebugValue(payload.providerHint, 120),
    extensionVersion: sanitizeAIDebugValue(payload.extensionVersion, 80),
    requestAttempt: Number.isFinite(Number(payload.requestAttempt))
      ? Number(payload.requestAttempt)
      : null
  };
}

function roundTiming(value) {
  const numeric = Number(value);
  if (!Number.isFinite(numeric) || numeric < 0) {
    return null;
  }
  return Math.round(numeric * 1000) / 1000;
}

const upsertAIDebugTraceBaseStmt = db.prepare(`
  INSERT INTO ai_debug_traces (
    trace_id,
    flow_id,
    source,
    status,
    origin,
    request_ip,
    user_agent,
    request_meta_json,
    created_at,
    updated_at
  ) VALUES (
    @trace_id,
    @flow_id,
    @source,
    @status,
    @origin,
    @request_ip,
    @user_agent,
    @request_meta_json,
    CURRENT_TIMESTAMP,
    CURRENT_TIMESTAMP
  )
  ON CONFLICT(trace_id) DO UPDATE SET
    flow_id = COALESCE(excluded.flow_id, ai_debug_traces.flow_id),
    source = COALESCE(excluded.source, ai_debug_traces.source),
    status = excluded.status,
    origin = COALESCE(excluded.origin, ai_debug_traces.origin),
    request_ip = COALESCE(excluded.request_ip, ai_debug_traces.request_ip),
    user_agent = COALESCE(excluded.user_agent, ai_debug_traces.user_agent),
    request_meta_json = COALESCE(excluded.request_meta_json, ai_debug_traces.request_meta_json),
    updated_at = CURRENT_TIMESTAMP
`);

const updateAIDebugTraceBackendStmt = db.prepare(`
  UPDATE ai_debug_traces
  SET flow_id = COALESCE(@flow_id, flow_id),
      source = COALESCE(@source, source),
      status = @status,
      backend_timing_json = @backend_timing_json,
      backend_attempts_json = @backend_attempts_json,
      backend_error = @backend_error,
      response_meta_json = @response_meta_json,
      total_duration_ms = @total_duration_ms,
      updated_at = CURRENT_TIMESTAMP
  WHERE trace_id = @trace_id
`);

const upsertAIDebugTraceFrontendStmt = db.prepare(`
  INSERT INTO ai_debug_traces (
    trace_id,
    flow_id,
    source,
    status,
    frontend_timing_json,
    frontend_steps_json,
    frontend_error,
    total_duration_ms,
    created_at,
    updated_at
  ) VALUES (
    @trace_id,
    @flow_id,
    @source,
    @status,
    @frontend_timing_json,
    @frontend_steps_json,
    @frontend_error,
    @total_duration_ms,
    CURRENT_TIMESTAMP,
    CURRENT_TIMESTAMP
  )
  ON CONFLICT(trace_id) DO UPDATE SET
    flow_id = COALESCE(excluded.flow_id, ai_debug_traces.flow_id),
    source = COALESCE(excluded.source, ai_debug_traces.source),
    status = excluded.status,
    frontend_timing_json = COALESCE(excluded.frontend_timing_json, ai_debug_traces.frontend_timing_json),
    frontend_steps_json = COALESCE(excluded.frontend_steps_json, ai_debug_traces.frontend_steps_json),
    frontend_error = COALESCE(excluded.frontend_error, ai_debug_traces.frontend_error),
    total_duration_ms = COALESCE(excluded.total_duration_ms, ai_debug_traces.total_duration_ms),
    updated_at = CURRENT_TIMESTAMP
`);

const getAIDebugTraceByIdStmt = db.prepare(`
  SELECT *
  FROM ai_debug_traces
  WHERE trace_id = @traceId
  LIMIT 1
`);

const listAIDebugTracesStmt = db.prepare(`
  SELECT *
  FROM ai_debug_traces
  WHERE (@status IS NULL OR status = @status)
    AND (@flowId IS NULL OR flow_id = @flowId)
    AND (@fromDate IS NULL OR datetime(created_at) >= datetime(@fromDate))
    AND (@toDate IS NULL OR datetime(created_at) <= datetime(@toDate))
  ORDER BY datetime(created_at) DESC
  LIMIT @limit
  OFFSET @offset
`);

function upsertAIDebugTraceBase(payload = {}) {
  const traceId = sanitizeAIDebugValue(payload.traceId, AI_DEBUG_TRACE_ID_MAX_LENGTH)
    || buildAIDebugTraceId();
  const flowId = sanitizeAIDebugValue(payload.flowId, AI_DEBUG_FLOW_ID_MAX_LENGTH);

  upsertAIDebugTraceBaseStmt.run({
    trace_id: traceId,
    flow_id: flowId,
    source: sanitizeAIDebugValue(payload.source, 80) || 'unknown',
    status: sanitizeTraceStatus(payload.status, 'created'),
    origin: sanitizeAIDebugValue(payload.origin, 240),
    request_ip: sanitizeAIDebugValue(payload.requestIp, 120),
    user_agent: sanitizeAIDebugValue(payload.userAgent, 800),
    request_meta_json: safeJsonStringify(payload.requestMeta || null)
  });

  return { traceId, flowId };
}

function updateAIDebugTraceBackend(payload = {}) {
  const traceId = sanitizeAIDebugValue(payload.traceId, AI_DEBUG_TRACE_ID_MAX_LENGTH);
  if (!traceId) {
    return;
  }

  updateAIDebugTraceBackendStmt.run({
    trace_id: traceId,
    flow_id: sanitizeAIDebugValue(payload.flowId, AI_DEBUG_FLOW_ID_MAX_LENGTH),
    source: sanitizeAIDebugValue(payload.source, 80),
    status: sanitizeTraceStatus(payload.status, 'backend_done'),
    backend_timing_json: safeJsonStringify(payload.backendTimings || null),
    backend_attempts_json: safeJsonStringify(payload.backendAttempts || null),
    backend_error: payload.backendError
      ? truncateString(String(payload.backendError), 1200)
      : null,
    response_meta_json: safeJsonStringify(payload.responseMeta || null),
    total_duration_ms: roundTiming(payload.totalDurationMs)
  });
}

function upsertAIDebugTraceFrontend(payload = {}) {
  const traceId = sanitizeAIDebugValue(payload.traceId, AI_DEBUG_TRACE_ID_MAX_LENGTH)
    || buildAIDebugTraceId();
  const flowId = sanitizeAIDebugValue(payload.flowId, AI_DEBUG_FLOW_ID_MAX_LENGTH);

  upsertAIDebugTraceFrontendStmt.run({
    trace_id: traceId,
    flow_id: flowId,
    source: sanitizeAIDebugValue(payload.source, 80) || 'extension',
    status: sanitizeTraceStatus(payload.status, 'frontend_reported'),
    frontend_timing_json: safeJsonStringify(payload.frontendTimings || null),
    frontend_steps_json: safeJsonStringify(payload.frontendSteps || null),
    frontend_error: payload.frontendError
      ? truncateString(String(payload.frontendError), 1200)
      : null,
    total_duration_ms: roundTiming(payload.totalDurationMs)
  });

  return { traceId, flowId };
}

function parseAIDebugTraceRow(row) {
  if (!row) {
    return null;
  }

  return {
    id: row.id,
    trace_id: row.trace_id,
    flow_id: row.flow_id || null,
    source: row.source || null,
    status: row.status || null,
    origin: row.origin || null,
    request_ip: row.request_ip || null,
    user_agent: row.user_agent || null,
    request_meta: safeJsonParse(row.request_meta_json, null),
    backend_timings: safeJsonParse(row.backend_timing_json, null),
    backend_attempts: safeJsonParse(row.backend_attempts_json, null),
    backend_error: row.backend_error || null,
    response_meta: safeJsonParse(row.response_meta_json, null),
    frontend_timings: safeJsonParse(row.frontend_timing_json, null),
    frontend_steps: safeJsonParse(row.frontend_steps_json, null),
    frontend_error: row.frontend_error || null,
    total_duration_ms: roundTiming(row.total_duration_ms),
    created_at: sqliteTimestampToUTC(row.created_at),
    updated_at: sqliteTimestampToUTC(row.updated_at)
  };
}

function buildServerTimingHeader(timingPairs = []) {
  if (!Array.isArray(timingPairs) || !timingPairs.length) {
    return '';
  }

  const parts = timingPairs
    .map((entry) => {
      if (!entry || typeof entry !== 'object') {
        return null;
      }
      const name = sanitizeAIDebugValue(entry.name, 24);
      const duration = roundTiming(entry.duration);
      if (!name || duration === null) {
        return null;
      }
      const normalizedName = name.replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase();
      if (!normalizedName) {
        return null;
      }
      return `${normalizedName};dur=${duration}`;
    })
    .filter(Boolean);

  return parts.join(', ');
}

function updateCredentialUsageOnSuccess(credentialId, info = {}) {
  if (!credentialId) {
    return;
  }

  try {
    const row = db.prepare(`
      SELECT usage_24h_count, usage_24h_reset_at, auto_disabled, auto_disabled_reason, auto_disabled_until, rate_limit_reset_at
      FROM ai_credentials
      WHERE id = ?
    `).get(credentialId) || {};

    const now = new Date();
    const nowIso = now.toISOString();

    let usageCount = Number(row.usage_24h_count) || 0;
    let usageResetDate = row.usage_24h_reset_at ? new Date(row.usage_24h_reset_at) : null;

    if (usageResetDate && usageResetDate <= now) {
      usageCount = 0;
      usageResetDate = null;
    }

    let rateLimitResetIso = ensureIsoDate(info.rateLimitResetAt || row.rate_limit_reset_at);

    if (rateLimitResetIso) {
      const rateReset = new Date(rateLimitResetIso);
      if (rateReset <= now) {
        rateLimitResetIso = null;
      }
    }

    if (!usageResetDate) {
      if (rateLimitResetIso) {
        usageResetDate = new Date(rateLimitResetIso);
      } else {
        usageResetDate = new Date(now.getTime() + 24 * 60 * 60 * 1000);
      }
    } else if (rateLimitResetIso) {
      const rateReset = new Date(rateLimitResetIso);
      if (rateReset < usageResetDate) {
        usageResetDate = rateReset;
      }
    }

    usageCount += 1;

    const shouldClearAutoDisable = row.auto_disabled === 1
      && (!row.auto_disabled_reason || String(row.auto_disabled_reason).startsWith('AUTO:'));

    const data = {
      id: credentialId,
      last_used_at: nowIso,
      last_success_at: nowIso,
      last_status_code: 200,
      last_rate_limit_remaining: info.rateLimitRemaining || null,
      rate_limit_reset_at: rateLimitResetIso,
      usage_24h_count: usageCount,
      usage_24h_reset_at: usageResetDate ? usageResetDate.toISOString() : null,
      auto_disabled: shouldClearAutoDisable ? 0 : row.auto_disabled || 0,
      auto_disabled_reason: shouldClearAutoDisable ? null : row.auto_disabled_reason || null,
      auto_disabled_until: shouldClearAutoDisable ? null : row.auto_disabled_until || null
    };

    db.prepare(`
      UPDATE ai_credentials
      SET last_used_at = @last_used_at,
          last_success_at = @last_success_at,
          last_status_code = @last_status_code,
          last_rate_limit_remaining = @last_rate_limit_remaining,
          rate_limit_reset_at = @rate_limit_reset_at,
          usage_24h_count = @usage_24h_count,
          usage_24h_reset_at = @usage_24h_reset_at,
          auto_disabled = @auto_disabled,
          auto_disabled_reason = @auto_disabled_reason,
          auto_disabled_until = @auto_disabled_until,
          updated_at = CURRENT_TIMESTAMP
      WHERE id = @id
    `).run(data);

    recordAIUsageEvent(credentialId, 'success', {
      statusCode: 200,
      message: 'Kommentar erfolgreich generiert',
      metadata: {
        rateLimitRemaining: info.rateLimitRemaining || null,
        rateLimitResetAt: rateLimitResetIso,
        usage24hCount: usageCount
      }
    });
  } catch (error) {
    console.warn('Failed to update credential success stats:', error.message);
  }
}

function updateCredentialUsageOnError(credentialId, error) {
  if (!credentialId || !error) {
    return { autoDisabled: false, autoDisabledUntil: null };
  }

  let decision = null;
  try {
    decision = determineAutoDisable(error);

    const now = new Date();
    const nowIso = now.toISOString();
    const rateLimitResetIso = ensureIsoDate(error.rateLimitResetAt);
    const rateLimitRemaining = error.rateLimitRemaining !== undefined && error.rateLimitRemaining !== null
      ? String(error.rateLimitRemaining)
      : null;

    const updateData = {
      id: credentialId,
      last_used_at: nowIso,
      last_error_message: truncateString(error.message || 'Unbekannter Fehler', 512),
      last_error_at: nowIso,
      last_status_code: error.status || error.statusCode || null,
      last_rate_limit_remaining: rateLimitRemaining,
      rate_limit_reset_at: rateLimitResetIso,
      auto_disabled: decision ? 1 : null,
      auto_disabled_reason: decision ? `AUTO:${decision.reason}` : null,
      auto_disabled_until: decision ? decision.until.toISOString() : null
    };

    db.prepare(`
      UPDATE ai_credentials
      SET last_used_at = @last_used_at,
          last_error_message = @last_error_message,
          last_error_at = @last_error_at,
          last_status_code = @last_status_code,
          last_rate_limit_remaining = @last_rate_limit_remaining,
          rate_limit_reset_at = @rate_limit_reset_at,
          auto_disabled = CASE WHEN @auto_disabled IS NULL THEN auto_disabled ELSE @auto_disabled END,
          auto_disabled_reason = CASE WHEN @auto_disabled_reason IS NULL THEN auto_disabled_reason ELSE @auto_disabled_reason END,
          auto_disabled_until = CASE WHEN @auto_disabled_until IS NULL THEN auto_disabled_until ELSE @auto_disabled_until END,
          updated_at = CURRENT_TIMESTAMP
      WHERE id = @id
    `).run(updateData);

    const eventMetadata = {
      provider: error.provider || null,
      retryAfterSeconds: error.retryAfterSeconds || null,
      rateLimitResetAt: rateLimitResetIso,
      rateLimitRemaining,
      autoDisabled: Boolean(decision)
    };

    recordAIUsageEvent(credentialId, 'error', {
      statusCode: error.status || error.statusCode || null,
      message: error.message || 'Unbekannter Fehler',
      metadata: eventMetadata
    });

    if (decision) {
      recordAIUsageEvent(credentialId, 'auto_disabled', {
        statusCode: error.status || error.statusCode || null,
        message: decision.reason,
        metadata: {
          autoDisabledUntil: decision.until.toISOString(),
          retryAfterSeconds: decision.seconds
        }
      });
    }
  } catch (updateError) {
    console.warn('Failed to update credential error stats:', updateError.message);
  }

  return {
    autoDisabled: Boolean(decision),
    autoDisabledUntil: decision ? decision.until.toISOString() : null
  };
}

function reactivateExpiredCredentials() {
  try {
    const nowIso = new Date().toISOString();
    const rows = db.prepare(`
      SELECT id, auto_disabled_until
      FROM ai_credentials
      WHERE auto_disabled = 1
        AND auto_disabled_until IS NOT NULL
        AND auto_disabled_until <= ?
    `).all(nowIso);

    for (const row of rows) {
      db.prepare(`
        UPDATE ai_credentials
        SET auto_disabled = 0,
            auto_disabled_reason = NULL,
            auto_disabled_until = NULL,
            updated_at = CURRENT_TIMESTAMP
        WHERE id = ?
      `).run(row.id);

      recordAIUsageEvent(row.id, 'auto_reenabled', {
        message: 'Automatisch wieder aktiviert',
        metadata: {
          previousUntil: row.auto_disabled_until
        }
      });
    }
  } catch (error) {
    console.warn('Failed to reactivate credentials:', error.message);
  }
}

const CREDENTIAL_SELECT_BASE = `
  SELECT
    c.id,
    c.name,
    c.provider,
    c.model,
    c.base_url,
    c.is_active,
    c.priority,
    c.auto_disabled,
    c.auto_disabled_reason,
    c.auto_disabled_until,
    c.last_used_at,
    c.last_success_at,
    c.last_error_message,
    c.last_error_at,
    c.last_status_code,
    c.last_rate_limit_remaining,
    c.rate_limit_reset_at,
    c.usage_24h_count,
    c.usage_24h_reset_at,
    c.created_at,
    c.updated_at,
    e.event_type AS latest_event_type,
    e.status_code AS latest_event_status_code,
    e.message AS latest_event_message,
    e.metadata AS latest_event_metadata,
    e.created_at AS latest_event_at
  FROM ai_credentials c
  LEFT JOIN (
    SELECT e1.*
    FROM ai_usage_events e1
    INNER JOIN (
      SELECT credential_id, MAX(id) AS latest_id
      FROM ai_usage_events
      GROUP BY credential_id
    ) latest ON latest.credential_id = e1.credential_id AND latest.latest_id = e1.id
  ) e ON e.credential_id = c.id
`;

function fetchCredentialRows(options = {}) {
  const { where = '', params = [], orderBy = 'ORDER BY c.priority ASC, c.id ASC' } = options;
  const query = `${CREDENTIAL_SELECT_BASE} ${where ? where : ''} ${orderBy}`;
  return db.prepare(query).all(...params);
}

function formatCredentialRow(row) {
  if (!row) {
    return null;
  }

  const now = Date.now();
  const formatted = { ...row };

  formatted.is_active = Number(row.is_active) === 1 ? 1 : 0;
  formatted.auto_disabled = Number(row.auto_disabled) === 1;

  let cooldownSeconds = null;
  if (row.auto_disabled_until) {
    const until = Date.parse(row.auto_disabled_until);
    if (!Number.isNaN(until)) {
      const diffSeconds = Math.round((until - now) / 1000);
      cooldownSeconds = diffSeconds > 0 ? diffSeconds : 0;
    }
  }

  formatted.cooldown_remaining_seconds = cooldownSeconds;

  if (row.latest_event_type) {
    let metadata = null;
    if (row.latest_event_metadata) {
      try {
        metadata = JSON.parse(row.latest_event_metadata);
      } catch (error) {
        metadata = row.latest_event_metadata;
      }
    }
    formatted.latest_event = {
      type: row.latest_event_type,
      status_code: row.latest_event_status_code,
      message: row.latest_event_message,
      metadata,
      created_at: sqliteTimestampToUTC(row.latest_event_at)
    };
  } else {
    formatted.latest_event = null;
  }

  formatted.status = formatted.is_active
    ? (formatted.auto_disabled ? 'cooldown' : 'active')
    : 'inactive';

  delete formatted.latest_event_type;
  delete formatted.latest_event_status_code;
  delete formatted.latest_event_message;
  delete formatted.latest_event_metadata;
  delete formatted.latest_event_at;

  return formatted;
}

function getAllCredentialsFormatted() {
  return fetchCredentialRows().map(formatCredentialRow);
}

function getFormattedCredentialById(id) {
  const row = fetchCredentialRows({ where: 'WHERE c.id = ?', params: [id], orderBy: '' });
  if (!row || !row.length) {
    return null;
  }
  return formatCredentialRow(row[0]);
}

function normalizeRetentionDays(value) {
  const parsed = parseInt(value, 10);
  if (Number.isNaN(parsed) || parsed <= 0) {
    return SEARCH_POST_RETENTION_DAYS;
  }
  return Math.min(365, Math.max(1, parsed));
}

function loadHiddenSettings() {
  let settings = db.prepare('SELECT * FROM maintenance_settings WHERE id = 1').get();
  if (!settings) {
    const defaults = {
      id: 1,
      search_retention_days: SEARCH_POST_RETENTION_DAYS,
      auto_purge_hidden: 1
    };
    db.prepare(`
      INSERT INTO maintenance_settings (id, search_retention_days, auto_purge_hidden, updated_at)
      VALUES (1, ?, ?, CURRENT_TIMESTAMP)
    `).run(defaults.search_retention_days, defaults.auto_purge_hidden);
    settings = defaults;
  }
  settings.search_retention_days = normalizeRetentionDays(settings.search_retention_days);
  settings.auto_purge_hidden = settings.auto_purge_hidden ? 1 : 0;
  return settings;
}

function persistHiddenSettings({ retentionDays, autoPurgeEnabled }) {
  const normalizedRetention = normalizeRetentionDays(retentionDays);
  const normalizedAuto = autoPurgeEnabled ? 1 : 0;
  const existing = db.prepare('SELECT id FROM maintenance_settings WHERE id = 1').get();
  if (existing) {
    db.prepare(`
      UPDATE maintenance_settings
      SET search_retention_days = ?, auto_purge_hidden = ?, updated_at = CURRENT_TIMESTAMP
      WHERE id = 1
    `).run(normalizedRetention, normalizedAuto);
  } else {
    db.prepare(`
      INSERT INTO maintenance_settings (id, search_retention_days, auto_purge_hidden, updated_at)
      VALUES (1, ?, ?, CURRENT_TIMESTAMP)
    `).run(normalizedRetention, normalizedAuto);
  }
  return {
    auto_purge_enabled: !!normalizedAuto,
    retention_days: normalizedRetention
  };
}

function cleanupExpiredSearchPosts() {
  try {
    const settings = loadHiddenSettings();
    if (!settings.auto_purge_hidden) {
      return;
    }
    const threshold = `-${settings.search_retention_days} day`;
    db.prepare(`
      DELETE FROM search_seen_posts
      WHERE last_seen_at < DATETIME('now', ?)
    `).run(threshold);
  } catch (error) {
    console.warn('Failed to cleanup expired search posts:', error.message);
  }
}

function safeParseSportsWeights(raw) {
  if (!raw) {
    return null;
  }
  if (typeof raw === 'object' && !Array.isArray(raw)) {
    return raw;
  }
  if (typeof raw !== 'string') {
    return null;
  }
  try {
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
      return parsed;
    }
  } catch (error) {
    return null;
  }
  return null;
}

function normalizeSportsScoreThreshold(value) {
  const parsed = parseFloat(value);
  if (Number.isNaN(parsed) || parsed < 0) {
    return SPORTS_SCORING_DEFAULTS.threshold;
  }
  return Math.min(50, Math.max(0, parsed));
}

function normalizeSportsWeights(weights) {
  const defaults = SPORTS_SCORING_DEFAULTS.weights;
  const normalized = {};
  const source = (weights && typeof weights === 'object' && !Array.isArray(weights))
    ? weights
    : {};

  for (const key of Object.keys(defaults)) {
    const raw = source[key];
    const parsed = typeof raw === 'number' ? raw : parseFloat(raw);
    const value = Number.isFinite(parsed) ? parsed : defaults[key];
    normalized[key] = Math.max(0, Math.min(10, value));
  }

  return normalized;
}

function normalizeSportsTerms(terms) {
  const defaults = SPORTS_SCORING_TERMS_DEFAULTS;
  const result = {};
  const source = (terms && typeof terms === 'object' && !Array.isArray(terms))
    ? terms
    : {};

  const normalizeList = (list, fallback) => {
    const arr = Array.isArray(list) ? list : [];
    const cleaned = arr
      .map((entry) => {
        if (typeof entry !== 'string') return '';
        return entry.trim().toLowerCase();
      })
      .filter((entry) => entry && entry.length <= 60);
    const unique = Array.from(new Set(cleaned)).slice(0, 200);
    if (unique.length) {
      return unique;
    }
    return fallback.slice();
  };

  for (const key of Object.keys(defaults)) {
    result[key] = normalizeList(source[key], defaults[key]);
  }

  return result;
}

function loadModerationSettings() {
  let settings = db.prepare('SELECT * FROM moderation_settings WHERE id = 1').get();

  if (!settings) {
    const serializedWeights = JSON.stringify(SPORTS_SCORING_DEFAULTS.weights);
    const serializedTerms = JSON.stringify(SPORTS_SCORING_TERMS_DEFAULTS);
    db.prepare(`
      INSERT INTO moderation_settings (id, sports_scoring_enabled, sports_score_threshold, sports_auto_hide_enabled, sports_score_weights, sports_terms, updated_at)
      VALUES (1, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
    `).run(SPORTS_SCORING_DEFAULTS.enabled, SPORTS_SCORING_DEFAULTS.threshold, SPORTS_SCORING_DEFAULTS.auto_hide_enabled, serializedWeights, serializedTerms);
    settings = {
      id: 1,
      sports_scoring_enabled: SPORTS_SCORING_DEFAULTS.enabled,
      sports_score_threshold: SPORTS_SCORING_DEFAULTS.threshold,
      sports_auto_hide_enabled: SPORTS_SCORING_DEFAULTS.auto_hide_enabled,
      sports_score_weights: serializedWeights,
      sports_terms: serializedTerms
    };
  }

  const weights = normalizeSportsWeights(safeParseSportsWeights(settings.sports_score_weights));
  const threshold = normalizeSportsScoreThreshold(settings.sports_score_threshold);
  let terms = SPORTS_SCORING_TERMS_DEFAULTS;
  try {
    const parsedTerms = settings.sports_terms ? JSON.parse(settings.sports_terms) : null;
    terms = normalizeSportsTerms(parsedTerms);
  } catch (error) {
    terms = SPORTS_SCORING_TERMS_DEFAULTS;
  }

  return {
    sports_scoring_enabled: !!settings.sports_scoring_enabled,
    sports_score_threshold: threshold,
    sports_auto_hide_enabled: !!settings.sports_auto_hide_enabled,
    sports_score_weights: weights,
    sports_terms: terms
  };
}

function persistModerationSettings({ enabled, threshold, weights, terms, autoHide }) {
  const normalizedEnabled = enabled ? 1 : 0;
  const normalizedAutoHide = autoHide ? 1 : 0;
  const normalizedThreshold = normalizeSportsScoreThreshold(threshold);
  const normalizedWeights = normalizeSportsWeights(weights);
  const serializedWeights = JSON.stringify(normalizedWeights);
  const normalizedTerms = normalizeSportsTerms(terms);
  const serializedTerms = JSON.stringify(normalizedTerms);

  const existing = db.prepare('SELECT id FROM moderation_settings WHERE id = 1').get();
  if (existing) {
    db.prepare(`
      UPDATE moderation_settings
      SET sports_scoring_enabled = ?, sports_score_threshold = ?, sports_auto_hide_enabled = ?, sports_score_weights = ?, sports_terms = ?, updated_at = CURRENT_TIMESTAMP
      WHERE id = 1
    `).run(normalizedEnabled, normalizedThreshold, normalizedAutoHide, serializedWeights, serializedTerms);
  } else {
    db.prepare(`
      INSERT INTO moderation_settings (id, sports_scoring_enabled, sports_score_threshold, sports_auto_hide_enabled, sports_score_weights, sports_terms, updated_at)
      VALUES (1, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
    `).run(normalizedEnabled, normalizedThreshold, normalizedAutoHide, serializedWeights, serializedTerms);
  }

  return {
    sports_scoring_enabled: !!normalizedEnabled,
    sports_score_threshold: normalizedThreshold,
    sports_auto_hide_enabled: !!normalizedAutoHide,
    sports_score_weights: normalizedWeights,
    sports_terms: normalizedTerms
  };
}

function normalizeSimilarityTextThreshold(value) {
  const parsed = parseFloat(value);
  if (Number.isNaN(parsed)) {
    return SIMILARITY_DEFAULTS.text_threshold;
  }
  return Math.min(0.99, Math.max(0.5, parsed));
}

function normalizeSimilarityImageDistance(value) {
  const parsed = parseInt(value, 10);
  if (Number.isNaN(parsed)) {
    return SIMILARITY_DEFAULTS.image_distance_threshold;
  }
  return Math.min(64, Math.max(0, parsed));
}

function loadSimilaritySettings() {
  let settings = db.prepare('SELECT * FROM similarity_settings WHERE id = 1').get();
  if (!settings) {
    db.prepare(`
      INSERT INTO similarity_settings (id, text_threshold, image_distance_threshold, updated_at)
      VALUES (1, ?, ?, CURRENT_TIMESTAMP)
    `).run(SIMILARITY_DEFAULTS.text_threshold, SIMILARITY_DEFAULTS.image_distance_threshold);
    settings = {
      text_threshold: SIMILARITY_DEFAULTS.text_threshold,
      image_distance_threshold: SIMILARITY_DEFAULTS.image_distance_threshold
    };
  }

  return {
    text_threshold: normalizeSimilarityTextThreshold(settings.text_threshold),
    image_distance_threshold: normalizeSimilarityImageDistance(settings.image_distance_threshold)
  };
}

function persistSimilaritySettings({ textThreshold, imageDistanceThreshold }) {
  const normalizedText = normalizeSimilarityTextThreshold(textThreshold);
  const normalizedImage = normalizeSimilarityImageDistance(imageDistanceThreshold);
  const existing = db.prepare('SELECT id FROM similarity_settings WHERE id = 1').get();
  if (existing) {
    db.prepare(`
      UPDATE similarity_settings
      SET text_threshold = ?, image_distance_threshold = ?, updated_at = CURRENT_TIMESTAMP
      WHERE id = 1
    `).run(normalizedText, normalizedImage);
  } else {
    db.prepare(`
      INSERT INTO similarity_settings (id, text_threshold, image_distance_threshold, updated_at)
      VALUES (1, ?, ?, CURRENT_TIMESTAMP)
    `).run(normalizedText, normalizedImage);
  }

  return {
    text_threshold: normalizedText,
    image_distance_threshold: normalizedImage
  };
}

function expandPhotoUrlHostVariants(url) {
  if (typeof url !== 'string' || !url) {
    return [];
  }

  try {
    const parsed = new URL(url);
    const hostname = parsed.hostname.toLowerCase();
    if (!hostname.endsWith('facebook.com')) {
      return [];
    }

    const pathname = parsed.pathname.toLowerCase();
    if (!pathname.startsWith('/photo')) {
      return [];
    }

    const protocol = parsed.protocol || 'https:';
    const search = parsed.search || '';
    const hosts = ['www.facebook.com', 'facebook.com', 'm.facebook.com'];
    const variants = [];

    for (const candidateHost of hosts) {
      if (candidateHost === hostname) {
        continue;
      }
      const candidateUrl = `${protocol}//${candidateHost}${parsed.pathname}${search}`;
      const normalized = normalizeFacebookPostUrl(candidateUrl);
      if (normalized && normalized !== url && !variants.includes(normalized)) {
        variants.push(normalized);
      }
    }

    return variants;
  } catch (error) {
    return [];
  }
}

function collectNormalizedFacebookUrls(primaryUrl, candidates = []) {
  const normalized = [];

  const pushNormalized = (value, expandVariants = true) => {
    const normalizedUrl = normalizeFacebookPostUrl(value);
    if (normalizedUrl && !normalized.includes(normalizedUrl)) {
      normalized.push(normalizedUrl);

      if (expandVariants) {
        const photoVariants = expandPhotoUrlHostVariants(normalizedUrl);
        for (const variant of photoVariants) {
          pushNormalized(variant, false);
        }
      }
    }
  };

  if (primaryUrl) {
    pushNormalized(primaryUrl);
  }

  if (Array.isArray(candidates)) {
    for (const candidate of candidates) {
      pushNormalized(candidate);
    }
  }

  return normalized;
}

function collectPostAlternateUrls(primaryUrl, candidates = []) {
  const normalizedPrimary = normalizeFacebookPostUrl(primaryUrl);
  if (!normalizedPrimary) {
    return [];
  }

  const primaryKey = extractFacebookContentKey(normalizedPrimary);
  const normalized = collectNormalizedFacebookUrls(normalizedPrimary, candidates);

  return normalized.filter((url) => {
    if (url === normalizedPrimary) {
      return false;
    }

    const candidateKey = extractFacebookContentKey(url);
    return candidateKey && candidateKey === primaryKey;
  });
}

const insertPostUrlStmt = db.prepare(`
  INSERT OR IGNORE INTO post_urls (post_id, url, is_primary)
  VALUES (?, ?, 0)
`);

const selectPostByPrimaryUrlStmt = db.prepare('SELECT * FROM posts WHERE url = ?');
const selectPostByAlternateUrlStmt = db.prepare(`
  SELECT p.*
  FROM post_urls pu
  JOIN posts p ON p.id = pu.post_id
  WHERE pu.url = ?
  LIMIT 1
`);
const selectPostIdByPrimaryUrlStmt = db.prepare('SELECT id FROM posts WHERE url = ?');
const selectPostIdByAlternateUrlStmt = db.prepare('SELECT post_id FROM post_urls WHERE url = ?');
const selectPostByContentKeyStmt = db.prepare('SELECT * FROM posts WHERE content_key = ? LIMIT 1');
const selectPostIdByContentKeyStmt = db.prepare('SELECT id FROM posts WHERE content_key = ? LIMIT 1');
const selectAlternateUrlsForPostStmt = db.prepare(`
  SELECT url
  FROM post_urls
  WHERE post_id = ?
  ORDER BY created_at ASC
`);
const selectPostByTextHashStmt = db.prepare('SELECT * FROM posts WHERE post_text_hash = ?');
const selectPostsForSimilarityStmt = db.prepare(`
  SELECT id, url, title, created_by_name, created_at, post_text, first_image_hash
  FROM posts
`);
const selectChecksForPostStmt = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ?');
const updateCheckPostStmt = db.prepare('UPDATE checks SET post_id = ? WHERE id = ?');
const updateCheckTimestampStmt = db.prepare('UPDATE checks SET checked_at = ? WHERE id = ?');
const deleteCheckByIdStmt = db.prepare('DELETE FROM checks WHERE id = ?');

function storePostUrls(postId, primaryUrl, additionalUrls = [], options = {}) {
  const { skipContentKeyCheck = false } = options;
  if (!postId || !primaryUrl) {
    return;
  }

  const normalizedPrimary = normalizeFacebookPostUrl(primaryUrl);
  if (!normalizedPrimary) {
    return;
  }

  const primaryKey = extractFacebookContentKey(normalizedPrimary);

  if (Array.isArray(additionalUrls)) {
    for (const candidate of additionalUrls) {
      const normalized = normalizeFacebookPostUrl(candidate);
      if (!normalized || normalized === normalizedPrimary) {
        continue;
      }

      if (!skipContentKeyCheck) {
        const candidateKey = extractFacebookContentKey(normalized);
        if (!candidateKey || candidateKey !== primaryKey) {
          continue;
        }
      }

      const existingPostId = findPostIdByUrl(normalized);
      if (existingPostId && existingPostId !== postId) {
        continue;
      }
      insertPostUrlStmt.run(postId, normalized);
    }
  }
}

function findPostIdByUrl(normalizedUrl) {
  if (!normalizedUrl) {
    return null;
  }

  const primaryRow = selectPostIdByPrimaryUrlStmt.get(normalizedUrl);
  if (primaryRow && primaryRow.id) {
    return primaryRow.id;
  }

  const alternateRow = selectPostIdByAlternateUrlStmt.get(normalizedUrl);
  if (alternateRow && alternateRow.post_id) {
    return alternateRow.post_id;
  }

  const contentKey = extractFacebookContentKey(normalizedUrl);
  if (contentKey) {
    const contentRow = selectPostIdByContentKeyStmt.get(contentKey);
    if (contentRow && contentRow.id) {
      return contentRow.id;
    }
  }

  return null;
}

function findPostByUrl(normalizedUrl) {
  if (!normalizedUrl) {
    return null;
  }

  const primary = selectPostByPrimaryUrlStmt.get(normalizedUrl);
  if (primary) {
    return primary;
  }

  const alternate = selectPostByAlternateUrlStmt.get(normalizedUrl);
  if (alternate) {
    return alternate;
  }

  const contentKey = extractFacebookContentKey(normalizedUrl);
  if (contentKey) {
    const contentMatch = selectPostByContentKeyStmt.get(contentKey);
    if (contentMatch) {
      return contentMatch;
    }
  }

  return null;
}

function removeSearchSeenEntries(urls) {
  if (!Array.isArray(urls) || urls.length === 0) {
    return;
  }

  const uniqueValidUrls = Array.from(new Set(urls.filter(url => typeof url === 'string' && url.trim())));
  if (!uniqueValidUrls.length) {
    return;
  }

  const stmt = db.prepare('DELETE FROM search_seen_posts WHERE url = ?');
  const runDeletion = db.transaction((values) => {
    for (const value of values) {
      stmt.run(value);
    }
  });

  try {
    runDeletion(uniqueValidUrls);
  } catch (error) {
    console.warn('Failed to remove search seen entries:', error.message);
  }
}

cleanupExpiredSearchPosts();

const selectSearchSeenStmt = db.prepare('SELECT url, seen_count, manually_hidden, sports_auto_hidden, first_seen_at, last_seen_at FROM search_seen_posts WHERE url = ?');
const insertSearchSeenStmt = db.prepare(`
  INSERT INTO search_seen_posts (url, seen_count, manually_hidden, sports_auto_hidden, first_seen_at, last_seen_at)
  VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`);
const updateSearchSeenStmt = db.prepare(`
  UPDATE search_seen_posts
  SET seen_count = ?, manually_hidden = ?, sports_auto_hidden = ?, last_seen_at = CURRENT_TIMESTAMP
  WHERE url = ?
`);
const checkIndexes = db.prepare("PRAGMA index_list('checks')").all();
for (const idx of checkIndexes) {
  if (idx.unique) {
    // Skip auto indexes created from PRIMARY KEY/UNIQUE constraints; SQLite refuses to drop them
    if (idx.origin !== 'c' || (idx.name && idx.name.startsWith('sqlite_autoindex'))) {
      continue;
    }

    const info = db.prepare(`PRAGMA index_info('${idx.name}')`).all();
    const columns = info.map(i => i.name).join(',');
    if (columns === 'post_id,profile_number' || columns === 'profile_number,post_id') {
      db.exec(`DROP INDEX IF EXISTS "${idx.name}"`);
    }
  }
}

function sqliteTimestampToUTC(timestamp) {
  if (!timestamp) {
    return null;
  }
  // SQLite CURRENT_TIMESTAMP returns UTC time in format "YYYY-MM-DD HH:MM:SS"
  // Convert to ISO-8601 with Z suffix to indicate UTC
  return timestamp.replace(' ', 'T') + 'Z';
}

function mapPostRow(post) {
  if (!post) {
    return null;
  }

  let postContentKey = post.content_key;
  if (!postContentKey) {
    const normalizedUrl = normalizeFacebookPostUrl(post.url);
    postContentKey = extractFacebookContentKey(normalizedUrl);
    if (postContentKey) {
      updateContentKeyStmt.run(postContentKey, post.id);
      post.content_key = postContentKey;
    }
  }

  if (post.post_text && (!post.post_text_hash || !post.post_text_hash.trim())) {
    const normalizedPostText = normalizePostText(post.post_text);
    const hash = computePostTextHash(normalizedPostText);
    post.post_text = normalizedPostText;
    post.post_text_hash = hash;
    updatePostTextColumnsStmt.run(normalizedPostText, hash, post.id);
  }

  const checks = db.prepare('SELECT id, profile_number, checked_at FROM checks WHERE post_id = ? ORDER BY checked_at ASC').all(post.id);
  const requiredProfiles = getRequiredProfiles(post.target_count);
  const { statuses, completedChecks } = buildProfileStatuses(requiredProfiles, checks);
  const checkedCount = Math.min(requiredProfiles.length, countUniqueProfileChecks(checks));
  const screenshotFile = post.screenshot_path ? path.join(screenshotDir, post.screenshot_path) : null;
  const screenshotPath = screenshotFile && fs.existsSync(screenshotFile)
    ? `/api/posts/${post.id}/screenshot`
    : null;

  let postLastChange = post.last_change;

  if (post.checked_count !== checkedCount || post.target_count !== requiredProfiles.length) {
    const updates = [];
    const params = [];

    if (post.checked_count !== checkedCount) {
      updates.push('checked_count = ?');
      params.push(checkedCount);
    }

    if (post.target_count !== requiredProfiles.length) {
      updates.push('target_count = ?');
      params.push(requiredProfiles.length);
    }

    updates.push('last_change = CURRENT_TIMESTAMP');
    params.push(post.id);
    db.prepare(`UPDATE posts SET ${updates.join(', ')} WHERE id = ?`).run(...params);

    const refreshed = db.prepare('SELECT last_change FROM posts WHERE id = ?').get(post.id);
    if (refreshed && refreshed.last_change) {
      postLastChange = refreshed.last_change;
    }
  }

  const nextRequired = statuses.find(status => status.status === 'available');
  const creatorProfile = sanitizeProfileNumber(post.created_by_profile);
  const creatorName = normalizeCreatorName(post.created_by_name);

  // Convert SQLite timestamps to UTC ISO-8601 format
  const checksWithUTC = completedChecks.map(check => ({
    ...check,
    checked_at: sqliteTimestampToUTC(check.checked_at)
  }));

  const statusesWithUTC = statuses.map(status => ({
    ...status,
    checked_at: sqliteTimestampToUTC(status.checked_at)
  }));

  const alternateUrlRows = selectAlternateUrlsForPostStmt.all(post.id);
  const alternateUrls = alternateUrlRows.map(row => row.url);

  return {
    ...post,
    created_at: sqliteTimestampToUTC(post.created_at),
    target_count: requiredProfiles.length,
    checked_count: checkedCount,
    last_change: sqliteTimestampToUTC(postLastChange),
    checks: checksWithUTC,
    is_complete: checkedCount >= requiredProfiles.length,
    screenshot_path: screenshotPath,
    required_profiles: requiredProfiles,
    profile_statuses: statusesWithUTC,
    next_required_profile: nextRequired ? nextRequired.profile_number : null,
    created_by_profile: creatorProfile,
    created_by_profile_name: creatorProfile ? getProfileName(creatorProfile) : null,
    created_by_name: creatorName,
    deadline_at: post.deadline_at || null,
    alternate_urls: alternateUrls,
    post_text: post.post_text || null,
    post_text_hash: post.post_text_hash || null,
    content_key: post.content_key || postContentKey || null,
    first_image_hash: post.first_image_hash || null,
    first_image_url: post.first_image_url || null
  };
}

function broadcastPostChange(post, options = {}) {
  if (!post || !post.id) {
    return;
  }

  const payload = {
    type: 'post-upsert',
    post
  };

  if (options && options.reason) {
    payload.reason = options.reason;
  }

  broadcastSseEvent(payload);
}

function broadcastPostChangeById(postId, options = {}) {
  if (!postId) {
    return;
  }
  try {
    const row = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    if (!row) {
      return;
    }
    const postPayload = mapPostRow(row);
    broadcastPostChange(postPayload, options);
  } catch (error) {
    console.warn(`Failed to broadcast post ${postId}:`, error.message);
  }
}

function broadcastPostDeletion(postId, options = {}) {
  if (!postId) {
    return;
  }

  const payload = {
    type: 'post-deleted',
    postId
  };

  if (options && options.reason) {
    payload.reason = options.reason;
  }

  broadcastSseEvent(payload);
}

function broadcastAutomationEvent(eventType, payload = {}) {
  if (!eventType) {
    return;
  }
  const data = { type: eventType, ...payload };
  broadcastSseEvent(data);
}

const automationRunningRequests = new Set();
let automationWorkerTimer = null;
let automationWorkerBusy = false;

async function executeAutomationRequest(request, options = {}) {
  if (!request || !request.id) {
    return null;
  }

  const trigger = options.trigger || 'schedule';
  const allowInactive = !!options.allowInactive;
  const current = getAutomationRequestStmt.get(request.id);
  if (!current || (!current.active && !allowInactive)) {
    return null;
  }

  if (automationRunningRequests.has(current.id)) {
    return null;
  }

  automationRunningRequests.add(current.id);

  const context = buildAutomationTemplateContext(new Date());
  const startedAt = new Date();
  let status = 'success';
  let statusCode = null;
  let responseText = '';
  let errorMessage = null;
  const stepResults = [];

  async function executeHttpStep(step, stepIndex, inheritedContext) {
    const method = (step.method || 'GET').toUpperCase();
    const url = renderAutomationTemplate(step.url_template || step.url, inheritedContext);
    const headers = renderAutomationHeaders(step.headers_json || serializeAutomationHeaders(step.headers || {}), inheritedContext);
    const shouldSendBody = !['GET', 'HEAD'].includes(method);
    const body = shouldSendBody && (step.body_template || step.body)
      ? renderAutomationTemplate(step.body_template || step.body, inheritedContext)
      : null;

    let localStatus = 'success';
    let localStatusCode = null;
    let localResponseText = '';
    let localError = null;

    try {
      const response = await fetch(url, {
        method,
        headers,
        body: shouldSendBody && body !== null ? body : undefined,
        redirect: 'follow'
      });
      localStatusCode = response.status;
      try {
        localResponseText = await response.text();
      } catch (error) {
        localResponseText = '';
      }
      if (!response.ok) {
        localStatus = 'error';
        localError = `HTTP ${response.status}`;
      }
    } catch (error) {
      localStatus = 'error';
      localError = error.message || 'Unbekannter Fehler';
    }

    let parsedJson = null;
    if (localResponseText) {
      try {
        parsedJson = JSON.parse(localResponseText);
      } catch (error) {
        parsedJson = null;
      }
    }

    const resultContext = {
      ...inheritedContext,
      [`step${stepIndex}_text`]: localResponseText,
      [`step${stepIndex}_status`]: localStatus,
      [`step${stepIndex}_status_code`]: localStatusCode,
      [`step${stepIndex}_json`]: parsedJson || null
    };

    return {
      status: localStatus,
      statusCode: localStatusCode,
      responseText: localResponseText,
      error: localError,
      context: resultContext
    };
  }

  async function executeEmailStep(stepContext) {
    if (!nodemailer) {
      throw new Error('E-Mail Versand nicht verfügbar (nodemailer fehlt)');
    }
    const {
      SMTP_HOST,
      SMTP_PORT,
      SMTP_USER,
      SMTP_PASS,
      SMTP_SECURE,
      SMTP_FROM
    } = process.env;

    const configSmtp = (automationConfig && automationConfig.smtp) || {};

    const host = SMTP_HOST || configSmtp.host;
    if (!host) {
      throw new Error('SMTP_HOST nicht gesetzt und keine Konfiguration in automation-config.json');
    }

    const port = SMTP_PORT ? Number(SMTP_PORT) : (configSmtp.port || 587);
    const secure = SMTP_SECURE === 'true' || SMTP_SECURE === '1'
      ? true
      : (typeof configSmtp.secure === 'boolean' ? configSmtp.secure : port === 465);
    const user = SMTP_USER || configSmtp.user || '';
    const pass = SMTP_PASS || configSmtp.pass || '';
    const from = SMTP_FROM || configSmtp.from || user || 'automation@example.com';

    const transporter = nodemailer.createTransport({
      host,
      port,
      secure,
      auth: user ? { user, pass } : undefined
    });

    const to = renderAutomationTemplate(current.email_to || '', stepContext);
    const subject = renderAutomationTemplate(current.email_subject_template || '', stepContext);
    const body = renderAutomationTemplate(current.email_body_template || '', stepContext);
    const bodyHtml = body ? body.replace(/\n/g, '<br>') : '';

    if (!to || !subject || !body) {
      throw new Error('E-Mail Felder unvollständig');
    }

    const info = await transporter.sendMail({
      from,
      to,
      subject,
      text: body,
      html: bodyHtml || body
    });

    return {
      status: 'success',
      statusCode: info && info.accepted ? 200 : null,
      responseText: info ? JSON.stringify({ messageId: info.messageId }) : '',
      error: null,
      context: { ...stepContext }
    };
  }

  try {
    if (current.type === AUTOMATION_TYPE_EMAIL) {
      const emailResult = await executeEmailStep(context);
      status = emailResult.status;
      statusCode = emailResult.statusCode;
      responseText = emailResult.responseText;
      errorMessage = emailResult.error;
    } else if (current.type === AUTOMATION_TYPE_FLOW) {
      let flowContext = { ...context };
      const steps = current.steps_json ? (() => {
        try {
          const parsed = JSON.parse(current.steps_json);
          return Array.isArray(parsed) ? parsed : [];
        } catch (error) {
          return [];
        }
      })() : [];
      let stepIndex = 1;
      for (const step of steps) {
        const stepResult = await executeHttpStep({
          method: step.method || step.http_method || 'GET',
          url_template: step.url_template || step.url,
          headers_json: serializeAutomationHeaders(step.headers || {}),
          body_template: step.body_template || step.body
        }, stepIndex, flowContext);
        stepResults.push(stepResult);
        flowContext = { ...flowContext, ...stepResult.context };
        if (stepResult.status === 'error') {
          status = 'error';
          errorMessage = stepResult.error;
          statusCode = stepResult.statusCode;
          responseText = stepResult.responseText;
          break;
        } else {
          statusCode = stepResult.statusCode;
          responseText = stepResult.responseText;
        }
        stepIndex += 1;
      }
    } else {
      const result = await executeHttpStep({
        method: current.method,
        url_template: current.url_template,
        headers_json: current.headers_json,
        body_template: current.body_template
      }, 1, context);
      status = result.status;
      statusCode = result.statusCode;
      responseText = result.responseText;
      errorMessage = result.error;
      stepResults.push(result);
    }
  } catch (error) {
    status = 'error';
    errorMessage = error.message || 'Unbekannter Fehler';
  }

  const completedAt = new Date();

  const runRecord = {
    request_id: current.id,
    trigger,
    started_at: startedAt.toISOString(),
    completed_at: completedAt.toISOString(),
    status,
    status_code: statusCode,
    error: truncateString(errorMessage, 900),
    response_body: truncateString(responseText, AUTOMATION_MAX_RESPONSE_PREVIEW),
    duration_ms: Math.max(0, completedAt.getTime() - startedAt.getTime())
  };

  try {
    insertAutomationRunStmt.run(runRecord);
  } catch (error) {
    console.warn('Failed to persist automation run:', error.message);
  }

  const nextRunAt = current.active
    ? computeNextAutomationRun(
      { ...current, last_run_at: runRecord.started_at },
      { fromDate: completedAt }
    )
    : null;

  broadcastAutomationEvent('automation-run', {
    request_id: current.id,
    status,
    status_code: statusCode,
    next_run_at: nextRunAt,
    last_run_at: runRecord.started_at,
    runs_count: (current.runs_count || 0) + 1
  });

  try {
    updateAutomationRequestStmt.run({
      ...current,
      last_run_at: runRecord.started_at,
      last_status: status,
      last_status_code: statusCode,
      last_error: runRecord.error,
      next_run_at: nextRunAt
    });
  } catch (error) {
    console.warn(`Failed to update automation request ${current.id}:`, error.message);
  } finally {
    automationRunningRequests.delete(current.id);
  }

  return runRecord;
}

async function processAutomationQueue() {
  if (automationWorkerBusy) {
    return;
  }

  automationWorkerBusy = true;

  try {
    const nowIso = new Date().toISOString();
    const dueRequests = listDueAutomationRequestsStmt.all({ now: nowIso }) || [];
    for (const item of dueRequests) {
      await executeAutomationRequest(item, { trigger: 'schedule' });
    }
  } catch (error) {
    console.error('Automation worker failed:', error);
  } finally {
    automationWorkerBusy = false;
  }
}

function startAutomationWorker() {
  if (automationWorkerTimer) {
    return;
  }

  automationWorkerTimer = setInterval(() => {
    processAutomationQueue().catch((error) => {
      console.error('Automation queue tick failed:', error);
    });
  }, AUTOMATION_WORKER_INTERVAL_MS);

  processAutomationQueue().catch((error) => {
    console.error('Automation initial run failed:', error);
  });
}

app.get('/api/automation/requests', (req, res) => {
  try {
    const rows = listAutomationRequestsStmt.all();
    const hydrated = rows.map((row) => {
      ensureNextRunForRequest(row, { fromDate: new Date() });
      return serializeAutomationRequest(row);
    });
    res.json(hydrated);
  } catch (error) {
    console.error('Failed to load automation requests:', error);
    res.status(500).json({ error: 'Automationen konnten nicht geladen werden' });
  }
});

app.get('/api/automation/requests/:requestId', (req, res) => {
  const { requestId } = req.params;
  try {
    const row = getAutomationRequestStmt.get(requestId);
    if (!row) {
      return res.status(404).json({ error: 'Automation nicht gefunden' });
    }

    const payload = serializeAutomationRequest(row);
    ensureNextRunForRequest(row, { fromDate: new Date() });
    if (req.query && req.query.includeRuns) {
      const limit = 10;
      const runs = listAutomationRunsStmt.all({ requestId, limit }).map(serializeAutomationRun);
      payload.runs = runs;
    }

    res.json(payload);
  } catch (error) {
    console.error('Failed to load automation request:', error);
    res.status(500).json({ error: 'Automation konnte nicht geladen werden' });
  }
});

app.get('/api/automation/requests/:requestId/runs', (req, res) => {
  const { requestId } = req.params;
  const limit = Math.max(1, Math.min(200, parseInt(req.query?.limit, 10) || 30));

  try {
    const rows = listAutomationRunsStmt.all({ requestId, limit });
    res.json(rows.map(serializeAutomationRun));
  } catch (error) {
    console.error('Failed to load automation runs:', error);
    res.status(500).json({ error: 'Runs konnten nicht geladen werden' });
  }
});

app.post('/api/automation/requests', (req, res) => {
  const payload = req.body || {};
  const { data, errors } = normalizeAutomationPayload(payload, {});

  if (errors.length) {
    return res.status(400).json({ error: errors[0], details: errors });
  }

  try {
    const id = uuidv4();
    let nextRunAt = data.active
      ? computeNextAutomationRun({ ...data, last_run_at: null }, { fromDate: new Date() })
      : null;

    insertAutomationRequestStmt.run({
      ...data,
      id,
      last_run_at: null,
      last_status: null,
      last_status_code: null,
      last_error: null,
      next_run_at: nextRunAt
    });

    let saved = getAutomationRequestStmt.get(id);
    if (data.active && (!saved || !saved.next_run_at)) {
      const ensured = ensureNextRunForRequest(saved || data, { fromDate: new Date() });
      if (ensured) {
        saved = getAutomationRequestStmt.get(id);
      }
    }

    res.status(201).json(serializeAutomationRequest(saved));
  } catch (error) {
    console.error('Failed to create automation:', error);
    res.status(500).json({ error: 'Automation konnte nicht erstellt werden' });
  }
});

app.put('/api/automation/requests/:requestId', (req, res) => {
  const { requestId } = req.params;
  const existing = getAutomationRequestStmt.get(requestId);
  if (!existing) {
    return res.status(404).json({ error: 'Automation nicht gefunden' });
  }

  const { data, errors } = normalizeAutomationPayload(req.body || {}, existing);
  if (errors.length) {
    return res.status(400).json({ error: errors[0], details: errors });
  }

  const merged = {
    ...existing,
    ...data,
    last_run_at: existing.last_run_at,
    last_status: existing.last_status,
    last_status_code: existing.last_status_code,
    last_error: existing.last_error
  };

  merged.next_run_at = merged.active
    ? computeNextAutomationRun(merged, { fromDate: new Date() })
    : null;

  try {
    updateAutomationRequestStmt.run(merged);
    let saved = getAutomationRequestStmt.get(requestId);
    if (merged.active && (!saved || !saved.next_run_at)) {
      const ensured = ensureNextRunForRequest(saved || merged, { fromDate: new Date() });
      if (ensured) {
        saved = getAutomationRequestStmt.get(requestId);
      }
    }
    res.json(serializeAutomationRequest(saved));
  } catch (error) {
    console.error('Failed to update automation:', error);
    res.status(500).json({ error: 'Automation konnte nicht aktualisiert werden' });
  }
});

app.post('/api/automation/requests/:requestId/run', async (req, res) => {
  const { requestId } = req.params;
  const existing = getAutomationRequestStmt.get(requestId);
  if (!existing) {
    return res.status(404).json({ error: 'Automation nicht gefunden' });
  }

  try {
    const run = await executeAutomationRequest(existing, {
      trigger: 'manual',
      allowInactive: true
    });
    const refreshed = getAutomationRequestStmt.get(requestId);
    res.json({
      request: serializeAutomationRequest(refreshed),
      run: serializeAutomationRun(run)
    });
  } catch (error) {
    console.error('Failed to trigger automation run:', error);
    res.status(500).json({ error: 'Automation-Run konnte nicht gestartet werden' });
  }
});

app.delete('/api/automation/requests/:requestId', (req, res) => {
  const { requestId } = req.params;
  try {
    const result = deleteAutomationRequestStmt.run(requestId);
    if (!result.changes) {
      return res.status(404).json({ error: 'Automation nicht gefunden' });
    }
    res.status(204).send();
  } catch (error) {
    console.error('Failed to delete automation:', error);
    res.status(500).json({ error: 'Automation konnte nicht gelöscht werden' });
  }
});

app.get('/api/bookmarks', (req, res) => {
  try {
    const rows = listBookmarksStmt.all();
    res.json(rows.map(serializeBookmark));
  } catch (error) {
    console.error('Failed to load bookmarks:', error);
    res.status(500).json({ error: 'Bookmarks konnten nicht geladen werden' });
  }
});

app.post('/api/bookmarks', (req, res) => {
  try {
    const payload = req.body || {};
    const normalizedQuery = normalizeBookmarkQuery(payload.query);
    if (!normalizedQuery) {
      return res.status(400).json({ error: 'Ungültiger Suchbegriff' });
    }

    if (findBookmarkByQueryStmt.get(normalizedQuery)) {
      return res.status(409).json({ error: 'Bookmark existiert bereits' });
    }

    const normalizedLabel = normalizeBookmarkLabel(payload.label, normalizedQuery);
    const id = uuidv4();
    insertBookmarkStmt.run(id, normalizedLabel, normalizedQuery);
    const saved = getBookmarkByIdStmt.get(id);

    res.status(201).json(serializeBookmark(saved));
  } catch (error) {
    console.error('Failed to create bookmark:', error);
    res.status(500).json({ error: 'Bookmark konnte nicht erstellt werden' });
  }
});

app.post('/api/bookmarks/:bookmarkId/click', (req, res) => {
  const { bookmarkId } = req.params;
  if (!bookmarkId) {
    return res.status(400).json({ error: 'Bookmark-ID fehlt' });
  }

  try {
    const result = updateBookmarkLastClickedStmt.run(bookmarkId);
    if (!result.changes) {
      return res.status(404).json({ error: 'Bookmark nicht gefunden' });
    }

    const updated = getBookmarkByIdStmt.get(bookmarkId);
    res.json(serializeBookmark(updated));
  } catch (error) {
    console.error('Failed to register bookmark click:', error);
    res.status(500).json({ error: 'Bookmark konnte nicht aktualisiert werden' });
  }
});

app.delete('/api/bookmarks/:bookmarkId', (req, res) => {
  const { bookmarkId } = req.params;
  if (!bookmarkId) {
    return res.status(400).json({ error: 'Bookmark-ID fehlt' });
  }

  try {
    const existing = getBookmarkByIdStmt.get(bookmarkId);
    if (!existing) {
      return res.status(404).json({ error: 'Bookmark nicht gefunden' });
    }
    if (isDefaultBookmarkQuery(existing.query)) {
      return res.status(403).json({ error: 'Standard-Bookmark kann nicht gelöscht werden' });
    }

    const result = deleteBookmarkStmt.run(bookmarkId);
    if (!result.changes) {
      return res.status(404).json({ error: 'Bookmark nicht gefunden' });
    }

    res.status(204).send();
  } catch (error) {
    console.error('Failed to delete bookmark:', error);
    res.status(500).json({ error: 'Bookmark konnte nicht gelöscht werden' });
  }
});

app.get('/api/daily-bookmarks', (req, res) => {
  const dayKey = resolveDayKey(req.query && req.query.day);

  try {
    const rows = listDailyBookmarksStmt.all({ dayKey });
    res.json(rows.map((row) => serializeDailyBookmark(row, dayKey)));
  } catch (error) {
    console.error('Failed to load daily bookmarks:', error);
    res.status(500).json({ error: 'Daily Bookmarks konnten nicht geladen werden' });
  }
});

app.post('/api/daily-bookmarks', (req, res) => {
  const payload = req.body || {};
  const rawDay = (payload && payload.day) || (req.query && req.query.day);
  const dayKey = rawDay ? normalizeDayKeyValue(rawDay) : resolveDayKey(rawDay);
  if (!dayKey) {
    return res.status(400).json({ error: 'Ungültiges Tagesformat' });
  }
  const normalizedUrl = normalizeDailyBookmarkUrlTemplate(payload.url_template || payload.url);
  const normalizedTitle = normalizeDailyBookmarkTitle(payload.title || payload.label, normalizedUrl);
  const normalizedNotes = normalizeDailyBookmarkNotes(payload.notes);
  const normalizedMarker = normalizeDailyBookmarkMarker(payload.marker || payload.tag);
  const normalizedActive = normalizeDailyBookmarkActive(
    payload.is_active ?? payload.active ?? true
  );

  if (!normalizedUrl) {
    return res.status(400).json({ error: 'URL-Template ist erforderlich' });
  }

  if (findDailyBookmarkByUrlStmt.get(normalizedUrl)) {
    return res.status(409).json({ error: 'Bookmark mit dieser URL existiert bereits' });
  }

  try {
    const id = uuidv4();
    insertDailyBookmarkStmt.run({
      id,
      title: normalizedTitle,
      url_template: normalizedUrl,
      notes: normalizedNotes,
      marker: normalizedMarker,
      is_active: normalizedActive
    });
    upsertDailyBookmarkCheckStmt.run({ bookmarkId: id, dayKey });
    const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
    res.status(201).json(serializeDailyBookmark(saved, dayKey));
  } catch (error) {
    console.error('Failed to create daily bookmark:', error);
    res.status(500).json({ error: 'Daily Bookmark konnte nicht erstellt werden' });
  }
});

app.post('/api/daily-bookmarks/import', (req, res) => {
  const payload = req.body || {};
  const rawDay = (payload && payload.day) || (req.query && req.query.day);
  const dayKey = rawDay ? normalizeDayKeyValue(rawDay) : resolveDayKey(rawDay);
  if (!dayKey) {
    return res.status(400).json({ error: 'Ungültiges Tagesformat' });
  }

  const normalizedMarker = normalizeDailyBookmarkMarker(payload.marker || payload.tag);
  const normalizedActive = normalizeDailyBookmarkActive(
    payload.is_active ?? payload.active ?? true
  );
  const collected = [];
  const addValue = (value) => {
    if (typeof value !== 'string') {
      return;
    }
    const trimmed = value.trim();
    if (trimmed) {
      collected.push(trimmed);
    }
  };

  if (Array.isArray(payload.urls)) {
    payload.urls.forEach(addValue);
  }
  if (Array.isArray(payload.url_templates)) {
    payload.url_templates.forEach(addValue);
  }
  if (typeof payload.text === 'string') {
    payload.text.split(/\r?\n|,/).forEach(addValue);
  }
  if (typeof payload.urls_text === 'string') {
    payload.urls_text.split(/\r?\n|,/).forEach(addValue);
  }

  const inputCount = collected.length;
  if (!inputCount) {
    return res.status(400).json({ error: 'Keine URLs zum Import übergeben' });
  }

  const normalizedTemplates = collected
    .map((raw) => normalizeDailyBookmarkUrlTemplate(raw))
    .filter(Boolean);
  const invalidCount = inputCount - normalizedTemplates.length;
  const uniqueTemplates = [...new Set(normalizedTemplates)];
  const duplicateCount = normalizedTemplates.length - uniqueTemplates.length;

  const createdItems = [];
  let skippedExisting = 0;

  const insertMany = db.transaction((templates) => {
    for (const template of templates) {
      if (findDailyBookmarkByUrlStmt.get(template)) {
        skippedExisting += 1;
        continue;
      }
      const id = uuidv4();
      const title = normalizeDailyBookmarkTitle('', template);
      insertDailyBookmarkStmt.run({
        id,
        title,
        url_template: template,
        notes: '',
        marker: normalizedMarker,
        is_active: normalizedActive
      });
      const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
      if (saved) {
        createdItems.push(saved);
      }
    }
  });

  try {
    insertMany(uniqueTemplates);
    res.json({
      created: createdItems.length,
      skipped_existing: skippedExisting,
      skipped_invalid: invalidCount,
      skipped_duplicates: duplicateCount,
      marker: normalizedMarker,
      day_key: dayKey,
      items: createdItems.map((row) => serializeDailyBookmark(row, dayKey))
    });
  } catch (error) {
    console.error('Failed to import daily bookmarks:', error);
    res.status(500).json({ error: 'Import fehlgeschlagen' });
  }
});

app.put('/api/daily-bookmarks/:bookmarkId', (req, res) => {
  const { bookmarkId } = req.params;
  if (!bookmarkId) {
    return res.status(400).json({ error: 'Bookmark-ID fehlt' });
  }

  const payload = req.body || {};
  const rawDay = (payload && payload.day) || (req.query && req.query.day);
  const dayKey = rawDay ? normalizeDayKeyValue(rawDay) : resolveDayKey(rawDay);
  if (!dayKey) {
    return res.status(400).json({ error: 'Ungültiges Tagesformat' });
  }

  const existing = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
  if (!existing) {
    return res.status(404).json({ error: 'Bookmark nicht gefunden' });
  }

  const normalizedUrl = normalizeDailyBookmarkUrlTemplate(
    payload.url_template ?? payload.url ?? existing.url_template
  );
  const normalizedTitle = normalizeDailyBookmarkTitle(
    payload.title ?? payload.label ?? existing.title,
    normalizedUrl || existing.url_template
  );
  const normalizedNotes = normalizeDailyBookmarkNotes(
    payload.notes ?? existing.notes ?? ''
  );
  const normalizedMarker = normalizeDailyBookmarkMarker(
    payload.marker ?? existing.marker ?? ''
  );
  const normalizedActive = normalizeDailyBookmarkActive(
    payload.is_active ?? payload.active ?? Number(existing.is_active ?? 1)
  );

  if (!normalizedUrl) {
    return res.status(400).json({ error: 'URL-Template ist erforderlich' });
  }

  const otherWithUrl = findOtherDailyBookmarkByUrlStmt.get({
    url: normalizedUrl,
    id: bookmarkId
  });
  if (otherWithUrl) {
    return res.status(409).json({ error: 'Bookmark mit dieser URL existiert bereits' });
  }

  try {
    updateDailyBookmarkStmt.run({
      id: bookmarkId,
      title: normalizedTitle,
      url_template: normalizedUrl,
      notes: normalizedNotes,
      marker: normalizedMarker,
      is_active: normalizedActive
    });
    const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
    res.json(serializeDailyBookmark(updated, dayKey));
  } catch (error) {
    console.error('Failed to update daily bookmark:', error);
    res.status(500).json({ error: 'Daily Bookmark konnte nicht aktualisiert werden' });
  }
});

app.delete('/api/daily-bookmarks/:bookmarkId', (req, res) => {
  const { bookmarkId } = req.params;
  if (!bookmarkId) {
    return res.status(400).json({ error: 'Bookmark-ID fehlt' });
  }

  try {
    const result = deleteDailyBookmarkStmt.run(bookmarkId);
    if (!result.changes) {
      return res.status(404).json({ error: 'Bookmark nicht gefunden' });
    }
    res.status(204).send();
  } catch (error) {
    console.error('Failed to delete daily bookmark:', error);
    res.status(500).json({ error: 'Daily Bookmark konnte nicht gelöscht werden' });
  }
});

app.post('/api/daily-bookmarks/:bookmarkId/check', (req, res) => {
  const { bookmarkId } = req.params;
  if (!bookmarkId) {
    return res.status(400).json({ error: 'Bookmark-ID fehlt' });
  }

  const rawDay = (req.body && req.body.day) || (req.query && req.query.day);
  const dayKey = rawDay ? normalizeDayKeyValue(rawDay) : resolveDayKey(rawDay);
  if (!dayKey) {
    return res.status(400).json({ error: 'Ungültiges Tagesformat' });
  }

  try {
    const existing = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
    if (!existing) {
      return res.status(404).json({ error: 'Bookmark nicht gefunden' });
    }
    if (Number(existing.is_active ?? 1) === 0) {
      return res.status(400).json({ error: 'Bookmark ist deaktiviert' });
    }

    upsertDailyBookmarkCheckStmt.run({ bookmarkId, dayKey });
    const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
    res.json(serializeDailyBookmark(updated, dayKey));
  } catch (error) {
    console.error('Failed to complete daily bookmark:', error);
    res.status(500).json({ error: 'Daily Bookmark konnte nicht abgehakt werden' });
  }
});

app.delete('/api/daily-bookmarks/:bookmarkId/check', (req, res) => {
  const { bookmarkId } = req.params;
  if (!bookmarkId) {
    return res.status(400).json({ error: 'Bookmark-ID fehlt' });
  }

  const rawDay = (req.body && req.body.day) || (req.query && req.query.day);
  const dayKey = rawDay ? normalizeDayKeyValue(rawDay) : resolveDayKey(rawDay);
  if (!dayKey) {
    return res.status(400).json({ error: 'Ungültiges Tagesformat' });
  }

  try {
    const existing = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
    if (!existing) {
      return res.status(404).json({ error: 'Bookmark nicht gefunden' });
    }
    if (Number(existing.is_active ?? 1) === 0) {
      return res.status(400).json({ error: 'Bookmark ist deaktiviert' });
    }

    deleteDailyBookmarkCheckStmt.run({ bookmarkId, dayKey });
    const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
    res.json(serializeDailyBookmark(updated, dayKey));
  } catch (error) {
    console.error('Failed to undo daily bookmark completion:', error);
    res.status(500).json({ error: 'Daily Bookmark konnte nicht zurückgesetzt werden' });
  }
});

// Get all posts
app.get('/api/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache, no-transform');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  if (typeof res.flushHeaders === 'function') {
    res.flushHeaders();
  } else {
    res.write('\n');
  }

  res.write(`retry: ${SSE_RETRY_INTERVAL_MS}\n\n`);

  const client = addSseClient(res);
  const initialPayload = {
    type: 'connected',
    clientId: client.id
  };
  res.write(`data: ${JSON.stringify(initialPayload)}\n\n`);

  const cleanup = () => {
    removeSseClient(client.id);
  };

  req.on('close', cleanup);
  res.on('close', cleanup);
});

app.get('/api/posts', (req, res) => {
  try {
    const posts = db.prepare(`
      SELECT *
      FROM posts
      ORDER BY created_at DESC
    `).all();

    res.json(posts.map(mapPostRow));
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Get post by URL
app.get('/api/posts/by-url', (req, res) => {
  try {
    const { url } = req.query;
    if (!url) {
      return res.status(400).json({ error: 'URL parameter required' });
    }

    const normalizedUrl = normalizeFacebookPostUrl(url);
    if (!normalizedUrl) {
      return res.status(400).json({ error: 'URL parameter must be a valid Facebook link' });
    }

    const post = findPostByUrl(normalizedUrl);
    if (!post) {
      return res.json(null);
    }

    const alternates = collectPostAlternateUrls(post.url, [normalizedUrl]);
    if (alternates.length) {
      storePostUrls(post.id, post.url, alternates);
      touchPost(post.id, 'alternate-urls');
    }

    res.json(mapPostRow(post));
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/posts/similar', (req, res) => {
  try {
    const { url, post_text, first_image_hash } = req.body || {};
    const normalizedUrl = normalizeFacebookPostUrl(url);
    if (!normalizedUrl) {
      return res.status(400).json({ error: 'url must be a valid Facebook link' });
    }

    const existing = findPostByUrl(normalizedUrl);
    if (existing) {
      return res.json({ match: null });
    }

    const settings = loadSimilaritySettings();
    const normalizedText = normalizePostText(post_text);
    const textEligible = normalizedText && normalizedText.length >= MIN_SIMILAR_TEXT_LENGTH;
    const normalizedImageHash = typeof first_image_hash === 'string'
      ? first_image_hash.trim().toLowerCase()
      : null;
    const cleanedImageHash = (normalizedImageHash && /^[0-9a-f]{16}$/.test(normalizedImageHash))
      ? normalizedImageHash
      : null;

    let bestText = null;
    let bestImage = null;

    const rows = selectPostsForSimilarityStmt.all();
    for (const row of rows) {
      if (!row) {
        continue;
      }
      if (textEligible && row.post_text) {
        const candidateText = normalizePostText(row.post_text);
        if (candidateText) {
          const score = computeTextSimilarity(normalizedText, candidateText);
          if (!bestText || score > bestText.score) {
            bestText = { post: row, score };
          }
        }
      }
      if (cleanedImageHash && row.first_image_hash) {
        const distance = hammingDistanceHex(cleanedImageHash, row.first_image_hash);
        if (distance !== null && (!bestImage || distance < bestImage.distance)) {
          bestImage = { post: row, distance };
        }
      }
    }

    const textMatch = bestText && bestText.score >= settings.text_threshold ? bestText : null;
    const imageMatch = bestImage && bestImage.distance <= settings.image_distance_threshold ? bestImage : null;

    if (!textMatch && !imageMatch) {
      return res.json({ match: null });
    }

    const imageScore = (match) => match ? 1 - (match.distance / 64) : 0;
    let selected = textMatch || imageMatch;
    let reason = textMatch ? 'text' : 'image';
    let textScore = textMatch ? textMatch.score : null;
    let imageDistance = imageMatch ? imageMatch.distance : null;

    if (textMatch && imageMatch) {
      if (textMatch.post.id === imageMatch.post.id) {
        selected = textMatch;
        reason = 'text+image';
      } else {
        const textScoreValue = textMatch.score;
        const imageScoreValue = imageScore(imageMatch);
        if (imageScoreValue > textScoreValue) {
          selected = imageMatch;
          reason = 'image';
        } else {
          selected = textMatch;
          reason = 'text';
        }
      }
    }

    const matchPost = selected.post;
    const responsePayload = {
      match: {
        id: matchPost.id,
        url: matchPost.url,
        title: matchPost.title,
        created_by_name: matchPost.created_by_name,
        created_at: sqliteTimestampToUTC(matchPost.created_at)
      },
      similarity: {
        text: textScore,
        image_distance: imageDistance
      },
      reason
    };

    res.json(responsePayload);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/search-posts', (req, res) => {
  try {
    const { url, candidates, skip_increment, force_hide, sports_auto_hide } = req.body || {};

    const normalizedUrls = collectNormalizedFacebookUrls(url, candidates);
    if (!normalizedUrls.length) {
      return res.status(400).json({ error: 'url must be a valid Facebook link' });
    }

    cleanupExpiredSearchPosts();

    let trackedPost = null;
    for (const candidate of normalizedUrls) {
      const found = findPostByUrl(candidate);
      if (found) {
        trackedPost = found;
        break;
      }
    }

    if (trackedPost) {
      const alternateUrls = collectPostAlternateUrls(trackedPost.url, normalizedUrls);
      storePostUrls(trackedPost.id, trackedPost.url, alternateUrls);
      removeSearchSeenEntries([trackedPost.url, ...alternateUrls]);
      if (alternateUrls.length) {
        touchPost(trackedPost.id, 'alternate-urls');
      }
      return res.json({ seen_count: 0, should_hide: false, tracked: true });
    }

    let existingRow = null;
    let existingUrl = null;

    for (const candidate of normalizedUrls) {
      const row = selectSearchSeenStmt.get(candidate);
      if (row) {
        existingRow = row;
        existingUrl = candidate;
        break;
      }
    }

    const targetUrl = existingUrl || normalizedUrls[0];
    const existingManualHidden = existingRow ? !!existingRow.manually_hidden : false;
    const existingSportsHidden = existingRow ? !!existingRow.sports_auto_hidden : false;
    const sportsHideRequested = !!sports_auto_hide;

    if (force_hide) {
      const desiredCount = Math.max(existingRow ? existingRow.seen_count : 0, SEARCH_POST_HIDE_THRESHOLD);
      const urlsToUpdate = Array.from(new Set(normalizedUrls));

      for (const candidate of urlsToUpdate) {
        const row = selectSearchSeenStmt.get(candidate);
        const candidateCount = row ? Math.max(row.seen_count, desiredCount) : desiredCount;
        const nextSportsHidden = sportsHideRequested || (row ? !!row.sports_auto_hidden : false);
        if (row) {
          updateSearchSeenStmt.run(candidateCount, 1, nextSportsHidden ? 1 : 0, candidate);
        } else {
          insertSearchSeenStmt.run(candidate, candidateCount, 1, nextSportsHidden ? 1 : 0);
        }
      }

      return res.json({ seen_count: desiredCount, should_hide: true, manually_hidden: true, sports_auto_hidden: sportsHideRequested || existingSportsHidden });
    }

    if (skip_increment) {
      if (!existingRow) {
        return res.json({ seen_count: 0, should_hide: false, manually_hidden: false, sports_auto_hidden: false });
      }
      const seenCount = existingRow.seen_count;
      const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || existingManualHidden || existingSportsHidden;
      return res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: existingManualHidden, sports_auto_hidden: existingSportsHidden });
    }

    let seenCount = existingRow ? existingRow.seen_count + 1 : 1;
    const manualHidden = existingManualHidden;
    const sportsHidden = sportsHideRequested || existingSportsHidden;

    if (existingRow) {
      updateSearchSeenStmt.run(seenCount, manualHidden ? 1 : 0, sportsHidden ? 1 : 0, targetUrl);
    } else {
      insertSearchSeenStmt.run(targetUrl, seenCount, manualHidden ? 1 : 0, sportsHidden ? 1 : 0);
    }

    const shouldHide = seenCount >= SEARCH_POST_HIDE_THRESHOLD || manualHidden || sportsHidden;
    res.json({ seen_count: seenCount, should_hide: shouldHide, manually_hidden: manualHidden, sports_auto_hidden: sportsHidden });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.delete('/api/search-posts', (req, res) => {
  try {
    db.prepare('DELETE FROM search_seen_posts').run();
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/profile-state', (req, res) => {
  try {
    const scopeId = req.profileScope;
    const profileNumber = getScopedProfileNumber(scopeId);
    res.json({ profile_number: profileNumber || null });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/profile-state', (req, res) => {
  try {
    const { profile_number } = req.body;
    if (typeof profile_number === 'undefined') {
      return res.status(400).json({ error: 'profile_number is required' });
    }

    const parsed = parseInt(profile_number, 10);
    if (Number.isNaN(parsed) || parsed < 1 || parsed > 5) {
      return res.status(400).json({ error: 'profile_number must be between 1 and 5' });
    }

    const scopeId = req.profileScope;
    const sanitized = sanitizeProfileNumber(parsed) || 1;
    setScopedProfileNumber(scopeId, sanitized);

    res.json({ profile_number: sanitized });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/posts/:postId/screenshot', (req, res) => {
  try {
    const { postId } = req.params;
    const { imageData } = req.body;

    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    if (!post) {
      return res.status(404).json({ error: 'Post not found' });
    }

    if (!imageData || typeof imageData !== 'string') {
      return res.status(400).json({ error: 'imageData is required' });
    }

    const match = imageData.match(/^data:(image\/\w+);base64,(.+)$/);
    if (!match) {
      return res.status(400).json({ error: 'Invalid image data format' });
    }

    const mimeType = match[1];
    const base64 = match[2];
    const extension = mimeType === 'image/jpeg' ? 'jpg' : 'png';
    const buffer = Buffer.from(base64, 'base64');
    const fileName = `${postId}.${extension}`;
    const filePath = path.join(screenshotDir, fileName);

    if (post.screenshot_path && post.screenshot_path !== fileName) {
      const existingPath = path.join(screenshotDir, post.screenshot_path);
      if (fs.existsSync(existingPath)) {
        try {
          fs.unlinkSync(existingPath);
        } catch (error) {
          console.warn('Failed to remove previous screenshot:', error.message);
        }
      }
    }

    fs.writeFileSync(filePath, buffer);
    db.prepare('UPDATE posts SET screenshot_path = ?, last_change = CURRENT_TIMESTAMP WHERE id = ?').run(fileName, postId);

    const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    const formattedPost = mapPostRow(updatedPost);
    res.json(formattedPost);
    broadcastPostChange(formattedPost, { reason: 'screenshot-updated' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/posts/:postId/screenshot', (req, res) => {
  try {
    const { postId } = req.params;
    const post = db.prepare('SELECT screenshot_path FROM posts WHERE id = ?').get(postId);

    // Placeholder path (mounted in Docker container)
    const placeholderPath = path.join(__dirname, 'noScreenshot.png');

    if (!post || !post.screenshot_path) {
      // Return placeholder image
      if (fs.existsSync(placeholderPath)) {
        res.set('Cache-Control', 'no-store');
        return res.sendFile(placeholderPath);
      }
      res.set('Cache-Control', 'no-store');
      return res.status(404).json({ error: 'Screenshot not found' });
    }

    const filePath = path.join(screenshotDir, post.screenshot_path);
    if (!fs.existsSync(filePath)) {
      // Return placeholder image
      if (fs.existsSync(placeholderPath)) {
        res.set('Cache-Control', 'no-store');
        return res.sendFile(placeholderPath);
      }
      res.set('Cache-Control', 'no-store');
      return res.status(404).json({ error: 'Screenshot not found' });
    }

    res.set('Cache-Control', 'no-store');
    res.sendFile(filePath);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Create new post
app.post('/api/posts', (req, res) => {
  try {
    const {
      url,
      title,
      target_count,
      created_by_profile,
      created_by_name,
      profile_number,
      deadline_at,
      post_text
    } = req.body;

    const validatedTargetCount = validateTargetCount(typeof target_count === 'undefined' ? 1 : target_count);
    const alternateUrlsInput = Array.isArray(req.body.alternate_urls) ? req.body.alternate_urls : [];

    const normalizedUrl = normalizeFacebookPostUrl(url);

    if (!normalizedUrl) {
      return res.status(400).json({ error: 'URL must be a valid Facebook link' });
    }

    if (!validatedTargetCount) {
      return res.status(400).json({ error: 'target_count must be between 1 and 5' });
    }

    const id = uuidv4();

    const normalizedPostText = normalizePostText(post_text);
    const postTextHash = computePostTextHash(normalizedPostText);
    const contentKey = extractFacebookContentKey(normalizedUrl);
    const normalizedImageHash = typeof req.body.first_image_hash === 'string'
      ? req.body.first_image_hash.trim().toLowerCase()
      : null;
    const cleanedImageHash = (normalizedImageHash && /^[0-9a-f]{16}$/.test(normalizedImageHash))
      ? normalizedImageHash
      : null;
    const normalizedImageUrl = typeof req.body.first_image_url === 'string'
      ? normalizeFacebookPostUrl(req.body.first_image_url) || req.body.first_image_url.trim()
      : null;
    const useTextHashDedup = normalizedPostText && normalizedPostText.length >= MIN_TEXT_HASH_LENGTH && postTextHash;

    if (useTextHashDedup) {
      let existingByHash = selectPostByTextHashStmt.get(postTextHash);
      if (existingByHash) {
        const alternateCandidates = [normalizedUrl, ...alternateUrlsInput];
        const alternateUrls = collectPostAlternateUrls(existingByHash.url, alternateCandidates);
        storePostUrls(existingByHash.id, existingByHash.url, alternateUrls);

        const cleanupSet = new Set([existingByHash.url, normalizedUrl, ...alternateUrls]);
        removeSearchSeenEntries(Array.from(cleanupSet));

        if (normalizedPostText && (!existingByHash.post_text || !existingByHash.post_text.trim())) {
          updatePostTextColumnsStmt.run(normalizedPostText, postTextHash, existingByHash.id);
          touchPost(existingByHash.id, 'post-text-normalized');
          existingByHash = db.prepare('SELECT * FROM posts WHERE id = ?').get(existingByHash.id);
        }

        return res.json(mapPostRow(existingByHash));
      }
    }

    let creatorProfile = sanitizeProfileNumber(created_by_profile);
    if (!creatorProfile) {
      creatorProfile = sanitizeProfileNumber(profile_number) || null;
    }

    let normalizedDeadline = null;
    if (typeof deadline_at !== 'undefined' && deadline_at !== null && String(deadline_at).trim() !== '') {
      normalizedDeadline = normalizeDeadline(deadline_at);
      if (!normalizedDeadline) {
        return res.status(400).json({ error: 'deadline_at must be a valid date string' });
      }
    }

    const creatorDisplayName = normalizeCreatorName(created_by_name);

    const stmt = db.prepare(`
      INSERT INTO posts (
        id,
        url,
        title,
        target_count,
        checked_count,
        screenshot_path,
        created_by_profile,
        created_by_name,
        deadline_at,
        post_text,
        post_text_hash,
        content_key,
        first_image_hash,
        first_image_url,
        last_change
      )
      VALUES (?, ?, ?, ?, 0, NULL, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
    `);
    stmt.run(
      id,
      normalizedUrl,
      title || '',
      validatedTargetCount,
      creatorProfile,
      creatorDisplayName,
      normalizedDeadline,
      normalizedPostText,
      postTextHash,
      contentKey || null,
      cleanedImageHash || null,
      normalizedImageUrl || null
    );

    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id);

    const alternateUrls = collectPostAlternateUrls(normalizedUrl, alternateUrlsInput);
    storePostUrls(id, normalizedUrl, alternateUrls);
    removeSearchSeenEntries([normalizedUrl, ...alternateUrls]);

    const formattedPost = mapPostRow(post);
    res.json(formattedPost);
    broadcastPostChange(formattedPost, { reason: 'created' });
  } catch (error) {
    if (error.message.includes('UNIQUE constraint failed')) {
      res.status(409).json({ error: 'Post with this URL already exists' });
    } else {
      res.status(500).json({ error: error.message });
    }
  }
});

app.put('/api/posts/:postId', (req, res) => {
  try {
    const { postId } = req.params;
    const {
      target_count,
      title,
      created_by_profile,
      created_by_name,
      deadline_at,
      url,
      post_text
    } = req.body || {};
    const alternateUrlsInput = Array.isArray(req.body && req.body.alternate_urls) ? req.body.alternate_urls : [];
    const existingPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);

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

    const updates = [];
    const params = [];
    let normalizedUrlForCleanup = null;
    let updatedContentKey = null;

    if (typeof target_count !== 'undefined') {
      const validatedTargetCount = validateTargetCount(target_count);
      if (!validatedTargetCount) {
        return res.status(400).json({ error: 'target_count must be between 1 and 5' });
      }
      updates.push('target_count = ?');
      params.push(validatedTargetCount);
    }

    if (typeof title !== 'undefined') {
      updates.push('title = ?');
      params.push(title || '');
    }

    if (typeof created_by_profile !== 'undefined') {
      const sanitized = sanitizeProfileNumber(created_by_profile);
      if (created_by_profile !== null && typeof created_by_profile !== 'undefined' && !sanitized) {
        return res.status(400).json({ error: 'created_by_profile must be between 1 and 5 or null' });
      }
      updates.push('created_by_profile = ?');
      params.push(sanitized || null);
    }

    if (typeof created_by_name !== 'undefined') {
      const normalizedName = normalizeCreatorName(created_by_name);
      updates.push('created_by_name = ?');
      params.push(normalizedName);
    }

    if (typeof deadline_at !== 'undefined') {
      let normalizedDeadline = null;
      const rawDeadline = deadline_at;
      if (rawDeadline !== null && String(rawDeadline).trim() !== '') {
        normalizedDeadline = normalizeDeadline(rawDeadline);
        if (!normalizedDeadline) {
          return res.status(400).json({ error: 'deadline_at must be a valid date string' });
        }
      }
      updates.push('deadline_at = ?');
      params.push(normalizedDeadline);
    }

    if (typeof url !== 'undefined') {
      const normalizedUrl = normalizeFacebookPostUrl(url);
      if (!normalizedUrl) {
        return res.status(400).json({ error: 'url must be a valid Facebook link' });
      }
      updates.push('url = ?');
      params.push(normalizedUrl);
      normalizedUrlForCleanup = normalizedUrl;
      const newContentKey = extractFacebookContentKey(normalizedUrl);
      updates.push('content_key = ?');
      params.push(newContentKey || null);
      updatedContentKey = newContentKey || null;
    }

    if (typeof post_text !== 'undefined') {
      const normalizedPostText = normalizePostText(post_text);
      const postTextHash = computePostTextHash(normalizedPostText);
      updates.push('post_text = ?');
      params.push(normalizedPostText);
      updates.push('post_text_hash = ?');
      params.push(postTextHash);
    }

    if (!updates.length) {
      return res.status(400).json({ error: 'No valid fields to update' });
    }

    updates.push('last_change = CURRENT_TIMESTAMP');
    params.push(postId);

    const stmt = db.prepare(`UPDATE posts SET ${updates.join(', ')} WHERE id = ?`);
    try {
      stmt.run(...params);
    } catch (error) {
      if (error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') {
        return res.status(409).json({ error: 'Post with this URL already exists' });
      }
      throw error;
    }

    recalcCheckedCount(postId);

    const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);

    const alternateCandidates = [...alternateUrlsInput];
    if (existingPost.url && existingPost.url !== updatedPost.url) {
      alternateCandidates.push(existingPost.url);
    }

    const alternateUrls = collectPostAlternateUrls(updatedPost.url, alternateCandidates);
    storePostUrls(updatedPost.id, updatedPost.url, alternateUrls);

    const cleanupUrls = new Set([updatedPost.url]);
    alternateUrls.forEach(urlValue => cleanupUrls.add(urlValue));
    if (normalizedUrlForCleanup && normalizedUrlForCleanup !== updatedPost.url) {
      cleanupUrls.add(normalizedUrlForCleanup);
    }
    removeSearchSeenEntries(Array.from(cleanupUrls));

    const formattedPost = mapPostRow(updatedPost);
    res.json(formattedPost);
    broadcastPostChange(formattedPost, { reason: 'updated' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Check a post for a profile
app.post('/api/posts/:postId/check', (req, res) => {
  try {
    const { postId } = req.params;
    const { profile_number, ignore_order } = req.body || {};

    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    if (!post) {
      return res.status(404).json({ error: 'Post not found' });
    }

    // Check if deadline has passed
    if (post.deadline_at) {
      const deadline = new Date(post.deadline_at);
      if (new Date() > deadline) {
        return res.status(400).json({ error: 'Deadline ist abgelaufen' });
      }
    }

    const requiredProfiles = getRequiredProfiles(post.target_count);
    let didChange = false;
    if (post.target_count !== requiredProfiles.length) {
      db.prepare('UPDATE posts SET target_count = ?, last_change = CURRENT_TIMESTAMP WHERE id = ?').run(requiredProfiles.length, postId);
      post.target_count = requiredProfiles.length;
      didChange = true;
    }

    let profileValue = sanitizeProfileNumber(profile_number) || getScopedProfileNumber(req.profileScope);
    if (!profileValue) {
      return res.status(400).json({ error: 'Profil muss zuerst ausgewählt werden.' });
    }

    const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(postId);
    const completedSet = buildCompletedProfileSet(completedRows);
    const shouldEnforceOrder = shouldEnforceProfileOrder({
      post,
      requiredProfiles,
      completedSet,
      ignoreOrder: !!ignore_order
    });

    const existingCheck = db.prepare(
      'SELECT id FROM checks WHERE post_id = ? AND profile_number = ?'
    ).get(postId, profileValue);

    if (existingCheck) {
      const existingPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
      return res.json(mapPostRow(existingPost));
    }

    // Allow creator to check immediately, regardless of profile number
    const isCreator = post.created_by_profile === profileValue;

    if (shouldEnforceOrder && requiredProfiles.length > 0 && !isCreator) {
      const prerequisiteProfiles = requiredProfiles.slice(0, requiredProfiles.indexOf(profileValue));
      if (prerequisiteProfiles.length) {
        const missingPrerequisites = prerequisiteProfiles.filter(num => !completedSet.has(num));
        if (missingPrerequisites.length) {
          return res.status(409).json({
            error: 'Vorherige Profile müssen zuerst bestätigen.',
            missing_profiles: missingPrerequisites
          });
        }
      }
    }

    const insertStmt = db.prepare('INSERT INTO checks (post_id, profile_number) VALUES (?, ?)');
    insertStmt.run(postId, profileValue);
    didChange = true;
    recalcCheckedCount(postId);
    if (didChange) {
      touchPost(postId, 'profile-status-update');
    }

    const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);

    res.json(mapPostRow(updatedPost));
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/posts/:postId/urls', (req, res) => {
  try {
    const { postId } = req.params;
    const { urls, skip_content_key_check, first_image_hash, first_image_url } = req.body || {};

    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    if (!post) {
      return res.status(404).json({ error: 'Post not found' });
    }

    const candidateList = Array.isArray(urls) ? urls : [];
    let alternateUrls = [];
    const skipContentKey = !!skip_content_key_check;
    if (skipContentKey) {
      const normalized = collectNormalizedFacebookUrls(post.url, candidateList);
      alternateUrls = normalized.filter(url => url !== post.url);
    } else {
      alternateUrls = collectPostAlternateUrls(post.url, candidateList);
    }
    storePostUrls(post.id, post.url, alternateUrls, { skipContentKeyCheck: skipContentKey });
    removeSearchSeenEntries([post.url, ...alternateUrls]);

    const normalizedImageHash = typeof first_image_hash === 'string'
      ? first_image_hash.trim().toLowerCase()
      : null;
    const cleanedImageHash = (normalizedImageHash && /^[0-9a-f]{16}$/.test(normalizedImageHash))
      ? normalizedImageHash
      : null;
    const normalizedImageUrl = typeof first_image_url === 'string'
      ? normalizeFacebookPostUrl(first_image_url) || first_image_url.trim()
      : null;

    if ((!post.first_image_hash && cleanedImageHash) || (!post.first_image_url && normalizedImageUrl)) {
      db.prepare(`
        UPDATE posts
        SET first_image_hash = COALESCE(first_image_hash, ?),
            first_image_url = COALESCE(first_image_url, ?),
            last_change = CURRENT_TIMESTAMP
        WHERE id = ?
      `).run(cleanedImageHash || null, normalizedImageUrl || null, post.id);
    }

    const storedAlternates = selectAlternateUrlsForPostStmt.all(post.id).map(row => row.url);
    if (alternateUrls.length) {
      touchPost(post.id, 'alternate-urls');
    }
    res.json({
      success: true,
      primary_url: post.url,
      alternate_urls: storedAlternates
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Check by URL (for web interface auto-check)
app.post('/api/check-by-url', (req, res) => {
  try {
    const { url, profile_number, ignore_order } = req.body || {};

    if (!url) {
      return res.status(400).json({ error: 'URL is required' });
    }

    const normalizedUrl = normalizeFacebookPostUrl(url);
    if (!normalizedUrl) {
      return res.status(400).json({ error: 'URL must be a valid Facebook link' });
    }

    const post = findPostByUrl(normalizedUrl);
    if (!post) {
      return res.status(404).json({ error: 'Post not found' });
    }

    const alternateUrls = collectPostAlternateUrls(post.url, [normalizedUrl]);
    storePostUrls(post.id, post.url, alternateUrls);
    removeSearchSeenEntries([post.url, ...alternateUrls]);

    // Check if deadline has passed
    if (post.deadline_at) {
      const deadline = new Date(post.deadline_at);
      if (new Date() > deadline) {
        return res.status(400).json({ error: 'Deadline ist abgelaufen' });
      }
    }

    const requiredProfiles = getRequiredProfiles(post.target_count);
    let didChange = false;
    if (post.target_count !== requiredProfiles.length) {
      db.prepare('UPDATE posts SET target_count = ?, last_change = CURRENT_TIMESTAMP WHERE id = ?').run(requiredProfiles.length, post.id);
      post.target_count = requiredProfiles.length;
      didChange = true;
    }

    let profileValue = sanitizeProfileNumber(profile_number) || getScopedProfileNumber(req.profileScope);
    if (!profileValue) {
      return res.status(400).json({ error: 'Profil muss zuerst ausgewählt werden.' });
    }

    const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(post.id);
    const completedSet = buildCompletedProfileSet(completedRows);
    const shouldEnforceOrder = shouldEnforceProfileOrder({
      post,
      requiredProfiles,
      completedSet,
      ignoreOrder: !!ignore_order
    });

    const existingCheck = db.prepare(
      'SELECT id FROM checks WHERE post_id = ? AND profile_number = ?'
    ).get(post.id, profileValue);

    if (existingCheck) {
      const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(post.id);
      return res.json(mapPostRow(updatedPost));
    }

    // Allow creator to check immediately, regardless of profile number
    const isCreator = post.created_by_profile === profileValue;

    if (shouldEnforceOrder && requiredProfiles.length > 0 && !isCreator) {
      const prerequisiteProfiles = requiredProfiles.slice(0, requiredProfiles.indexOf(profileValue));
      if (prerequisiteProfiles.length) {
        const missingPrerequisites = prerequisiteProfiles.filter(num => !completedSet.has(num));
        if (missingPrerequisites.length) {
          return res.status(409).json({
            error: 'Vorherige Profile müssen zuerst bestätigen.',
            missing_profiles: missingPrerequisites
          });
        }
      }
    }

    db.prepare('INSERT INTO checks (post_id, profile_number) VALUES (?, ?)').run(post.id, profileValue);
    didChange = true;
    recalcCheckedCount(post.id);
    if (didChange) {
      touchPost(post.id, 'check-by-url');
    }

    const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(post.id);

    res.json(mapPostRow(updatedPost));
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/posts/:postId/profile-status', (req, res) => {
  try {
    const { postId } = req.params;
    const { profile_number, status, ignore_order } = req.body || {};

    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    if (!post) {
      return res.status(404).json({ error: 'Post not found' });
    }

    // Check if deadline has passed (only for setting to 'done')
    if (status === 'done' && post.deadline_at) {
      const deadline = new Date(post.deadline_at);
      if (new Date() > deadline) {
        return res.status(400).json({ error: 'Deadline ist abgelaufen' });
      }
    }

    const requiredProfiles = getRequiredProfiles(post.target_count);
    let didChange = false;
    if (post.target_count !== requiredProfiles.length) {
      db.prepare('UPDATE posts SET target_count = ?, last_change = CURRENT_TIMESTAMP WHERE id = ?').run(requiredProfiles.length, post.id);
      post.target_count = requiredProfiles.length;
      didChange = true;
    }

    const profileValue = sanitizeProfileNumber(profile_number);
    if (!profileValue) {
      return res.status(400).json({ error: 'Valid profile_number required' });
    }

    if (!requiredProfiles.includes(profileValue)) {
      return res.status(409).json({ error: 'Dieses Profil ist für diesen Beitrag nicht erforderlich.' });
    }

    const normalizedStatus = status === 'done' ? 'done' : 'pending';

    if (normalizedStatus === 'done') {
      // Allow creator to check immediately, regardless of profile number
      const isCreator = post.created_by_profile === profileValue;

      const completedRows = db.prepare('SELECT profile_number FROM checks WHERE post_id = ?').all(postId);
      const completedSet = buildCompletedProfileSet(completedRows);
      const shouldEnforceOrder = shouldEnforceProfileOrder({
        post,
        requiredProfiles,
        completedSet,
        ignoreOrder: !!ignore_order
      });

      if (shouldEnforceOrder && !isCreator) {
        const prerequisiteProfiles = requiredProfiles.slice(0, requiredProfiles.indexOf(profileValue));
        if (prerequisiteProfiles.length) {
          const missingPrerequisites = prerequisiteProfiles.filter(num => !completedSet.has(num));
          if (missingPrerequisites.length) {
            return res.status(409).json({
              error: 'Vorherige Profile müssen zuerst bestätigen.',
              missing_profiles: missingPrerequisites
            });
          }
        }
      }

      const existingCheck = db.prepare(
        'SELECT id FROM checks WHERE post_id = ? AND profile_number = ?'
      ).get(postId, profileValue);

      if (!existingCheck) {
        db.prepare('INSERT INTO checks (post_id, profile_number) VALUES (?, ?)').run(postId, profileValue);
        didChange = true;
      }
    } else {
      db.prepare('DELETE FROM checks WHERE post_id = ? AND profile_number = ?').run(postId, profileValue);
      didChange = true;
    }

    recalcCheckedCount(postId);
    if (didChange) {
      touchPost(postId, 'profile-status-update');
    }

    const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    res.json(mapPostRow(updatedPost));
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Update post URL or success status
app.patch('/api/posts/:postId', (req, res) => {
  try {
    const { postId } = req.params;
    const { url, is_successful } = req.body;

    // Check if post exists
    const existingPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
    if (!existingPost) {
      return res.status(404).json({ error: 'Post not found' });
    }

    if (url !== undefined) {
      const normalizedUrl = normalizeFacebookPostUrl(url);
      if (!normalizedUrl) {
        return res.status(400).json({ error: 'Invalid Facebook URL' });
      }

      // Check for URL conflicts
      const conflict = db.prepare('SELECT id FROM posts WHERE url = ? AND id != ?').get(normalizedUrl, postId);
      if (conflict) {
        return res.status(409).json({ error: 'URL already used by another post' });
      }

      const contentKey = extractFacebookContentKey(normalizedUrl);
      // Update URL
      db.prepare('UPDATE posts SET url = ?, content_key = ?, last_change = CURRENT_TIMESTAMP WHERE id = ?').run(normalizedUrl, contentKey || null, postId);

      const alternateCandidates = [];
      if (existingPost.url && existingPost.url !== normalizedUrl) {
        alternateCandidates.push(existingPost.url);
      }

      const alternateUrls = collectPostAlternateUrls(normalizedUrl, alternateCandidates);
      storePostUrls(postId, normalizedUrl, alternateUrls);
      removeSearchSeenEntries([normalizedUrl, ...alternateUrls]);
      queuePostBroadcast(postId, { reason: 'url-updated' });
      return res.json({ success: true, url: normalizedUrl });
    }

    if (is_successful !== undefined) {
      const successValue = is_successful ? 1 : 0;
      db.prepare('UPDATE posts SET is_successful = ?, last_change = CURRENT_TIMESTAMP WHERE id = ?').run(successValue, postId);

      const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(postId);
      const formattedPost = mapPostRow(updatedPost);
      res.json(formattedPost);
      broadcastPostChange(formattedPost, { reason: 'success-flag' });
      return;
    }

    return res.status(400).json({ error: 'No valid update parameter provided' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/posts/merge', (req, res) => {
  const { primary_post_id, secondary_post_id, primary_url } = req.body || {};

  if (!primary_post_id || !secondary_post_id) {
    return res.status(400).json({ error: 'primary_post_id und secondary_post_id sind erforderlich' });
  }

  if (primary_post_id === secondary_post_id) {
    return res.status(400).json({ error: 'Die Post-IDs müssen unterschiedlich sein' });
  }

  try {
    const primaryPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(primary_post_id);
    const secondaryPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(secondary_post_id);

    if (!primaryPost || !secondaryPost) {
      return res.status(404).json({ error: 'Einer der Beiträge wurde nicht gefunden' });
    }

    const normalizedPrimaryUrl = primary_url
      ? normalizeFacebookPostUrl(primary_url)
      : normalizeFacebookPostUrl(primaryPost.url);

    if (!normalizedPrimaryUrl) {
      return res.status(400).json({ error: 'primary_url ist ungültig' });
    }

    const conflictId = findPostIdByUrl(normalizedPrimaryUrl);
    if (conflictId && conflictId !== primary_post_id && conflictId !== secondary_post_id) {
      return res.status(409).json({ error: 'Die gewählte Haupt-URL gehört bereits zu einem anderen Beitrag' });
    }

    const collectUrlsForPost = (post) => {
      const urls = [];
      if (post && post.url) {
        urls.push(post.url);
      }
      if (post && post.id) {
        const alternates = selectAlternateUrlsForPostStmt.all(post.id).map(row => row.url);
        urls.push(...alternates);
      }
      return urls;
    };

    const mergedUrlSet = new Set();
    for (const url of [...collectUrlsForPost(primaryPost), ...collectUrlsForPost(secondaryPost)]) {
      const normalized = normalizeFacebookPostUrl(url);
      if (normalized) {
        mergedUrlSet.add(normalized);
      }
    }
    mergedUrlSet.add(normalizedPrimaryUrl);

    const alternateUrls = Array.from(mergedUrlSet).filter(url => url !== normalizedPrimaryUrl);

    const mergedDeadline = (() => {
      if (primaryPost.deadline_at && secondaryPost.deadline_at) {
        return new Date(primaryPost.deadline_at) <= new Date(secondaryPost.deadline_at)
          ? primaryPost.deadline_at
          : secondaryPost.deadline_at;
      }
      return primaryPost.deadline_at || secondaryPost.deadline_at || null;
    })();

    const mergedTargetCount = Math.max(
      validateTargetCount(primaryPost.target_count) || 1,
      validateTargetCount(secondaryPost.target_count) || 1
    );

    const mergedPostText = (primaryPost.post_text && primaryPost.post_text.trim())
      ? primaryPost.post_text
      : (secondaryPost.post_text || null);
    const normalizedMergedPostText = mergedPostText ? normalizePostText(mergedPostText) : null;
    const mergedPostTextHash = normalizedMergedPostText
      ? computePostTextHash(normalizedMergedPostText)
      : null;

    const mergedCreatorName = (primaryPost.created_by_name && primaryPost.created_by_name.trim())
      ? primaryPost.created_by_name
      : (secondaryPost.created_by_name || null);
    const mergedCreatorProfile = primaryPost.created_by_profile || secondaryPost.created_by_profile || null;

    const mergedTitle = (primaryPost.title && primaryPost.title.trim())
      ? primaryPost.title
      : (secondaryPost.title || null);
    const mergedImageHash = primaryPost.first_image_hash || secondaryPost.first_image_hash || null;
    const mergedImageUrl = primaryPost.first_image_url || secondaryPost.first_image_url || null;

    const mergeTransaction = db.transaction(() => {
      // Move checks from secondary to primary (one per profile)
      const primaryChecks = selectChecksForPostStmt.all(primary_post_id);
      const secondaryChecks = selectChecksForPostStmt.all(secondary_post_id);

      const primaryByProfile = new Map();
      for (const check of primaryChecks) {
        if (!check || !check.profile_number) {
          continue;
        }
        const existing = primaryByProfile.get(check.profile_number);
        if (!existing) {
          primaryByProfile.set(check.profile_number, check);
        } else {
          if (new Date(check.checked_at) < new Date(existing.checked_at)) {
            primaryByProfile.set(check.profile_number, check);
          } else {
            deleteCheckByIdStmt.run(check.id);
          }
        }
      }

      for (const check of secondaryChecks) {
        if (!check || !check.profile_number) {
          continue;
        }
        const existing = primaryByProfile.get(check.profile_number);
        if (!existing) {
          updateCheckPostStmt.run(primary_post_id, check.id);
          primaryByProfile.set(check.profile_number, check);
        } else {
          const existingDate = new Date(existing.checked_at);
          const candidateDate = new Date(check.checked_at);
          if (candidateDate < existingDate) {
            updateCheckTimestampStmt.run(check.checked_at, existing.id);
          }
          deleteCheckByIdStmt.run(check.id);
        }
      }

      // Delete secondary post (post_urls are cascaded)
      db.prepare('DELETE FROM posts WHERE id = ?').run(secondary_post_id);

      const primaryContentKey = extractFacebookContentKey(normalizedPrimaryUrl);
      db.prepare(`
        UPDATE posts
        SET url = ?, content_key = ?, target_count = ?, created_by_name = ?, created_by_profile = ?,
            deadline_at = ?, title = ?, post_text = ?, post_text_hash = ?, first_image_hash = ?, first_image_url = ?, last_change = CURRENT_TIMESTAMP
        WHERE id = ?
      `).run(
        normalizedPrimaryUrl,
        primaryContentKey || null,
        mergedTargetCount,
        mergedCreatorName,
        mergedCreatorProfile,
        mergedDeadline,
        mergedTitle,
        normalizedMergedPostText,
        mergedPostTextHash,
        mergedImageHash,
        mergedImageUrl,
        primary_post_id
      );

      storePostUrls(primary_post_id, normalizedPrimaryUrl, alternateUrls, { skipContentKeyCheck: true });
      recalcCheckedCount(primary_post_id);

      const updated = db.prepare('SELECT * FROM posts WHERE id = ?').get(primary_post_id);
      return updated;
    });

    const merged = mergeTransaction();
    if (secondaryPost && secondaryPost.screenshot_path) {
      const secondaryFile = path.join(screenshotDir, secondaryPost.screenshot_path);
      if (fs.existsSync(secondaryFile)) {
        try {
          fs.unlinkSync(secondaryFile);
        } catch (cleanupError) {
          console.warn('Konnte Screenshot des zusammengeführten Beitrags nicht entfernen:', cleanupError.message);
        }
      }
    }
    const mapped = mapPostRow(merged);
    broadcastPostChange(mapped, { reason: 'merged' });
    broadcastPostDeletion(secondary_post_id, { reason: 'merged' });

    res.json(mapped);
  } catch (error) {
    console.error('Merge failed:', error);
    res.status(500).json({ error: 'Beiträge konnten nicht gemerged werden' });
  }
});

// Delete post
app.delete('/api/posts/:postId', (req, res) => {
  try {
    const { postId } = req.params;

    const post = db.prepare('SELECT screenshot_path FROM posts WHERE id = ?').get(postId);

    db.prepare('DELETE FROM checks WHERE post_id = ?').run(postId);
    const result = db.prepare('DELETE FROM posts WHERE id = ?').run(postId);

    if (result.changes === 0) {
      return res.status(404).json({ error: 'Post not found' });
    }

    if (post && post.screenshot_path) {
      const filePath = path.join(screenshotDir, post.screenshot_path);
      if (fs.existsSync(filePath)) {
        try {
          fs.unlinkSync(filePath);
        } catch (error) {
          console.warn('Failed to remove screenshot:', error.message);
        }
      }
    }

    res.json({ success: true });
    broadcastPostDeletion(postId, { reason: 'deleted' });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// AI Credentials endpoints
app.get('/api/ai-credentials', (req, res) => {
  try {
    reactivateExpiredCredentials();
    const credentials = getAllCredentialsFormatted();
    res.json(credentials);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/ai-credentials', (req, res) => {
  try {
    const { name, provider, api_key, model, base_url } = req.body;

    const trimmedName = typeof name === 'string' ? name.trim() : '';
    const trimmedProvider = typeof provider === 'string' ? provider.trim() : '';
    const trimmedApiKey = typeof api_key === 'string' ? api_key.trim() : '';
    const trimmedModel = typeof model === 'string' ? model.trim() : '';
    const rawBaseUrl = typeof base_url === 'string' ? base_url.trim() : '';
    const normalizedBaseUrl = (trimmedProvider === 'openai' && rawBaseUrl)
      ? rawBaseUrl.replace(/\/+$/, '')
      : '';

    if (!trimmedName || !trimmedProvider) {
      return res.status(400).json({ error: 'Name und Provider sind erforderlich' });
    }

    if (normalizedBaseUrl && !/^https?:\/\//i.test(normalizedBaseUrl)) {
      return res.status(400).json({ error: 'Basis-URL muss mit http:// oder https:// beginnen' });
    }

    let finalApiKey = trimmedApiKey;
    if (!finalApiKey) {
      if (trimmedProvider === 'openai' && normalizedBaseUrl) {
        finalApiKey = '';
      } else {
        return res.status(400).json({ error: 'API-Schlüssel wird benötigt' });
      }
    }

    const result = db.prepare(`
      INSERT INTO ai_credentials (name, provider, api_key, model, base_url)
      VALUES (?, ?, ?, ?, ?)
    `).run(
      trimmedName,
      trimmedProvider,
      finalApiKey,
      trimmedModel || null,
      normalizedBaseUrl || null
    );

    const credential = getFormattedCredentialById(result.lastInsertRowid);
    res.json(credential);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.put('/api/ai-credentials/:id', (req, res) => {
  try {
    const { id } = req.params;
    const { name, provider, api_key, model, base_url } = req.body;

    const credentialId = parseInt(id, 10);
    const existing = db.prepare('SELECT * FROM ai_credentials WHERE id = ?').get(credentialId);

    if (!existing) {
      return res.status(404).json({ error: 'Credential nicht gefunden' });
    }

    const trimmedName = typeof name === 'string' ? name.trim() : existing.name;
    const trimmedProvider = typeof provider === 'string' ? provider.trim() : existing.provider;
    if (!trimmedName || !trimmedProvider) {
      return res.status(400).json({ error: 'Name und Provider sind erforderlich' });
    }
    const trimmedModel = typeof model === 'string' ? model.trim() : existing.model || '';
    const rawBaseUrl = typeof base_url === 'string' ? base_url.trim() : (existing.base_url || '');
    const normalizedBaseUrl = (trimmedProvider === 'openai' && rawBaseUrl)
      ? rawBaseUrl.replace(/\/+$/, '')
      : '';

    let apiKeyProvided = api_key !== undefined;
    let trimmedApiKey = typeof api_key === 'string' ? api_key.trim() : '';

    if (!apiKeyProvided) {
      trimmedApiKey = existing.api_key;
    } else if (!trimmedApiKey && !(trimmedProvider === 'openai' && normalizedBaseUrl)) {
      return res.status(400).json({ error: 'API-Schlüssel wird benötigt' });
    }

    if (normalizedBaseUrl && !/^https?:\/\//i.test(normalizedBaseUrl)) {
      return res.status(400).json({ error: 'Basis-URL muss mit http:// oder https:// beginnen' });
    }

    if (trimmedProvider === 'openai' && !trimmedApiKey && !normalizedBaseUrl) {
      return res.status(400).json({ error: 'Für OpenAI wird ein API-Schlüssel benötigt, wenn keine Basis-URL angegeben ist' });
    }

    db.prepare(`
      UPDATE ai_credentials
      SET name = ?, provider = ?, api_key = ?, model = ?, base_url = ?, updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `).run(
      trimmedName,
      trimmedProvider,
      trimmedApiKey,
      trimmedModel || null,
      normalizedBaseUrl || null,
      credentialId
    );

    const credential = getFormattedCredentialById(credentialId);
    res.json(credential);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.patch('/api/ai-credentials/:id', (req, res) => {
  try {
    const { id } = req.params;
    const { is_active } = req.body;

    if (is_active === undefined) {
      return res.status(400).json({ error: 'is_active is required' });
    }

    const isActiveInt = is_active ? 1 : 0;

    db.prepare(`
      UPDATE ai_credentials
      SET is_active = ?,
          updated_at = CURRENT_TIMESTAMP,
          auto_disabled = CASE WHEN ? = 1 THEN 0 ELSE auto_disabled END,
          auto_disabled_reason = CASE WHEN ? = 1 THEN NULL ELSE auto_disabled_reason END,
          auto_disabled_until = CASE WHEN ? = 1 THEN NULL ELSE auto_disabled_until END
      WHERE id = ?
    `).run(isActiveInt, isActiveInt, isActiveInt, isActiveInt, id);

    const credential = getFormattedCredentialById(id);
    res.json(credential);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/ai-credentials/reorder', (req, res) => {
  try {
    const { order } = req.body; // Array of IDs in new order

    if (!Array.isArray(order)) {
      return res.status(400).json({ error: 'order must be an array' });
    }

    // Update priorities based on order
    order.forEach((id, index) => {
      db.prepare('UPDATE ai_credentials SET priority = ? WHERE id = ?').run(index, id);
    });

    const credentials = getAllCredentialsFormatted();
    res.json(credentials);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.delete('/api/ai-credentials/:id', (req, res) => {
  try {
    const { id } = req.params;

    // Check if this credential is active
    const settings = db.prepare('SELECT active_credential_id FROM ai_settings WHERE id = 1').get();
    if (settings && settings.active_credential_id === parseInt(id)) {
      return res.status(400).json({ error: 'Cannot delete active credential' });
    }

    db.prepare('DELETE FROM ai_credentials WHERE id = ?').run(id);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// AI Settings endpoints
app.get('/api/ai-settings', (req, res) => {
  try {
    let settings = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();

    if (!settings) {
      settings = {
        id: 1,
        active_credential_id: null,
        prompt_prefix: 'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n',
        enabled: 0,
        updated_at: null
      };
    }

    // Get active credential if set
    let activeCredential = null;
    if (settings.active_credential_id) {
      activeCredential = getFormattedCredentialById(settings.active_credential_id);
    }

    res.json({
      ...settings,
      active_credential: activeCredential
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.put('/api/ai-settings', (req, res) => {
  try {
    const { active_credential_id, prompt_prefix, enabled } = req.body;

    const existing = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();

    if (existing) {
      db.prepare(`
        UPDATE ai_settings
        SET active_credential_id = ?, prompt_prefix = ?, enabled = ?, updated_at = CURRENT_TIMESTAMP
        WHERE id = 1
      `).run(active_credential_id || null, prompt_prefix, enabled ? 1 : 0);
    } else {
      db.prepare(`
        INSERT INTO ai_settings (id, active_credential_id, prompt_prefix, enabled, updated_at)
        VALUES (1, ?, ?, ?, CURRENT_TIMESTAMP)
      `).run(active_credential_id || null, prompt_prefix, enabled ? 1 : 0);
    }

    const updated = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();

    let activeCredential = null;
    if (updated.active_credential_id) {
      activeCredential = getFormattedCredentialById(updated.active_credential_id);
    }

    res.json({
      ...updated,
      active_credential: activeCredential
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/ai/debug-traces', (req, res) => {
  try {
    const status = sanitizeTraceStatus(req.query.status, '') || null;
    const flowId = sanitizeAIDebugValue(req.query.flowId || req.query.flow_id, AI_DEBUG_FLOW_ID_MAX_LENGTH);
    const fromDate = ensureIsoDate(req.query.from || req.query.from_date);
    const toDate = ensureIsoDate(req.query.to || req.query.to_date);
    const limitRaw = parseInt(req.query.limit, 10);
    const offsetRaw = parseInt(req.query.offset, 10);
    const limit = Number.isFinite(limitRaw)
      ? Math.max(1, Math.min(AI_DEBUG_MAX_LIST_LIMIT, limitRaw))
      : AI_DEBUG_DEFAULT_LIST_LIMIT;
    const offset = Number.isFinite(offsetRaw)
      ? Math.max(0, offsetRaw)
      : 0;

    const rows = listAIDebugTracesStmt.all({
      status,
      flowId: flowId || null,
      fromDate: fromDate || null,
      toDate: toDate || null,
      limit,
      offset
    });

    res.json({
      items: rows.map(parseAIDebugTraceRow),
      limit,
      offset
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/ai/debug-traces/:traceId', (req, res) => {
  try {
    const traceId = sanitizeAIDebugValue(req.params.traceId, AI_DEBUG_TRACE_ID_MAX_LENGTH);
    if (!traceId) {
      return res.status(400).json({ error: 'traceId is required' });
    }

    const row = getAIDebugTraceByIdStmt.get({ traceId });
    if (!row) {
      return res.status(404).json({ error: 'Trace nicht gefunden' });
    }

    res.json(parseAIDebugTraceRow(row));
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/api/ai/debug-traces/frontend', (req, res) => {
  try {
    const payload = req.body || {};
    const traceId = sanitizeAIDebugValue(payload.traceId, AI_DEBUG_TRACE_ID_MAX_LENGTH);
    const flowId = sanitizeAIDebugValue(payload.flowId, AI_DEBUG_FLOW_ID_MAX_LENGTH);
    const source = sanitizeAIDebugValue(payload.source, 80) || 'extension';
    const status = sanitizeTraceStatus(payload.status, 'frontend_reported');

    const ensured = upsertAIDebugTraceBase({
      traceId,
      flowId,
      source,
      status,
      requestMeta: payload.requestMeta || null
    });

    const upserted = upsertAIDebugTraceFrontend({
      traceId: ensured.traceId,
      flowId: flowId || ensured.flowId,
      source,
      status,
      frontendTimings: payload.frontendTimings || null,
      frontendSteps: Array.isArray(payload.frontendSteps) ? payload.frontendSteps : null,
      frontendError: payload.frontendError || null,
      totalDurationMs: payload.totalDurationMs
    });

    const row = getAIDebugTraceByIdStmt.get({ traceId: upserted.traceId });
    res.json({
      success: true,
      trace: parseAIDebugTraceRow(row)
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/moderation-settings', (req, res) => {
  try {
    const settings = loadModerationSettings();
    res.json(settings);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.put('/api/moderation-settings', (req, res) => {
  try {
    const body = req.body || {};
    const saved = persistModerationSettings({
      enabled: !!body.sports_scoring_enabled,
      threshold: body.sports_score_threshold,
      weights: body.sports_score_weights,
      terms: body.sports_terms,
      autoHide: !!body.sports_auto_hide_enabled
    });
    res.json(saved);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/similarity-settings', (req, res) => {
  try {
    const settings = loadSimilaritySettings();
    res.json(settings);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.put('/api/similarity-settings', (req, res) => {
  try {
    const body = req.body || {};
    const saved = persistSimilaritySettings({
      textThreshold: body.text_threshold,
      imageDistanceThreshold: body.image_distance_threshold
    });
    res.json(saved);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/hidden-settings', (req, res) => {
  try {
    const settings = loadHiddenSettings();
    res.json({
      auto_purge_enabled: !!settings.auto_purge_hidden,
      retention_days: settings.search_retention_days
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.put('/api/hidden-settings', (req, res) => {
  try {
    const body = req.body || {};
    const retentionDays = normalizeRetentionDays(body.retention_days);
    const autoPurgeEnabled = !!body.auto_purge_enabled;
    const saved = persistHiddenSettings({ retentionDays, autoPurgeEnabled });
    if (saved.auto_purge_enabled) {
      cleanupExpiredSearchPosts();
    }
    res.json(saved);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

function sanitizeAIComment(text) {
  if (!text) {
    return '';
  }

  let cleaned = text
    .replace(/[\u200b-\u200f\u202a-\u202e\ufeff]/g, '') // strip zero-width/control spacing
    .replace(/\u00a0/g, ' '); // normalize NBSP

  // Strip leading label noise some models prepend (e.g. "**Kommentar**, **Inhalt**:")
  const markerPattern = /^(?:\s*(?:\*\*|__)?\s*(kommentar|inhalt|text|content)\s*(?:\*\*|__)?\s*[,;:.\-–—`'"]*\s*)+/i;
  cleaned = cleaned.replace(markerPattern, '');
  cleaned = cleaned.replace(/```+/g, '');
  cleaned = cleaned.replace(/\*\*[^*]+\*\*/g, '');
  cleaned = cleaned.replace(/`@([^`]+)`/g, '@$1');
  cleaned = cleaned.replace(/`([^`]+)`/g, '$1');

  const lines = cleaned.split(/\r?\n/);
  while (lines.length) {
    const line = lines[0].trim();
    if (!line) {
      lines.shift();
      continue;
    }
    const labelLine = /^(?:\*\*|__)?\s*(kommentar|inhalt|text|content)\s*(?:\*\*|__)?\s*[:\-–—]*\s*$/i;
    if (labelLine.test(line)) {
      lines.shift();
      continue;
    }
    break;
  }
  cleaned = lines.join('\n').replace(/\s*,\s*/g, ', ').replace(/,+\s*$/g, '');

  // Clean up AI output: drop hidden tags, replace dashes, normalize spacing.
  return cleaned
    .replace(/<think>[\s\S]*?<\/think>/gi, '')
    .replace(/[-–—]+/g, (match, offset, full) => {
      const prev = full[offset - 1];
      const next = full[offset + match.length];
      const prevIsWord = prev && /[A-Za-z0-9ÄÖÜäöüß]/.test(prev);
      const nextIsWord = next && /[A-Za-z0-9ÄÖÜäöüß]/.test(next);
      return prevIsWord && nextIsWord ? match : ', ';
    })
    .replace(/^[\s,;:.\-–—!?\u00a0"'`]+/, '')
    .replace(/,+$/g, '')
    .replace(/\s{2,}/g, ' ')
    .trim();
}

function shouldRetryAIComment(text) {
  if (!text || typeof text !== 'string') {
    return false;
  }
  const lower = text.toLowerCase();
  const hasCommentOrCharacter = lower.includes('comment') || lower.includes('character');
  const hasLengthOrCount = lower.includes('length') || lower.includes('count');
  return hasCommentOrCharacter && hasLengthOrCount;
}

async function tryGenerateComment(credential, promptPrefix, postText, options = {}) {
  const provider = credential.provider;
  const apiKey = credential.api_key;
  const model = credential.model;
  const traceId = sanitizeAIDebugValue(options.traceId, AI_DEBUG_TRACE_ID_MAX_LENGTH);
  const flowId = sanitizeAIDebugValue(options.flowId, AI_DEBUG_FLOW_ID_MAX_LENGTH);

  let lastResponse = null;
  const providerAttempts = [];

  try {
    for (let attempt = 1; attempt <= AI_COMMENT_RETRY_LIMIT; attempt += 1) {
      const attemptStartMs = Date.now();
      const attemptStartedAt = new Date(attemptStartMs).toISOString();
      let attemptRecorded = false;
      let modelName = model || null;
      let comment = '';

      const recordAttempt = (status, extra = {}) => {
        if (attemptRecorded) {
          return;
        }
        attemptRecorded = true;
        providerAttempts.push({
          attempt,
          provider,
          model: modelName,
          traceId: traceId || null,
          flowId: flowId || null,
          status,
          started_at: attemptStartedAt,
          duration_ms: roundTiming(Date.now() - attemptStartMs),
          ...extra
        });
      };

      try {
        if (provider === 'gemini') {
          modelName = model || 'gemini-2.0-flash-exp';
          const prompt = promptPrefix + postText;

          const response = await fetch(
            `https://generativelanguage.googleapis.com/v1beta/models/${modelName}:generateContent?key=${apiKey}`,
            {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                contents: [{
                  parts: [{ text: prompt }]
                }]
              })
            }
          );

          lastResponse = response;

          if (!response.ok) {
            let errorPayload = null;
            let message = response.statusText;
            try {
              errorPayload = await response.json();
              message = errorPayload?.error?.message || message;
            } catch (jsonError) {
              try {
                const textBody = await response.text();
                if (textBody) {
                  message = textBody;
                }
              } catch (textError) {
                // ignore
              }
            }

            const rateInfo = extractRateLimitInfo(response, provider);
            recordAttempt('http_error', {
              http_status: response.status,
              error: message,
              rate_limit_remaining: rateInfo.rateLimitRemaining ?? null,
              rate_limit_reset_at: rateInfo.rateLimitResetAt ?? null
            });

            const error = new Error(`Gemini API error: ${message}`);
            error.status = response.status;
            error.provider = provider;
            error.apiError = errorPayload;
            if (rateInfo.retryAfterSeconds !== undefined) {
              error.retryAfterSeconds = rateInfo.retryAfterSeconds;
            }
            if (rateInfo.rateLimitResetAt) {
              error.rateLimitResetAt = rateInfo.rateLimitResetAt;
            }
            if (rateInfo.rateLimitRemaining !== undefined) {
              error.rateLimitRemaining = rateInfo.rateLimitRemaining;
            }
            error.rateLimitHeaders = rateInfo.headers;
            throw error;
          }

          const data = await response.json();
          comment = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
        } else if (provider === 'openai') {
          modelName = model || 'gpt-3.5-turbo';
          const prompt = promptPrefix + postText;

          const baseUrl = (credential.base_url || 'https://api.openai.com/v1').trim().replace(/\/+$/, '');
          const endpoint = baseUrl.endsWith('/chat/completions') ? baseUrl : `${baseUrl}/chat/completions`;

          const headers = {
            'Content-Type': 'application/json'
          };
          if (apiKey) {
            headers.Authorization = `Bearer ${apiKey}`;
          }

          const response = await fetch(endpoint, {
            method: 'POST',
            headers,
            body: JSON.stringify({
              model: modelName,
              messages: [{ role: 'user', content: prompt }],
              max_tokens: 150
            })
          });

          lastResponse = response;

          if (!response.ok) {
            let errorPayload = null;
            let message = response.statusText;
            try {
              errorPayload = await response.json();
              message = errorPayload?.error?.message || message;
            } catch (jsonError) {
              try {
                const textBody = await response.text();
                if (textBody) {
                  message = textBody;
                }
              } catch (textError) {
                // ignore
              }
            }

            const rateInfo = extractRateLimitInfo(response, provider);
            recordAttempt('http_error', {
              http_status: response.status,
              error: message,
              rate_limit_remaining: rateInfo.rateLimitRemaining ?? null,
              rate_limit_reset_at: rateInfo.rateLimitResetAt ?? null
            });

            const error = new Error(`OpenAI API error: ${message}`);
            error.status = response.status;
            error.provider = provider;
            error.apiError = errorPayload;
            if (rateInfo.retryAfterSeconds !== undefined) {
              error.retryAfterSeconds = rateInfo.retryAfterSeconds;
            }
            if (rateInfo.rateLimitResetAt) {
              error.rateLimitResetAt = rateInfo.rateLimitResetAt;
            }
            if (rateInfo.rateLimitRemaining !== undefined) {
              error.rateLimitRemaining = rateInfo.rateLimitRemaining;
            }
            error.rateLimitHeaders = rateInfo.headers;
            throw error;
          }

          const data = await response.json();
          comment = data.choices?.[0]?.message?.content || '';
        } else if (provider === 'claude') {
          modelName = model || 'claude-3-5-haiku-20241022';
          const prompt = promptPrefix + postText;

          const response = await fetch('https://api.anthropic.com/v1/messages', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'x-api-key': apiKey,
              'anthropic-version': '2023-06-01'
            },
            body: JSON.stringify({
              model: modelName,
              max_tokens: 150,
              messages: [{ role: 'user', content: prompt }]
            })
          });

          lastResponse = response;

          if (!response.ok) {
            let errorPayload = null;
            let message = response.statusText;
            try {
              errorPayload = await response.json();
              message = errorPayload?.error?.message || message;
            } catch (jsonError) {
              try {
                const textBody = await response.text();
                if (textBody) {
                  message = textBody;
                }
              } catch (textError) {
                // ignore
              }
            }

            const rateInfo = extractRateLimitInfo(response, provider);
            recordAttempt('http_error', {
              http_status: response.status,
              error: message,
              rate_limit_remaining: rateInfo.rateLimitRemaining ?? null,
              rate_limit_reset_at: rateInfo.rateLimitResetAt ?? null
            });

            const error = new Error(`Claude API error: ${message}`);
            error.status = response.status;
            error.provider = provider;
            error.apiError = errorPayload;
            if (rateInfo.retryAfterSeconds !== undefined) {
              error.retryAfterSeconds = rateInfo.retryAfterSeconds;
            }
            if (rateInfo.rateLimitResetAt) {
              error.rateLimitResetAt = rateInfo.rateLimitResetAt;
            }
            if (rateInfo.rateLimitRemaining !== undefined) {
              error.rateLimitRemaining = rateInfo.rateLimitRemaining;
            }
            error.rateLimitHeaders = rateInfo.headers;
            throw error;
          }

          const data = await response.json();
          comment = data.content?.[0]?.text || '';
        } else {
          throw new Error(`Unsupported AI provider: ${provider}`);
        }

        if (shouldRetryAIComment(comment)) {
          recordAttempt('retry_filtered', {
            http_status: lastResponse ? lastResponse.status : null,
            comment_length: comment.length
          });
          if (attempt < AI_COMMENT_RETRY_LIMIT) {
            continue;
          }
          const error = new Error('AI response contains forbidden comment length/count metadata');
          error.provider = provider;
          throw error;
        }

        const rateInfo = extractRateLimitInfo(lastResponse, provider);
        rateInfo.status = lastResponse ? lastResponse.status : null;

        recordAttempt('success', {
          http_status: lastResponse ? lastResponse.status : null,
          comment_length: comment.length,
          rate_limit_remaining: rateInfo.rateLimitRemaining ?? null,
          rate_limit_reset_at: rateInfo.rateLimitResetAt ?? null
        });

        return {
          comment: sanitizeAIComment(comment),
          rateInfo,
          providerAttempts
        };
      } catch (error) {
        if (!attemptRecorded) {
          recordAttempt(error && error.name === 'AbortError' ? 'aborted' : 'error', {
            http_status: lastResponse ? lastResponse.status : null,
            error: error && error.message ? String(error.message) : 'Unbekannter Fehler'
          });
        }
        throw error;
      }
    }
  } catch (error) {
    if (error && !error.provider) {
      error.provider = provider;
    }
    if (error && !error.providerAttempts) {
      error.providerAttempts = providerAttempts;
    }
    throw error;
  }
}

app.post('/api/ai/generate-comment', async (req, res) => {
  const requestStartedMs = Date.now();
  const requestBody = req.body || {};
  const requestMeta = normalizeAIDebugRequestMeta(requestBody);
  const incomingTraceId = sanitizeAIDebugValue(req.headers['x-ai-trace-id'] || requestBody.traceId, AI_DEBUG_TRACE_ID_MAX_LENGTH);
  const incomingFlowId = sanitizeAIDebugValue(req.headers['x-ai-flow-id'] || requestBody.flowId, AI_DEBUG_FLOW_ID_MAX_LENGTH);
  const traceId = incomingTraceId || buildAIDebugTraceId();
  const flowId = incomingFlowId || traceId;
  const traceSource = sanitizeAIDebugValue(req.headers['x-ai-trace-source'] || requestBody.traceSource, 80) || 'extension-ai-button';
  const backendTimings = {};
  const credentialTimingDetails = [];

  upsertAIDebugTraceBase({
    traceId,
    flowId,
    source: traceSource,
    status: 'backend_in_progress',
    origin: req.headers.origin || null,
    requestIp: req.ip || req.socket?.remoteAddress || null,
    userAgent: req.headers['user-agent'] || null,
    requestMeta
  });

  const timingStart = () => Date.now();
  const timingEnd = (key, startedAt) => {
    const value = roundTiming(Date.now() - startedAt);
    backendTimings[key] = value;
    return value;
  };

  const applyTraceHeaders = () => {
    res.setHeader('x-ai-trace-id', traceId);
    if (flowId) {
      res.setHeader('x-ai-flow-id', flowId);
    }
  };

  const applyServerTimingHeader = () => {
    const header = buildServerTimingHeader([
      { name: 'settings', duration: backendTimings.loadSettingsMs },
      { name: 'reactivate', duration: backendTimings.reactivateCredentialsMs },
      { name: 'credentials', duration: backendTimings.loadCredentialsMs },
      { name: 'prompt', duration: backendTimings.buildPromptMs },
      { name: 'ai', duration: backendTimings.credentialLoopMs },
      { name: 'total', duration: backendTimings.totalMs }
    ]);
    if (header) {
      res.setHeader('Server-Timing', header);
    }
  };

  const respondWithTrackedError = (statusCode, message, options = {}) => {
    backendTimings.totalMs = roundTiming(Date.now() - requestStartedMs);

    updateAIDebugTraceBackend({
      traceId,
      flowId,
      source: traceSource,
      status: statusCode >= 500 ? 'backend_error' : 'backend_rejected',
      backendTimings,
      backendAttempts: credentialTimingDetails,
      backendError: message,
      responseMeta: {
        statusCode,
        ...options.responseMeta
      },
      totalDurationMs: backendTimings.totalMs
    });

    applyTraceHeaders();
    applyServerTimingHeader();

    const payload = {
      error: message,
      traceId,
      flowId,
      timings: {
        backend: backendTimings
      }
    };

    if (options.attempts) {
      payload.attempts = options.attempts;
    }

    return res.status(statusCode).json(payload);
  };

  try {
    const { postText, profileNumber, preferredCredentialId } = requestBody;

    if (!postText) {
      return respondWithTrackedError(400, 'postText is required');
    }

    const loadSettingsStartedMs = timingStart();
    const settings = db.prepare('SELECT * FROM ai_settings WHERE id = 1').get();
    timingEnd('loadSettingsMs', loadSettingsStartedMs);

    if (!settings || !settings.enabled) {
      return respondWithTrackedError(400, 'AI comment generation is not enabled');
    }

    const reactivateStartedMs = timingStart();
    reactivateExpiredCredentials();
    timingEnd('reactivateCredentialsMs', reactivateStartedMs);

    const loadCredentialsStartedMs = timingStart();
    const credentials = db.prepare(`
      SELECT *
      FROM ai_credentials
      WHERE is_active = 1
        AND COALESCE(auto_disabled, 0) = 0
      ORDER BY priority ASC, id ASC
    `).all();
    timingEnd('loadCredentialsMs', loadCredentialsStartedMs);

    if (!credentials || credentials.length === 0) {
      return respondWithTrackedError(400, 'No active AI credentials available');
    }

    let orderedCredentials = credentials;
    if (typeof preferredCredentialId !== 'undefined' && preferredCredentialId !== null) {
      const parsedPreferredId = Number(preferredCredentialId);
      if (!Number.isNaN(parsedPreferredId)) {
        const idx = credentials.findIndex(credential => credential.id === parsedPreferredId);
        if (idx > 0) {
          const preferred = credentials[idx];
          orderedCredentials = [preferred, ...credentials.slice(0, idx), ...credentials.slice(idx + 1)];
        }
      }
    }

    const promptBuildStartedMs = timingStart();
    let promptPrefix = settings.prompt_prefix || '';
    const normalizedProfileNumber = sanitizeProfileNumber(profileNumber);

    if (normalizedProfileNumber) {
      const friends = db.prepare('SELECT friend_names FROM profile_friends WHERE profile_number = ?').get(normalizedProfileNumber);
      if (friends && friends.friend_names) {
        promptPrefix = promptPrefix.replace('{FREUNDE}', friends.friend_names);
      } else {
        promptPrefix = promptPrefix.replace('{FREUNDE}', '');
      }
    } else {
      promptPrefix = promptPrefix.replace('{FREUNDE}', '');
    }

    const today = new Date();
    const todayDisplay = today.toLocaleDateString('de-DE');
    promptPrefix = promptPrefix.replaceAll('{DATUM}', todayDisplay);
    promptPrefix = applyRandomNumberTemplates(promptPrefix);
    promptPrefix = applyProfileVariantTemplates(promptPrefix, normalizedProfileNumber);
    timingEnd('buildPromptMs', promptBuildStartedMs);

    let lastError = null;
    const attemptDetails = [];
    const credentialLoopStartedMs = timingStart();

    for (const credential of orderedCredentials) {
      const credentialStartedMs = Date.now();
      try {
        console.log(`Trying credential: ${credential.name} (ID: ${credential.id})`);
        const { comment, rateInfo, providerAttempts } = await tryGenerateComment(
          credential,
          promptPrefix,
          postText,
          { traceId, flowId }
        );
        const credentialDurationMs = roundTiming(Date.now() - credentialStartedMs);

        console.log(`Success with credential: ${credential.name}`);
        updateCredentialUsageOnSuccess(credential.id, rateInfo || {});

        credentialTimingDetails.push({
          credentialId: credential.id,
          credentialName: credential.name,
          provider: credential.provider,
          model: credential.model || null,
          status: 'success',
          duration_ms: credentialDurationMs,
          provider_attempts: providerAttempts || []
        });

        attemptDetails.push({
          credentialId: credential.id,
          credentialName: credential.name,
          status: 'success',
          durationMs: credentialDurationMs,
          rateLimitRemaining: rateInfo?.rateLimitRemaining ?? null,
          rateLimitResetAt: rateInfo?.rateLimitResetAt ?? null
        });

        backendTimings.credentialLoopMs = roundTiming(Date.now() - credentialLoopStartedMs);
        backendTimings.totalMs = roundTiming(Date.now() - requestStartedMs);

        updateAIDebugTraceBackend({
          traceId,
          flowId,
          source: traceSource,
          status: 'backend_success',
          backendTimings,
          backendAttempts: credentialTimingDetails,
          responseMeta: {
            usedCredential: credential.name,
            usedCredentialId: credential.id,
            attempts: attemptDetails
          },
          totalDurationMs: backendTimings.totalMs
        });

        applyTraceHeaders();
        applyServerTimingHeader();

        return res.json({
          comment,
          usedCredential: credential.name,
          usedCredentialId: credential.id,
          attempts: attemptDetails,
          rateLimitInfo: rateInfo || null,
          traceId,
          flowId,
          timings: {
            backend: backendTimings,
            credentials: credentialTimingDetails
          }
        });
      } catch (error) {
        const credentialDurationMs = roundTiming(Date.now() - credentialStartedMs);
        console.error(`Failed with credential ${credential.name}:`, error.message);
        lastError = error;
        const errorUpdate = updateCredentialUsageOnError(credential.id, error);
        credentialTimingDetails.push({
          credentialId: credential.id,
          credentialName: credential.name,
          provider: credential.provider,
          model: credential.model || null,
          status: 'error',
          duration_ms: credentialDurationMs,
          error: error.message,
          status_code: error.status || error.statusCode || null,
          provider_attempts: error.providerAttempts || []
        });

        attemptDetails.push({
          credentialId: credential.id,
          credentialName: credential.name,
          status: 'error',
          durationMs: credentialDurationMs,
          message: error.message,
          statusCode: error.status || error.statusCode || null,
          autoDisabled: Boolean(errorUpdate.autoDisabled),
          autoDisabledUntil: errorUpdate.autoDisabledUntil || null
        });
      }
    }

    backendTimings.credentialLoopMs = roundTiming(Date.now() - credentialLoopStartedMs);

    const finalError = lastError || new Error('All AI credentials failed');
    finalError.attempts = attemptDetails;
    throw finalError;
  } catch (error) {
    console.error('AI comment generation error:', error);
    return respondWithTrackedError(
      500,
      error && error.message ? error.message : 'Unbekannter Fehler',
      {
        attempts: error && error.attempts ? error.attempts : null,
        responseMeta: {
          attempts: error && error.attempts ? error.attempts : null
        }
      }
    );
  }
});

// ============================================================================
// PROFILE FRIENDS API
// ============================================================================

// Get friends for a profile
app.get('/api/profile-friends/:profileNumber', (req, res) => {
  try {
    const profileNumber = parseInt(req.params.profileNumber);
    const friends = db.prepare('SELECT * FROM profile_friends WHERE profile_number = ?').get(profileNumber);

    if (!friends) {
      return res.json({ profile_number: profileNumber, friend_names: '' });
    }

    res.json(friends);
  } catch (error) {
    console.error('Error fetching profile friends:', error);
    res.status(500).json({ error: error.message });
  }
});

// Update friends for a profile
app.put('/api/profile-friends/:profileNumber', (req, res) => {
  try {
    const profileNumber = parseInt(req.params.profileNumber);
    const { friend_names } = req.body;

    if (friend_names === undefined) {
      return res.status(400).json({ error: 'friend_names is required' });
    }

    const existing = db.prepare('SELECT * FROM profile_friends WHERE profile_number = ?').get(profileNumber);

    if (existing) {
      db.prepare('UPDATE profile_friends SET friend_names = ?, updated_at = CURRENT_TIMESTAMP WHERE profile_number = ?')
        .run(friend_names, profileNumber);
    } else {
      db.prepare('INSERT INTO profile_friends (profile_number, friend_names) VALUES (?, ?)')
        .run(profileNumber, friend_names);
    }

    const updated = db.prepare('SELECT * FROM profile_friends WHERE profile_number = ?').get(profileNumber);
    res.json(updated);
  } catch (error) {
    console.error('Error updating profile friends:', error);
    res.status(500).json({ error: error.message });
  }
});

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

startAutomationWorker();

function logRuntimeInfo() {
  let osPretty = '';
  try {
    const raw = fs.readFileSync('/etc/os-release', 'utf8');
    const match = raw.match(/^PRETTY_NAME="?(.*?)"?$/m);
    if (match && match[1]) {
      osPretty = match[1];
    }
  } catch (error) {
    // ignore
  }
  const osInfo = osPretty || `${os.platform()} ${os.release()}`;
  console.log(`Runtime: Node ${process.version}, OS ${osInfo}`);
}

app.listen(PORT, '0.0.0.0', () => {
  logRuntimeInfo();
  console.log(`Server running on port ${PORT}`);
});
