// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http';
const express = require('express');
const router = express.Router();
const { getDb, rollbackMigration } = require('../db/database.cts');
const {
  getBankSyncConfig,
  setBankSyncEnabled,
  setSyncIntervalHours,
  setSyncDays,
  setDebugLogging,
} = require('../services/bankSyncConfigService.cts');
const { getStatus: getBankSyncWorkerStatus } = require('../services/bankSyncWorker.cts');
const { isEnvKeyActive, keyFingerprint } = require('../services/encryptionService.cts');
const { hashPassword } = require('../services/authService.cts');
const { logAudit } = require('../services/auditService.cts');
const {
  createBackup,
  deleteBackup,
  getBackupFile,
  importBackupBuffer,
  listBackups,
  restoreBackup,
} = require('../services/backupService.cts');
const {
  getScheduleStatus,
  runScheduledBackupNow,
  saveSettings: saveBackupScheduleSettings,
} = require('../services/backupScheduler.cts');
const {
  getCleanupStatus,
  runAllCleanup,
  validateAndApplySettings: applyCleanupSettings,
} = require('../services/cleanupService.cts');
const { backupOperationLimiter } = require('../middleware/rateLimiter.cts');

// All routes mounted at /api/admin (requireAuth + requireAdmin applied at server level)

// Returns a validated positive integer from req.params.id, or null.
function parseUserId(params) {
  const n = parseInt(params.id, 10);
  return Number.isInteger(n) && n > 0 ? n : null;
}

function sendError(res, err) {
  const status = err.status || 500;
  res.status(status).json({ error: status === 500 ? 'Backup operation failed' : err.message });
}

// GET /api/admin/has-users
router.get('/has-users', (req: Req, res: Res) => {
  const count = getDb().prepare('SELECT COUNT(*) AS n FROM users WHERE id != ?').get(req.user.id).n;
  res.json({ has_users: count > 0 });
});

// GET /api/admin/users
router.get('/users', (req: Req, res: Res) => {
  res.json(
    getDb()
      .prepare(
        'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users ORDER BY is_default_admin DESC, role DESC, username ASC',
      )
      .all(),
  );
});

// POST /api/admin/backups
router.post('/backups', backupOperationLimiter, async (req: Req, res: Res) => {
  try {
    const backup = await createBackup();
    res.status(201).json(backup);
  } catch (err) {
    sendError(res, err);
  }
});

// GET /api/admin/backups
router.get('/backups', backupOperationLimiter, (req: Req, res: Res) => {
  try {
    res.json({ backups: listBackups() });
  } catch (err) {
    sendError(res, err);
  }
});

// GET /api/admin/backups/settings
router.get('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => {
  try {
    res.json(getScheduleStatus());
  } catch (err) {
    sendError(res, err);
  }
});

// PUT /api/admin/backups/settings
router.put('/backups/settings', backupOperationLimiter, (req: Req, res: Res) => {
  try {
    res.json(saveBackupScheduleSettings(req.body));
  } catch (err) {
    sendError(res, err);
  }
});

// POST /api/admin/backups/run-scheduled-now
router.post('/backups/run-scheduled-now', backupOperationLimiter, async (req: Req, res: Res) => {
  try {
    res.status(201).json(await runScheduledBackupNow());
  } catch (err) {
    sendError(res, err);
  }
});

// POST /api/admin/backups/import
router.post(
  '/backups/import',
  backupOperationLimiter,
  express.raw({
    type: ['application/octet-stream', 'application/x-sqlite3', 'application/vnd.sqlite3'],
    limit: '100mb',
  }),
  async (req: Req, res: Res) => {
    try {
      // Extract expected checksum from request headers or query
      const expectedChecksum = req.headers['x-checksum-sha256'] || req.query.checksum;

      const backup = await importBackupBuffer(req.body, {
        expectedChecksum: expectedChecksum ? String(expectedChecksum).trim() : undefined,
      });
      res.status(201).json(backup);
    } catch (err) {
      sendError(res, err);
    }
  },
);

// GET /api/admin/backups/:id/download
router.get('/backups/:id/download', (req: Req, res: Res) => {
  try {
    const backup = getBackupFile(req.params.id);
    res.setHeader('Content-Type', 'application/octet-stream');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.download(backup.path, backup.metadata.id, (err) => {
      if (err && !res.headersSent) sendError(res, err);
    });
  } catch (err) {
    sendError(res, err);
  }
});

// POST /api/admin/backups/:id/restore
router.post('/backups/:id/restore', backupOperationLimiter, async (req: Req, res: Res) => {
  try {
    res.json(await restoreBackup(req.params.id));
  } catch (err) {
    sendError(res, err);
  }
});

// DELETE /api/admin/backups/:id
router.delete('/backups/:id', backupOperationLimiter, (req: Req, res: Res) => {
  try {
    res.json(deleteBackup(req.params.id));
  } catch (err) {
    sendError(res, err);
  }
});

// POST /api/admin/users
router.post('/users', async (req: Req, res: Res) => {
  const { username, password } = req.body;
  if (!username || username.length < 3)
    return res.status(400).json({ error: 'Username must be at least 3 characters' });
  if (!password || password.length < 8)
    return res.status(400).json({ error: 'Password must be at least 8 characters' });

  const db = getDb();
  if (db.prepare('SELECT id FROM users WHERE username = ?').get(username))
    return res.status(409).json({ error: 'Username already taken' });

  try {
    const hash = await hashPassword(password);
    const result = db
      .prepare(
        "INSERT INTO users (username, password_hash, role, first_login) VALUES (?, ?, 'user', 1)",
      )
      .run(username, hash);

    const created = db
      .prepare(
        'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users WHERE id = ?',
      )
      .get(result.lastInsertRowid);

    logAudit({
      user_id: req.user.id,
      action: 'admin.user.create',
      entity_type: 'user',
      entity_id: created.id,
      details: { created_username: username },
      ip_address: req.ip,
      user_agent: req.get('user-agent'),
    });

    res.status(201).json(created);
  } catch (err) {
    console.error('[admin] create-user error:', err.message);
    res.status(500).json({ error: 'Failed to create user' });
  }
});

// PUT /api/admin/users/:id/password
router.put('/users/:id/password', async (req: Req, res: Res) => {
  const targetId = parseUserId(req.params);
  if (!targetId) return res.status(400).json({ error: 'Invalid user ID' });

  const { password } = req.body;
  if (!password || password.length < 8)
    return res.status(400).json({ error: 'Password must be at least 8 characters' });

  const db = getDb();
  const user = db.prepare('SELECT * FROM users WHERE id = ?').get(targetId);
  if (!user) return res.status(404).json({ error: 'User not found' });

  try {
    const hash = await hashPassword(password);
    db.prepare(
      "UPDATE users SET password_hash=?, must_change_password=1, updated_at=datetime('now') WHERE id=?",
    ).run(hash, targetId);
    db.prepare('DELETE FROM sessions WHERE user_id = ?').run(targetId);

    logAudit({
      user_id: req.user.id,
      action: 'admin.password.reset',
      entity_type: 'user',
      entity_id: targetId,
      details: { target_username: user.username },
      ip_address: req.ip,
      user_agent: req.get('user-agent'),
    });

    res.json({ success: true });
  } catch (err) {
    console.error('[admin] reset-password error:', err.message);
    res.status(500).json({ error: 'Failed to reset password' });
  }
});

// PUT /api/admin/users/:id/role
// Promote/demote an existing user. Prevents removing the last admin or
// changing your own role mid-session.
router.put('/users/:id/role', (req: Req, res: Res) => {
  const targetId = parseUserId(req.params);
  if (!targetId) return res.status(400).json({ error: 'Invalid user ID' });

  const { role } = req.body;
  if (!['admin', 'user'].includes(role)) {
    return res.status(400).json({ error: 'role must be "admin" or "user"' });
  }
  const db = getDb();
  const user = db.prepare('SELECT * FROM users WHERE id = ?').get(targetId);
  if (!user) return res.status(404).json({ error: 'User not found' });

  if (req.user?.id === targetId) {
    return res.status(400).json({ error: 'You cannot change your own admin role.' });
  }

  if (user.role === 'admin' && role === 'user') {
    const adminCount = db.prepare("SELECT COUNT(*) AS n FROM users WHERE role = 'admin'").get().n;
    if (adminCount <= 1) {
      return res.status(400).json({ error: 'Cannot remove the last admin account.' });
    }
  }

  // SECURITY FIX (2026-05-08): Delete all sessions for the target user when role changes.
  // This forces re-authentication with the new role, preventing session hijacking
  // from being used to bypass privilege checks.
  db.prepare('DELETE FROM sessions WHERE user_id = ?').run(targetId);

  const previousRole = user.role;
  db.prepare("UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?").run(
    role,
    targetId,
  );

  logAudit({
    user_id: req.user.id,
    action: 'role.change',
    entity_type: 'user',
    entity_id: targetId,
    details: { old_role: previousRole, new_role: role },
    ip_address: req.ip,
    user_agent: req.get('user-agent'),
  });

  const updated = db
    .prepare(
      'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users WHERE id = ?',
    )
    .get(targetId);

  res.json(updated);
});

// PUT /api/admin/users/:id/active
router.put('/users/:id/active', (req: Req, res: Res) => {
  const targetId = parseUserId(req.params);
  if (!targetId) return res.status(400).json({ error: 'Invalid user ID' });

  const active = req.body?.active ? 1 : 0;
  const db = getDb();
  const user = db.prepare('SELECT * FROM users WHERE id = ?').get(targetId);
  if (!user) return res.status(404).json({ error: 'User not found' });
  if (req.user?.id === targetId) {
    return res.status(400).json({ error: 'You cannot deactivate your own account.' });
  }

  db.prepare("UPDATE users SET active = ?, updated_at = datetime('now') WHERE id = ?").run(
    active,
    targetId,
  );
  if (!active) db.prepare('DELETE FROM sessions WHERE user_id = ?').run(targetId);

  res.json(
    db
      .prepare(
        'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users WHERE id = ?',
      )
      .get(targetId),
  );
});

// PUT /api/admin/users/:id/username
router.put('/users/:id/username', (req: Req, res: Res) => {
  const { username } = req.body;
  if (!username || typeof username !== 'string') {
    return res.status(400).json({ error: 'username is required' });
  }
  const trimmed = username.trim();
  if (trimmed.length < 3) {
    return res.status(400).json({ error: 'Username must be at least 3 characters' });
  }
  if (trimmed.length > 50) {
    return res.status(400).json({ error: 'Username must be 50 characters or fewer' });
  }

  const targetId = parseUserId(req.params);
  if (!targetId) return res.status(400).json({ error: 'Invalid user ID' });

  const db = getDb();
  const user = db.prepare('SELECT id, username FROM users WHERE id = ?').get(targetId);
  if (!user) return res.status(404).json({ error: 'User not found' });

  const taken = db
    .prepare('SELECT id FROM users WHERE username = ? COLLATE NOCASE AND id != ?')
    .get(trimmed, targetId);
  if (taken) return res.status(409).json({ error: 'Username already taken' });

  const previousUsername = user.username;
  db.prepare("UPDATE users SET username = ?, updated_at = datetime('now') WHERE id = ?").run(
    trimmed,
    targetId,
  );

  logAudit({
    user_id: req.user.id,
    action: 'admin.username.change',
    entity_type: 'user',
    entity_id: targetId,
    details: { old_username: previousUsername, new_username: trimmed },
    ip_address: req.ip,
    user_agent: req.get('user-agent'),
  });

  res.json(
    db
      .prepare(
        'SELECT id, username, role, active, is_default_admin, must_change_password, first_login, created_at FROM users WHERE id = ?',
      )
      .get(targetId),
  );
});

// DELETE /api/admin/users/:id
router.delete('/users/:id', (req: Req, res: Res) => {
  const targetId = parseUserId(req.params);
  if (!targetId) return res.status(400).json({ error: 'Invalid user ID' });

  const db = getDb();
  const user = db.prepare('SELECT * FROM users WHERE id = ?').get(targetId);
  if (!user) return res.status(404).json({ error: 'User not found' });
  if (req.user?.id === user.id)
    return res.status(400).json({ error: 'You cannot delete your own account.' });

  const deleteUser = db.transaction(() => {
    // These three tables have no FK/CASCADE to users — must delete explicitly.
    // Sessions also has CASCADE but we keep the explicit delete as a safety net
    // for the rare case where foreign_keys is temporarily OFF during a migration.
    db.prepare('DELETE FROM import_sessions WHERE user_id = ?').run(user.id);
    db.prepare('DELETE FROM import_history WHERE user_id = ?').run(user.id);
    db.prepare('DELETE FROM audit_log WHERE user_id = ?').run(user.id);
    db.prepare('DELETE FROM sessions WHERE user_id = ?').run(user.id);
    db.prepare('DELETE FROM users WHERE id = ?').run(user.id);
    // ON DELETE CASCADE handles: bills, payments, categories, monthly_bill_state,
    // bill_history_ranges, notifications, data_sources, financial_accounts,
    // transactions, user_settings, user_login_history, monthly_income,
    // monthly_starting_amounts, autopay_suggestion_dismissals, bill_templates,
    // match_suggestion_rejections, declined_subscription_hints, bill_merchant_rules,
    // snowball_plans.
  });
  deleteUser();
  res.json({ success: true, deleted_user_id: user.id });
});

// ── Cleanup endpoints ─────────────────────────────────────────────────────────

// GET /api/admin/cleanup
// Returns current cleanup settings and the result of the last cleanup run.
router.get('/cleanup', (req: Req, res: Res) => {
  try {
    res.json(getCleanupStatus());
  } catch (err) {
    sendError(res, err);
  }
});

// PUT /api/admin/cleanup
// Updates one or more cleanup settings. Accepts partial objects.
//   import_sessions_enabled        boolean   prune expired import preview sessions
//   temp_exports_enabled           boolean   prune stale SQLite export temp files
//   temp_export_max_age_hours      1–72      hours before an orphaned export file is removed
//   backup_partials_enabled        boolean   prune orphaned .partial/.upload backup files
//   import_history_enabled         boolean   prune old import history rows (disabled by default)
//   import_history_max_age_days    30–3650   age threshold for import history rows
router.put('/cleanup', (req: Req, res: Res) => {
  try {
    res.json(applyCleanupSettings(req.body));
  } catch (err) {
    sendError(res, err);
  }
});

// POST /api/admin/cleanup/run
// Runs all enabled cleanup tasks immediately and returns the result.
router.post('/cleanup/run', backupOperationLimiter, async (req: Req, res: Res) => {
  try {
    const result = await runAllCleanup();
    res.json(result);
  } catch (err) {
    sendError(res, err);
  }
});

// ── Auth-mode helpers ─────────────────────────────────────────────────────────

const {
  applyAuthModeSettings,
  buildAuthModeStatus,
  buildSubmittedOidcConfig,
  testOidcConfiguration,
} = require('../services/oidcService.cts');

// GET /api/admin/auth-mode
router.get('/auth-mode', (req: Req, res: Res) => {
  res.json(buildAuthModeStatus());
});

// POST /api/admin/auth-mode/oidc-test
// Tests submitted or saved OIDC provider settings with OIDC discovery.
// Never returns client secret or token material.
router.post('/auth-mode/oidc-test', async (req: Req, res: Res) => {
  const config = buildSubmittedOidcConfig(req.body || {});
  const result = await testOidcConfiguration(config);
  res.status(result.ok ? 200 : 400).json(result);
});

// PUT /api/admin/auth-mode
// Accepts legacy auth_mode/default_user_id fields plus new auth method settings.
// Validates lockout protection before saving.
router.put('/auth-mode', (req: Req, res: Res) => {
  try {
    res.json(applyAuthModeSettings(req.body || {}));
  } catch (err) {
    res
      .status(err.status || 500)
      .json({ error: err.status ? err.message : 'Failed to update authentication settings' });
  }
});

// ── Bank Sync Config ──────────────────────────────────────────────────────────

// GET /api/admin/bank-sync-config
router.get('/bank-sync-config', (req: Req, res: Res) => {
  res.json({
    ...getBankSyncConfig(),
    worker: getBankSyncWorkerStatus(),
    encryption_key_source: isEnvKeyActive() ? 'env' : 'db',
    key_fingerprint: keyFingerprint(),
  });
});

// PUT /api/admin/bank-sync-config
router.put('/bank-sync-config', (req: Req, res: Res) => {
  const { enabled, sync_interval_hours, sync_days, debug_logging } = req.body || {};
  try {
    let config = getBankSyncConfig();
    if (typeof enabled === 'boolean') config = setBankSyncEnabled(enabled);
    if (sync_interval_hours !== undefined) config = setSyncIntervalHours(sync_interval_hours);
    if (sync_days !== undefined) config = setSyncDays(sync_days);
    if (typeof debug_logging === 'boolean') config = setDebugLogging(debug_logging);
    res.json(config);
  } catch (err) {
    res
      .status(err.status || 500)
      .json({ error: err.message || 'Failed to update bank sync config' });
  }
});

// ── Migration Rollback ────────────────────────────────────────────────────────
router.post('/migrations/rollback', async (req: Req, res: Res) => {
  const { version } = req.body;
  if (!version) {
    return res.status(400).json({ error: 'Version is required' });
  }

  try {
    const result = rollbackMigration(version);
    logAudit({
      user_id: req.user.id,
      action: 'migration.rollback',
      entity_type: 'migration',
      entity_id: null,
      details: { version, performed_by: req.user.username },
      ip_address: req.ip,
      user_agent: req.get('user-agent'),
    });
    res.json({ success: true, ...result });
  } catch (err) {
    logAudit({
      user_id: req.user.id,
      action: 'migration.rollback.failure',
      entity_type: 'migration',
      entity_id: null,
      details: { version, error: err.message, performed_by: req.user.username },
      ip_address: req.ip,
      user_agent: req.get('user-agent'),
    });

    if (err.code === 'NOT_APPLIED') {
      return res.status(404).json({ error: err.message });
    }
    if (err.code === 'ROLLBACK_NOT_SUPPORTED') {
      return res.status(422).json({ error: err.message });
    }
    res.status(500).json({ error: 'Rollback failed', details: err.message });
  }
});

module.exports = router;
