import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { randomUUID } from 'node:crypto'
import { sanitizeSiteContent } from './helpers.js'
import {
  DATA_DIR,
  DATA_FILE,
  DRAFT_DATA_FILE,
  HIT_STATS_FILE,
  VISITOR_STATS_FILE,
  CONTACT_SUBMISSIONS_FILE,
  QUESTIONS_FILE,
  DRAFT_QUESTIONS_FILE,
  STUDY_USERS_FILE,
  STUDY_COMMUNITY_FILE,
  STUDY_NOTES_DIR,
  STUDY_NOTES_FILE,
  STUDY_PROGRESS_DIR,
  STUDY_REMINDERS_FILE,
  STUDY_COMMENTS_FILE,
  STUDY_CERTIFICATES_FILE,
  EPISODE_SCRIPTS_FILE,
  QR_CODES_FILE,
  REPLY_TEMPLATES_FILE,
  REPLY_HISTORY_FILE,
  PODCAST_CHECKLIST_FILE,
  BACKUP_DIR,
  UPLOADS_DIR,
  UPLOADS_META_FILE,
  DOWNLOAD_COUNTS_FILE,
  EPISODE_PLAYS_FILE,
  ANALYTICS_EVENTS_FILE,
  EMAIL_SETTINGS_FILE,
  CALENDAR_EVENTS_FILE,
  AUDIT_LOG_FILE,
  WEBHOOKS_FILE,
  EMPTY_HIT_STATS,
  EMPTY_VISITOR_STATS,
  DEFAULT_REPLY_TEMPLATES,
  MAX_QUESTIONS,
  MAX_CONTACT_SUBMISSIONS,
  MAX_RECENT_VISITS,
  BACKUP_RETENTION_DAYS,
  MAX_STUDY_USERS,
  MAX_STUDY_ENROLLMENTS_PER_USER,
  MAX_STUDY_NOTES_PER_USER,
  MAX_STUDY_NOTE_LENGTH,
  MAX_STUDY_COMMENTS,
  DEFAULT_PODCAST_CHECKLIST_TASKS,
  buildDefaultPodcastChecklist,
} from './config.js'
import { state } from './state.js'

// ── Helpers ────────────────────────────────────────────────────────────────

export async function loadSiteContentFile(filePath) {
  const raw = await readFile(filePath, 'utf8')
  const parsed = JSON.parse(raw)
  const safeSiteContent = sanitizeSiteContent(parsed?.siteContent)
  return {
    ...parsed,
    siteContent: safeSiteContent,
  }
}

export async function checkDataDirWritable() {
  try {
    await mkdir(DATA_DIR, { recursive: true })
    const marker = path.join(DATA_DIR, `.write-test-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`)
    await writeFile(marker, 'ok', 'utf8')
    await unlink(marker)
    return { ok: true, error: null }
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err.message : 'Unknown write test error' }
  }
}

export async function getStorageStatus() {
  const writable = await checkDataDirWritable()
  const files = {}
  for (const [key, filePath] of Object.entries({
    adminContent: DATA_FILE,
    adminContentDraft: DRAFT_DATA_FILE,
    studyUsers: STUDY_USERS_FILE,
  })) {
    try {
      const fileStat = await stat(filePath)
      files[key] = {
        path: filePath,
        exists: true,
        sizeBytes: fileStat.size,
        mtime: fileStat.mtime.toISOString(),
      }
    } catch {
      files[key] = {
        path: filePath,
        exists: false,
        sizeBytes: 0,
        mtime: null,
      }
    }
  }

  return {
    dataDir: DATA_DIR,
    writable,
    files,
  }
}

export async function refreshContentCaches() {
  try {
    const published = await loadSiteContentFile(DATA_FILE)
    state.cachedSiteContent = published.siteContent
    if (typeof published?.updatedAt === 'string') {
      state.publishState.publishedAt = published.updatedAt
    }
  } catch {
    state.cachedSiteContent = null
  }

  try {
    const draft = await loadSiteContentFile(DRAFT_DATA_FILE)
    state.cachedDraftSiteContent = draft.siteContent
    if (typeof draft?.updatedAt === 'string') {
      state.publishState.draftUpdatedAt = draft.updatedAt
    }
  } catch {
    state.cachedDraftSiteContent = null
  }
}

// ── Hit stats ──────────────────────────────────────────────────────────────

export function queueHitStatsWrite() {
  state.hitStatsWritePromise = state.hitStatsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        HIT_STATS_FILE,
        JSON.stringify({
          ...state.hitStats,
          updatedAt: new Date().toISOString(),
        }, null, 2),
        'utf8',
      )
      state.lastHitStatsWrite = { ok: true, at: new Date().toISOString(), error: null }
    })
    .catch(err => {
      console.error('[stats] failed to write hit stats:', err)
      state.lastHitStatsWrite = { ok: false, at: new Date().toISOString(), error: String(err) }
    })
}

export function loadHitStatsFromDisk() {
  return readFile(HIT_STATS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.hitStats = {
        totalHits: Number(parsed?.totalHits) || 0,
        realHits: Number(parsed?.realHits) || 0,
        botHits: Number(parsed?.botHits) || 0,
        firstHitAt: typeof parsed?.firstHitAt === 'string' ? parsed.firstHitAt : null,
        lastHitAt: typeof parsed?.lastHitAt === 'string' ? parsed.lastHitAt : null,
        byPath: parsed?.byPath && typeof parsed.byPath === 'object' ? parsed.byPath : {},
        byPathReal: parsed?.byPathReal && typeof parsed.byPathReal === 'object' ? parsed.byPathReal : {},
        byPathBot: parsed?.byPathBot && typeof parsed.byPathBot === 'object' ? parsed.byPathBot : {},
        byDay: parsed?.byDay && typeof parsed.byDay === 'object' ? parsed.byDay : {},
        byDayReal: parsed?.byDayReal && typeof parsed.byDayReal === 'object' ? parsed.byDayReal : {},
        byDayBot: parsed?.byDayBot && typeof parsed.byDayBot === 'object' ? parsed.byDayBot : {},
        botReasons: parsed?.botReasons && typeof parsed.botReasons === 'object' ? parsed.botReasons : {},
      }
    })
    .catch(() => {
      state.hitStats = { ...EMPTY_HIT_STATS }
    })
}

// ── Visitor stats ──────────────────────────────────────────────────────────

export function queueVisitorStatsWrite() {
  state.visitorStatsWritePromise = state.visitorStatsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        VISITOR_STATS_FILE,
        JSON.stringify({
          ...state.visitorStats,
          updatedAt: new Date().toISOString(),
        }, null, 2),
        'utf8',
      )
      state.lastVisitorStatsWrite = { ok: true, at: new Date().toISOString(), error: null }
    })
    .catch(err => {
      console.error('[visitor-stats] failed to write visitor stats:', err)
      state.lastVisitorStatsWrite = { ok: false, at: new Date().toISOString(), error: String(err) }
    })
}

export function loadVisitorStatsFromDisk() {
  return readFile(VISITOR_STATS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      const loadedVisitors = parsed?.visitors && typeof parsed.visitors === 'object' ? parsed.visitors : {}

      let ipHashIndex = parsed?.ipHashIndex && typeof parsed.ipHashIndex === 'object' ? parsed.ipHashIndex : {}
      if (Object.keys(ipHashIndex).length === 0 && Object.keys(loadedVisitors).length > 0) {
        for (const [vid, visitor] of Object.entries(loadedVisitors)) {
          if (visitor?.ipHash && typeof visitor.ipHash === 'string') {
            ipHashIndex[visitor.ipHash] = vid
          }
        }
      }

      state.visitorStats = {
        totalVisits: Number(parsed?.totalVisits) || 0,
        uniqueVisitors: Number(parsed?.uniqueVisitors) || 0,
        returningVisits: Number(parsed?.returningVisits) || 0,
        firstVisitAt: typeof parsed?.firstVisitAt === 'string' ? parsed.firstVisitAt : null,
        lastVisitAt: typeof parsed?.lastVisitAt === 'string' ? parsed.lastVisitAt : null,
        visitors: loadedVisitors,
        ipHashIndex,
        recentVisits: Array.isArray(parsed?.recentVisits) ? parsed.recentVisits.slice(0, MAX_RECENT_VISITS) : [],
        geoCacheByIp: parsed?.geoCacheByIp && typeof parsed.geoCacheByIp === 'object' ? parsed.geoCacheByIp : {},
      }
    })
    .catch(() => {
      state.visitorStats = { ...EMPTY_VISITOR_STATS }
    })
}

// ── Contact submissions ────────────────────────────────────────────────────

export function queueContactSubmissionsWrite() {
  state.contactSubmissionsWritePromise = state.contactSubmissionsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        CONTACT_SUBMISSIONS_FILE,
        JSON.stringify({
          submissions: state.contactSubmissions,
          updatedAt: new Date().toISOString(),
        }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[contact] failed to write submissions:', err)
    })
}

export function loadContactSubmissionsFromDisk() {
  return readFile(CONTACT_SUBMISSIONS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.contactSubmissions = sanitizeLoadedContactSubmissions(parsed?.submissions)
    })
    .catch(() => {
      state.contactSubmissions = []
    })
}

// ── Questions ──────────────────────────────────────────────────────────────

export function queueQuestionsWrite() {
  state.questionsWritePromise = state.questionsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        QUESTIONS_FILE,
        JSON.stringify({
          questions: state.questions,
          updatedAt: new Date().toISOString(),
        }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[questions] failed to write questions:', err)
    })
}

export function queueDraftQuestionsWrite() {
  if (state.draftQuestions === null) return
  state.draftQuestionsWritePromise = state.draftQuestionsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        DRAFT_QUESTIONS_FILE,
        JSON.stringify({ questions: state.draftQuestions, updatedAt: new Date().toISOString() }, null, 2),
        'utf8',
      )
      state.publishState.draftUpdatedAt = new Date().toISOString()
    })
    .catch(err => {
      console.error('[draft-questions] failed to write draft questions:', err)
    })
}

export function loadQuestionsFromDisk() {
  return readFile(QUESTIONS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      if (Array.isArray(parsed)) {
        state.questions = parsed.slice(0, MAX_QUESTIONS)
      } else if (Array.isArray(parsed?.questions)) {
        state.questions = parsed.questions.slice(0, MAX_QUESTIONS)
      } else {
        state.questions = []
      }
    })
    .catch(() => {
      state.questions = []
    })
}

export async function loadDraftQuestionsFromDisk() {
  return readFile(DRAFT_QUESTIONS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      if (Array.isArray(parsed)) {
        state.draftQuestions = parsed.slice(0, MAX_QUESTIONS)
      } else if (Array.isArray(parsed?.questions)) {
        state.draftQuestions = parsed.questions.slice(0, MAX_QUESTIONS)
      } else {
        state.draftQuestions = null
      }
      if (typeof parsed?.updatedAt === 'string') {
        state.publishState.draftUpdatedAt = parsed.updatedAt
      }
    })
    .catch(() => {
      state.draftQuestions = null
    })
}

// ── Reply templates / history ──────────────────────────────────────────────

export function queueReplyTemplatesWrite() {
  state.replyTemplatesWritePromise = state.replyTemplatesWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        REPLY_TEMPLATES_FILE,
        JSON.stringify({ templates: state.replyTemplates, updatedAt: new Date().toISOString() }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[reply-templates] failed to write templates:', err)
    })
}

export function queueReplyHistoryWrite() {
  state.replyHistoryWritePromise = state.replyHistoryWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        REPLY_HISTORY_FILE,
        JSON.stringify({ items: state.replyHistory, updatedAt: new Date().toISOString() }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[reply-history] failed to write history:', err)
    })
}

export function loadReplyTemplatesFromDisk() {
  return readFile(REPLY_TEMPLATES_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.replyTemplates = sanitizeReplyTemplates(parsed?.templates)
    })
    .catch(() => {
      state.replyTemplates = [...DEFAULT_REPLY_TEMPLATES]
    })
}

export function loadReplyHistoryFromDisk() {
  return readFile(REPLY_HISTORY_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.replyHistory = sanitizeReplyHistory(parsed?.items)
    })
    .catch(() => {
      state.replyHistory = []
    })
}

// ── Email settings ────────────────────────────────────────────────────────

export function queueEmailSettingsWrite() {
  state.emailSettingsWritePromise = state.emailSettingsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        EMAIL_SETTINGS_FILE,
        JSON.stringify({ settings: state.emailSettings, updatedAt: new Date().toISOString() }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[email-settings] failed to write settings:', err)
    })
}

export function loadEmailSettingsFromDisk() {
  return readFile(EMAIL_SETTINGS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      if (parsed?.settings && typeof parsed.settings === 'object') {
        state.emailSettings = { ...state.emailSettings, ...parsed.settings }
      }
    })
    .catch(() => {
      // Keep defaults from state initializer
    })
}

// ── Calendar events ────────────────────────────────────────────────────────

export function queueCalendarEventsWrite() {
  state.calendarEventsWritePromise = state.calendarEventsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        CALENDAR_EVENTS_FILE,
        JSON.stringify({ events: state.calendarEvents, updatedAt: new Date().toISOString() }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[calendar-events] failed to write:', err)
    })
}

export function loadCalendarEventsFromDisk() {
  return readFile(CALENDAR_EVENTS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      if (Array.isArray(parsed?.events)) {
        state.calendarEvents = parsed.events
      }
    })
    .catch(() => {
      // No file yet — start with empty array
    })
}

// ── Audit log ─────────────────────────────────────────────────────────────

export function appendAuditEntry(action, details) {
  state.auditLog.unshift({
    id: randomUUID(),
    action: String(action).slice(0, 80),
    details: typeof details === 'string' ? details.slice(0, 400) : JSON.stringify(details ?? {}).slice(0, 400),
    at: new Date().toISOString(),
  })
  if (state.auditLog.length > 500) state.auditLog = state.auditLog.slice(0, 500)
  state.auditLogWritePromise = state.auditLogWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(AUDIT_LOG_FILE, JSON.stringify({ entries: state.auditLog }, null, 2), 'utf8')
    })
    .catch(err => { console.error('[audit-log] write failed:', err) })
}

export function loadAuditLogFromDisk() {
  return readFile(AUDIT_LOG_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      if (Array.isArray(parsed?.entries)) state.auditLog = parsed.entries.slice(0, 500)
    })
    .catch(() => {})
}

// ── Webhooks ──────────────────────────────────────────────────────────────

export function queueWebhooksWrite() {
  state.webhooksWritePromise = state.webhooksWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(WEBHOOKS_FILE, JSON.stringify({ webhooks: state.webhooks }, null, 2), 'utf8')
    })
    .catch(err => { console.error('[webhooks] write failed:', err) })
}

export function loadWebhooksFromDisk() {
  return readFile(WEBHOOKS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      if (Array.isArray(parsed?.webhooks)) state.webhooks = parsed.webhooks.slice(0, 50)
    })
    .catch(() => {})
}

export async function fireWebhooks(eventType, payload) {
  const targets = state.webhooks.filter(w =>
    Array.isArray(w.events) && (w.events.includes(eventType) || w.events.includes('*'))
  )
  if (targets.length === 0) return
  const body = JSON.stringify({ event: eventType, data: payload, firedAt: new Date().toISOString() })
  for (const wh of targets) {
    fetch(wh.url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'User-Agent': 'Siteforge-Webhook/1.0' },
      body,
      signal: AbortSignal.timeout(8000),
    })
      .then(r => {
        appendAuditEntry('webhook-fired', `${eventType} → ${wh.url} (${r.status})`)
      })
      .catch(err => {
        appendAuditEntry('webhook-error', `${eventType} → ${wh.url}: ${String(err?.message ?? err).slice(0, 200)}`)
      })
  }
}

// ── Podcast checklist ──────────────────────────────────────────────────────

export function queuePodcastChecklistWrite() {
  const updatedAt = new Date().toISOString()
  state.podcastChecklistWritePromise = state.podcastChecklistWritePromise.then(async () => {
    await mkdir(DATA_DIR, { recursive: true })
    await writeFile(
      PODCAST_CHECKLIST_FILE,
      JSON.stringify({ checklist: state.podcastChecklist, updatedAt }, null, 2),
      'utf8',
    )
  })
  return state.podcastChecklistWritePromise
}

export function loadPodcastChecklistFromDisk() {
  return readFile(PODCAST_CHECKLIST_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.podcastChecklist = sanitizePodcastChecklist(parsed?.checklist)
    })
    .catch(() => {
      state.podcastChecklist = buildDefaultPodcastChecklist()
    })
}

// ── Study users ────────────────────────────────────────────────────────────

export function queueStudyUsersWrite() {
  state.studyUsersWritePromise = state.studyUsersWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        STUDY_USERS_FILE,
        JSON.stringify({ users: state.studyUsers, updatedAt: new Date().toISOString() }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[study-users] failed to write users:', err)
    })
}

export function loadStudyUsersFromDisk() {
  return readFile(STUDY_USERS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      const source = Array.isArray(parsed) ? parsed : parsed?.users
      state.studyUsers = sanitizeStudyUsers(source)
    })
    .catch(() => {
      state.studyUsers = []
    })
}

// ── Study community ────────────────────────────────────────────────────────

export function queueStudyCommunityWrite() {
  state.studyCommunityWritePromise = state.studyCommunityWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(
        STUDY_COMMUNITY_FILE,
        JSON.stringify({ posts: state.studyCommunityPosts, updatedAt: new Date().toISOString() }, null, 2),
        'utf8',
      )
    })
    .catch(err => {
      console.error('[study-community] failed to write discussion posts:', err)
    })
}

export async function loadStudyCommunityFromDisk() {
  return readFile(STUDY_COMMUNITY_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.studyCommunityPosts = sanitizeStudyCommunityPosts(parsed?.posts ?? parsed)
    })
    .catch(() => {
      state.studyCommunityPosts = []
    })
}

// ── Study notes (per-user files) ───────────────────────────────────────────

export function getUserNotesFilePath(userId) {
  return path.join(STUDY_NOTES_DIR, `${userId}.json`)
}

export async function loadUserNotes(userId) {
  if (state.studyNotesCache.has(userId)) return state.studyNotesCache.get(userId)
  try {
    const raw = await readFile(getUserNotesFilePath(userId), 'utf8')
    const notes = sanitizeUserNotes(JSON.parse(raw))
    state.studyNotesCache.set(userId, notes)
    return notes
  } catch {
    const notes = {}
    state.studyNotesCache.set(userId, notes)
    return notes
  }
}

export function queueUserNotesWrite(userId) {
  const prev = state.studyNotesWriteQueues.get(userId) ?? Promise.resolve()
  const next = prev
    .then(async () => {
      const notes = state.studyNotesCache.get(userId) ?? {}
      await mkdir(STUDY_NOTES_DIR, { recursive: true })
      await writeFile(getUserNotesFilePath(userId), JSON.stringify(notes, null, 2), 'utf8')
    })
    .catch(err => {
      console.error(`[study-notes] failed to write notes for user ${userId}:`, err)
    })
  state.studyNotesWriteQueues.set(userId, next)
}

export async function migrateStudyNotesIfNeeded() {
  try {
    const raw = await readFile(STUDY_NOTES_FILE, 'utf8')
    const parsed = JSON.parse(raw)
    const notesByUser = parsed?.notesByUser ?? {}
    const userIds = Object.keys(notesByUser)
    if (userIds.length === 0) return
    await mkdir(STUDY_NOTES_DIR, { recursive: true })
    let migrated = 0
    for (const [userId, notes] of Object.entries(notesByUser)) {
      const sanitized = sanitizeUserNotes(notes)
      if (Object.keys(sanitized).length === 0) continue
      const filePath = getUserNotesFilePath(userId)
      try { await readFile(filePath, 'utf8'); continue } catch { /* doesn't exist yet */ }
      await writeFile(filePath, JSON.stringify(sanitized, null, 2), 'utf8')
      migrated += 1
    }
    if (migrated > 0) console.log(`[study-notes] migrated ${migrated} users to per-user files`)
  } catch { /* no legacy file — nothing to migrate */ }
}

// ── Study progress ─────────────────────────────────────────────────────────

export function getUserProgressFilePath(userId) {
  return path.join(STUDY_PROGRESS_DIR, `${userId}.json`)
}

export async function loadUserProgress(userId) {
  if (state.studyProgressCache.has(userId)) return state.studyProgressCache.get(userId)
  try {
    const raw = await readFile(getUserProgressFilePath(userId), 'utf8')
    const progress = sanitizeStudyProgress(JSON.parse(raw))
    state.studyProgressCache.set(userId, progress)
    return progress
  } catch {
    const progress = { byStudy: {}, updatedAt: new Date().toISOString() }
    state.studyProgressCache.set(userId, progress)
    return progress
  }
}

export function queueUserProgressWrite(userId) {
  const prev = state.studyProgressWriteQueues.get(userId) ?? Promise.resolve()
  const next = prev
    .then(async () => {
      const progress = state.studyProgressCache.get(userId) ?? { byStudy: {}, updatedAt: new Date().toISOString() }
      await mkdir(STUDY_PROGRESS_DIR, { recursive: true })
      await writeFile(getUserProgressFilePath(userId), JSON.stringify(progress, null, 2), 'utf8')
    })
    .catch(err => {
      console.error(`[study-progress] failed to write progress for user ${userId}:`, err)
    })
  state.studyProgressWriteQueues.set(userId, next)
}

// ── Study reminders ────────────────────────────────────────────────────────

export function queueStudyRemindersWrite() {
  state.studyRemindersWritePromise = state.studyRemindersWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(STUDY_REMINDERS_FILE, JSON.stringify(state.studyReminders, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[study-reminders] failed to write reminders:', err)
    })
}

export async function loadStudyRemindersFromDisk() {
  try {
    const raw = await readFile(STUDY_REMINDERS_FILE, 'utf8')
    state.studyReminders = sanitizeStudyReminders(JSON.parse(raw))
  } catch {
    state.studyReminders = { users: {}, updatedAt: new Date().toISOString() }
  }
}

// ── Study section comments ─────────────────────────────────────────────────

export function queueStudyCommentsWrite() {
  state.studyCommentsWritePromise = state.studyCommentsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(STUDY_COMMENTS_FILE, JSON.stringify(state.studyComments, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[study-comments] failed to write:', err)
    })
}

export async function loadStudyCommentsFromDisk() {
  try {
    const raw = await readFile(STUDY_COMMENTS_FILE, 'utf8')
    const parsed = JSON.parse(raw)
    state.studyComments = Array.isArray(parsed) ? parsed.slice(0, MAX_STUDY_COMMENTS) : []
  } catch {
    state.studyComments = []
  }
}

// ── Study certificates ─────────────────────────────────────────────────────

export function queueStudyCertificatesWrite() {
  state.studyCertificatesWritePromise = state.studyCertificatesWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(STUDY_CERTIFICATES_FILE, JSON.stringify(state.studyCertificates, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[study-certificates] failed to write:', err)
    })
}

export async function loadStudyCertificatesFromDisk() {
  try {
    const raw = await readFile(STUDY_CERTIFICATES_FILE, 'utf8')
    const parsed = JSON.parse(raw)
    state.studyCertificates = Array.isArray(parsed) ? parsed : []
  } catch {
    state.studyCertificates = []
  }
}

// ── Episode scripts ───────────────────────────────────────────────────────

export function queueEpisodeScriptsWrite() {
  state.episodeScriptsWritePromise = state.episodeScriptsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(EPISODE_SCRIPTS_FILE, JSON.stringify(state.episodeScripts, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[episode-scripts] failed to write:', err)
    })
}

export async function loadEpisodeScriptsFromDisk() {
  try {
    const raw = await readFile(EPISODE_SCRIPTS_FILE, 'utf8')
    const parsed = JSON.parse(raw)
    state.episodeScripts = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}
  } catch {
    state.episodeScripts = {}
  }
}

// ── Download counts ────────────────────────────────────────────────────────

export function queueDownloadCountsWrite() {
  state.downloadCountsWritePromise = state.downloadCountsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(DOWNLOAD_COUNTS_FILE, JSON.stringify(state.downloadCounts, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[download-counts] failed to write:', err)
    })
}

export function loadDownloadCountsFromDisk() {
  return readFile(DOWNLOAD_COUNTS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.downloadCounts = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}
    })
    .catch(() => {
      state.downloadCounts = {}
    })
}

export function incrementDownloadCount(resourceKey) {
  state.downloadCounts[resourceKey] = (state.downloadCounts[resourceKey] ?? 0) + 1
  queueDownloadCountsWrite()
}

// ── Episode play counts ────────────────────────────────────────────────────

export function queueEpisodePlaysWrite() {
  state.episodePlaysWritePromise = state.episodePlaysWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(EPISODE_PLAYS_FILE, JSON.stringify(state.episodePlays, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[episode-plays] failed to write:', err)
    })
}

export function loadEpisodePlaysFromDisk() {
  return readFile(EPISODE_PLAYS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.episodePlays = (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {}
    })
    .catch(() => {
      state.episodePlays = {}
    })
}

export function recordEpisodePlay(title) {
  const today = new Date().toISOString().slice(0, 10)
  if (!state.episodePlays[title]) {
    state.episodePlays[title] = { total: 0, byDay: {} }
  }
  state.episodePlays[title].total += 1
  state.episodePlays[title].byDay[today] = (state.episodePlays[title].byDay[today] ?? 0) + 1
  queueEpisodePlaysWrite()
}

// ── Analytics events ───────────────────────────────────────────────────────

const EMPTY_ANALYTICS_EVENTS = {
  scrollDepth: {},
  timeOnPage: {},
  outboundClicks: {},
  linkClicks: {},
  utmSources: {},
  searchQueries: {},
  notFound: {},
  audioEvents: {},
}

export function queueAnalyticsEventsWrite() {
  state.analyticsEventsWritePromise = state.analyticsEventsWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(ANALYTICS_EVENTS_FILE, JSON.stringify(state.analyticsEvents, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[analytics-events] failed to write:', err)
    })
}

export function loadAnalyticsEventsFromDisk() {
  return readFile(ANALYTICS_EVENTS_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.analyticsEvents = {
        scrollDepth: parsed?.scrollDepth ?? {},
        timeOnPage: parsed?.timeOnPage ?? {},
        outboundClicks: parsed?.outboundClicks ?? {},
        linkClicks: parsed?.linkClicks ?? {},
        utmSources: parsed?.utmSources ?? {},
        searchQueries: parsed?.searchQueries ?? {},
        notFound: parsed?.notFound ?? {},
        audioEvents: parsed?.audioEvents ?? {},
      }
    })
    .catch(() => {
      state.analyticsEvents = { ...EMPTY_ANALYTICS_EVENTS }
    })
}

export function recordAnalyticsEvent(type, data) {
  const ev = state.analyticsEvents
  if (type === 'scroll_depth') {
    const path = data.path ?? '/'
    if (!ev.scrollDepth[path]) ev.scrollDepth[path] = { 25: 0, 50: 0, 75: 0, 90: 0 }
    const mark = String(data.depth)
    ev.scrollDepth[path][mark] = (ev.scrollDepth[path][mark] ?? 0) + 1
  } else if (type === 'time_on_page') {
    const path = data.path ?? '/'
    const seconds = Number(data.seconds) || 0
    if (!ev.timeOnPage[path]) ev.timeOnPage[path] = { totalSeconds: 0, count: 0 }
    ev.timeOnPage[path].totalSeconds += seconds
    ev.timeOnPage[path].count += 1
  } else if (type === 'outbound_click') {
    const url = typeof data.url === 'string' ? data.url.slice(0, 500) : ''
    if (url) ev.outboundClicks[url] = (ev.outboundClicks[url] ?? 0) + 1
  } else if (type === 'link_click') {
    const url = typeof data.url === 'string' ? data.url.slice(0, 500) : ''
    if (url) {
      if (!ev.linkClicks[url]) ev.linkClicks[url] = { count: 0, internal: data.internal === true }
      ev.linkClicks[url].count += 1
    }
  } else if (type === 'utm') {
    const source = typeof data.utm_source === 'string' ? data.utm_source.slice(0, 100) : 'unknown'
    ev.utmSources[source] = (ev.utmSources[source] ?? 0) + 1
  } else if (type === 'search_query') {
    const q = typeof data.query === 'string' ? data.query.trim().slice(0, 200) : ''
    if (q) ev.searchQueries[q] = (ev.searchQueries[q] ?? 0) + 1
  } else if (type === 'not_found') {
    const path = typeof data.path === 'string' ? data.path.slice(0, 300) : '/'
    ev.notFound[path] = (ev.notFound[path] ?? 0) + 1
  } else if (type === 'audio_pause') {
    const title = typeof data.title === 'string' ? data.title.trim().slice(0, 200) : ''
    if (title) {
      if (!ev.audioEvents[title]) ev.audioEvents[title] = { pauses: 0, completions: 0, totalListenSeconds: 0 }
      ev.audioEvents[title].pauses += 1
    }
  } else if (type === 'audio_completion') {
    const title = typeof data.title === 'string' ? data.title.trim().slice(0, 200) : ''
    if (title) {
      if (!ev.audioEvents[title]) ev.audioEvents[title] = { pauses: 0, completions: 0, totalListenSeconds: 0 }
      ev.audioEvents[title].completions += 1
    }
  } else if (type === 'audio_listen_time') {
    const title = typeof data.title === 'string' ? data.title.trim().slice(0, 200) : ''
    const seconds = Number(data.seconds) || 0
    if (title && seconds > 0) {
      if (!ev.audioEvents[title]) ev.audioEvents[title] = { pauses: 0, completions: 0, totalListenSeconds: 0 }
      ev.audioEvents[title].totalListenSeconds += seconds
    }
  }
  queueAnalyticsEventsWrite()
}

// ── Uploads ────────────────────────────────────────────────────────────────

export async function readUploadsMetadata() {
  try {
    const raw = await readFile(UPLOADS_META_FILE, 'utf8')
    return JSON.parse(raw)
  } catch {
    return {}
  }
}

export async function writeUploadsMetadata(metadata) {
  await mkdir(DATA_DIR, { recursive: true })
  await writeFile(UPLOADS_META_FILE, JSON.stringify(metadata, null, 2), 'utf8')
}

export async function listUploadedAssets() {
  await mkdir(UPLOADS_DIR, { recursive: true })
  const files = await readdir(UPLOADS_DIR)
  const imageFiles = files.filter(name => /\.(png|jpe?g|webp|gif|pdf|docx?)$/i.test(name)).sort()
  const metadata = await readUploadsMetadata()

  const withStats = await Promise.all(imageFiles.map(async filename => {
    const info = await stat(path.join(UPLOADS_DIR, filename))
    return {
      filename,
      url: `/uploads/${filename}`,
      sizeBytes: info.size,
      updatedAt: info.mtime.toISOString(),
      tags: Array.isArray(metadata[filename]) ? metadata[filename].filter(tag => typeof tag === 'string') : [],
    }
  }))

  return withStats
}

// ── Backups ────────────────────────────────────────────────────────────────

export async function createBackupSnapshot(reason = 'scheduled') {
  try {
    await mkdir(BACKUP_DIR, { recursive: true })
    const stamp = new Date().toISOString().replace(/[:.]/g, '-')
    const backupPath = path.join(BACKUP_DIR, `snapshot-${stamp}-${reason}.json`)

    const payload = {
      createdAt: new Date().toISOString(),
      reason,
      adminContent: null,
      draftContent: null,
      podcastChecklist: state.podcastChecklist,
      publishState: state.publishState,
      hitStats: state.hitStats,
      visitorStats: state.visitorStats,
      contactSubmissions: state.contactSubmissions,
      studyCommunityPosts: state.studyCommunityPosts,
      replyTemplates: state.replyTemplates,
      replyHistory: state.replyHistory,
    }

    try {
      const contentRaw = await readFile(DATA_FILE, 'utf8')
      payload.adminContent = JSON.parse(contentRaw)
    } catch {
      payload.adminContent = null
    }

    try {
      const draftRaw = await readFile(DRAFT_DATA_FILE, 'utf8')
      payload.draftContent = JSON.parse(draftRaw)
    } catch {
      payload.draftContent = null
    }

    await writeFile(backupPath, JSON.stringify(payload, null, 2), 'utf8')

    const files = (await readdir(BACKUP_DIR)).filter(name => name.endsWith('.json')).sort()
    const maxFiles = BACKUP_RETENTION_DAYS
    if (files.length > maxFiles) {
      const toDelete = files.slice(0, files.length - maxFiles)
      await Promise.all(toDelete.map(name => unlink(path.join(BACKUP_DIR, name)).catch(() => {})))
    }

    state.lastBackupStatus = { ok: true, at: new Date().toISOString(), error: null, file: path.basename(backupPath) }
  } catch (err) {
    state.lastBackupStatus = { ok: false, at: new Date().toISOString(), error: String(err), file: null }
    console.error('[backup] failed to create snapshot:', err)
  }
}

export async function listBackupFiles() {
  await mkdir(BACKUP_DIR, { recursive: true })
  const files = (await readdir(BACKUP_DIR)).filter(name => name.endsWith('.json')).sort().reverse()
  return files
}

export async function readBackupPreview(filename) {
  if (typeof filename !== 'string' || filename.includes('/') || filename.includes('..') || !filename.endsWith('.json')) {
    throw new Error('Invalid backup filename')
  }

  const fullPath = path.join(BACKUP_DIR, filename)
  const [fileInfo, raw] = await Promise.all([
    stat(fullPath),
    readFile(fullPath, 'utf8'),
  ])
  const parsed = JSON.parse(raw)

  return {
    filename,
    sizeBytes: fileInfo.size,
    createdAt: typeof parsed?.createdAt === 'string' ? parsed.createdAt : null,
    reason: typeof parsed?.reason === 'string' ? parsed.reason : 'unknown',
    adminUpdatedAt: typeof parsed?.adminContent?.updatedAt === 'string' ? parsed.adminContent.updatedAt : null,
    totalHits: Number(parsed?.hitStats?.totalHits) || 0,
    totalVisits: Number(parsed?.visitorStats?.totalVisits) || 0,
  }
}

export async function listBackupPreviews() {
  const files = await listBackupFiles()
  const previews = await Promise.all(files.map(async filename => {
    try {
      return await readBackupPreview(filename)
    } catch {
      return {
        filename,
        sizeBytes: 0,
        createdAt: null,
        reason: 'unknown',
        adminUpdatedAt: null,
        totalHits: 0,
        totalVisits: 0,
      }
    }
  }))
  return previews
}

export async function restoreFromBackup(filename) {
  if (typeof filename !== 'string' || filename.includes('/') || filename.includes('..') || !filename.endsWith('.json')) {
    throw new Error('Invalid backup filename')
  }

  const fullPath = path.join(BACKUP_DIR, filename)
  const raw = await readFile(fullPath, 'utf8')
  const parsed = JSON.parse(raw)

  await createBackupSnapshot('pre-restore')

  if (parsed?.adminContent && typeof parsed.adminContent === 'object') {
    await mkdir(DATA_DIR, { recursive: true })
    await writeFile(DATA_FILE, JSON.stringify(parsed.adminContent, null, 2), 'utf8')
  }

  if (parsed?.draftContent && typeof parsed.draftContent === 'object') {
    await mkdir(DATA_DIR, { recursive: true })
    await writeFile(DRAFT_DATA_FILE, JSON.stringify(parsed.draftContent, null, 2), 'utf8')
  }

  if (parsed?.publishState && typeof parsed.publishState === 'object') {
    state.publishState = {
      draftUpdatedAt: typeof parsed.publishState.draftUpdatedAt === 'string' ? parsed.publishState.draftUpdatedAt : null,
      publishedAt: typeof parsed.publishState.publishedAt === 'string' ? parsed.publishState.publishedAt : null,
    }
  }

  state.hitStats = sanitizeLoadedHitStats(parsed?.hitStats)
  state.visitorStats = sanitizeLoadedVisitorStats(parsed?.visitorStats)
  state.contactSubmissions = sanitizeLoadedContactSubmissions(parsed?.contactSubmissions)
  state.replyTemplates = sanitizeReplyTemplates(parsed?.replyTemplates)
  state.replyHistory = sanitizeReplyHistory(parsed?.replyHistory)
  state.podcastChecklist = sanitizePodcastChecklist(parsed?.podcastChecklist)

  queueHitStatsWrite()
  queueVisitorStatsWrite()
  queueContactSubmissionsWrite()
  queueReplyTemplatesWrite()
  queueReplyHistoryWrite()
  queuePodcastChecklistWrite()

  await Promise.all([
    state.hitStatsWritePromise,
    state.visitorStatsWritePromise,
    state.contactSubmissionsWritePromise,
    state.replyTemplatesWritePromise,
    state.replyHistoryWritePromise,
    state.podcastChecklistWritePromise,
  ])
  await refreshContentCaches()
  await createBackupSnapshot('post-restore')
}

// ── Sanitize helpers used by load functions ────────────────────────────────
// (These are defined here rather than study-helpers to avoid circular imports)

export function normalizeMessageType(value) {
  if (value === 'question' || value === 'testimony' || value === 'topic') return value
  return 'general'
}

function createEmailDeliveryState(status = 'pending') {
  return {
    status,
    lastEventAt: null,
    lastEventType: null,
    resendEmailId: null,
    error: null,
  }
}

function normalizeEmailDeliveryState(value, fallbackStatus = 'pending') {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return createEmailDeliveryState(fallbackStatus)
  }
  return {
    status: typeof value.status === 'string' && value.status.trim() ? value.status.trim().slice(0, 40) : fallbackStatus,
    lastEventAt: typeof value.lastEventAt === 'string' ? value.lastEventAt : null,
    lastEventType: typeof value.lastEventType === 'string' ? value.lastEventType.trim().slice(0, 120) : null,
    resendEmailId: typeof value.resendEmailId === 'string' && value.resendEmailId.trim() ? value.resendEmailId.trim().slice(0, 200) : null,
    error: typeof value.error === 'string' && value.error.trim() ? value.error.trim().slice(0, 600) : null,
  }
}

export function normalizeContactEmailStatus(value, subscribe) {
  const base = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
  return {
    welcome: normalizeEmailDeliveryState(base.welcome, subscribe === true ? 'pending' : 'not-requested'),
    adminNotification: normalizeEmailDeliveryState(base.adminNotification, 'pending'),
    adminReply: normalizeEmailDeliveryState(base.adminReply, 'idle'),
  }
}

function sanitizeLoadedContactSubmissions(value) {
  if (!Array.isArray(value)) return []
  return value
    .slice(0, MAX_CONTACT_SUBMISSIONS)
    .filter(entry => entry && typeof entry === 'object')
    .map(entry => ({
      id: typeof entry.id === 'string' && entry.id.trim() ? entry.id.trim() : randomUUID(),
      submittedAt: typeof entry.submittedAt === 'string' ? entry.submittedAt : new Date().toISOString(),
      name: typeof entry.name === 'string' ? entry.name.trim().slice(0, 200) : '',
      email: typeof entry.email === 'string' ? entry.email.trim().slice(0, 320) : '',
      message: typeof entry.message === 'string' ? entry.message.trim().slice(0, 3000) : '',
      messageType: normalizeMessageType(entry.messageType),
      subscribe: entry.subscribe === true,
      archived: entry.archived === true,
      starred: entry.starred === true,
      snoozedUntil: typeof entry.snoozedUntil === 'string' && !isNaN(Date.parse(entry.snoozedUntil)) ? entry.snoozedUntil : null,
      threadId: typeof entry.threadId === 'string' && entry.threadId.trim() ? entry.threadId.trim() : randomUUID(),
      emailStatus: normalizeContactEmailStatus(entry.emailStatus, entry.subscribe === true),
      source: ['inbound-email', 'download', 'manual'].includes(entry.source) ? entry.source : 'contact-form',
      notes: typeof entry.notes === 'string' ? entry.notes.trim().slice(0, 2000) : '',
      tags: Array.isArray(entry.tags)
        ? [...new Set(entry.tags.filter(t => typeof t === 'string' && t.trim()).map(t => t.trim().slice(0, 50)))].slice(0, 20)
        : [],
      htmlBody: typeof entry.htmlBody === 'string' && entry.htmlBody.trim() ? entry.htmlBody : null,
      inboundTo: typeof entry.inboundTo === 'string' ? entry.inboundTo : '',
      messageId: typeof entry.messageId === 'string' ? entry.messageId : '',
      attachments: Array.isArray(entry.attachments)
        ? entry.attachments.filter(a => a && typeof a.filename === 'string').slice(0, 10).map(a => ({
            id: typeof a.id === 'string' ? a.id : randomUUID(),
            filename: String(a.filename).slice(0, 255),
            contentType: typeof a.contentType === 'string' ? a.contentType : 'application/octet-stream',
            size: typeof a.size === 'number' ? a.size : 0,
            data: typeof a.data === 'string' ? a.data : '',
          }))
        : [],
    }))
}

function sanitizeLoadedHitStats(value) {
  return {
    totalHits: Number(value?.totalHits) || 0,
    realHits: Number(value?.realHits) || 0,
    botHits: Number(value?.botHits) || 0,
    firstHitAt: typeof value?.firstHitAt === 'string' ? value.firstHitAt : null,
    lastHitAt: typeof value?.lastHitAt === 'string' ? value.lastHitAt : null,
    byPath: value?.byPath && typeof value.byPath === 'object' ? value.byPath : {},
    byPathReal: value?.byPathReal && typeof value.byPathReal === 'object' ? value.byPathReal : {},
    byPathBot: value?.byPathBot && typeof value.byPathBot === 'object' ? value.byPathBot : {},
    byDay: value?.byDay && typeof value.byDay === 'object' ? value.byDay : {},
    byDayReal: value?.byDayReal && typeof value.byDayReal === 'object' ? value.byDayReal : {},
    byDayBot: value?.byDayBot && typeof value.byDayBot === 'object' ? value.byDayBot : {},
    botReasons: value?.botReasons && typeof value.botReasons === 'object' ? value.botReasons : {},
  }
}

function sanitizeLoadedVisitorStats(value) {
  const loadedVisitors = value?.visitors && typeof value.visitors === 'object' ? value.visitors : {}

  let ipHashIndex = value?.ipHashIndex && typeof value.ipHashIndex === 'object' ? value.ipHashIndex : {}
  if (Object.keys(ipHashIndex).length === 0 && Object.keys(loadedVisitors).length > 0) {
    for (const [vid, visitor] of Object.entries(loadedVisitors)) {
      if (visitor?.ipHash && typeof visitor.ipHash === 'string') {
        ipHashIndex[visitor.ipHash] = vid
      }
    }
  }

  return {
    totalVisits: Number(value?.totalVisits) || 0,
    uniqueVisitors: Number(value?.uniqueVisitors) || 0,
    returningVisits: Number(value?.returningVisits) || 0,
    firstVisitAt: typeof value?.firstVisitAt === 'string' ? value.firstVisitAt : null,
    lastVisitAt: typeof value?.lastVisitAt === 'string' ? value.lastVisitAt : null,
    visitors: loadedVisitors,
    ipHashIndex,
    recentVisits: Array.isArray(value?.recentVisits) ? value.recentVisits.slice(0, MAX_RECENT_VISITS) : [],
    geoCacheByIp: value?.geoCacheByIp && typeof value.geoCacheByIp === 'object' ? value.geoCacheByIp : {},
  }
}

export function sanitizeReplyTemplates(value) {
  if (!Array.isArray(value)) return [...DEFAULT_REPLY_TEMPLATES]
  const out = value
    .filter(item => item && typeof item === 'object')
    .map(item => ({
      id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
      label: typeof item.label === 'string' ? item.label.trim().slice(0, 80) : '',
      subject: typeof item.subject === 'string' ? item.subject.trim().slice(0, 180) : '',
      message: typeof item.message === 'string' ? item.message.trim().slice(0, 6000) : '',
    }))
    .filter(item => item.label && item.subject && item.message)
  return out.length > 0 ? out : [...DEFAULT_REPLY_TEMPLATES]
}

export function sanitizeReplyHistory(value) {
  if (!Array.isArray(value)) return []
  return value
    .filter(item => item && typeof item === 'object')
    .slice(0, 500)
    .map(item => ({
      id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
      submissionId: typeof item.submissionId === 'string' ? item.submissionId : '',
      toEmail: typeof item.toEmail === 'string' ? item.toEmail.trim().slice(0, 320) : '',
      toName: typeof item.toName === 'string' ? item.toName.trim().slice(0, 200) : '',
      fromEmail: typeof item.fromEmail === 'string' ? item.fromEmail.trim().slice(0, 320) : 'hello@versebyversewithnate.us',
      subject: typeof item.subject === 'string' ? item.subject.trim().slice(0, 180) : '',
      preview: typeof item.preview === 'string' ? item.preview.trim().slice(0, 500) : '',
      sentAt: typeof item.sentAt === 'string' ? item.sentAt : new Date().toISOString(),
    }))
}

function sanitizeChecklistTask(task) {
  const label = typeof task?.label === 'string' ? task.label.trim().slice(0, 120) : ''
  if (!label) return null
  const phase = task?.phase === 'post' ? 'post' : 'pre'
  const id = typeof task?.id === 'string' && task.id.trim() ? task.id.trim() : randomUUID()
  return { id, label, phase }
}

export function sanitizePodcastChecklist(value) {
  const fallback = buildDefaultPodcastChecklist()
  const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}

  const taskInput = Array.isArray(source.tasks) ? source.tasks : fallback.tasks
  const seenTaskIds = new Set()
  const tasks = []

  for (const item of taskInput) {
    const safeTask = sanitizeChecklistTask(item)
    if (!safeTask) continue
    if (seenTaskIds.has(safeTask.id)) continue
    seenTaskIds.add(safeTask.id)
    tasks.push(safeTask)
  }

  if (tasks.length === 0) {
    for (const task of fallback.tasks) {
      tasks.push({ ...task })
      seenTaskIds.add(task.id)
    }
  }

  const taskIds = tasks.map(task => task.id)
  const episodesInput = Array.isArray(source.episodes) ? source.episodes : fallback.episodes
  const episodes = []

  for (const item of episodesInput) {
    if (!item || typeof item !== 'object' || Array.isArray(item)) continue
    const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID()
    const series = typeof item.series === 'string' ? item.series.trim().slice(0, 80) : ''
    const title = typeof item.title === 'string' ? item.title.trim().slice(0, 180) : ''
    const rawEpisodeNumber = Number(item.episodeNumber)
    const episodeNumber = Number.isFinite(rawEpisodeNumber) && rawEpisodeNumber >= 0
      ? Math.round(rawEpisodeNumber)
      : null
    const datePublished = typeof item.datePublished === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(item.datePublished.trim())
      ? item.datePublished.trim()
      : ''
    const sourceTasks = item.tasks && typeof item.tasks === 'object' && !Array.isArray(item.tasks)
      ? item.tasks
      : {}
    const taskState = {}
    for (const taskId of taskIds) {
      taskState[taskId] = sourceTasks[taskId] === true
    }
    const reminderDays = Number.isFinite(Number(item.reminderDays)) && Number(item.reminderDays) >= 0 ? Number(item.reminderDays) : 0
    const reminderSentAt = typeof item.reminderSentAt === 'string' && item.reminderSentAt ? item.reminderSentAt : undefined
    const startTime = typeof item.startTime === 'string' && /^\d{2}:\d{2}$/.test(item.startTime) ? item.startTime : undefined
    const PROD_STATUSES = ['idea', 'recorded', 'edited', 'scheduled', 'published']
    const productionStatus = PROD_STATUSES.includes(item.productionStatus) ? item.productionStatus : undefined
    episodes.push({ id, series, episodeNumber, title, datePublished, expanded: item.expanded === true, tasks: taskState, reminderDays, reminderSentAt, ...(startTime ? { startTime } : {}), ...(productionStatus ? { productionStatus } : {}) })
  }

  if (episodes.length === 0) {
    return fallback
  }

  return { tasks, episodes }
}

function normalizeStudyUsername(value) {
  if (typeof value !== 'string') return ''
  return value.trim().toLowerCase()
}

function isValidStudyUsername(value) {
  return /^[a-zA-Z0-9._%+\-]{1,64}@[a-zA-Z0-9.\-]{1,253}\.[a-zA-Z]{2,}$/.test(value) && value.length <= 254
}

function normalizeStudySlug(value) {
  if (typeof value !== 'string') return ''
  const trimmed = value.trim().toLowerCase()
  return /^[a-z0-9-]{1,80}$/.test(trimmed) ? trimmed : ''
}

export function sanitizeStudyUsers(value) {
  if (!Array.isArray(value)) return []
  const out = []
  const seen = new Set()

  for (const item of value) {
    const username = normalizeStudyUsername(item?.username)
    const passwordHash = typeof item?.passwordHash === 'string' ? item.passwordHash.trim() : ''
    if (!isValidStudyUsername(username) || !passwordHash || seen.has(username)) continue
    const enrolledStudySlugs = Array.isArray(item?.enrolledStudySlugs)
      ? Array.from(new Set(item.enrolledStudySlugs.map(normalizeStudySlug).filter(Boolean))).slice(0, MAX_STUDY_ENROLLMENTS_PER_USER)
      : []
    const displayName = typeof item?.displayName === 'string' ? item.displayName.trim().slice(0, 80) : ''
    const subscribeNewsletter = item?.subscribeNewsletter !== false
    const studyRemindersEnabled = item?.studyRemindersEnabled === true
    const pendingEmailChange = item?.pendingEmailChange && typeof item.pendingEmailChange === 'object' && !Array.isArray(item.pendingEmailChange)
      ? {
        newEmail: isValidStudyUsername(normalizeStudyUsername(item.pendingEmailChange.newEmail))
          ? normalizeStudyUsername(item.pendingEmailChange.newEmail)
          : '',
        tokenHash: typeof item.pendingEmailChange.tokenHash === 'string' ? item.pendingEmailChange.tokenHash.trim() : '',
        expiresAt: typeof item.pendingEmailChange.expiresAt === 'number' ? item.pendingEmailChange.expiresAt : 0,
        requestedAt: typeof item.pendingEmailChange.requestedAt === 'string' ? item.pendingEmailChange.requestedAt : null,
      }
      : null
    seen.add(username)
    out.push({
      id: typeof item?.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
      username,
      passwordHash,
      displayName,
      subscribeNewsletter,
      studyRemindersEnabled,
      enrolledStudySlugs,
      avatarUrl: typeof item?.avatarUrl === 'string' ? item.avatarUrl.trim() : '',
      createdAt: typeof item?.createdAt === 'string' ? item.createdAt : null,
      updatedAt: typeof item?.updatedAt === 'string' ? item.updatedAt : null,
      lastLoginAt: typeof item?.lastLoginAt === 'string' ? item.lastLoginAt : null,
      reengagementSentAt: typeof item?.reengagementSentAt === 'object' && item.reengagementSentAt !== null && !Array.isArray(item.reengagementSentAt) ? item.reengagementSentAt : {},
      pendingEmailChange,
      twoFaMethod: item?.twoFaMethod === 'app' || item?.twoFaMethod === 'email' ? item.twoFaMethod : null,
      totpSecret: typeof item?.totpSecret === 'string' && item.totpSecret ? item.totpSecret : null,
      totpVerified: item?.totpVerified === true,
      totpEnabledAt: typeof item?.totpEnabledAt === 'string' ? item.totpEnabledAt : null,
      totpRecoveryCodes: Array.isArray(item?.totpRecoveryCodes) ? item.totpRecoveryCodes.filter(h => typeof h === 'string') : [],
      totpSecretPending: typeof item?.totpSecretPending === 'string' ? item.totpSecretPending : undefined,
    })
  }
  return out.slice(0, MAX_STUDY_USERS)
}

export function sanitizeUserNotes(value) {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
  const out = {}
  let count = 0
  for (const [sectionId, note] of Object.entries(value)) {
    if (count >= MAX_STUDY_NOTES_PER_USER) break
    if (!/^[a-z0-9-]{1,80}$/i.test(sectionId)) continue
    if (typeof note !== 'string') continue
    const trimmed = note.trim().slice(0, MAX_STUDY_NOTE_LENGTH)
    if (!trimmed) continue
    out[sectionId] = trimmed
    count += 1
  }
  return out
}

export function sanitizeStudyProgress(value) {
  const defaultResult = { byStudy: {}, updatedAt: new Date().toISOString() }
  if (!value || typeof value !== 'object') return defaultResult

  const progress = { byStudy: {}, updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : new Date().toISOString() }
  if (value.byStudy && typeof value.byStudy === 'object') {
    for (const [studySlug, studyData] of Object.entries(value.byStudy)) {
      if (typeof studySlug !== 'string' || !studySlug.trim()) continue
      const completedSectionIds = Array.isArray(studyData?.completedSectionIds)
        ? studyData.completedSectionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
        : []
      const quizAnswers = studyData?.quizAnswers && typeof studyData?.quizAnswers === 'object' && !Array.isArray(studyData.quizAnswers)
        ? Object.fromEntries(
          Object.entries(studyData.quizAnswers)
            .filter(([sectionId]) => typeof sectionId === 'string' && sectionId.trim())
            .map(([sectionId, answers]) => [
              sectionId.trim(),
              Array.isArray(answers)
                ? answers.filter(answer => typeof answer === 'string').map(answer => answer.trim())
                : [],
            ])
        )
        : {}
      progress.byStudy[studySlug.trim().toLowerCase()] = {
        completedSectionIds: Array.from(new Set(completedSectionIds)),
        quizAnswers,
      }
    }
  }
  return progress
}

export function sanitizeStudyReminders(value) {
  const defaultResult = { users: {}, updatedAt: new Date().toISOString() }
  if (!value || typeof value !== 'object' || Array.isArray(value)) return defaultResult

  const reminders = { users: {}, updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : new Date().toISOString() }
  if (value.users && typeof value.users === 'object') {
    for (const [userId, userData] of Object.entries(value.users)) {
      if (typeof userId !== 'string' || !userId.trim()) continue
      const studies = typeof userData === 'object' && userData && !Array.isArray(userData) ? userData : {}
      const normalizedStudies = {}
      for (const [studySlug, sectionIds] of Object.entries(studies)) {
        if (typeof studySlug !== 'string' || !studySlug.trim()) continue
        const ids = Array.isArray(sectionIds)
          ? sectionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
          : []
        if (ids.length > 0) normalizedStudies[studySlug.trim().toLowerCase()] = Array.from(new Set(ids))
      }
      reminders.users[userId.trim()] = normalizedStudies
    }
  }
  return reminders
}

function sanitizeStudyCommunityReply(reply) {
  if (!reply || typeof reply !== 'object' || Array.isArray(reply)) return null
  const message = typeof reply.message === 'string' ? reply.message.trim().slice(0, 3000) : ''
  if (!message) return null
  return {
    id: typeof reply.id === 'string' && reply.id.trim() ? reply.id.trim() : randomUUID(),
    authorUserId: typeof reply.authorUserId === 'string' && reply.authorUserId.trim() ? reply.authorUserId.trim() : '',
    authorName: typeof reply.authorName === 'string' ? reply.authorName.trim().slice(0, 120) : '',
    message,
    createdAt: typeof reply.createdAt === 'string' ? reply.createdAt : new Date().toISOString(),
  }
}

export function sanitizeStudyCommunityPosts(value) {
  if (!Array.isArray(value)) return []
  return value
    .filter(item => item && typeof item === 'object' && !Array.isArray(item))
    .map(item => {
      const studySlug = normalizeStudySlug(item.studySlug)
      const sectionId = typeof item.sectionId === 'string' && /^[a-z0-9-]{1,80}$/i.test(item.sectionId) ? item.sectionId.trim() : ''
      const message = typeof item.message === 'string' ? item.message.trim().slice(0, 3000) : ''
      const replies = Array.isArray(item.replies)
        ? item.replies.map(sanitizeStudyCommunityReply).filter(Boolean).slice(0, 50)
        : []
      if (!studySlug || !message) return null
      return {
        id: typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID(),
        studySlug,
        sectionId,
        authorUserId: typeof item.authorUserId === 'string' && item.authorUserId.trim() ? item.authorUserId.trim() : '',
        authorName: typeof item.authorName === 'string' ? item.authorName.trim().slice(0, 120) : '',
        message,
        createdAt: typeof item.createdAt === 'string' ? item.createdAt : new Date().toISOString(),
        replies,
      }
    })
    .filter(Boolean)
}

// ── QR Codes ───────────────────────────────────────────────────────────────

export function queueQrCodesWrite() {
  state.qrCodesWritePromise = state.qrCodesWritePromise
    .then(async () => {
      await mkdir(DATA_DIR, { recursive: true })
      await writeFile(QR_CODES_FILE, JSON.stringify({ codes: state.qrCodes, scans: state.qrScans }, null, 2), 'utf8')
    })
    .catch(err => {
      console.error('[qr-codes] failed to write:', err)
    })
}

export function loadQrCodesFromDisk() {
  return readFile(QR_CODES_FILE, 'utf8')
    .then(raw => {
      const parsed = JSON.parse(raw)
      state.qrCodes = Array.isArray(parsed?.codes) ? parsed.codes : []
      state.qrScans = Array.isArray(parsed?.scans) ? parsed.scans : []
    })
    .catch(() => {
      state.qrCodes = []
      state.qrScans = []
    })
}
