import { mkdir, mkdtemp, readdir, rm, cp, writeFile, unlink } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import express from 'express'
import * as tar from 'tar'
import { requireAdminAuth } from '../auth.js'
import { DATA_DIR } from '../config.js'
import { state } from '../state.js'
import {
  loadHitStatsFromDisk,
  loadVisitorStatsFromDisk,
  loadContactSubmissionsFromDisk,
  loadReplyTemplatesFromDisk,
  loadReplyHistoryFromDisk,
  loadQuestionsFromDisk,
  loadDraftQuestionsFromDisk,
  loadStudyUsersFromDisk,
  loadStudyCommunityFromDisk,
  loadStudyRemindersFromDisk,
  loadStudyCommentsFromDisk,
  loadStudyCertificatesFromDisk,
  loadEpisodeScriptsFromDisk,
  migrateStudyNotesIfNeeded,
  loadDownloadCountsFromDisk,
  loadQrCodesFromDisk,
  loadEpisodePlaysFromDisk,
  loadPodcastChecklistFromDisk,
  loadAnalyticsEventsFromDisk,
  createBackupSnapshot,
  refreshContentCaches,
} from '../data.js'

// Portable server settings captured into env-snapshot.env at export time so a
// backup carries the runtime configuration too (Docker deployments have no
// .env file — settings live in container env vars). Machine-specific vars
// (PORT, NODE_ENV, SITEFORGE_DATA_DIR, ALLOW_INSECURE_COOKIES, build info)
// are deliberately excluded. Values are NOT applied automatically on restore.
const ENV_SNAPSHOT_KEYS = [
  'ADMIN_PASSWORD',
  'RESEND_API_KEY',
  'RESEND_CONTACTS_API_KEY',
  'RESEND_WEBHOOK_TOKEN',
  'INBOUND_EMAIL_SECRET',
  'RESEND_FROM',
  'RESEND_TO',
  'RESEND_REPLY_TO',
  'RESEND_AUTOMATION_WELCOME',
  'RESEND_SEGMENT_ID',
  'RESEND_REMINDER_SUBJECT',
  'RESEND_WELCOME_SUBJECT',
  'RESEND_WELCOME_IMAGE_URL',
  'RESEND_WELCOME_EPISODE_URL',
  'RESEND_WELCOME_WEBSITE_URL',
  'RESEND_WELCOME_SPOTIFY_URL',
  'RESEND_WELCOME_APPLE_URL',
  'RESEND_WELCOME_AMAZON_URL',
  'CACHE_PURGE_WEBHOOK_URL',
  'DEPLOY_WEBHOOK_URL',
  'CONTACT_EMAIL_COOLDOWN_MS',
  'TRUST_PROXY_HOPS',
  'TITUS_STUDY_FILE',
  'TITUS_STUDY_DOWNLOAD_NAME',
]

// Write env snapshot into DATA_DIR for inclusion in the archive, then delete
// it immediately after the stream finishes so secrets don't linger on disk.
async function writeEnvSnapshot() {
  const lines = [
    '# Siteforge environment snapshot — regenerated on every full backup export.',
    '# Contains secrets (admin password, API keys): keep this backup private.',
    '# These values are NOT applied automatically on restore. Set them as',
    '# container environment variables (or in .env) on the new server.',
    `# Exported at ${new Date().toISOString()}`,
    '',
  ]
  for (const key of ENV_SNAPSHOT_KEYS) {
    const value = process.env[key]
    if (typeof value === 'string' && value.trim() !== '') lines.push(`${key}=${value}`)
  }
  await writeFile(path.join(DATA_DIR, 'env-snapshot.env'), `${lines.join('\n')}\n`, 'utf8')
}

async function deleteEnvSnapshot() {
  await unlink(path.join(DATA_DIR, 'env-snapshot.env')).catch(() => {})
}

// Files that identify an archive as a Siteforge data backup. At least one
// must be present at the top level of an uploaded archive before we restore.
const KNOWN_DATA_FILES = [
  'admin-content.json',
  'admin-content-draft.json',
  'study-users.json',
  'hit-stats.json',
  'visitor-stats.json',
  'questions.json',
]

// Wait for every queued disk write so the archive reflects current state.
async function flushPendingWrites() {
  await Promise.allSettled([
    state.hitStatsWritePromise,
    state.visitorStatsWritePromise,
    state.contactSubmissionsWritePromise,
    state.questionsWritePromise,
    state.draftQuestionsWritePromise,
    state.replyTemplatesWritePromise,
    state.replyHistoryWritePromise,
    state.podcastChecklistWritePromise,
    state.studyUsersWritePromise,
    state.studyCommunityWritePromise,
    state.studyRemindersWritePromise,
    state.studyCommentsWritePromise,
    state.studyCertificatesWritePromise,
    state.episodeScriptsWritePromise,
    state.downloadCountsWritePromise,
    state.episodePlaysWritePromise,
    state.analyticsEventsWritePromise,
    state.qrCodesWritePromise,
    ...state.studyNotesWriteQueues.values(),
    ...state.studyProgressWriteQueues.values(),
  ])
}

// Rebuild all in-memory state from whatever is now on disk (mirrors startup).
async function reloadStateFromDisk() {
  state.studyNotesCache.clear()
  state.studyNotesWriteQueues.clear()
  state.studyProgressCache.clear()
  state.studyProgressWriteQueues.clear()
  // Restored study users may not match current sessions — force re-login.
  state.studySessions.clear()

  await Promise.all([
    loadHitStatsFromDisk(),
    loadVisitorStatsFromDisk(),
    loadContactSubmissionsFromDisk(),
    loadReplyTemplatesFromDisk(),
    loadReplyHistoryFromDisk(),
    loadQuestionsFromDisk(),
    loadDraftQuestionsFromDisk(),
    loadStudyUsersFromDisk(),
    loadStudyCommunityFromDisk(),
    loadStudyRemindersFromDisk(),
    loadStudyCommentsFromDisk(),
    loadStudyCertificatesFromDisk(),
    loadEpisodeScriptsFromDisk(),
    migrateStudyNotesIfNeeded(),
    loadDownloadCountsFromDisk(),
    loadQrCodesFromDisk(),
    loadEpisodePlaysFromDisk(),
    loadPodcastChecklistFromDisk(),
    loadAnalyticsEventsFromDisk(),
    refreshContentCaches(),
  ])
}

export function register(app) {
  // Download the entire data directory as a tar.gz (excluding the automatic
  // snapshot folder, which is derived from the other files).
  app.get('/api/admin-backup/export', requireAdminAuth, async (_req, res) => {
    try {
      await flushPendingWrites()
      await writeEnvSnapshot()
      const entries = (await readdir(DATA_DIR)).filter(name => name !== 'backups')
      if (entries.length === 0) {
        res.status(500).json({ message: 'Data directory is empty — nothing to export.' })
        return
      }

      const stamp = new Date().toISOString().replace(/[:.]/g, '-')
      res.setHeader('Content-Type', 'application/gzip')
      res.setHeader('Content-Disposition', `attachment; filename="siteforge-data-${stamp}.tar.gz"`)

      const archive = tar.create({ gzip: true, cwd: DATA_DIR, portable: true }, entries)
      archive.on('error', err => {
        console.error('[admin-backup] export stream failed:', err)
        res.destroy(err)
      })
      archive.on('end', () => { deleteEnvSnapshot() })
      res.on('close', () => { deleteEnvSnapshot() })
      archive.pipe(res)
    } catch (err) {
      console.error('[admin-backup] export failed:', err)
      deleteEnvSnapshot()
      if (!res.headersSent) res.status(500).json({ message: 'Full backup export failed.' })
    }
  })

  // Restore the entire data directory from an uploaded tar.gz produced by the
  // export endpoint, then reload all in-memory state from the restored files.
  app.post(
    '/api/admin-backup/import',
    requireAdminAuth,
    express.raw({ type: () => true, limit: '500mb' }),
    async (req, res) => {
      let workDir = null
      try {
        const body = req.body
        if (!Buffer.isBuffer(body) || body.length === 0) {
          res.status(400).json({ message: 'Upload the .tar.gz file produced by the full backup export.' })
          return
        }
        if (body[0] !== 0x1f || body[1] !== 0x8b) {
          res.status(400).json({ message: 'File is not a gzip archive (.tar.gz expected).' })
          return
        }

        workDir = await mkdtemp(path.join(os.tmpdir(), 'siteforge-import-'))
        const archivePath = path.join(workDir, 'import.tar.gz')
        await writeFile(archivePath, body)

        const extractDir = path.join(workDir, 'extracted')
        await mkdir(extractDir)
        // node-tar strips absolute paths and rejects entries that escape cwd.
        await tar.extract({ file: archivePath, cwd: extractDir })

        const extractedEntries = await readdir(extractDir)
        if (!extractedEntries.some(name => KNOWN_DATA_FILES.includes(name))) {
          res.status(400).json({ message: 'Archive does not look like a Siteforge data backup.' })
          return
        }

        // Settle queued writes so nothing overwrites the restored files, and
        // keep a snapshot of the pre-import state in the backups folder.
        await flushPendingWrites()
        await createBackupSnapshot('pre-import')

        // Replace current data with the archive contents. The snapshot folder
        // is preserved unless the archive itself contains one.
        const currentEntries = await readdir(DATA_DIR)
        for (const name of currentEntries) {
          if (name === 'backups' && !extractedEntries.includes('backups')) continue
          await rm(path.join(DATA_DIR, name), { recursive: true, force: true })
        }
        for (const name of extractedEntries) {
          await cp(path.join(extractDir, name), path.join(DATA_DIR, name), { recursive: true })
        }

        await reloadStateFromDisk()
        await createBackupSnapshot('post-import')

        res.json({ ok: true, restoredEntries: extractedEntries.length })
      } catch (err) {
        console.error('[admin-backup] import failed:', err)
        res.status(500).json({ message: 'Full data restore failed. Check the server logs — data may need manual attention.' })
      } finally {
        if (workDir) await rm(workDir, { recursive: true, force: true }).catch(() => {})
      }
    },
  )
}
