const cron = require('node-cron');
const fs = require('fs');
const path = require('path');
const foodsharingClient = require('./foodsharingClient');
const sessionStore = require('./sessionStore');
const { DEFAULT_SETTINGS } = require('./adminConfig');
const notificationService = require('./notificationService');
const { readConfig, writeConfig } = require('./configStore');
const { readStoreWatch, writeStoreWatch } = require('./storeWatchStore');
const { readJournal, writeJournal } = require('./journalStore');
const { getStoreStatus, setStoreStatus, persistStoreStatusCache } = require('./storeStatusCache');
const { sendDormantPickupWarning, sendJournalReminderNotification } = require('./notificationService');
const { ensureSession, withSessionRetry } = require('./sessionRefresh');
const maintenanceMode = require('./maintenanceMode');
const { computeReminderSchedule } = require('./journalReminderUtils');
const { startStoreMessageWatcher } = require('./storeMessageWatcher');
const { getBlockingProfilePickup } = require('./profileCheckTolerance');
const { getStaleLastPickupStoreIds, parseLastFetchTimestamp, updateLastPickupInfo } = require('./lastPickupInfo');

function wait(ms) {
  if (!ms || ms <= 0) {
    return Promise.resolve();
  }
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function shouldPauseForMaintenance(scope) {
  if (!maintenanceMode.isActive()) {
    return false;
  }
  console.warn(`[MAINTENANCE] ${scope} übersprungen, weil Wartungsmodus aktiv ist.`);
  return true;
}

const DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES = 120;
const storeWatchInFlight = new Map();
const pickupCheckInFlight = new Map();
const pickupCheckLastRun = new Map();
const PICKUP_CHECK_DEDUP_MS = 30 * 1000;
const regularPickupCache = new Map();
const REGULAR_PICKUP_CACHE_FILE = path.join(__dirname, '..', 'config', 'regular-pickup-cache.json');
const REGULAR_PICKUP_CACHE_WRITE_DEBOUNCE_MS = 2000;
let regularPickupCacheWriteTimer = null;
let regularPickupCacheDirty = false;
const REGULAR_PICKUP_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
const REGULAR_PICKUP_ERROR_CACHE_TTL_MS = 5 * 60 * 1000;
const REGULAR_PICKUP_TRANSIENT_ERROR_TTL_MS = 30 * 1000;
const REGULAR_PICKUP_MAX_CONCURRENT = 3;
const REGULAR_PICKUP_MIN_DELAY_MS = 150;
const REGULAR_PICKUP_MAX_DELAY_MS = 350;
const regularPickupInFlight = new Map();
const regularPickupQueue = [];
let regularPickupActive = 0;
let regularPickupRefreshJob = null;
const dormantWarningCooldowns = new Map();
const DORMANT_WARNING_COOLDOWN_MS = 6 * 60 * 60 * 1000;
const lastPickupRefreshInFlight = new Map();

function ensureRegularPickupCacheDir() {
  const dir = path.dirname(REGULAR_PICKUP_CACHE_FILE);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
}

function persistRegularPickupCache() {
  if (!regularPickupCacheDirty) {
    return;
  }
  try {
    ensureRegularPickupCacheDir();
    const now = Date.now();
    const entries = {};
    regularPickupCache.forEach((value, key) => {
      if (value?.expiresAt && value.expiresAt < now) {
        return;
      }
      entries[key] = value;
    });
    fs.writeFileSync(
      REGULAR_PICKUP_CACHE_FILE,
      JSON.stringify({ version: 1, entries }, null, 2)
    );
    regularPickupCacheDirty = false;
  } catch (error) {
    console.warn('[PICKUP] Regular-Pickup-Cache konnte nicht geschrieben werden:', error.message);
  }
}

function scheduleRegularPickupCachePersist() {
  if (regularPickupCacheWriteTimer) {
    return;
  }
  regularPickupCacheWriteTimer = setTimeout(() => {
    regularPickupCacheWriteTimer = null;
    persistRegularPickupCache();
  }, REGULAR_PICKUP_CACHE_WRITE_DEBOUNCE_MS);
}

function markRegularPickupCacheDirty() {
  regularPickupCacheDirty = true;
  scheduleRegularPickupCachePersist();
}

function loadRegularPickupCacheFromDisk() {
  try {
    if (!fs.existsSync(REGULAR_PICKUP_CACHE_FILE)) {
      return;
    }
    const raw = fs.readFileSync(REGULAR_PICKUP_CACHE_FILE, 'utf8');
    const parsed = JSON.parse(raw);
    const entries = parsed?.entries && typeof parsed.entries === 'object' ? parsed.entries : {};
    const now = Date.now();
    Object.entries(entries).forEach(([key, value]) => {
      if (!value || typeof value !== 'object') {
        return;
      }
      if (value.expiresAt && value.expiresAt < now) {
        return;
      }
      regularPickupCache.set(String(key), {
        fetchedAt: Number(value.fetchedAt) || Date.now(),
        expiresAt: Number(value.expiresAt) || null,
        rules: Array.isArray(value.rules) ? value.rules : [],
        error: value.error || null
      });
    });
  } catch (error) {
    console.warn('[PICKUP] Regular-Pickup-Cache konnte nicht geladen werden:', error.message);
  }
}
const PICKUP_FALLBACK_RETRY_MS = 60 * 60 * 1000;
const TIME_ZONE = 'Europe/Berlin';

function hasDormantMemberAccess(store) {
  if (!store) {
    return false;
  }
  if (store.isManaging) {
    return true;
  }
  const membershipStatus = Number(store.membershipStatus);
  if (!Number.isFinite(membershipStatus)) {
    return true;
  }
  return membershipStatus === 1;
}

function resolveStoreTeamStatus(details) {
  const status = Number(details?.teamStatus ?? details?.teamSearchStatus);
  return Number.isFinite(status) ? status : null;
}

function isStoreStatusAccessError(error) {
  const status = error?.response?.status;
  return status === 403 || status === 404;
}

async function fetchSharedStoreStatus(session, storeId, { forceRefresh = false, maxAgeMs } = {}) {
  if (!storeId) {
    return { status: null, fetchedAt: null, fromCache: false };
  }
  const cacheEntry = getStoreStatus(storeId);
  const cachedStatus = cacheEntry?.teamSearchStatus;
  const hasCachedStatus = cacheEntry != null;
  const cachedAt = Number(cacheEntry?.fetchedAt) || 0;
  const effectiveMaxAge =
    Number.isFinite(maxAgeMs) && maxAgeMs >= 0
      ? maxAgeMs
      : DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES * 60 * 1000;
  const cacheFresh = hasCachedStatus && Date.now() - cachedAt <= effectiveMaxAge;
  if (cacheFresh && !forceRefresh) {
    return { status: cachedStatus, fetchedAt: cachedAt, fromCache: true };
  }

  const key = String(storeId);
  if (!forceRefresh && storeWatchInFlight.has(key)) {
    return storeWatchInFlight.get(key);
  }

  const fetchPromise = (async () => {
    const fetchedAt = Date.now();
    try {
      const marker = await withSessionRetry(
        session,
        () => foodsharingClient.fetchStoreMarker(storeId, session.cookieHeader, session),
        { label: 'fetchStoreMarker' }
      );
      const status = resolveStoreTeamStatus(marker);
      setStoreStatus(storeId, { teamSearchStatus: status, fetchedAt });
      return { status, fetchedAt, fromCache: false };
    } catch (error) {
      if (!isStoreStatusAccessError(error)) {
        throw error;
      }
      setStoreStatus(storeId, { teamSearchStatus: null, fetchedAt });
      return { status: null, fetchedAt, fromCache: false };
    }
  })();

  storeWatchInFlight.set(key, fetchPromise);
  try {
    return await fetchPromise;
  } finally {
    if (storeWatchInFlight.get(key) === fetchPromise) {
      storeWatchInFlight.delete(key);
    }
  }
}

const weekdayMap = {
  Montag: 'Monday',
  Dienstag: 'Tuesday',
  Mittwoch: 'Wednesday',
  Donnerstag: 'Thursday',
  Freitag: 'Friday',
  Samstag: 'Saturday',
  Sonntag: 'Sunday'
};

function randomDelayMs(minSeconds = 10, maxSeconds = 120) {
  const min = minSeconds * 1000;
  const max = maxSeconds * 1000;
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function randomDelayMsBetween(minMs, maxMs) {
  const min = Math.max(0, Number(minMs) || 0);
  const max = Math.max(min, Number(maxMs) || 0);
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

function getTimeZoneParts(date, timeZone) {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    weekday: 'short',
    hour12: false
  });
  const parts = formatter.formatToParts(date);
  const values = {};
  parts.forEach((part) => {
    if (part.type !== 'literal') {
      values[part.type] = part.value;
    }
  });
  const weekdayMap = {
    Sun: 0,
    Mon: 1,
    Tue: 2,
    Wed: 3,
    Thu: 4,
    Fri: 5,
    Sat: 6
  };
  return {
    year: Number(values.year),
    month: Number(values.month),
    day: Number(values.day),
    hour: Number(values.hour),
    minute: Number(values.minute),
    second: Number(values.second),
    weekday: weekdayMap[values.weekday] ?? null
  };
}

function getTimeZoneOffsetMinutes(timeZone, date) {
  const parts = getTimeZoneParts(date, timeZone);
  const utcFromParts = Date.UTC(
    parts.year,
    parts.month - 1,
    parts.day,
    parts.hour,
    parts.minute,
    parts.second
  );
  return (utcFromParts - date.getTime()) / 60000;
}

function makeDateInTimeZone(timeZone, year, month, day, hour, minute, second = 0) {
  const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, second));
  const offset = getTimeZoneOffsetMinutes(timeZone, utcGuess);
  return new Date(utcGuess.getTime() - offset * 60000);
}

function getStartOfDayInTimeZone(date, timeZone) {
  const parts = getTimeZoneParts(date, timeZone);
  if (!parts.year || !parts.month || !parts.day) {
    return null;
  }
  return makeDateInTimeZone(timeZone, parts.year, parts.month, parts.day, 0, 0, 0);
}

function normalizeRegularPickupWeekday(value) {
  const num = Number(value);
  if (!Number.isFinite(num)) {
    return null;
  }
  if (num === 0) {
    return 0;
  }
  if (num >= 1 && num <= 7) {
    return num % 7;
  }
  return null;
}

function parseTimeString(value) {
  if (!value || typeof value !== 'string') {
    return null;
  }
  const [hourText, minuteText, secondText] = value.split(':');
  const hour = Number(hourText);
  const minute = Number(minuteText);
  const second = Number(secondText || 0);
  if (![hour, minute, second].every((entry) => Number.isFinite(entry))) {
    return null;
  }
  return { hour, minute, second };
}

function addDays(date, days) {
  return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
}

function startOfDay(date) {
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}

function isSameDay(a, b) {
  return (
    a &&
    b &&
    a.getFullYear() === b.getFullYear() &&
    a.getMonth() === b.getMonth() &&
    a.getDate() === b.getDate()
  );
}

function addMonths(date, months) {
  const copy = new Date(date.getTime());
  copy.setMonth(copy.getMonth() + months);
  return copy;
}

function getIntervalMonths(interval) {
  if (interval === 'monthly') {
    return 1;
  }
  if (interval === 'quarterly') {
    return 3;
  }
  return 12;
}

function getNextOccurrence(baseDate, intervalMonths, todayStart) {
  if (!baseDate || Number.isNaN(baseDate.getTime())) {
    return null;
  }
  let candidate = startOfDay(baseDate);
  const guardYear = todayStart.getFullYear() + 200;
  while (candidate < todayStart && candidate.getFullYear() < guardYear) {
    candidate = startOfDay(addMonths(candidate, intervalMonths));
  }
  return candidate;
}

function resolveSettings(settings) {
  if (!settings) {
    return { ...DEFAULT_SETTINGS };
  }
  return {
    scheduleCron: settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
    pickupFallbackCron: settings.pickupFallbackCron || DEFAULT_SETTINGS.pickupFallbackCron,
    pickupWindowOffsetsMinutes:
      Array.isArray(settings.pickupWindowOffsetsMinutes) && settings.pickupWindowOffsetsMinutes.length > 0
        ? settings.pickupWindowOffsetsMinutes
        : DEFAULT_SETTINGS.pickupWindowOffsetsMinutes,
    randomDelayMinSeconds: Number.isFinite(settings.randomDelayMinSeconds)
      ? settings.randomDelayMinSeconds
      : DEFAULT_SETTINGS.randomDelayMinSeconds,
    randomDelayMaxSeconds: Number.isFinite(settings.randomDelayMaxSeconds)
      ? settings.randomDelayMaxSeconds
      : DEFAULT_SETTINGS.randomDelayMaxSeconds,
    initialDelayMinSeconds: Number.isFinite(settings.initialDelayMinSeconds)
      ? settings.initialDelayMinSeconds
      : DEFAULT_SETTINGS.initialDelayMinSeconds,
    initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
      ? settings.initialDelayMaxSeconds
      : DEFAULT_SETTINGS.initialDelayMaxSeconds,
    dormantMembershipCron: settings.dormantMembershipCron || DEFAULT_SETTINGS.dormantMembershipCron,
    storeWatchCron: settings.storeWatchCron || DEFAULT_SETTINGS.storeWatchCron,
    storeWatchInitialDelayMinSeconds: Number.isFinite(settings.storeWatchInitialDelayMinSeconds)
      ? settings.storeWatchInitialDelayMinSeconds
      : DEFAULT_SETTINGS.storeWatchInitialDelayMinSeconds,
    storeWatchInitialDelayMaxSeconds: Number.isFinite(settings.storeWatchInitialDelayMaxSeconds)
      ? settings.storeWatchInitialDelayMaxSeconds
      : DEFAULT_SETTINGS.storeWatchInitialDelayMaxSeconds,
    storeWatchRequestDelayMs: Number.isFinite(settings.storeWatchRequestDelayMs)
      ? settings.storeWatchRequestDelayMs
      : DEFAULT_SETTINGS.storeWatchRequestDelayMs,
    storeWatchStatusCacheMaxAgeMinutes: Number.isFinite(settings.storeWatchStatusCacheMaxAgeMinutes)
      ? settings.storeWatchStatusCacheMaxAgeMinutes
      : DEFAULT_SETTINGS.storeWatchStatusCacheMaxAgeMinutes,
    messageWatcherSocketEnabled:
      settings.messageWatcherSocketEnabled === undefined
        ? DEFAULT_SETTINGS.messageWatcherSocketEnabled
        : !!settings.messageWatcherSocketEnabled,
    messageWatcherFallbackEnabled:
      settings.messageWatcherFallbackEnabled === undefined
        ? DEFAULT_SETTINGS.messageWatcherFallbackEnabled
        : !!settings.messageWatcherFallbackEnabled,
    messageWatcherFallbackCron:
      settings.messageWatcherFallbackCron || DEFAULT_SETTINGS.messageWatcherFallbackCron,
    messageWatcherDebugLogging:
      settings.messageWatcherDebugLogging === undefined
        ? DEFAULT_SETTINGS.messageWatcherDebugLogging
        : !!settings.messageWatcherDebugLogging,
    ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots,
    notifications: {
      ntfy: {
        enabled: !!settings.notifications?.ntfy?.enabled,
        serverUrl: settings.notifications?.ntfy?.serverUrl || DEFAULT_SETTINGS.notifications.ntfy.serverUrl,
        topicPrefix: settings.notifications?.ntfy?.topicPrefix || DEFAULT_SETTINGS.notifications.ntfy.topicPrefix,
        username: settings.notifications?.ntfy?.username || DEFAULT_SETTINGS.notifications.ntfy.username,
        password: settings.notifications?.ntfy?.password || DEFAULT_SETTINGS.notifications.ntfy.password
      },
      telegram: {
        enabled: !!settings.notifications?.telegram?.enabled,
        botToken: settings.notifications?.telegram?.botToken || DEFAULT_SETTINGS.notifications.telegram.botToken
      }
    }
  };
}

function describeRegularPickupError(error) {
  const status = error?.response?.status;
  if (status) {
    return `HTTP ${status}`;
  }
  const code = error?.code;
  if (code) {
    return code;
  }
  const message = error?.message;
  if (message) {
    return message;
  }
  return 'Unknown error';
}

function getRegularPickupErrorTtlMs(error) {
  const status = error?.response?.status;
  if (status === 403 || status === 404) {
    return REGULAR_PICKUP_ERROR_CACHE_TTL_MS;
  }
  const code = error?.code;
  if (code) {
    return REGULAR_PICKUP_TRANSIENT_ERROR_TTL_MS;
  }
  return REGULAR_PICKUP_ERROR_CACHE_TTL_MS;
}

function runWithRegularPickupLimiter(task) {
  if (regularPickupActive < REGULAR_PICKUP_MAX_CONCURRENT) {
    regularPickupActive += 1;
    return Promise.resolve()
      .then(async () => {
        const delayMs = randomDelayMsBetween(REGULAR_PICKUP_MIN_DELAY_MS, REGULAR_PICKUP_MAX_DELAY_MS);
        if (delayMs > 0) {
          await wait(delayMs);
        }
        return task();
      })
      .finally(() => {
        regularPickupActive -= 1;
        const next = regularPickupQueue.shift();
        if (next) {
          next();
        }
      });
  }

  return new Promise((resolve, reject) => {
    regularPickupQueue.push(() => {
      runWithRegularPickupLimiter(task).then(resolve).catch(reject);
    });
  });
}

function getRegularPickupCacheEntry(storeId) {
  const entry = regularPickupCache.get(String(storeId));
  if (!entry) {
    return null;
  }
  if (entry.expiresAt && entry.expiresAt < Date.now()) {
    regularPickupCache.delete(String(storeId));
    markRegularPickupCacheDirty();
    return null;
  }
  return entry;
}

function setRegularPickupCacheEntry(storeId, rules, ttlMs, error) {
  regularPickupCache.set(String(storeId), {
    fetchedAt: Date.now(),
    expiresAt: Date.now() + ttlMs,
    rules: Array.isArray(rules) ? rules : [],
    error: error || null
  });
  markRegularPickupCacheDirty();
}

async function getRegularPickupSchedule(session, storeId) {
  if (!session?.profile?.id || !storeId) {
    return { rules: [], error: 'missing-session-or-store', fromCache: false };
  }
  if (shouldPauseForMaintenance(`Regular-Pickup-Refresh für Store ${storeId}`)) {
    return { rules: [], error: 'maintenance-mode', fromCache: false };
  }
  const key = String(storeId);
  const cached = getRegularPickupCacheEntry(key);
  if (cached) {
    return { rules: cached.rules, error: cached.error, fromCache: true };
  }
  const existing = regularPickupInFlight.get(key);
  if (existing) {
    return existing;
  }
  const fetchPromise = runWithRegularPickupLimiter(async () => {
    try {
      const rules = await withSessionRetry(
        session,
        () => foodsharingClient.fetchRegularPickup(storeId, session.cookieHeader, session),
        { label: 'fetchRegularPickup' }
      );
      const normalized = Array.isArray(rules) ? rules : [];
      setRegularPickupCacheEntry(key, normalized, REGULAR_PICKUP_CACHE_TTL_MS);
      return { rules: normalized, error: null, fromCache: false };
    } catch (error) {
      const message = describeRegularPickupError(error);
      const ttl = getRegularPickupErrorTtlMs(error);
      setRegularPickupCacheEntry(key, [], ttl, message);
      return { rules: [], error: message, fromCache: false };
    }
  });
  regularPickupInFlight.set(key, fetchPromise);
  try {
    return await fetchPromise;
  } finally {
    if (regularPickupInFlight.get(key) === fetchPromise) {
      regularPickupInFlight.delete(key);
    }
  }
}

async function getNextPickupCheckTime(session, entry, settings) {
  const now = new Date();
  const offsets = (settings.pickupWindowOffsetsMinutes || [])
    .map((value) => Number(value))
    .filter((value) => Number.isFinite(value))
    .map((value) => value * 60 * 1000)
    .sort((a, b) => a - b);

  if (offsets.length === 0) {
    return null;
  }

  const { rules } = await getRegularPickupSchedule(session, entry.id);
  if (!Array.isArray(rules) || rules.length === 0) {
    return null;
  }

  const nowParts = getTimeZoneParts(now, TIME_ZONE);
  if (nowParts.weekday === null) {
    return null;
  }
  const todayStart = getStartOfDayInTimeZone(now, TIME_ZONE);
  if (!todayStart) {
    return null;
  }

  let best = null;

  rules.forEach((rule) => {
    const weekday = normalizeRegularPickupWeekday(rule.weekday);
    const timeParts = parseTimeString(rule.startTimeOfPickup);
    if (weekday === null || !timeParts) {
      return;
    }
    let daysAhead = (weekday - nowParts.weekday + 7) % 7;
    let candidateStart = addDays(todayStart, daysAhead);
    const candidateParts = getTimeZoneParts(candidateStart, TIME_ZONE);
    let slotDate = makeDateInTimeZone(
      TIME_ZONE,
      candidateParts.year,
      candidateParts.month,
      candidateParts.day,
      timeParts.hour,
      timeParts.minute,
      timeParts.second
    );

    let candidate = offsets
      .map((offset) => new Date(slotDate.getTime() + offset))
      .find((date) => date.getTime() > now.getTime());

    if (!candidate) {
      slotDate = addDays(slotDate, 7);
      candidate = offsets
        .map((offset) => new Date(slotDate.getTime() + offset))
        .find((date) => date.getTime() > now.getTime());
    }

    if (candidate && (!best || candidate.getTime() < best.getTime())) {
      best = candidate;
    }
  });

  return best;
}

function deactivateEntryInMemory(entry) {
  if (entry) {
    entry.active = false;
  }
}

function persistEntryDeactivation(profileId, entryId, options = {}) {
  if (!profileId || !entryId) {
    return false;
  }
  try {
    const config = readConfig(profileId);
    let changed = false;
    const updated = config.map((item) => {
      if (String(item?.id) === String(entryId)) {
        let mutated = false;
        const updatedEntry = { ...item };
        if (item?.active !== false) {
          updatedEntry.active = false;
          mutated = true;
        }
        if (options.resetDesiredWindow) {
          if (updatedEntry.desiredDate !== undefined) {
            delete updatedEntry.desiredDate;
            mutated = true;
          }
          if (updatedEntry.desiredDateRange !== undefined) {
            delete updatedEntry.desiredDateRange;
            mutated = true;
          }
        }
        if (mutated) {
          changed = true;
          return updatedEntry;
        }
      }
      return item;
    });
    if (changed) {
      writeConfig(profileId, updated);
    }
    return changed;
  } catch (error) {
    console.error(`[CONFIG] Konnte Eintrag ${entryId} für Profil ${profileId} nicht deaktivieren:`, error.message);
    return false;
  }
}

function deactivateEntry(session, entry, options = {}) {
  if (!session?.profile?.id || !entry?.id) {
    deactivateEntryInMemory(entry);
    return false;
  }
  deactivateEntryInMemory(entry);
  const changed = persistEntryDeactivation(session.profile.id, entry.id, options);
  if (changed) {
    const config = readConfig(session.profile.id);
    const settings = resolveSettings();
    scheduleConfig(session.id, config, settings);
  }
  return changed;
}

function shouldAutoDeactivateEntry(entry) {
  return entry?.autoDeactivate !== false;
}

function deactivateEntryAfterSuccess(session, entry, options = {}) {
  if (!shouldAutoDeactivateEntry(entry)) {
    return false;
  }
  return deactivateEntry(session, entry, options);
}

function isEntryActiveInConfig(profileId, entryId) {
  if (!profileId || !entryId) {
    return false;
  }
  const config = readConfig(profileId);
  const entry = config.find((item) => String(item?.id) === String(entryId));
  return !!entry && entry.active !== false;
}

function toDateValue(input) {
  if (!input) {
    return null;
  }
  const date = input instanceof Date ? new Date(input.getTime()) : new Date(input);
  if (Number.isNaN(date.getTime())) {
    return null;
  }
  return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
}

function formatDesiredWindowLabel(entry) {
  if (!entry) {
    return null;
  }
  const formatValue = (value) => {
    if (!value) {
      return null;
    }
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) {
      return null;
    }
    return date.toLocaleDateString('de-DE', {
      day: '2-digit',
      month: '2-digit',
      year: 'numeric'
    });
  };
  if (entry.desiredDateRange) {
    const startLabel = formatValue(entry.desiredDateRange.start);
    const endLabel = formatValue(entry.desiredDateRange.end);
    if (startLabel && endLabel) {
      return startLabel === endLabel ? startLabel : `${startLabel} – ${endLabel}`;
    }
    return startLabel || endLabel;
  }
  return formatValue(entry.desiredDate);
}

function desiredWindowExpired(entry) {
  if (!entry?.active) {
    return false;
  }
  const todayValue = toDateValue(new Date());
  if (todayValue === null) {
    return false;
  }
  if (entry.desiredDateRange?.start || entry.desiredDateRange?.end) {
    const endValue = toDateValue(entry.desiredDateRange.end) ?? toDateValue(entry.desiredDateRange.start);
    return endValue !== null && endValue < todayValue;
  }
  const desiredValue = toDateValue(entry.desiredDate);
  return desiredValue !== null && desiredValue < todayValue;
}

function resetDesiredWindow(entry) {
  if (!entry) {
    return;
  }
  if (entry.desiredDate !== undefined) {
    delete entry.desiredDate;
  }
  if (entry.desiredDateRange !== undefined) {
    delete entry.desiredDateRange;
  }
}

async function handleExpiredDesiredWindow(session, entry) {
  const profileId = session?.profile?.id;
  const storeName = entry.label || entry.id;
  const desiredLabel = formatDesiredWindowLabel(entry);
  console.log(
    `[INFO] Wunschzeitraum abgelaufen für ${storeName}${desiredLabel ? ` (${desiredLabel})` : ''}. Eintrag wird deaktiviert.`
  );
  resetDesiredWindow(entry);
  deactivateEntry(session, entry, { resetDesiredWindow: true });
  if (profileId) {
    try {
      await notificationService.sendDesiredWindowMissedNotification({
        profileId,
        profileName: session.profile?.name,
        storeName,
        desiredWindowLabel: desiredLabel
      });
    } catch (error) {
      console.error(
        `[NOTIFY] Benachrichtigung zum verpassten Zeitraum für ${storeName} fehlgeschlagen:`,
        error.message
      );
    }
  }
}

function matchesDesiredDate(pickupDate, desiredDate, desiredDateRange) {
  const pickupValue = toDateValue(pickupDate);
  if (pickupValue === null) {
    return false;
  }

  const hasRange = Boolean(desiredDateRange && (desiredDateRange.start || desiredDateRange.end));
  if (hasRange) {
    const startValue = toDateValue(desiredDateRange.start);
    const endValue = toDateValue(desiredDateRange.end);
    const normalizedStart = startValue !== null ? startValue : endValue;
    const normalizedEnd = endValue !== null ? endValue : startValue;
    if (normalizedStart !== null && pickupValue < normalizedStart) {
      return false;
    }
    if (normalizedEnd !== null && pickupValue > normalizedEnd) {
      return false;
    }
    return true;
  }

  const desiredValue = toDateValue(desiredDate);
  if (desiredValue === null) {
    return true;
  }
  return pickupValue === desiredValue;
}

function matchesDesiredWeekday(pickupDate, desiredWeekday) {
  if (!desiredWeekday) {
    return true;
  }
  const weekday = pickupDate.toLocaleDateString('en-US', { weekday: 'long' });
  return weekday === desiredWeekday;
}

function shouldIgnoreSlot(entry, pickup, settings) {
  const rules = settings.ignoredSlots || [];
  return rules.some((rule) => {
    if (!rule?.storeId) {
      return false;
    }
    if (String(rule.storeId) !== entry.id) {
      return false;
    }
    const slotName = rule.slotName || rule.description;
    return slotName ? pickup.description === slotName : true;
  });
}

async function processBooking(session, entry, pickup) {
  const readableDate = new Date(pickup.date).toLocaleString('de-DE');
  const storeName = entry.label || entry.id;

  if (entry.onlyNotify) {
    console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
    await notificationService.sendSlotNotification({
      profileId: session.profile.id,
      profileName: session.profile?.name,
      storeName,
      pickupDate: pickup.date,
      onlyNotify: true,
      booked: false,
      storeId: entry.id
    });
    deactivateEntryAfterSuccess(session, entry);
    return;
  }

  const utcDate = new Date(pickup.date).toISOString();
  try {
    const allowed = await withSessionRetry(
      session,
      () => foodsharingClient.pickupRuleCheck(entry.id, utcDate, session.profile.id, session),
      { label: 'pickupRuleCheck' }
    );
    if (!allowed) {
      console.warn(`[WARN] Rule-Check fehlgeschlagen für ${storeName} am ${readableDate}`);
      return;
    }
    await withSessionRetry(
      session,
      () => foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session),
      { label: 'bookSlot' }
    );
    console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
    await notificationService.sendSlotNotification({
      profileId: session.profile.id,
      profileName: session.profile?.name,
      storeName,
      pickupDate: pickup.date,
      onlyNotify: false,
      booked: true,
      storeId: entry.id
    });
    deactivateEntryAfterSuccess(session, entry);
  } catch (error) {
    console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
    try {
      await notificationService.sendAdminBookingErrorNotification({
        profileId: session.profile.id,
        profileEmail: session.profile.email,
        storeName,
        storeId: entry.id,
        pickupDate: pickup.date,
        error: error.message
      });
    } catch (notifyError) {
      console.error(
        `[NOTIFY] Admin-Benachrichtigung für fehlgeschlagene Buchung bei ${storeName} fehlgeschlagen:`,
        notifyError.message
      );
    }
  }
}

async function checkEntry(sessionId, entry, settings) {
  if (shouldPauseForMaintenance(`Pickup-Check für Store ${entry?.id || 'unbekannt'}`)) {
    return;
  }
  if (entry?.active === false) {
    return;
  }
  const session = sessionStore.get(sessionId);
  if (!session) {
    return;
  }
  const dedupKey = `${sessionId}:${entry?.id ?? 'unknown'}`;
  const lastRun = pickupCheckLastRun.get(dedupKey);
  if (lastRun && Date.now() - lastRun < PICKUP_CHECK_DEDUP_MS) {
    return;
  }
  pickupCheckLastRun.set(dedupKey, Date.now());
  const inFlightKey = `${sessionId}:${entry?.id ?? 'unknown'}`;
  if (pickupCheckInFlight.has(inFlightKey)) {
    return;
  }
  pickupCheckInFlight.set(inFlightKey, Date.now());

  try {
    if (desiredWindowExpired(entry)) {
      await handleExpiredDesiredWindow(session, entry);
      return;
    }

    if (!isEntryActiveInConfig(session.profile.id, entry.id)) {
      deactivateEntryInMemory(entry);
      return;
    }

    const ready = await ensureSession(session);
    if (!ready) {
      return;
    }

    const pickups = await withSessionRetry(
      session,
      () => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
      { label: 'fetchPickups' }
    );
    const profilePickups = [];
    let availablePickup = null;

    const desiredWeekday = entry.desiredWeekday ? weekdayMap[entry.desiredWeekday] || entry.desiredWeekday : null;

    pickups.forEach((pickup) => {
      const pickupDate = new Date(pickup.date);
      if (
        entry.checkProfileId &&
        pickup.occupiedSlots?.some((slot) => String(slot.profile?.id) === String(session.profile.id))
      ) {
        profilePickups.push(pickup);
      }
      if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) {
        return;
      }
      if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
        return;
      }
      if (pickup.isAvailable && !availablePickup) {
        availablePickup = pickup;
      }
    });

    if (!availablePickup) {
      console.log(
        `[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
          profilePickups.length > 0 ? 'ja' : 'nein'
        }`
      );
      return;
    }

    const blockingProfilePickup = entry.checkProfileId
      ? getBlockingProfilePickup({
          storeId: entry.id,
          profilePickups,
          targetPickup: availablePickup
        })
      : null;

    if (entry.checkProfileId && blockingProfilePickup) {
      console.log(
        `[INFO] Profil bereits in einem Slot für ${entry.label || entry.id} eingetragen – überspringe Buchung.`
      );
      return;
    }

    if (entry.checkProfileId && profilePickups.length > 0 && !blockingProfilePickup) {
      console.log(
        `[INFO] Heutige Profileintragung für ${entry.label || entry.id} wird toleriert; ` +
          `Zieltermin: ${new Date(availablePickup.date).toLocaleString('de-DE')}.`
      );
    }

    if (shouldIgnoreSlot(entry, availablePickup, settings)) {
      console.log(`[INFO] Slot für ${entry.id} aufgrund einer Admin-Regel ignoriert.`);
      return;
    }

    if (!entry.checkProfileId || !blockingProfilePickup) {
      await processBooking(session, entry, availablePickup);
    }
  } catch (error) {
    console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
  } finally {
    pickupCheckInFlight.delete(inFlightKey);
  }
}

async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, options = {}) {
  if (shouldPauseForMaintenance(`Store-Watch für Session ${sessionId}`)) {
    return [];
  }
  const session = sessionStore.get(sessionId);
  if (!session?.profile?.id || !session.isAdmin) {
    return [];
  }
  const watchers = readStoreWatch(session.profile.id);
  if (!Array.isArray(watchers) || watchers.length === 0) {
    return [];
  }

  const ready = await ensureSession(session);
  if (!ready) {
    return [];
  }

  const perRequestDelay = Math.max(0, Number(settings?.storeWatchRequestDelayMs) || 0);
  let changed = false;
  let statusCacheUpdated = false;
  const summary = [];
  for (let index = 0; index < watchers.length; index += 1) {
    const watcher = watchers[index];
    try {
      const cacheMaxAgeMs = Math.max(
        0,
        Number(settings?.storeWatchStatusCacheMaxAgeMinutes ?? DEFAULT_STORE_WATCH_STATUS_MAX_AGE_MINUTES)
      ) * 60 * 1000;
      const { status, fromCache } = await fetchSharedStoreStatus(session, watcher.storeId, {
        maxAgeMs: cacheMaxAgeMs
      });
      const checkedAt = Date.now();
      if (status === 1 && watcher.lastTeamSearchStatus !== 1) {
        await notificationService.sendStoreWatchNotification({
          profileId: session.profile.id,
          profileName: session.profile?.name,
          storeName: watcher.storeName,
          storeId: watcher.storeId,
          regionName: watcher.regionName
        });
      }
      if (watcher.lastTeamSearchStatus !== status) {
        watcher.lastTeamSearchStatus = status;
        changed = true;
      }
      watcher.lastStatusCheckAt = checkedAt;
      changed = true;
      if (!fromCache) {
        statusCacheUpdated = true;
      }
      summary.push({
        storeId: watcher.storeId,
        storeName: watcher.storeName,
        regionName: watcher.regionName,
        status,
        checkedAt
      });
    } catch (error) {
      console.error(`[WATCH] Prüfung für Store ${watcher.storeId} fehlgeschlagen:`, error.message);
      summary.push({
        storeId: watcher.storeId,
        storeName: watcher.storeName,
        regionName: watcher.regionName,
        status: null,
        checkedAt: Date.now(),
        error: error.message || 'Unbekannter Fehler'
      });
    } finally {
      const hasNext = index < watchers.length - 1;
      if (hasNext && perRequestDelay > 0) {
        await wait(perRequestDelay);
      }
    }
  }

  if (changed) {
    writeStoreWatch(session.profile.id, watchers);
  }
  if (statusCacheUpdated) {
    persistStoreStatusCache();
  }
  if (options.sendSummary && summary.length > 0) {
    try {
      await notificationService.sendStoreWatchSummaryNotification({
        profileId: session.profile.id,
        profileName: session.profile?.name,
        entries: summary,
        triggeredBy: options.triggeredBy || 'manual'
      });
    } catch (error) {
      console.error('[WATCH] Zusammenfassung konnte nicht versendet werden:', error.message);
    }
  }
  return summary;
}

function scheduleStoreWatchers(sessionId, settings) {
  const effectiveSettings = settings || DEFAULT_SETTINGS;
  const session = sessionStore.get(sessionId);
  if (!session?.profile?.id || !session.isAdmin) {
    return false;
  }
  const watchers = readStoreWatch(session.profile.id);
  if (!Array.isArray(watchers) || watchers.length === 0) {
    return false;
  }
  const cronExpression = effectiveSettings.storeWatchCron || DEFAULT_SETTINGS.storeWatchCron;
  const cronJob = cron.schedule(
    cronExpression,
    () => {
      checkWatchedStores(sessionId, effectiveSettings).catch((error) => {
        console.error('[WATCH] Regelmäßige Prüfung fehlgeschlagen:', error.message);
      });
    },
    { timezone: 'Europe/Berlin' }
  );
  let initialTimeout = setTimeout(
    () => checkWatchedStores(sessionId, effectiveSettings),
    randomDelayMs(
      effectiveSettings.storeWatchInitialDelayMinSeconds,
      effectiveSettings.storeWatchInitialDelayMaxSeconds
    )
  );
  sessionStore.attachJob(sessionId, {
    stop: () => {
      cronJob.stop();
      if (initialTimeout) {
        clearTimeout(initialTimeout);
        initialTimeout = null;
      }
    }
  });
  console.log(
    `[WATCH] Überwache ${watchers.length} Betriebe für Session ${sessionId} (Cron: ${cronExpression}).`
  );
  return true;
}

function scheduleRegularPickupRefresh(settings) {
  if (regularPickupRefreshJob) {
    regularPickupRefreshJob.stop();
    regularPickupRefreshJob = null;
  }
  const cronExpression = settings.regularPickupRefreshCron || DEFAULT_SETTINGS.regularPickupRefreshCron;
  if (!cronExpression) {
    return null;
  }
  regularPickupRefreshJob = cron.schedule(
    cronExpression,
    async () => {
      if (shouldPauseForMaintenance('Regular-Pickup-Cronlauf')) {
        return;
      }
      const sessions = sessionStore.list();
      const storeSessionMap = new Map();
      for (const session of sessions) {
        if (!session?.profile?.id) {
          continue;
        }
        const config = readConfig(session.profile.id);
        const entries = Array.isArray(config) ? config : [];
        const storeIds = Array.from(
          new Set(entries.filter((entry) => entry?.id && !entry.hidden).map((entry) => String(entry.id)))
        );
        const sessionUpdatedAt = Number(session.updatedAt) || 0;
        storeIds.forEach((storeId) => {
          const existing = storeSessionMap.get(storeId);
          if (!existing || (Number(existing.updatedAt) || 0) < sessionUpdatedAt) {
            storeSessionMap.set(storeId, session);
          }
        });
      }

      for (const [storeId, session] of storeSessionMap.entries()) {
        const ready = await ensureSession(session);
        if (!ready) {
          continue;
        }
        await getRegularPickupSchedule(session, storeId);
      }
    },
    { timezone: TIME_ZONE }
  );
  return regularPickupRefreshJob;
}

function scheduleFallbackPickupChecks(sessionId, settings) {
  const cronExpression = settings.pickupFallbackCron || DEFAULT_SETTINGS.pickupFallbackCron;
  if (!cronExpression) {
    console.log(`[PICKUP] Fallback-Check für Session ${sessionId} nicht geplant: kein Cron-Ausdruck konfiguriert.`);
    return null;
  }
  if (!cron.validate(cronExpression)) {
    console.warn(
      `[PICKUP] Fallback-Check für Session ${sessionId} nicht geplant: ungültiger Cron-Ausdruck "${cronExpression}".`
    );
    return null;
  }
  const job = cron.schedule(
    cronExpression,
    () => {
      const session = sessionStore.get(sessionId);
      if (!session?.profile?.id) {
        console.warn(`[PICKUP] Fallback-Check übersprungen: Session ${sessionId} nicht gefunden.`);
        return;
      }
      const delay = randomDelayMs(settings.randomDelayMinSeconds, settings.randomDelayMaxSeconds);
      console.log(
        `[PICKUP] Fallback-Check ausgelöst für Session ${sessionId} ` +
          `(Profil ${session.profile.id}, Delay ${delay}ms, Cron: ${cronExpression}).`
      );
      setTimeout(() => {
        const config = readConfig(session.profile.id);
        runImmediatePickupCheck(sessionId, config, settings)
          .then((result) => {
            console.log(
              `[PICKUP] Fallback-Check abgeschlossen für Session ${sessionId}: ` +
                `${result.checked} aktive Einträge geprüft.`
            );
          })
          .catch((error) => {
            console.error('[PICKUP] Fallback-Check fehlgeschlagen:', error.message);
          });
      }, delay);
    },
    { timezone: TIME_ZONE }
  );
  sessionStore.attachJob(sessionId, job);
  console.log(`[PICKUP] Fallback-Check geplant für Session ${sessionId} (Cron: ${cronExpression}).`);
  return job;
}

function scheduleEntry(sessionId, entry, settings) {
  let timeoutId = null;
  let stopped = false;

  const scheduleNext = async () => {
    if (stopped) {
      return;
    }
    const session = sessionStore.get(sessionId);
    if (!session) {
      return;
    }
    let nextTime = null;
    try {
      nextTime = await getNextPickupCheckTime(session, entry, settings);
    } catch (error) {
      console.warn(`[PICKUP] Konnte nächste Slot-Zeit für ${entry?.id} nicht berechnen:`, error.message);
    }
    const delayMs = nextTime
      ? Math.max(0, nextTime.getTime() - Date.now())
      : PICKUP_FALLBACK_RETRY_MS;
    timeoutId = setTimeout(async () => {
      timeoutId = null;
      if (stopped) {
        return;
      }
      await checkEntry(sessionId, entry, settings);
      scheduleNext();
    }, delayMs);
  };

  scheduleNext();
  const job = {
    stop: () => {
      stopped = true;
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
    }
  };
  sessionStore.attachJob(sessionId, job);
}

function scheduleConfig(sessionId, config, settings) {
  const resolvedSettings = resolveSettings(settings);
  const entries = Array.isArray(config) ? config : [];
  sessionStore.clearJobs(sessionId);
  scheduleJournalReminders(sessionId);
  if (shouldPauseForMaintenance(`Scheduler für Session ${sessionId}`)) {
    return;
  }
  scheduleDormantMembershipCheck(sessionId, resolvedSettings);
  const watchScheduled = scheduleStoreWatchers(sessionId, resolvedSettings);
  scheduleFallbackPickupChecks(sessionId, resolvedSettings);
  const activeEntries = entries.filter((entry) => entry.active);
  if (activeEntries.length === 0) {
    if (watchScheduled) {
      console.log(
        `[INFO] Keine aktiven Pickup-Einträge für Session ${sessionId} – Store-Watch bleibt aktiv.`
      );
    } else {
      console.log(`[INFO] Keine aktiven Einträge für Session ${sessionId} – Scheduler ruht.`);
    }
    return;
  }
  sessionStore.attachJob(
    sessionId,
    startStoreMessageWatcher({
      sessionId,
      config: entries,
      settings: resolvedSettings,
      runPickupCheck: runImmediatePickupCheck
    })
  );
  activeEntries.forEach((entry) => scheduleEntry(sessionId, entry, resolvedSettings));
  console.log(
    `[INFO] Scheduler für Session ${sessionId} mit ${activeEntries.length} Jobs aktiv.`
  );
}

async function runStoreWatchCheck(sessionId, settings, options = {}) {
  maintenanceMode.ensureInactive('Store-Watch ist während des Wartungsmodus deaktiviert.');
  const resolvedSettings = resolveSettings(settings);
  return checkWatchedStores(sessionId, resolvedSettings, options);
}

async function runImmediatePickupCheck(sessionId, config, settings) {
  maintenanceMode.ensureInactive('Pickup-Prüfungen sind während des Wartungsmodus deaktiviert.');
  const resolvedSettings = resolveSettings(settings);
  const entries = Array.isArray(config) ? config : [];
  const activeEntries = entries.filter((entry) => entry?.active);
  if (activeEntries.length === 0) {
    console.log(`[PICKUP] Sofortprüfung für Session ${sessionId}: keine aktiven Einträge.`);
    return { checked: 0 };
  }
  console.log(
    `[PICKUP] Sofortprüfung für Session ${sessionId}: ${activeEntries.length} aktive Einträge ` +
      `(${activeEntries.map((entry) => entry.id).join(', ')}).`
  );
  for (const entry of activeEntries) {
    await checkEntry(sessionId, entry, resolvedSettings);
  }
  return { checked: activeEntries.length };
}

function setMonthOffset(date, offset) {
  const copy = new Date(date.getTime());
  copy.setMonth(copy.getMonth() + offset);
  return copy;
}

function getMissingLastPickupStoreIds(config = []) {
  if (!Array.isArray(config)) {
    return [];
  }
  return config
    .filter((entry) => entry && entry.id && !entry.hidden && !entry.skipDormantCheck && !entry.lastPickupAt)
    .map((entry) => String(entry.id));
}

async function checkDormantMembers(sessionId, options = {}) {
  if (shouldPauseForMaintenance(`Dormant-Check für Session ${sessionId}`)) {
    return { checked: 0, updated: false };
  }
  const session = sessionStore.get(sessionId);
  if (!session?.profile?.id) {
    return { checked: 0, updated: false };
  }
  const storeIdSet = Array.isArray(options.storeIds)
    ? new Set(options.storeIds.map((storeId) => String(storeId)))
    : null;
  const includeSkippedDormant = !!options.includeSkippedDormant;
  const sendWarnings = options.sendWarnings !== false;
  const profileId = session.profile.id;
  const ensured = await ensureSession(session);
  if (!ensured) {
    return { checked: 0, updated: false };
  }
  const config = readConfig(profileId);
  const skipMap = new Map();
  const configEntryMap = new Map();
  config.forEach((entry, index) => {
    if (entry?.id) {
      const id = String(entry.id);
      skipMap.set(id, !!entry.skipDormantCheck);
      configEntryMap.set(id, { entry, index });
    }
  });
  let configChanged = false;

  const storeTargets = new Map();
  config.forEach((entry) => {
    if (!entry?.id || entry.hidden) {
      return;
    }
    const storeId = String(entry.id);
    if (storeIdSet && !storeIdSet.has(storeId)) {
      return;
    }
    if (!includeSkippedDormant && skipMap.get(storeId)) {
      return;
    }
    storeTargets.set(storeId, {
      storeId,
      storeName: entry.label || `Store ${storeId}`
    });
  });

  const stores = Array.isArray(session.storesCache?.data) ? session.storesCache.data : [];
  if (stores.length === 0) {
    console.warn(`[DORMANT] Keine Stores für Session ${sessionId} im Cache gefunden.`);
    return { checked: 0, updated: false };
  }

  const activeStoreIds = new Set();
  const activeStoresById = new Map();
  stores.forEach((store) => {
    const storeId = store?.id ? String(store.id) : null;
    if (!storeId) {
      return;
    }
    activeStoreIds.add(storeId);
    activeStoresById.set(storeId, store);
    if (!storeTargets.has(storeId)) {
      return;
    }
    const target = storeTargets.get(storeId);
    storeTargets.set(storeId, {
      ...target,
      storeName: store.name || target.storeName
    });
  });

  Array.from(storeTargets.keys()).forEach((storeId) => {
    if (activeStoreIds.has(storeId)) {
      return;
    }
    console.info(
      `[DORMANT] Store ${storeId} wird für Profil ${profileId} übersprungen, weil er nicht mehr in den aktuellen Store-Mitgliedschaften enthalten ist.`
    );
    storeTargets.delete(storeId);
  });

  Array.from(storeTargets.keys()).forEach((storeId) => {
    const store = activeStoresById.get(storeId);
    if (hasDormantMemberAccess(store)) {
      return;
    }
    console.info(
      `[DORMANT] Store ${storeId} wird für Profil ${profileId} übersprungen, weil die aktuelle Mitgliedschaft (${store?.membershipStatus ?? 'unbekannt'}) keinen Zugriff auf die Mitgliederliste erlaubt.`
    );
    storeTargets.delete(storeId);
  });

  if (storeTargets.size === 0) {
    return { checked: 0, updated: false };
  }
  const fourMonthsAgo = setMonthOffset(new Date(), -4).getTime();
  const hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000;

  let checked = 0;
  for (const target of storeTargets.values()) {
    const storeId = target.storeId;
    let members = [];
    try {
      members = await withSessionRetry(
        session,
        () => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader, session),
        { label: 'fetchStoreMembers' }
      );
    } catch (error) {
      const status = error?.response?.status;
      if (status === 403 || status === 404) {
        console.warn(
          `[DORMANT] Mitglieder von Store ${storeId} konnten für Profil ${profileId} nicht geladen werden (${status}). Der Store wird für diese Session übersprungen.`
        );
        continue;
      }
      console.warn(`[DORMANT] Mitglieder von Store ${storeId} konnten nicht geladen werden:`, error.message);
      continue;
    }
    const memberEntry = members.find((m) => String(m?.id) === String(profileId));
    if (!memberEntry) {
      continue;
    }
    checked += 1;
    const reasons = [];
    const lastFetchRaw = memberEntry.lastFetch ?? memberEntry.last_fetch ?? null;
    const lastFetchMs = parseLastFetchTimestamp(lastFetchRaw);
    const configEntry = configEntryMap.get(storeId)?.entry;
    const memberSinceRaw = memberEntry.memberSince ?? memberEntry.member_since ?? null;
    if (updateLastPickupInfo(configEntry, lastFetchRaw, memberSinceRaw)) {
      configChanged = true;
    }
    if (sendWarnings && !skipMap.get(storeId) && (!lastFetchMs || lastFetchMs < fourMonthsAgo)) {
      const lastFetchLabel = lastFetchMs ? new Date(lastFetchMs).toLocaleDateString('de-DE') : 'unbekannt';
      reasons.push(`Letzte Abholung: ${lastFetchLabel} (älter als 4 Monate)`);
    }
    const hygieneCertificateUntil =
      memberEntry.hygieneCertificateUntil ?? memberEntry.hygiene_certificate_until ?? null;
    if (sendWarnings && !skipMap.get(storeId) && hygieneCertificateUntil) {
      const expiry = new Date(String(hygieneCertificateUntil).replace(' ', 'T'));
      if (!Number.isNaN(expiry.getTime()) && expiry.getTime() < hygieneCutoff) {
        reasons.push(
          `Hygiene-Nachweis läuft bald ab: ${expiry.toLocaleDateString('de-DE')} (unter 6 Wochen)`
        );
      }
    }
    if (sendWarnings && !skipMap.get(storeId) && reasons.length > 0) {
      const cooldownKey = `${profileId}:${storeId}`;
      const lastSentAt = dormantWarningCooldowns.get(cooldownKey);
      if (lastSentAt && Date.now() - lastSentAt < DORMANT_WARNING_COOLDOWN_MS) {
        continue;
      }
      try {
        await sendDormantPickupWarning({
          profileId,
          profileName: session.profile?.name,
          storeName: target.storeName,
          storeId,
          reasonLines: reasons
        });
        dormantWarningCooldowns.set(cooldownKey, Date.now());
      } catch (error) {
        console.error(`[DORMANT] Warnung für Store ${storeId} konnte nicht versendet werden:`, error.message);
      }
    }
  }
  if (configChanged) {
    try {
      writeConfig(profileId, config);
    } catch (error) {
      console.error(`[DORMANT] Letzte Abholung für Profil ${profileId} konnte nicht gespeichert werden:`, error.message);
    }
  }
  return { checked, updated: configChanged };
}

async function refreshStaleLastPickupInfo(sessionId) {
  const session = sessionStore.get(sessionId);
  const profileId = session?.profile?.id ? String(session.profile.id) : null;
  if (!profileId) {
    return { checked: 0, updated: false, stale: 0 };
  }
  if (lastPickupRefreshInFlight.has(profileId)) {
    return lastPickupRefreshInFlight.get(profileId);
  }
  const refresh = (async () => {
    const config = readConfig(profileId);
    const staleStoreIds = getStaleLastPickupStoreIds(config);
    if (staleStoreIds.length === 0) {
      return { checked: 0, updated: false, stale: 0 };
    }
    const result = await checkDormantMembers(sessionId, {
      storeIds: staleStoreIds,
      includeSkippedDormant: true,
      sendWarnings: false
    });
    return { ...result, stale: staleStoreIds.length };
  })();
  lastPickupRefreshInFlight.set(profileId, refresh);
  try {
    return await refresh;
  } finally {
    lastPickupRefreshInFlight.delete(profileId);
  }
}

function scheduleDormantMembershipCheck(sessionId, settings) {
  const cronExpression =
    settings?.dormantMembershipCron || DEFAULT_SETTINGS.dormantMembershipCron || '0 4 */14 * *';
  const job = cron.schedule(
    cronExpression,
    () => {
      checkDormantMembers(sessionId).catch((error) => {
        console.error('[DORMANT] Prüfung fehlgeschlagen:', error.message);
      });
    },
    { timezone: 'Europe/Berlin' }
  );
  sessionStore.attachJob(sessionId, job);
  const session = sessionStore.get(sessionId);
  const profileId = session?.profile?.id;
  if (!profileId) {
    return;
  }
  const config = readConfig(profileId);
  const missingIds = getMissingLastPickupStoreIds(config);
  if (missingIds.length === 0) {
    if (session.dormantBootstrapSignature) {
      sessionStore.update(sessionId, { dormantBootstrapSignature: null });
    }
    return;
  }
  const signature = missingIds.sort().join(',');
  if (session.dormantBootstrapSignature === signature) {
    return;
  }
  sessionStore.update(sessionId, { dormantBootstrapSignature: signature });
  setTimeout(() => checkDormantMembers(sessionId), randomDelayMs(30, 180));
}

async function runDormantMembershipCheck(sessionId, options = {}) {
  maintenanceMode.ensureInactive('Dormant-Prüfungen sind während des Wartungsmodus deaktiviert.');
  await checkDormantMembers(sessionId, options);
}

async function checkJournalReminders(sessionId) {
  const session = sessionStore.get(sessionId);
  if (!session?.profile?.id) {
    return;
  }
  const profileId = session.profile.id;
  const profileName = session.profile?.name;
  const entries = readJournal(profileId);
  if (!Array.isArray(entries) || entries.length === 0) {
    return;
  }
  const todayStart = startOfDay(new Date());
  const todayLabel = todayStart.toISOString().slice(0, 10);
  let updated = false;

  for (const entry of entries) {
    if (!entry?.reminder?.enabled || !entry.pickupDate) {
      continue;
    }
    const schedule = computeReminderSchedule(entry, todayStart);
    if (!schedule?.occurrence || !schedule?.reminderDate) {
      continue;
    }
    const { occurrence, reminderDate } = schedule;

    if (!isSameDay(reminderDate, todayStart)) {
      continue;
    }
    if (entry.lastReminderAt === todayLabel) {
      continue;
    }
    await sendJournalReminderNotification({
      profileId,
      profileName,
      storeName: entry.storeName || `Store ${entry.storeId || ''}`,
      pickupDate: occurrence,
      reminderDate,
      note: entry.note || ''
    });
    entry.lastReminderAt = todayLabel;
    entry.updatedAt = new Date().toISOString();
    updated = true;
  }

  if (updated) {
    writeJournal(profileId, entries);
  }
}

function scheduleJournalReminders(sessionId) {
  const cronExpression = '0 8 * * *';
  const job = cron.schedule(
    cronExpression,
    () => {
      checkJournalReminders(sessionId).catch((error) => {
        console.error('[JOURNAL] Erinnerung fehlgeschlagen:', error.message);
      });
    },
    { timezone: 'Europe/Berlin' }
  );
  sessionStore.attachJob(sessionId, job);
  setTimeout(() => {
    checkJournalReminders(sessionId).catch((error) => {
      console.error('[JOURNAL] Initiale Erinnerung fehlgeschlagen:', error.message);
    });
  }, randomDelayMs(30, 120));
}

module.exports = {
  scheduleConfig,
  runStoreWatchCheck,
  runImmediatePickupCheck,
  runDormantMembershipCheck,
  refreshStaleLastPickupInfo,
  getRegularPickupSchedule,
  scheduleRegularPickupRefresh
};

loadRegularPickupCacheFromDisk();
