const express      = require('express');
const cookieParser = require('cookie-parser');
const path         = require('path');

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

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'));

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

// 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'));

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

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

// Export / Import — per-IP rate limited to deter abuse and resource exhaustion
const importRoutes = require('./routes/import');
app.use('/api/export',       csrfMiddleware, requireAuth, requireUser,  exportLimiter, require('./routes/export'));
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');
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);
  recordError('Express', err);
  console.error('[error]', err.message || String(err));

  if (/^\/api\/imports?\//.test(req.path || '')) {
    const isBodyError = err.type === 'entity.too.large' || err instanceof SyntaxError;
    return res.status(err.status || (isBodyError ? 400 : 500)).json({
      error: 'Import request failed',
      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: isBodyError ? 'IMPORT_REQUEST_ERROR' : 'IMPORT_SERVER_ERROR',
    });
  }

  res.status(err.status || 500).json({
    error: 'Internal server error',
    code: 'INTERNAL_ERROR',
  });
});

// ── Safety net: log unhandled promise rejections instead of crashing ─────────
process.on('unhandledRejection', (reason) => {
  console.error('[server] Unhandled promise rejection:', reason?.message || reason);
});

// ── 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');
  try {
    reEncryptWithEnvKey(db);
  } catch (err) {
    console.error('[encryption] Startup key migration failed:', err.message);
  }

  // Run session cleanup on startup
  const { cleanupExpiredSessions } = require('./db/database');
  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').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();
  }

  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');
    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').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');
      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').start();
    } catch (err) {
      console.error('[dailyWorker] Failed to start daily worker:', err.message);
    }
  });
}

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