// @ts-nocheck — app entry/bootstrap converted to .cts (Node type-stripping); Express wiring, typing deferred.
const express = require('express');
const cookieParser = require('cookie-parser');
const path = require('path');

const { getDb } = require('./db/database.cts');
const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth.cts');
const { recordError } = require('./services/statusRuntime.cts');
const { securityHeaders } = require('./middleware/securityHeaders.cts');
const { logAudit } = require('./services/auditService.cts');
const { errorFormatter } = require('./middleware/errorFormatter.cts');
const {
  importLimiter,
  exportLimiter,
  adminActionLimiter,
  oidcLimiter,
  loginLimiter,
  loginUsernameLimiter,
  passwordLimiter,
  backupOperationLimiter,
} = require('./middleware/rateLimiter.cts');
const { csrfMiddleware, csrfTokenProvider } = require('./middleware/csrf.cts');

const app = express();
const PORT = process.env.PORT || 3000;
const HOST = process.env.BIND_HOST || '0.0.0.0';
const DIST = path.join(__dirname, 'dist');

// ── Trust proxy ───────────────────────────────────────────────────────────────
// Required when running behind a reverse proxy (Docker, nginx, Traefik, etc.) so
// req.ip reflects the real client (rate limiting, audit logs) and req.secure
// reflects the original protocol (Secure cookies). Examples:
//   TRUST_PROXY=true       → trust first proxy hop (most common)
//   TRUST_PROXY=2          → trust two hops
//   TRUST_PROXY=loopback   → trust loopback addresses only
// Unset/false → no proxy trusted (direct deployment).
const TRUST_PROXY = (process.env.TRUST_PROXY || '').trim();
if (TRUST_PROXY && !/^(false|0|no|off)$/i.test(TRUST_PROXY)) {
  if (/^(true|yes|on)$/i.test(TRUST_PROXY)) {
    app.set('trust proxy', 1);
  } else if (/^\d+$/.test(TRUST_PROXY)) {
    app.set('trust proxy', parseInt(TRUST_PROXY, 10));
  } else {
    app.set('trust proxy', TRUST_PROXY); // 'loopback', named subnet, or CIDR list
  }
}

// ── Security headers (applied to every response) ──────────────────────────────
app.use(securityHeaders);

// ── CORS — disabled unless CORS_ORIGIN is explicitly configured ───────────────
// The default same-origin deployment (Express serves both API and UI) needs no
// CORS headers. Set CORS_ORIGIN=https://your-frontend.example.com only when
// the frontend is hosted on a separate origin.
if (process.env.CORS_ORIGIN) {
  const cors = require('cors');
  const allowed = process.env.CORS_ORIGIN.split(',')
    .map((s) => s.trim())
    .filter(Boolean);
  app.use(cors({ credentials: true, origin: allowed }));
}

app.use(express.json({ limit: '100kb' })); // import routes override this per-endpoint
app.use(cookieParser());

// ── CSRF token provider - sets CSRF cookie on every response ────────────────
// This ensures the CSRF token cookie is always present for API clients
app.use(csrfTokenProvider);

// ── Error response formatter ─────────────────────────────────────────────────
// Patches res.json so all error bodies follow the standardized format.
// Must be mounted BEFORE the routes so the patch is in place when they respond.
app.use(errorFormatter);

// ── Health check ────────────────────────────────────────────────────────────
// Unauthenticated liveness probe — used by Docker healthchecks and by the
// mobile app's local mode (nodejs-mobile) to detect when the embedded server
// is ready to serve requests.
app.get('/api/health', (req, res) => {
  res.json({ ok: true });
});

// ── API ───────────────────────────────────────────────────────────────────────

// Auth — rate limiters applied at middleware level to prevent bypass
// Login endpoint is public entry point, exempt from CSRF (no session to hijack yet)
// Note: passwordLimiter is NOT applied here — it's for password change endpoints

// Helper: skip rate limiting if no users exist (first-run scenario)
function skipRateLimitIfNoUsers(limiter) {
  return (req, res, next) => {
    try {
      const db = getDb();
      const userCount = db.prepare('SELECT COUNT(*) AS count FROM users').get().count;
      if (userCount === 0) {
        return next(); // first run — no rate limiting
      }
    } catch (err) {
      // DB not ready yet — allow request to proceed
      console.log('[skipRateLimit] DB not initialized, allowing request');
      return next();
    }
    // User exists — apply rate limiter
    return limiter(req, res, next);
  };
}

// Mount login router with conditional rate limiting
// If no users exist, rate limit is bypassed; otherwise it applies.
// Two layers: per-IP (all attempts) and per-username (failed attempts only),
// so one IP can't burn 10 tries against every account, and a distributed
// attacker can't brute-force a single account from many IPs.
app.use('/api/auth/login', skipRateLimitIfNoUsers(loginLimiter));
app.use('/api/auth/login', skipRateLimitIfNoUsers(loginUsernameLimiter));
// Login skips CSRF inside routes/auth because no authenticated session exists yet.
// Authenticated state-changing auth routes, including logout-all and password
// changes, require the SPA's x-csrf-token header like other mutating requests.
// All other auth routes require CSRF (loginLimiter applied via /api/auth/login above)
// Note: passwordLimiter is applied individually on routes that actually change passwords
app.use('/api/auth', csrfMiddleware, require('./routes/auth.cts'));

// OIDC — rate-limited; returns 501 gracefully when OIDC is not configured
app.use('/api/auth/oidc', csrfMiddleware, oidcLimiter, require('./routes/authOidc.cts'));

// Admin — all routes already require auth+admin; mutation-heavy routes get
// an additional per-IP rate limit applied to the whole admin namespace
// Backup operations have dedicated rate limiting (5 per hour) to prevent resource exhaustion
// NOTE: backupOperationLimiter is applied per-route in routes/admin.js to avoid blocking non-backup admin actions
app.use(
  '/api/admin',
  csrfMiddleware,
  requireAuth,
  requireAdmin,
  adminActionLimiter,
  require('./routes/admin.cts'),
);

app.use('/api/tracker', csrfMiddleware, requireAuth, requireUser, require('./routes/tracker.cts'));
app.use('/api/bills', csrfMiddleware, requireAuth, requireUser, require('./routes/bills.cts'));
app.use(
  '/api/subscriptions',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/subscriptions.cts'),
);
app.use(
  '/api/payments',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/payments.cts'),
);
app.use(
  '/api/data-sources',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/dataSources.cts'),
);
app.use(
  '/api/transactions',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/transactions.cts'),
);
app.use('/api/matches', csrfMiddleware, requireAuth, requireUser, require('./routes/matches.cts'));
app.use(
  '/api/categories',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/categories.cts'),
);
app.use(
  '/api/settings',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/settings.cts'),
);
app.use('/api/user', csrfMiddleware, requireAuth, requireUser, require('./routes/user.cts'));
app.use('/api/calendar', require('./routes/calendarFeed.cts'));
app.use(
  '/api/calendar',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/calendar.cts'),
);
app.use('/api/summary', csrfMiddleware, requireAuth, requireUser, require('./routes/summary.cts'));
app.use(
  '/api/monthly-starting-amounts',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/monthly-starting-amounts.cts'),
);
app.use(
  '/api/analytics',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/analytics.cts'),
);
app.use(
  '/api/spending',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/spending.cts'),
);
app.use(
  '/api/snowball',
  csrfMiddleware,
  requireAuth,
  requireUser,
  require('./routes/snowball.cts'),
);
app.use('/api/notifications', csrfMiddleware, requireAuth, require('./routes/notifications.cts'));
app.use('/api/status', csrfMiddleware, requireAuth, requireAdmin, require('./routes/status.cts'));
app.use('/api/about', require('./routes/about.cts')); // public
app.use(
  '/api/about-admin',
  adminActionLimiter,
  csrfMiddleware,
  requireAuth,
  requireAdmin,
  require('./routes/aboutAdmin.cts'),
); // admin-only
app.use('/api/privacy', require('./routes/privacy.cts')); // public
app.use('/api/version', require('./routes/version.cts')); // public

// Profile — rate limit only on password-change, not all profile reads
app.use('/api/profile', csrfMiddleware, requireAuth, requireUser, require('./routes/profile.cts'));

// Export / Import — per-IP rate limited to deter abuse and resource exhaustion
const importRoutes = require('./routes/import.cts');
app.use(
  '/api/export',
  csrfMiddleware,
  requireAuth,
  requireUser,
  exportLimiter,
  require('./routes/export.cts'),
);
app.use('/api/import', csrfMiddleware, requireAuth, requireUser, importLimiter, importRoutes);
app.use('/api/imports', csrfMiddleware, requireAuth, requireUser, importLimiter, importRoutes);

// ── API 404 — unknown /api/* routes must return JSON, not the SPA shell ──────
// Without this, the catch-all below serves index.html with HTTP 200 for any
// unrecognized API path, which API clients then fail to parse as JSON.
app.all('/api/*splat', (req, res) => {
  res.status(404).json({
    error: 'NOT_FOUND',
    message: 'Unknown API route',
    code: 'NOT_FOUND',
  });
});

// ── Retired legacy UI ────────────────────────────────────────────────────────
app.all(['/legacy', '/legacy/*splat'], (req, res) => {
  res.status(410).send('The legacy UI has been retired.');
});

// ── Modern UI (Vite build) ────────────────────────────────────────────────────
app.get('/login.html', (req, res) => res.redirect(302, '/login'));
app.use(express.static(DIST));
// Ensure CSRF cookie is set for SPA by calling getCsrfToken before sending index.html
const { getCsrfToken } = require('./middleware/csrf.cts');
app.get('/{*splat}', (req, res) => {
  // Set CSRF cookie if not present (needed for SPA to read token)
  getCsrfToken(req, res);
  res.sendFile(path.join(DIST, 'index.html'));
});

// ── Global error handler ──────────────────────────────────────────────────────
// Never expose stack traces, internal paths, or raw error objects in responses.
app.use((err, req, res, next) => {
  // If a response was already (partially) sent, we can't write a new body/status —
  // delegate to Express's default handler, which closes the connection cleanly.
  if (res.headersSent) return next(err);

  const status = err.status || 500;
  // Only server errors (5xx) are logged/recorded; 4xx are expected client errors
  // (validation, auth, not-found) and must not spam the error tracker.
  if (status >= 500) {
    recordError('Express', err);
    console.error('[error]', err.stack || err.message || String(err));
  }

  if (/^\/api\/imports?\//.test(req.path || '')) {
    const isBodyError = err.type === 'entity.too.large' || err instanceof SyntaxError;
    const code = isBodyError ? 'IMPORT_REQUEST_ERROR' : 'IMPORT_SERVER_ERROR';
    return res.status(err.status || (isBodyError ? 400 : 500)).json({
      error: code,
      message: isBodyError
        ? 'The import request could not be read. Please retry with a smaller or valid file.'
        : 'Unexpected import server error. Please try again.',
      code,
    });
  }

  // Single canonical error body: { error, message, code, field? } (utils/apiError).
  const { formatError } = require('./utils/apiError.cts');
  res.status(status).json(formatError(err));
});

// ── Safety net: unhandled promise rejections — log + record, don't crash ─────
// (Most rejections here are non-fatal side effects; a crash would be worse than
// surfacing them. Truly fatal states are handled by uncaughtException below.)
process.on('unhandledRejection', (reason) => {
  console.error(
    '[server] Unhandled promise rejection:',
    reason?.stack || reason?.message || reason,
  );
  try {
    recordError('unhandledRejection', reason instanceof Error ? reason : new Error(String(reason)));
  } catch {}
});

// An uncaught exception leaves the process in an undefined state — record it and
// exit non-zero so the supervisor (Docker `restart: unless-stopped`) restarts clean.
process.on('uncaughtException', (err) => {
  console.error('[server] Uncaught exception:', err?.stack || err);
  try {
    recordError('uncaughtException', err);
  } catch {}
  process.exit(1);
});

// ── Graceful shutdown ────────────────────────────────────────────────────────
// Docker `stop` sends SIGTERM. Stop accepting connections, let in-flight requests
// finish, then checkpoint the WAL and close the DB so no write is lost. Hard-exit
// if it stalls.
let shuttingDown = false;
function gracefulShutdown(signal, server) {
  if (shuttingDown) return;
  shuttingDown = true;
  console.log(`[server] ${signal} received — shutting down gracefully`);

  const finish = (code) => {
    try {
      const { getDb, closeDb } = require('./db/database.cts');
      try {
        getDb().pragma('wal_checkpoint(TRUNCATE)');
      } catch {}
      closeDb();
      console.log('[server] database closed cleanly');
    } catch (e) {
      console.error('[server] error closing database:', e?.message || e);
    }
    process.exit(code);
  };

  server.close(() => finish(0));
  // Don't let a hung connection block shutdown forever.
  setTimeout(() => {
    console.error('[server] shutdown timed out after 10s — forcing exit');
    finish(1);
  }, 10000).unref();
}

// ── Bootstrap ─────────────────────────────────────────────────────────────────
async function main() {
  const db = getDb();

  // Migrate DB-key-encrypted secrets to env key when TOKEN_ENCRYPTION_KEY is set
  const { reEncryptWithEnvKey } = require('./services/encryptionService.cts');
  try {
    reEncryptWithEnvKey(db);
  } catch (err) {
    console.error('[encryption] Startup key migration failed:', err.message);
  }

  // Run session cleanup on startup
  const { cleanupExpiredSessions } = require('./db/database.cts');
  try {
    console.log('[cleanup] Running session cleanup on startup');
    cleanupExpiredSessions();
  } catch (err) {
    console.error('[cleanup-error] Failed to run startup session cleanup:', err.message);
  }

  const userCount = db.prepare('SELECT COUNT(*) AS count FROM users').get().count;
  if (userCount === 0) await require('./setup/firstRun.cts').run(db);

  // [seed] Check for and create regular user if INIT_REGULAR_USER/INIT_REGULAR_PASS are set
  if (process.env.INIT_REGULAR_USER && process.env.INIT_REGULAR_PASS) {
    const regularUser = process.env.INIT_REGULAR_USER;
    const regularPass = process.env.INIT_REGULAR_PASS;

    // Validate password length
    if (regularPass && regularPass.length < 8) {
      console.error('[seed] INIT_REGULAR_PASS must be at least 8 characters');
      process.exit(1);
    }

    // Wrap user creation in a transaction to prevent race conditions
    const createRegularUser = db.transaction(() => {
      const existingRegular = db
        .prepare('SELECT id FROM users WHERE username = ?')
        .get(regularUser);
      if (!existingRegular) {
        const bcrypt = require('bcryptjs');
        const regularHash = bcrypt.hashSync(regularPass, 12);
        db.prepare(
          `
          INSERT INTO users (username, password_hash, role, first_login, must_change_password, is_default_admin)
          VALUES (?, ?, 'user', 0, 0, 0)
        `,
        ).run(regularUser, regularHash);
        console.log(`[seed] Regular user "${regularUser}" created.`);
        return true;
      } else {
        // Update existing regular user's password and reset flags
        const bcrypt = require('bcryptjs');
        const regularHash = bcrypt.hashSync(regularPass, 12);
        db.prepare(
          'UPDATE users SET password_hash = ?, first_login = 0, must_change_password = 0 WHERE id = ?',
        ).run(regularHash, existingRegular.id);
        logAudit({
          user_id: existingRegular.id,
          action: 'seed.flag_reset',
          entity_type: 'user',
          details: {
            username: regularUser,
            flags: ['first_login', 'must_change_password'],
            source: 'server-seed',
          },
        });
        console.log(`[seed] Regular user "${regularUser}" password updated and flags reset.`);
        return false;
      }
    });
    createRegularUser();
  }

  const server = app.listen(PORT, HOST, () => {
    console.log(`Bill Tracker running on ${HOST}:${PORT}`);
    if (userCount > 0) console.log(`Users found: ${userCount}`);

    // Set up periodic session cleanup
    const { cleanupExpiredSessions } = require('./db/database.cts');
    const rawInterval = process.env.SESSION_CLEANUP_INTERVAL_MS;
    let CLEANUP_INTERVAL_MS = 86400000; // 24 hours default
    if (rawInterval !== undefined) {
      const parsed = parseInt(rawInterval, 10);
      if (!isNaN(parsed) && parsed > 0 && parsed <= 604800000) {
        // max 7 days
        CLEANUP_INTERVAL_MS = parsed;
      } else {
        console.warn(
          `[cleanup] Invalid SESSION_CLEANUP_INTERVAL_MS: "${rawInterval}". Using default 24h.`,
        );
      }
    }

    // Run cleanup periodically
    setInterval(() => {
      try {
        console.log('[cleanup] Running periodic session cleanup');
        cleanupExpiredSessions();
      } catch (err) {
        console.error('[cleanup-error] Failed to run periodic session cleanup:', err.message);
      }
    }, CLEANUP_INTERVAL_MS);

    console.log(`[cleanup] Scheduled periodic cleanup every ${CLEANUP_INTERVAL_MS}ms`);

    // Start SimpleFIN auto-sync worker (no-op when bank sync is disabled)
    try {
      require('./services/bankSyncWorker.cts').start();
    } catch (err) {
      console.error('[bankSync] Failed to start auto-sync worker:', err.message);
    }

    // Start scheduled database backups only when enabled in Admin settings.
    try {
      const backupScheduler = require('./services/backupScheduler.cts');
      if (backupScheduler.getScheduleStatus().enabled) {
        backupScheduler.start();
      }
    } catch (err) {
      console.error('[backupScheduler] Failed to start scheduled backup worker:', err.message);
    }

    // Start daily worker (autopay marking, notifications, session pruning, cleanup)
    try {
      require('./workers/dailyWorker.cts').start();
    } catch (err) {
      console.error('[dailyWorker] Failed to start daily worker:', err.message);
    }
  });

  // Wire graceful shutdown once the server handle exists.
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM', server));
  process.on('SIGINT', () => gracefulShutdown('SIGINT', server));
}

main().catch((err) => {
  console.error('Failed to start server:', err);
  process.exit(1);
});
