import cors from 'cors';
import express from 'express';
import { OAuth2Client } from 'google-auth-library';
import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
import { copyFile, mkdir, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const app = express();
const __dirname = dirname(fileURLToPath(import.meta.url));
const dataDir = join(__dirname, 'data');
const dataFile = join(dataDir, 'db.json');
const backupDir = join(dataDir, 'backups');
const clientDistDir = join(__dirname, '..', 'dist');
const port = Number(process.env.PORT ?? 8787);
const isProduction = process.env.NODE_ENV === 'production';
const sessionTtlMs = 1000 * 60 * 60 * 24 * 7;
const sessionCookieMaxAgeSeconds = Math.floor(sessionTtlMs / 1000);
const backupRetention = Math.max(5, Number(process.env.DB_BACKUP_RETENTION ?? 30));
const publicBaseUrl = String(process.env.APP_BASE_URL ?? `http://127.0.0.1:${port}`).replace(/\/$/, '');
const corsOrigins = String(process.env.CORS_ORIGINS ?? '')
  .split(',')
  .map((entry) => entry.trim())
  .filter(Boolean);
const googleClientIds = String(process.env.GOOGLE_CLIENT_ID ?? '')
  .split(',')
  .map((entry) => entry.trim())
  .filter(Boolean);
const googleClient = new OAuth2Client();

app.disable('x-powered-by');
app.set('trust proxy', 1);
app.use(
  cors({
    origin(origin, callback) {
      if (!origin) {
        return callback(null, true);
      }

      if (!isProduction) {
        return callback(null, true);
      }

      if (corsOrigins.length === 0) {
        return callback(null, true);
      }

      return callback(null, corsOrigins.includes(origin));
    },
    credentials: true,
  }),
);
app.use(express.urlencoded({ extended: false }));
app.use(express.json({ limit: '8mb' }));

const limiterState = new Map();

function readIp(req) {
  const forwarded = req.headers['x-forwarded-for'];
  if (typeof forwarded === 'string' && forwarded.trim()) {
    return forwarded.split(',')[0].trim();
  }

  return req.ip ?? req.socket?.remoteAddress ?? 'unknown';
}

function rateLimit({ keyPrefix, windowMs, max }) {
  return (req, res, next) => {
    const now = Date.now();
    const ip = readIp(req);
    const key = `${keyPrefix}:${ip}`;
    const current = limiterState.get(key);

    if (!current || current.expiresAt <= now) {
      limiterState.set(key, { count: 1, expiresAt: now + windowMs });
      return next();
    }

    if (current.count >= max) {
      const retryAfterSeconds = Math.max(1, Math.ceil((current.expiresAt - now) / 1000));
      res.setHeader('Retry-After', String(retryAfterSeconds));
      return res.status(429).json({ message: 'Too many requests. Please try again shortly.' });
    }

    current.count += 1;
    return next();
  };
}

setInterval(() => {
  const now = Date.now();
  for (const [key, value] of limiterState.entries()) {
    if (value.expiresAt <= now) {
      limiterState.delete(key);
    }
  }
}, 60 * 1000).unref();

const authRateLimit = rateLimit({ keyPrefix: 'auth', windowMs: 15 * 60 * 1000, max: 30 });
const profileRateLimit = rateLimit({ keyPrefix: 'profile', windowMs: 10 * 60 * 1000, max: 40 });
const transactionRateLimit = rateLimit({ keyPrefix: 'txn', windowMs: 5 * 60 * 1000, max: 25 });

function sanitizeText(value, maxLen = 120) {
  return String(value ?? '')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, maxLen);
}

function isValidEmail(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function isSafeAvatar(avatarUrl) {
  if (!avatarUrl) {
    return true;
  }

  if (avatarUrl.length > 2_500_000) {
    return false;
  }

  if (/^https:\/\//i.test(avatarUrl)) {
    return true;
  }

  return /^data:image\/(png|jpe?g|webp|gif);base64,[A-Za-z0-9+/=\s]+$/i.test(avatarUrl);
}

app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
  res.setHeader('Cross-Origin-Resource-Policy', 'same-site');
  if (isProduction) {
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  }
  next();
});

app.use((req, res, next) => {
  const startedAt = Date.now();
  const ip = readIp(req);
  res.on('finish', () => {
    const ms = Date.now() - startedAt;
    console.log(`[${nowIso()}] ${req.method} ${req.originalUrl} ${res.statusCode} ${ms}ms ip=${ip}`);
  });
  next();
});

const defaultDb = {
  users: [],
  sessions: [],
  emailOutbox: [],
  smsOutbox: [],
  notifications: [],
  depositRequests: [],
  withdrawalRequests: [],
  prices: {
    goldOunce: 2425,
    updatedAt: new Date().toISOString(),
    source: 'fallback',
  },
  backupMetadata: {
    lastBackupAt: null,
    lastBackupFile: null,
  },
};

async function ensureDb() {
  await mkdir(dataDir, { recursive: true });
  await mkdir(backupDir, { recursive: true });

  try {
    await readFile(dataFile, 'utf8');
  } catch {
    await writeFile(dataFile, JSON.stringify(defaultDb, null, 2), 'utf8');
  }
}

async function readDb() {
  await ensureDb();
  const raw = await readFile(dataFile, 'utf8');
  const parsed = JSON.parse(raw);
  if (!parsed.backupMetadata || typeof parsed.backupMetadata !== 'object') {
    parsed.backupMetadata = {
      lastBackupAt: null,
      lastBackupFile: null,
    };
  }
  if (!Array.isArray(parsed.users)) {
    parsed.users = [];
  }
  parsed.users = parsed.users.map((user) => {
    user.activated = user.activated ?? Boolean(user.verified);
    user.activationToken = user.activationToken ?? null;
    user.activationExpiresAt = user.activationExpiresAt ?? null;
    user.verificationLink = user.verificationLink ?? null;
    user.securityEmails = Array.isArray(user.securityEmails) ? user.securityEmails : [];
    user.transactions = Array.isArray(user.transactions) ? user.transactions : [];
    user.transactionLedger = Array.isArray(user.transactionLedger) ? user.transactionLedger : [];
    user.twoFactor = {
      enabled: Boolean(user?.twoFactor?.enabled),
      method: user?.twoFactor?.method === 'sms' ? 'sms' : 'email',
      pending: user?.twoFactor?.pending ?? null,
      pendingMethod: user?.twoFactor?.pendingMethod === 'sms' ? 'sms' : 'email',
      lastVerifiedAt: user?.twoFactor?.lastVerifiedAt ?? null,
    };
    return user;
  });
  return parsed;
}

async function createDbBackupSnapshot(db) {
  await ensureDb();
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
  const fileName = `db-backup-${stamp}-${randomUUID().slice(0, 6)}.json`;
  const backupPath = join(backupDir, fileName);
  await copyFile(dataFile, backupPath);

  const files = (await readdir(backupDir)).filter((name) => name.endsWith('.json')).sort().reverse();
  const stale = files.slice(backupRetention);
  for (const entry of stale) {
    try {
      await unlink(join(backupDir, entry));
    } catch {
      // no-op cleanup fallback when pruning is blocked
    }
  }

  db.backupMetadata = {
    lastBackupAt: nowIso(),
    lastBackupFile: fileName,
  };
}

async function writeDb(db, options = { createBackup: true }) {
  await ensureDb();
  if (!db.backupMetadata || typeof db.backupMetadata !== 'object') {
    db.backupMetadata = {
      lastBackupAt: null,
      lastBackupFile: null,
    };
  }
  await writeFile(dataFile, JSON.stringify(db, null, 2), 'utf8');
  if (options.createBackup) {
    await createDbBackupSnapshot(db);
    await writeFile(dataFile, JSON.stringify(db, null, 2), 'utf8');
  }
}

function normalizeEmail(email) {
  return String(email ?? '').trim().toLowerCase();
}

function nowIso() {
  return new Date().toISOString();
}

async function hasClientBundle() {
  try {
    const bundleStat = await stat(clientDistDir);
    return bundleStat.isDirectory();
  } catch {
    return false;
  }
}

function securityStandardsSnapshot() {
  return [
    { name: 'Password Hashing', status: 'enforced', detail: 'Passwords are hashed using scrypt with per-user salt.' },
    { name: 'Session Security', status: 'enforced', detail: 'HttpOnly session cookies, SameSite=Lax, expiry controls.' },
    { name: 'Transport Security', status: 'enforced', detail: 'HSTS in production and strict browser security headers.' },
    { name: 'Rate Limiting', status: 'enforced', detail: 'Auth/profile/transaction endpoints are throttled per IP.' },
    { name: 'KYC + Anti-Fraud', status: 'enforced', detail: 'Phone, document, and anti-fraud checks gate approval.' },
    { name: 'Disaster Recovery', status: 'enforced', detail: 'Automatic JSON snapshots retained for controlled restore.' },
  ];
}

function transactionStoragePolicy() {
  return {
    model: 'append-only event log + tamper-evident hash chain',
    location: 'server/data/db.json and server/data/backups/*.json',
    retention: `Most recent ${backupRetention} backup snapshots retained`,
    integrity: 'Each transaction stores prevRecordHash + recordHash',
  };
}

function addMonthsIso(baseIso, months) {
  const date = new Date(baseIso);
  date.setUTCMonth(date.getUTCMonth() + months);
  return date.toISOString();
}

function addDaysIso(baseIso, days) {
  const date = new Date(baseIso);
  date.setUTCDate(date.getUTCDate() + days);
  return date.toISOString();
}

function createCertificateNumber() {
  return `HKKO-${new Date().getUTCFullYear()}-${randomUUID().slice(0, 8).toUpperCase()}`;
}

function hashPassword(password, salt = randomBytes(16).toString('hex')) {
  const derived = scryptSync(password, salt, 64).toString('hex');
  return { salt, derived };
}

function verifyPassword(password, salt, storedHash) {
  const candidate = scryptSync(password, salt, 64);
  const actual = Buffer.from(storedHash, 'hex');
  if (actual.length !== candidate.length) {
    return false;
  }
  return timingSafeEqual(actual, candidate);
}

function hashOtp(code) {
  return createHash('sha256').update(String(code)).digest('hex');
}

function ensureTwoFactorProfile(user) {
  user.twoFactor = {
    enabled: Boolean(user?.twoFactor?.enabled),
    method: user?.twoFactor?.method === 'sms' ? 'sms' : 'email',
    pending: user?.twoFactor?.pending ?? null,
    pendingMethod: user?.twoFactor?.pendingMethod === 'sms' ? 'sms' : 'email',
    lastVerifiedAt: user?.twoFactor?.lastVerifiedAt ?? null,
  };
  return user.twoFactor;
}

function transactionHashPayload(tx, previousHash) {
  return JSON.stringify({
    id: tx.id,
    type: tx.type,
    amount: tx.amount,
    status: tx.status,
    method: tx.method ?? null,
    channel: tx.channel ?? null,
    destination: tx.destination ?? null,
    account: tx.account ?? null,
    reference: tx.reference ?? null,
    createdAt: tx.createdAt,
    previousHash,
  });
}

function appendTransaction(user, transaction) {
  user.transactionLedger = Array.isArray(user.transactionLedger) ? user.transactionLedger : [];
  const previous = user.transactionLedger[0];
  const previousHash = previous?.recordHash ?? null;
  const recordHash = createHash('sha256').update(transactionHashPayload(transaction, previousHash)).digest('hex');
  const enriched = {
    ...transaction,
    prevRecordHash: previousHash,
    recordHash,
    storageVersion: 'v1',
  };
  user.transactions.unshift(enriched);
  user.transactionLedger.unshift({
    id: enriched.id,
    createdAt: enriched.createdAt,
    type: enriched.type,
    amount: enriched.amount,
    recordHash,
    prevRecordHash: previousHash,
  });
  user.transactionLedger = user.transactionLedger.slice(0, 5000);
  return enriched;
}

function buildTwoFactorChallenge({ user, purpose, method = 'email' }) {
  const code = String(Math.floor(100000 + Math.random() * 900000));
  const challenge = {
    id: randomUUID(),
    purpose,
    method,
    codeHash: hashOtp(code),
    attempts: 0,
    createdAt: nowIso(),
    expiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
  };
  const destination = method === 'sms' ? sanitizeText(user?.profile?.phone ?? '', 32) : String(user?.email ?? '');
  return { challenge, code, destination };
}

function isDataUpload(value) {
  const raw = String(value ?? '').trim();
  return /^data:(image\/(png|jpe?g|webp)|application\/pdf);base64,[A-Za-z0-9+/=\s]+$/i.test(raw);
}

function toSafeUploadSummary(document, label) {
  const fileName = sanitizeText(document?.name ?? `${label}.file`, 120) || `${label}.file`;
  const mimeType = sanitizeText(document?.type ?? '', 80).toLowerCase();
  const encodedData = String(document?.dataUrl ?? '').trim();
  const size = Number(document?.size ?? 0);

  if (!encodedData || !isDataUpload(encodedData)) {
    return null;
  }

  if (!Number.isFinite(size) || size <= 0 || size > 5_000_000) {
    return null;
  }

  return {
    fileName,
    mimeType,
    size,
    hash: createHash('sha256').update(encodedData).digest('hex'),
    uploadedAt: nowIso(),
  };
}

function createVerificationProfile(phone = '') {
  return {
    phoneVerification: {
      status: 'not_started',
      phone: String(phone ?? '').trim(),
      codeHash: null,
      codeExpiresAt: null,
      attempts: 0,
      lastSentAt: null,
      verifiedAt: null,
    },
    documents: {
      idCardFront: null,
      idCardBack: null,
      passport: null,
      status: 'missing',
      submittedAt: null,
    },
    antiFraud: {
      status: 'pending',
      score: 0,
      flags: [],
      reviewedAt: null,
    },
    kyc: {
      status: 'not_started',
      submittedAt: null,
      verifiedAt: null,
      notes: '',
    },
  };
}

function ensureVerificationProfile(user) {
  if (!user.verification || typeof user.verification !== 'object') {
    user.verification = createVerificationProfile(user?.profile?.phone ?? '');
    return user.verification;
  }

  const defaults = createVerificationProfile(user?.profile?.phone ?? '');
  user.verification = {
    ...defaults,
    ...user.verification,
    phoneVerification: {
      ...defaults.phoneVerification,
      ...(user.verification.phoneVerification ?? {}),
      phone: sanitizeText(user.verification.phoneVerification?.phone ?? user?.profile?.phone ?? '', 32),
    },
    documents: {
      ...defaults.documents,
      ...(user.verification.documents ?? {}),
    },
    antiFraud: {
      ...defaults.antiFraud,
      ...(user.verification.antiFraud ?? {}),
      flags: Array.isArray(user.verification.antiFraud?.flags) ? user.verification.antiFraud.flags.slice(0, 12) : [],
    },
    kyc: {
      ...defaults.kyc,
      ...(user.verification.kyc ?? {}),
    },
  };

  return user.verification;
}

function evaluateAntiFraud(user) {
  const verification = ensureVerificationProfile(user);
  const flags = [];
  let score = 0;

  const idFront = verification.documents.idCardFront?.hash;
  const idBack = verification.documents.idCardBack?.hash;
  const passport = verification.documents.passport?.hash;
  const phone = verification.phoneVerification.phone;
  const email = String(user.email ?? '').toLowerCase();

  if (!verification.phoneVerification.verifiedAt) {
    flags.push('Phone number not yet verified');
    score += 30;
  }

  if (!idFront || !idBack || !passport) {
    flags.push('Missing required KYC document uploads');
    score += 40;
  }

  if (idFront && passport && idFront === passport) {
    flags.push('ID card and passport appear identical');
    score += 35;
  }

  if (idFront && idBack && idFront === idBack) {
    flags.push('ID front and back appear identical');
    score += 20;
  }

  if (!/^\+?[0-9]{9,15}$/.test(String(phone).replace(/\s+/g, ''))) {
    flags.push('Phone number format appears invalid');
    score += 20;
  }

  if (/(mailinator|10minutemail|guerrillamail|tempmail)/.test(email)) {
    flags.push('Disposable email domain detected');
    score += 45;
  }

  const status = score >= 70 ? 'blocked' : score >= 35 ? 'review' : 'passed';
  verification.antiFraud = {
    status,
    score,
    flags,
    reviewedAt: nowIso(),
  };

  return verification.antiFraud;
}

function verificationSummary(user) {
  const verification = ensureVerificationProfile(user);
  return {
    phoneVerification: {
      status: verification.phoneVerification.status,
      phone: verification.phoneVerification.phone,
      lastSentAt: verification.phoneVerification.lastSentAt,
      verifiedAt: verification.phoneVerification.verifiedAt,
    },
    documents: {
      status: verification.documents.status,
      submittedAt: verification.documents.submittedAt,
      idCardFront: verification.documents.idCardFront,
      idCardBack: verification.documents.idCardBack,
      passport: verification.documents.passport,
    },
    antiFraud: verification.antiFraud,
    kyc: verification.kyc,
  };
}

function parseCookies(cookieHeader = '') {
  return Object.fromEntries(
    cookieHeader
      .split(';')
      .map((item) => item.trim())
      .filter(Boolean)
      .map((item) => {
        const index = item.indexOf('=');
        const key = index >= 0 ? item.slice(0, index) : item;
        const value = index >= 0 ? item.slice(index + 1) : '';
        return [key, decodeURIComponent(value)];
      }),
  );
}

function setSessionCookie(res, token) {
  const secure = isProduction ? '; Secure' : '';
  res.setHeader(
    'Set-Cookie',
    `hkko_session=${encodeURIComponent(token)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${sessionCookieMaxAgeSeconds}${secure}`,
  );
}

function clearSessionCookie(res) {
  const secure = isProduction ? '; Secure' : '';
  res.setHeader('Set-Cookie', `hkko_session=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0${secure}`);
}

async function getSessionUser(req) {
  const cookies = parseCookies(req.headers.cookie);
  const token = cookies.hkko_session;
  if (!token) {
    return null;
  }

  const db = await readDb();
  const session = db.sessions.find((entry) => entry.token === token && new Date(entry.expiresAt).getTime() > Date.now());
  if (!session) {
    db.sessions = db.sessions.filter((entry) => new Date(entry.expiresAt).getTime() > Date.now());
    await writeDb(db);
    return null;
  }

  return db.users.find((user) => user.id === session.userId) ?? null;
}

function publicUser(user) {
  if (!user) {
    return null;
  }

  return {
    id: user.id,
    email: user.email,
    verified: user.verified,
    profile: user.profile,
    wallet: user.wallet,
    storage: user.storage,
    verification: verificationSummary(user),
    twoFactor: {
      enabled: Boolean(user?.twoFactor?.enabled),
      method: user?.twoFactor?.method === 'sms' ? 'sms' : 'email',
      lastVerifiedAt: user?.twoFactor?.lastVerifiedAt ?? null,
    },
    securityEmails: user.securityEmails.slice(0, 20),
    investmentJourney: user.investmentJourney,
    notifications: user.notifications.slice(0, 12),
    transactions: user.transactions.slice(0, 12),
    orders: user.orders.slice(0, 12),
    createdAt: user.createdAt,
    lastLoginAt: user.lastLoginAt,
  };
}

function createUserRecord({ email, password, fullName, phone, country, role }) {
  const { salt, derived } = hashPassword(password);
  const activationToken = randomUUID();
  const verificationToken = randomUUID();
  const activationLink = `${publicBaseUrl}/api/auth/activate-account?token=${activationToken}`;
  const verificationLink = `${publicBaseUrl}/api/auth/verify-email?token=${verificationToken}`;

  return {
    id: randomUUID(),
    email,
    emailLower: normalizeEmail(email),
    passwordHash: derived,
    passwordSalt: salt,
    verified: false,
    activated: false,
    activationToken,
    activationExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
    verificationToken,
    verificationExpiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
    profile: {
      fullName,
      phone,
      country,
      role,
    },
    wallet: {
      cash: 0,
      pending: 0,
      goldGrams: 0,
    },
    storage: {
      insured: true,
      provider: 'HKKO Insured Vault',
      coverage: 'Vault assets are insured while held in storage.',
    },
    verification: createVerificationProfile(phone),
    investmentJourney: createInvestmentJourney({ email, fullName, phone, country, role }),
    notifications: [],
    transactions: [],
    transactionLedger: [],
    orders: [],
    twoFactor: {
      enabled: false,
      method: 'email',
      pending: null,
      pendingMethod: 'email',
      lastVerifiedAt: null,
    },
    securityEmails: [],
    loginHistory: [],
    createdAt: nowIso(),
    updatedAt: nowIso(),
    activationLink,
    verificationLink,
  };
}

function createGoogleUserRecord({ email, fullName, phone = '', country = 'Ghana', role = 'Gold Saver', googleSub = '' }) {
  return {
    id: randomUUID(),
    email,
    emailLower: normalizeEmail(email),
    passwordHash: null,
    passwordSalt: null,
    verified: true,
    activated: true,
    activationToken: null,
    activationExpiresAt: null,
    verificationToken: null,
    verificationExpiresAt: null,
    authProvider: 'google',
    googleSub,
    profile: {
      fullName,
      phone,
      country,
      role,
    },
    wallet: {
      cash: 0,
      pending: 0,
      goldGrams: 0,
    },
    storage: {
      insured: true,
      provider: 'HKKO Insured Vault',
      coverage: 'Vault assets are insured while held in storage.',
    },
    verification: createVerificationProfile(phone),
    investmentJourney: createInvestmentJourney({ email, fullName, phone, country, role }),
    notifications: [],
    transactions: [],
    transactionLedger: [],
    orders: [],
    twoFactor: {
      enabled: false,
      method: 'email',
      pending: null,
      pendingMethod: 'email',
      lastVerifiedAt: null,
    },
    securityEmails: [],
    loginHistory: [],
    createdAt: nowIso(),
    updatedAt: nowIso(),
    activationLink: null,
    verificationLink: null,
  };
}

function createInvestmentJourney(profile = {}) {
  const createdAt = nowIso();
  return {
    id: randomUUID(),
    productName: 'HKKO Gold Growth Plan',
    status: 'draft',
    minimumInvestment: 100000,
    validationPeriodDays: 14,
    contractDurationMonths: 12,
    contractDurationLabel: '12 Months + 1 Day',
    investmentCertificateNumber: createCertificateNumber(),
    investmentStartDate: null,
    registrationCompletedAt: null,
    kycStatus: 'not_started',
    kycCompletedAt: null,
    applicationStatus: 'not_submitted',
    applicationSubmittedAt: null,
    validationStatus: 'pending',
    validatedAt: null,
    agreementStatus: 'not_signed',
    agreementSignedAt: null,
    fundingStatus: 'unfunded',
    fundedAt: null,
    maturityStatus: 'not_started',
    maturityReachedAt: null,
    maturityDecision: null,
    maturityDecidedAt: null,
    validationEndsAt: null,
    nextRoiPaymentDate: null,
    investmentMaturityDate: null,
    principal: 0,
    currency: 'USD',
    monthlyDistribution: 0,
    termMonths: 12,
    monthsElapsed: 0,
    monthlyRoiRate: 0.0125,
    roiPayments: [],
    totalRoiPaid: 0,
    applicant: {
      fullName: String(profile.fullName ?? ''),
      email: String(profile.email ?? ''),
      phone: String(profile.phone ?? ''),
      country: String(profile.country ?? 'Ghana'),
      role: String(profile.role ?? 'Investor'),
    },
    createdAt,
  };
}

function ensureInvestmentJourney(user) {
  if (!user.investmentJourney) {
    user.investmentJourney = createInvestmentJourney(user.profile);
  }

  const journey = user.investmentJourney;
  if (journey.investmentStartDate && !journey.nextRoiPaymentDate) {
    journey.nextRoiPaymentDate = addMonthsIso(journey.investmentStartDate, journey.monthsElapsed + 1);
  }

  if (journey.investmentStartDate && !journey.investmentMaturityDate) {
    journey.investmentMaturityDate = addDaysIso(addMonthsIso(journey.investmentStartDate, journey.contractDurationMonths), 1);
  }

  if (journey.principal && !journey.monthlyDistribution) {
    journey.monthlyDistribution = Number((journey.principal * journey.monthlyRoiRate).toFixed(2));
  }

  return journey;
}

function getInvestmentDecisionLabel(journey) {
  if (journey.maturityDecision === 'reinvest') {
    return 'Reinvested';
  }

  if (journey.maturityDecision === 'redeem') {
    return 'Redeemed';
  }

  return 'Pending';
}

function localCheckoutPage(reference, amount, userEmail) {
  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HKKO Local Checkout</title>
    <style>
      body {
        margin: 0;
        min-height: 100vh;
        display: grid;
        place-items: center;
        font-family: Arial, sans-serif;
        background: linear-gradient(160deg, #1b1114, #08080b 65%);
        color: #f5ead3;
      }
      .card {
        width: min(480px, calc(100vw - 32px));
        padding: 28px;
        border-radius: 24px;
        border: 1px solid rgba(255, 255, 255, 0.08);
        background: rgba(20, 20, 26, 0.96);
        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
      }
      h1 { margin: 0 0 10px; font-size: 1.6rem; }
      p { line-height: 1.6; color: #d0c6b0; }
      .meta { margin: 18px 0; padding: 14px; border-radius: 16px; background: rgba(255,255,255,0.04); }
      button {
        width: 100%;
        min-height: 48px;
        border: 0;
        border-radius: 999px;
        background: linear-gradient(135deg, #f4d27a, #be8c14);
        color: #140f03;
        font-weight: 700;
        cursor: pointer;
      }
      small { display: block; margin-top: 12px; color: #9f9581; }
      code { word-break: break-all; }
    </style>
  </head>
  <body>
    <div class="card">
      <h1>Complete Local Funding</h1>
      <p>This is the built-in HKKO checkout used when no external gateway is configured.</p>
      <div class="meta">
        <p><strong>Reference:</strong> <code>${reference}</code></p>
        <p><strong>Amount:</strong> GHS ${Number(amount).toFixed(2)}</p>
        <p><strong>Account:</strong> ${userEmail}</p>
      </div>
      <form method="post" action="/api/wallet/deposit/confirm">
        <input type="hidden" name="reference" value="${reference}" />
        <input type="hidden" name="status" value="success" />
        <input type="hidden" name="providerRef" value="LOCAL-${reference}" />
        <button type="submit">Confirm and Fund Investment</button>
      </form>
      <small>Submitting this page will credit the wallet and move the investment into funded status.</small>
    </div>
  </body>
</html>`;
}

function issueSession(db, user, req) {
  const token = randomUUID();
  db.sessions.unshift({ token, userId: user.id, createdAt: nowIso(), expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString() });
  user.lastLoginAt = nowIso();
  user.loginHistory.unshift({ id: randomUUID(), at: user.lastLoginAt, ip: req.ip ?? 'unknown', userAgent: req.get('user-agent') ?? 'unknown' });
  user.loginHistory = user.loginHistory.slice(0, 20);
  user.securityEmails.unshift({ id: randomUUID(), subject: 'New login detected', body: `Your HKKO Gold account logged in from ${req.ip ?? 'unknown'}.`, createdAt: nowIso() });
  user.securityEmails = user.securityEmails.slice(0, 20);
  addNotification(user, 'security', 'New login detected.');

  return token;
}

function addNotification(user, type, message) {
  user.notifications.unshift({
    id: randomUUID(),
    type,
    message,
    read: false,
    createdAt: nowIso(),
  });
  user.notifications = user.notifications.slice(0, 30);
}

function addOutbox(db, entry) {
  db.emailOutbox.unshift({ id: randomUUID(), ...entry, createdAt: nowIso() });
  db.emailOutbox = db.emailOutbox.slice(0, 100);
}

function addSmsOutbox(db, entry) {
  db.smsOutbox.unshift({ id: randomUUID(), ...entry, createdAt: nowIso() });
  db.smsOutbox = db.smsOutbox.slice(0, 100);
}

async function getLiveGoldPrice() {
  const fallback = { goldOunce: 2425, updatedAt: nowIso(), source: 'fallback' };

  try {
    const response = await fetch('https://api.metals.live/v1/spot/gold', { headers: { accept: 'application/json' } });
    if (response.ok) {
      const data = await response.json();
      const price = Array.isArray(data) ? Number(data?.[0]?.[1]) : Number(data?.price ?? data?.gold ?? data?.goldPrice);
      if (Number.isFinite(price)) {
        return {
          goldOunce: Number(price.toFixed(2)),
          updatedAt: nowIso(),
          source: 'metals.live',
        };
      }
    }
  } catch {
    // Try secondary provider below.
  }

  try {
    const response = await fetch('https://api.gold-api.com/price/XAU', { headers: { accept: 'application/json' } });
    if (!response.ok) {
      return fallback;
    }

    const data = await response.json();
    const price = Number(data?.price ?? data?.gold ?? data?.goldPrice);
    if (!Number.isFinite(price)) {
      return fallback;
    }

    const updatedAt = typeof data?.updatedAt === 'string' ? data.updatedAt : nowIso();

    return {
      goldOunce: Number(price.toFixed(2)),
      updatedAt,
      source: 'gold-api.com',
    };
  } catch {
    return fallback;
  }
}

async function authRequired(req, res, next) {
  const user = await getSessionUser(req);
  if (!user) {
    return res.status(401).json({ message: 'Authentication required.' });
  }

  req.user = user;
  next();
}

app.get('/api/health', async (_req, res) => {
  res.json({ ok: true, time: nowIso(), uptimeSeconds: Math.floor(process.uptime()), env: process.env.NODE_ENV ?? 'development' });
});

app.post('/api/auth/signup', authRateLimit, async (req, res) => {
  const { fullName, email, password, phone = '', country = 'Ghana', role = 'Gold Saver' } = req.body ?? {};
  const normalizedEmail = normalizeEmail(email);
  const safeName = sanitizeText(fullName, 80);
  const safePhone = sanitizeText(phone, 32);
  const safeCountry = sanitizeText(country, 64) || 'Ghana';
  const safeRole = sanitizeText(role, 64) || 'Gold Saver';

  if (!safeName || !normalizedEmail || !isValidEmail(normalizedEmail) || !password || password.length < 8 || password.length > 128) {
    return res.status(400).json({ message: 'Name, email, and an 8+ character password are required.' });
  }

  const db = await readDb();
  const duplicate = db.users.find((user) => user.emailLower === normalizedEmail);
  if (duplicate) {
    return res.status(409).json({ message: 'An account with that email already exists.' });
  }

  const user = createUserRecord({ email: normalizedEmail, password, fullName: safeName, phone: safePhone, country: safeCountry, role: safeRole });
  db.users.unshift(user);
  db.notifications.unshift({ id: randomUUID(), userId: user.id, type: 'welcome', message: 'Welcome to HKKO Gold.', read: false, createdAt: nowIso() });
  addOutbox(db, {
    to: user.email,
    subject: 'Activate your HKKO Gold account',
    body: `Activate your account: ${user.activationLink} \nThen verify your email: ${user.verificationLink}`,
    link: user.activationLink,
    verificationLink: user.verificationLink,
    type: 'activation',
    userId: user.id,
  });
  await writeDb(db);

  return res.status(201).json({
    message: 'Account created. Activate your account and then verify your email.',
    activationLink: user.activationLink,
    verificationLink: user.verificationLink,
    user: publicUser(user),
  });
});

app.get('/api/auth/activate-account', async (req, res) => {
  const token = String(req.query.token ?? '');
  const db = await readDb();
  const user = db.users.find((entry) => entry.activationToken === token);

  if (!user) {
    return res.status(404).json({ message: 'Activation link is invalid or expired.' });
  }

  if (user.activationExpiresAt && new Date(user.activationExpiresAt).getTime() < Date.now()) {
    return res.status(400).json({ message: 'Activation link has expired. Please request another one.' });
  }

  user.activated = true;
  user.activationToken = null;
  user.activationExpiresAt = null;
  user.updatedAt = nowIso();
  addNotification(user, 'security', 'Account activated successfully.');
  addOutbox(db, {
    to: user.email,
    subject: 'HKKO account activated',
    body: `Your account has been activated. Verify your email using: ${user.verificationLink}`,
    type: 'activation-success',
    userId: user.id,
  });
  await writeDb(db);

  return res.json({
    message: 'Account activated successfully. Verify your email to finish setup.',
    verificationLink: user.verificationLink,
  });
});

app.get('/api/auth/verify-email', async (req, res) => {
  const token = String(req.query.token ?? '');
  const db = await readDb();
  const user = db.users.find((entry) => entry.verificationToken === token);

  if (!user) {
    return res.status(404).json({ message: 'Activation link is invalid or expired.' });
  }

  if (!user.activated) {
    return res.status(403).json({ message: 'Activate your account before verifying email.' });
  }

  if (new Date(user.verificationExpiresAt).getTime() < Date.now()) {
    return res.status(400).json({ message: 'Activation link has expired. Please request another one.' });
  }

  user.verified = true;
  user.verificationToken = null;
  user.verificationExpiresAt = null;
  user.updatedAt = nowIso();
  addNotification(user, 'security', 'Email verified successfully.');
  addOutbox(db, {
    to: user.email,
    subject: 'Your HKKO Gold account was verified',
    body: 'Your email has been activated and your account is ready for login.',
    type: 'verification-success',
    userId: user.id,
  });
  await writeDb(db);

  return res.json({ message: 'Email verified successfully. You can now log in.' });
});

app.post('/api/auth/login', authRateLimit, async (req, res) => {
  const { email, password } = req.body ?? {};
  const normalizedEmail = normalizeEmail(email);

  if (!normalizedEmail || !isValidEmail(normalizedEmail) || typeof password !== 'string' || !password) {
    return res.status(400).json({ message: 'Email and password are required.' });
  }

  const db = await readDb();
  const user = db.users.find((entry) => entry.emailLower === normalizedEmail);
  if (!user) {
    return res.status(401).json({ message: 'Invalid email or password.' });
  }

  if (!user.passwordSalt || !user.passwordHash) {
    return res.status(400).json({ message: 'This account uses Google sign-in. Continue with Google to log in.' });
  }

  if (!verifyPassword(password, user.passwordSalt, user.passwordHash)) {
    return res.status(401).json({ message: 'Invalid email or password.' });
  }

  if (!user.verified) {
    return res.status(403).json({ message: 'Please verify your email before logging in.', activationLink: user.activationLink });
  }

  if (!user.activated) {
    return res.status(403).json({ message: 'Please activate your account before logging in.', activationLink: user.activationLink });
  }

  const twoFactor = ensureTwoFactorProfile(user);
  if (twoFactor.enabled) {
    const { challenge, code, destination } = buildTwoFactorChallenge({ user, purpose: 'login', method: twoFactor.method });
    twoFactor.pending = challenge;
    if (challenge.method === 'sms') {
      addSmsOutbox(db, { to: destination, message: `HKKO 2FA code: ${code}. Expires in 10 minutes.`, userId: user.id });
    } else {
      addOutbox(db, {
        to: user.email,
        subject: 'Your HKKO 2FA code',
        body: `Your one-time login code is ${code}. It expires in 10 minutes.`,
        type: 'two-factor-login',
        userId: user.id,
      });
    }
    await writeDb(db);
    return res.status(202).json({
      message: 'Two-factor verification required. Enter the code we sent.',
      requiresTwoFactor: true,
      challengeId: challenge.id,
      email: user.email,
      method: challenge.method,
      demoCode: isProduction ? undefined : code,
    });
  }

  const token = issueSession(db, user, req);
  addOutbox(db, {
    to: user.email,
    subject: 'HKKO Gold security alert',
    body: `A new login was detected for your account at ${user.lastLoginAt}.`,
    type: 'security-login',
    userId: user.id,
  });
  await writeDb(db);
  setSessionCookie(res, token);

  return res.json({ message: 'Login successful.', user: publicUser(user) });
});

app.post('/api/auth/google', authRateLimit, async (req, res) => {
  const { credential, country = 'Ghana', role = 'Gold Saver', phone = '' } = req.body ?? {};

  if (!credential || typeof credential !== 'string') {
    return res.status(400).json({ message: 'Google credential is required.' });
  }

  let payload;
  try {
    const ticket = await googleClient.verifyIdToken(
      googleClientIds.length
        ? {
            idToken: credential,
            audience: googleClientIds,
          }
        : {
            idToken: credential,
          },
    );
    payload = ticket.getPayload();
  } catch {
    return res.status(401).json({ message: 'Google credential verification failed.' });
  }

  const normalizedEmail = normalizeEmail(payload?.email);
  const fullName = String(payload?.name ?? '').trim();
  const safePhone = sanitizeText(phone, 32);
  const safeCountry = sanitizeText(country, 64) || 'Ghana';
  const safeRole = sanitizeText(role, 64) || 'Gold Saver';
  const emailVerified = Boolean(payload?.email_verified);
  const googleSub = String(payload?.sub ?? '');

  if (!normalizedEmail || !isValidEmail(normalizedEmail) || !fullName || !emailVerified || !googleSub) {
    return res.status(400).json({ message: 'Google account must provide a verified email and profile name.' });
  }

  const db = await readDb();
  let user = db.users.find((entry) => entry.emailLower === normalizedEmail);

  if (!user) {
    user = createGoogleUserRecord({
      email: normalizedEmail,
      fullName,
      phone: safePhone,
      country: safeCountry,
      role: safeRole,
      googleSub,
    });
    db.users.unshift(user);
    db.notifications.unshift({ id: randomUUID(), userId: user.id, type: 'welcome', message: 'Welcome to HKKO Gold.', read: false, createdAt: nowIso() });
  } else {
    user.verified = true;
    user.authProvider = user.authProvider ?? 'google';
    user.googleSub = user.googleSub ?? googleSub;
    user.verificationToken = null;
    user.verificationExpiresAt = null;
    user.profile.fullName = user.profile.fullName || fullName;
    user.updatedAt = nowIso();
  }

  const twoFactor = ensureTwoFactorProfile(user);
  if (twoFactor.enabled) {
    const { challenge, code, destination } = buildTwoFactorChallenge({ user, purpose: 'login', method: twoFactor.method });
    twoFactor.pending = challenge;
    if (challenge.method === 'sms') {
      addSmsOutbox(db, { to: destination, message: `HKKO 2FA code: ${code}. Expires in 10 minutes.`, userId: user.id });
    } else {
      addOutbox(db, {
        to: user.email,
        subject: 'Your HKKO 2FA code',
        body: `Your one-time login code is ${code}. It expires in 10 minutes.`,
        type: 'two-factor-login',
        userId: user.id,
      });
    }
    await writeDb(db);
    return res.status(202).json({
      message: 'Two-factor verification required. Enter the code we sent.',
      requiresTwoFactor: true,
      challengeId: challenge.id,
      email: user.email,
      method: challenge.method,
      demoCode: isProduction ? undefined : code,
    });
  }

  const token = issueSession(db, user, req);
  addOutbox(db, {
    to: user.email,
    subject: 'HKKO Gold security alert',
    body: `A new login was detected for your account at ${user.lastLoginAt}.`,
    type: 'security-login',
    userId: user.id,
  });

  await writeDb(db);
  setSessionCookie(res, token);

  return res.status(200).json({
    message: 'Google sign-in successful.',
    user: publicUser(user),
  });
});

app.post('/api/auth/2fa/verify', authRateLimit, async (req, res) => {
  const email = normalizeEmail(req.body?.email ?? '');
  const challengeId = sanitizeText(req.body?.challengeId ?? '', 128);
  const code = sanitizeText(req.body?.code ?? '', 12);

  if (!email || !challengeId || !/^\d{6}$/.test(code)) {
    return res.status(400).json({ message: 'Email, challenge, and 6-digit code are required.' });
  }

  const db = await readDb();
  const user = db.users.find((entry) => entry.emailLower === email);
  if (!user) {
    return res.status(404).json({ message: 'Account not found for 2FA verification.' });
  }

  const twoFactor = ensureTwoFactorProfile(user);
  const pending = twoFactor.pending;
  if (!pending || pending.id !== challengeId || pending.purpose !== 'login') {
    return res.status(400).json({ message: '2FA challenge is invalid or expired.' });
  }

  if (new Date(pending.expiresAt).getTime() < Date.now()) {
    twoFactor.pending = null;
    await writeDb(db);
    return res.status(400).json({ message: '2FA challenge expired. Log in again.' });
  }

  pending.attempts += 1;
  if (pending.attempts > 5) {
    twoFactor.pending = null;
    await writeDb(db);
    return res.status(429).json({ message: 'Too many incorrect 2FA attempts. Log in again.' });
  }

  if (hashOtp(code) !== pending.codeHash) {
    await writeDb(db);
    return res.status(400).json({ message: '2FA code is incorrect.' });
  }

  twoFactor.pending = null;
  twoFactor.lastVerifiedAt = nowIso();
  const token = issueSession(db, user, req);
  addOutbox(db, {
    to: user.email,
    subject: 'HKKO Gold security alert',
    body: `A new login was detected for your account at ${user.lastLoginAt}.`,
    type: 'security-login',
    userId: user.id,
  });
  await writeDb(db);
  setSessionCookie(res, token);

  return res.json({ message: 'Two-factor verification successful.', user: publicUser(user) });
});

app.post('/api/auth/2fa/setup/send', authRequired, profileRateLimit, async (req, res) => {
  const requestedMethod = req.body?.method === 'sms' ? 'sms' : 'email';
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const twoFactor = ensureTwoFactorProfile(user);
  const { challenge, code, destination } = buildTwoFactorChallenge({ user, purpose: 'enable', method: requestedMethod });
  twoFactor.pending = challenge;
  twoFactor.pendingMethod = requestedMethod;

  if (requestedMethod === 'sms') {
    if (!destination) {
      return res.status(400).json({ message: 'Add a valid phone number before enabling SMS 2FA.' });
    }
    addSmsOutbox(db, { to: destination, message: `HKKO 2FA setup code: ${code}.`, userId: user.id });
  } else {
    addOutbox(db, {
      to: user.email,
      subject: 'Enable 2FA on HKKO Gold',
      body: `Your one-time setup code is ${code}. It expires in 10 minutes.`,
      type: 'two-factor-setup',
      userId: user.id,
    });
  }

  await writeDb(db);
  return res.json({
    message: '2FA setup code sent.',
    method: requestedMethod,
    challengeId: challenge.id,
    demoCode: isProduction ? undefined : code,
  });
});

app.post('/api/auth/2fa/setup/confirm', authRequired, profileRateLimit, async (req, res) => {
  const challengeId = sanitizeText(req.body?.challengeId ?? '', 128);
  const code = sanitizeText(req.body?.code ?? '', 12);
  if (!challengeId || !/^\d{6}$/.test(code)) {
    return res.status(400).json({ message: 'Challenge and 6-digit code are required.' });
  }

  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const twoFactor = ensureTwoFactorProfile(user);
  const pending = twoFactor.pending;
  if (!pending || pending.id !== challengeId || pending.purpose !== 'enable') {
    return res.status(400).json({ message: '2FA setup challenge is invalid or expired.' });
  }

  if (new Date(pending.expiresAt).getTime() < Date.now()) {
    twoFactor.pending = null;
    await writeDb(db);
    return res.status(400).json({ message: '2FA setup code expired. Request a new one.' });
  }

  pending.attempts += 1;
  if (pending.attempts > 5) {
    twoFactor.pending = null;
    await writeDb(db);
    return res.status(429).json({ message: 'Too many incorrect attempts. Request a new setup code.' });
  }

  if (hashOtp(code) !== pending.codeHash) {
    await writeDb(db);
    return res.status(400).json({ message: '2FA setup code is incorrect.' });
  }

  twoFactor.enabled = true;
  twoFactor.method = twoFactor.pendingMethod === 'sms' ? 'sms' : 'email';
  twoFactor.pending = null;
  twoFactor.lastVerifiedAt = nowIso();
  addNotification(user, 'security', `Two-factor authentication enabled (${twoFactor.method.toUpperCase()}).`);
  await writeDb(db);

  return res.json({ message: 'Two-factor authentication enabled.', twoFactor: publicUser(user).twoFactor });
});

app.post('/api/auth/2fa/disable', authRequired, profileRateLimit, async (req, res) => {
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const twoFactor = ensureTwoFactorProfile(user);
  twoFactor.enabled = false;
  twoFactor.pending = null;
  addNotification(user, 'security', 'Two-factor authentication disabled.');
  await writeDb(db);

  return res.json({ message: 'Two-factor authentication disabled.', twoFactor: publicUser(user).twoFactor });
});

app.post('/api/auth/logout', async (req, res) => {
  const cookies = parseCookies(req.headers.cookie);
  const token = cookies.hkko_session;
  if (!token) {
    clearSessionCookie(res);
    return res.json({ message: 'Logged out.' });
  }

  const db = await readDb();
  db.sessions = db.sessions.filter((session) => session.token !== token);
  await writeDb(db);
  clearSessionCookie(res);
  return res.json({ message: 'Logged out.' });
});

app.get('/api/me', authRequired, async (req, res) => {
  return res.json({ user: publicUser(req.user) });
});

app.post('/api/profile', authRequired, profileRateLimit, async (req, res) => {
  const { profile = {} } = req.body ?? {};
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);

  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const profileEmail = normalizeEmail(profile.email ?? user.profile.email ?? user.email ?? '');
  const fullName = sanitizeText(profile.fullName ?? user.profile.fullName ?? '', 80);
  const phone = sanitizeText(profile.phone ?? user.profile.phone ?? '', 32);
  const country = sanitizeText(profile.country ?? user.profile.country ?? 'Ghana', 64) || 'Ghana';
  const role = sanitizeText(profile.role ?? user.profile.role ?? 'Investor', 64) || 'Investor';
  const avatarUrl = String(profile.avatarUrl ?? user.profile.avatarUrl ?? '').trim();

  if (!profileEmail || !isValidEmail(profileEmail)) {
    return res.status(400).json({ message: 'Profile email is invalid.' });
  }

  if (!fullName) {
    return res.status(400).json({ message: 'Profile name is required.' });
  }

  if (!isSafeAvatar(avatarUrl)) {
    return res.status(400).json({ message: 'Profile photo must be a secure image URL or base64 image under size limits.' });
  }

  user.profile = {
    ...user.profile,
    fullName,
    email: profileEmail,
    phone,
    country,
    role,
    avatarUrl,
  };
  user.email = user.profile.email;
  user.emailLower = normalizeEmail(user.profile.email);
  user.updatedAt = nowIso();
  await writeDb(db);

  return res.json({ message: 'Profile updated.', user: publicUser(user) });
});

app.get('/api/dashboard', authRequired, async (req, res) => {
  const price = await getLiveGoldPrice();
  const db = await readDb();
  const user = req.user;
  const verification = ensureVerificationProfile(user);
  const investmentJourney = ensureInvestmentJourney(user);

  return res.json({
    profile: user.profile,
    wallet: user.wallet,
    storage: user.storage,
    verification: verificationSummary(user),
    twoFactor: publicUser(user).twoFactor,
    securityEmails: user.securityEmails.slice(0, 20),
    securityStandards: securityStandardsSnapshot(),
    transactionStorage: transactionStoragePolicy(),
    backupStatus: {
      retention: backupRetention,
      location: 'server/data/backups',
      lastBackupAt: db.backupMetadata?.lastBackupAt ?? null,
      lastBackupFile: db.backupMetadata?.lastBackupFile ?? null,
    },
    investmentJourney,
    notifications: user.notifications.slice(0, 12),
    transactions: user.transactions.slice(0, 12),
    orders: user.orders.slice(0, 12),
    price,
    insuredStorageNote: user.storage.coverage,
  });
});

app.get('/api/notifications', authRequired, async (req, res) => {
  return res.json({ notifications: req.user.notifications });
});

app.get('/api/security-emails', authRequired, async (req, res) => {
  return res.json({ securityEmails: req.user.securityEmails.slice(0, 20) });
});

app.post('/api/notifications/read', authRequired, async (req, res) => {
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  user.notifications.forEach((notification) => {
    notification.read = true;
  });
  await writeDb(db);
  return res.json({ message: 'Notifications marked as read.' });
});

app.get('/api/storage', authRequired, async (req, res) => {
  return res.json({ storage: req.user.storage });
});

app.get('/api/security/standards', authRequired, async (_req, res) => {
  return res.json({ standards: securityStandardsSnapshot() });
});

app.get('/api/transactions/storage-policy', authRequired, async (_req, res) => {
  return res.json({ transactionStorage: transactionStoragePolicy() });
});

app.get('/api/system/backup/status', authRequired, async (_req, res) => {
  await ensureDb();
  const files = (await readdir(backupDir)).filter((name) => name.endsWith('.json')).sort().reverse();
  return res.json({
    retention: backupRetention,
    backupCount: files.length,
    latestBackupFile: files[0] ?? null,
    latestBackupAt: files[0] ?? null,
    location: backupDir,
    recovery: {
      strategy: 'Point-in-time snapshot restore',
      trigger: 'Restore via /api/system/backup/restore with a retained backup file name',
    },
  });
});

app.post('/api/system/backup/create', authRequired, async (_req, res) => {
  const db = await readDb();
  await writeDb(db, { createBackup: true });
  return res.json({ message: 'Backup snapshot created.' });
});

app.post('/api/system/backup/restore', authRequired, async (req, res) => {
  const backupFile = sanitizeText(req.body?.backupFile ?? '', 180);
  if (!backupFile || backupFile.includes('/') || backupFile.includes('..')) {
    return res.status(400).json({ message: 'Provide a valid backup file name.' });
  }

  await ensureDb();
  const source = join(backupDir, backupFile);
  try {
    const raw = await readFile(source, 'utf8');
    JSON.parse(raw);
    await writeFile(dataFile, raw, 'utf8');
    return res.json({ message: `Backup restored from ${backupFile}.` });
  } catch {
    return res.status(404).json({ message: 'Backup file not found or invalid.' });
  }
});

app.get('/api/investment', authRequired, async (req, res) => {
  return res.json({ investmentJourney: ensureInvestmentJourney(req.user) });
});

app.get('/api/verification', authRequired, async (req, res) => {
  return res.json({ verification: verificationSummary(req.user) });
});

app.post('/api/verification/phone/send', authRequired, profileRateLimit, async (req, res) => {
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const verification = ensureVerificationProfile(user);
  const phone = sanitizeText(req.body?.phone ?? user.profile.phone ?? verification.phoneVerification.phone ?? '', 32);
  const normalizedPhone = phone.replace(/\s+/g, '');

  if (!/^\+?[0-9]{9,15}$/.test(normalizedPhone)) {
    return res.status(400).json({ message: 'Enter a valid phone number for verification.' });
  }

  const code = String(Math.floor(100000 + Math.random() * 900000));
  verification.phoneVerification.phone = normalizedPhone;
  verification.phoneVerification.codeHash = hashOtp(code);
  verification.phoneVerification.codeExpiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
  verification.phoneVerification.attempts = 0;
  verification.phoneVerification.lastSentAt = nowIso();
  verification.phoneVerification.status = 'pending';
  user.profile.phone = normalizedPhone;
  user.updatedAt = nowIso();

  addSmsOutbox(db, {
    to: normalizedPhone,
    message: `HKKO verification code: ${code}. Expires in 10 minutes.`,
    userId: user.id,
  });

  await writeDb(db);
  return res.json({
    message: 'Verification code sent by SMS.',
    verification: verificationSummary(user),
    demoCode: isProduction ? undefined : code,
  });
});

app.post('/api/verification/phone/confirm', authRequired, profileRateLimit, async (req, res) => {
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const verification = ensureVerificationProfile(user);
  const code = sanitizeText(req.body?.code ?? '', 12);

  if (!/^\d{6}$/.test(code)) {
    return res.status(400).json({ message: 'Enter the 6-digit verification code.' });
  }

  if (!verification.phoneVerification.codeHash || !verification.phoneVerification.codeExpiresAt) {
    return res.status(400).json({ message: 'Request a verification code first.' });
  }

  if (new Date(verification.phoneVerification.codeExpiresAt).getTime() < Date.now()) {
    verification.phoneVerification.status = 'expired';
    await writeDb(db);
    return res.status(400).json({ message: 'Verification code expired. Request a new code.' });
  }

  verification.phoneVerification.attempts += 1;
  if (verification.phoneVerification.attempts > 5) {
    verification.phoneVerification.status = 'locked';
    await writeDb(db);
    return res.status(429).json({ message: 'Too many incorrect attempts. Request a new code.' });
  }

  if (hashOtp(code) !== verification.phoneVerification.codeHash) {
    await writeDb(db);
    return res.status(400).json({ message: 'Verification code is incorrect.' });
  }

  verification.phoneVerification.status = 'verified';
  verification.phoneVerification.verifiedAt = nowIso();
  verification.phoneVerification.codeHash = null;
  verification.phoneVerification.codeExpiresAt = null;
  verification.phoneVerification.attempts = 0;
  user.updatedAt = nowIso();

  evaluateAntiFraud(user);
  addNotification(user, 'security', 'Phone number verified successfully.');
  await writeDb(db);

  return res.json({ message: 'Phone number verified.', verification: verificationSummary(user) });
});

app.post('/api/verification/kyc/submit', authRequired, profileRateLimit, async (req, res) => {
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const verification = ensureVerificationProfile(user);
  const idCardFront = toSafeUploadSummary(req.body?.idCardFront, 'id-card-front');
  const idCardBack = toSafeUploadSummary(req.body?.idCardBack, 'id-card-back');
  const passport = toSafeUploadSummary(req.body?.passport, 'passport');

  if (!idCardFront || !idCardBack || !passport) {
    return res.status(400).json({ message: 'Upload valid ID card front, ID card back, and passport files.' });
  }

  verification.documents.idCardFront = idCardFront;
  verification.documents.idCardBack = idCardBack;
  verification.documents.passport = passport;
  verification.documents.status = 'submitted';
  verification.documents.submittedAt = nowIso();
  verification.kyc.status = 'submitted';
  verification.kyc.submittedAt = nowIso();

  const antiFraud = evaluateAntiFraud(user);
  if (antiFraud.status === 'passed' && verification.phoneVerification.status === 'verified') {
    verification.kyc.status = 'verified';
    verification.kyc.verifiedAt = nowIso();
    const journey = ensureInvestmentJourney(user);
    journey.kycStatus = 'approved';
    journey.kycCompletedAt = verification.kyc.verifiedAt;
    journey.status = journey.status === 'registered' ? 'kyc_complete' : journey.status;
    addNotification(user, 'investment', 'KYC/AML verification completed.');
  } else {
    verification.kyc.status = antiFraud.status === 'blocked' ? 'rejected' : 'under_review';
    verification.kyc.verifiedAt = null;
    verification.kyc.notes = antiFraud.status === 'blocked' ? 'Anti-fraud checks blocked automatic KYC approval.' : 'Pending manual compliance review.';
  }

  user.updatedAt = nowIso();
  addNotification(user, 'security', 'KYC documents submitted for review.');
  await writeDb(db);

  return res.json({
    message: verification.kyc.status === 'verified' ? 'KYC approved successfully.' : 'KYC submitted and queued for review.',
    verification: verificationSummary(user),
  });
});

app.post('/api/investment/step', authRequired, async (req, res) => {
  const { action, payload = {} } = req.body ?? {};
  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);

  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  const journey = ensureInvestmentJourney(user);
  const now = nowIso();

  const success = (message) => {
    user.updatedAt = now;
    return { message, investmentJourney: journey };
  };

  switch (action) {
    case 'register': {
      const productName = String(payload.productName ?? journey.productName).trim() || 'HKKO Gold Growth Plan';
      const principal = Number(payload.principal ?? journey.principal ?? 0);
      const termMonths = Number(payload.termMonths ?? journey.termMonths ?? 12);
      const monthlyRoiRate = Number(payload.monthlyRoiRate ?? journey.monthlyRoiRate ?? 0.0125);

      if (!Number.isFinite(principal) || principal < journey.minimumInvestment) {
        return res.status(400).json({ message: `Minimum investment is USD $${journey.minimumInvestment.toLocaleString()}.` });
      }

      journey.productName = productName;
      journey.principal = Number(principal.toFixed(2));
      journey.termMonths = Math.max(1, Math.round(termMonths));
      journey.monthlyRoiRate = Math.max(0.001, Math.min(monthlyRoiRate, 0.5));
      journey.monthlyDistribution = Number((journey.principal * journey.monthlyRoiRate).toFixed(2));
      journey.status = 'registered';
      journey.registrationCompletedAt = now;
      journey.validationEndsAt = addDaysIso(now, journey.validationPeriodDays);
      journey.applicant = {
        fullName: String(payload.fullName ?? user.profile.fullName ?? '').trim(),
        email: String(payload.email ?? user.email ?? '').trim(),
        phone: String(payload.phone ?? user.profile.phone ?? '').trim(),
        country: String(payload.country ?? user.profile.country ?? 'Ghana').trim(),
        role: String(payload.role ?? user.profile.role ?? 'Investor').trim(),
      };
      addNotification(user, 'investment', 'Investment account registered.');
      appendTransaction(user, {
        id: randomUUID(),
        type: 'deposit',
        amount: 0,
        status: 'completed',
        method: 'investment-registration',
        reference: journey.id,
        createdAt: now,
      });
      await writeDb(db);
      return res.json(success('Investment account registered.'));
    }

    case 'kyc': {
      const verification = ensureVerificationProfile(user);
      evaluateAntiFraud(user);

      if (verification.phoneVerification.status !== 'verified') {
        return res.status(400).json({ message: 'Complete phone verification before KYC approval.' });
      }

      if (verification.documents.status !== 'submitted') {
        return res.status(400).json({ message: 'Upload required ID card and passport documents before KYC approval.' });
      }

      if (verification.antiFraud.status === 'blocked') {
        verification.kyc.status = 'rejected';
        verification.kyc.notes = 'Blocked by anti-fraud checks.';
        await writeDb(db);
        return res.status(400).json({ message: 'KYC blocked by anti-fraud checks. Contact compliance support.' });
      }

      journey.kycStatus = 'approved';
      journey.kycCompletedAt = now;
      journey.status = 'kyc_complete';
      verification.kyc.status = verification.antiFraud.status === 'review' ? 'under_review' : 'verified';
      verification.kyc.submittedAt = verification.kyc.submittedAt ?? now;
      verification.kyc.verifiedAt = verification.antiFraud.status === 'review' ? null : now;
      verification.kyc.notes = verification.antiFraud.status === 'review' ? 'Anti-fraud review pending final compliance sign-off.' : '';
      addNotification(user, 'investment', 'KYC/AML verification completed.');
      await writeDb(db);
      return res.json(success('KYC/AML completed.'));
    }

    case 'application': {
      journey.applicationStatus = 'submitted';
      journey.applicationSubmittedAt = now;
      journey.status = 'application_submitted';
      addNotification(user, 'investment', 'Investment application submitted.');
      await writeDb(db);
      return res.json(success('Investment application submitted.'));
    }

    case 'validation': {
      journey.validationStatus = 'approved';
      journey.validatedAt = now;
      journey.status = 'validated';
      addNotification(user, 'investment', 'Investment validation approved.');
      await writeDb(db);
      return res.json(success('Validation passed.'));
    }

    case 'agreement': {
      journey.agreementStatus = 'signed';
      journey.agreementSignedAt = now;
      journey.status = 'agreement_signed';
      addNotification(user, 'investment', 'Investment agreement signed.');
      await writeDb(db);
      return res.json(success('Investment agreement signed.'));
    }

    case 'fund': {
      const amount = Number(payload.amount ?? journey.principal ?? 0);
      if (!Number.isFinite(amount) || amount <= 0) {
        return res.status(400).json({ message: 'Enter a valid funding amount.' });
      }

      if (user.wallet.cash < amount) {
        return res.status(400).json({ message: 'Insufficient wallet balance to fund the investment.' });
      }

      user.wallet.cash = Number((user.wallet.cash - amount).toFixed(2));
      journey.principal = Number(amount.toFixed(2));
      journey.fundingStatus = 'funded';
      journey.fundedAt = now;
      journey.status = 'funded';
      journey.maturityStatus = 'in_progress';
      journey.investmentStartDate = now;
      journey.nextRoiPaymentDate = addMonthsIso(now, 1);
      journey.investmentMaturityDate = addDaysIso(addMonthsIso(now, journey.contractDurationMonths), 1);
      journey.monthlyDistribution = Number((journey.principal * journey.monthlyRoiRate).toFixed(2));
      addNotification(user, 'investment', `Investment funded with GHS ${amount.toFixed(2)}.`);
      appendTransaction(user, {
        id: randomUUID(),
        type: 'withdrawal',
        amount,
        status: 'completed',
        destination: 'Investment funding',
        reference: journey.id,
        createdAt: now,
      });
      user.orders.unshift({
        id: randomUUID(),
        type: 'investment-funding',
        status: 'completed',
        amount,
        reference: journey.id,
        createdAt: now,
      });
      await writeDb(db);
      return res.json(success('Investment funded.'));
    }

    case 'roi': {
      if (journey.fundingStatus !== 'funded') {
        return res.status(400).json({ message: 'Fund the investment before recording ROI.' });
      }

      const monthNumber = journey.roiPayments.length + 1;
      const payout = Number((journey.principal * journey.monthlyRoiRate).toFixed(2));
      const transactionReference = `ROI-${journey.id.slice(0, 8).toUpperCase()}-${String(monthNumber).padStart(2, '0')}`;
      journey.roiPayments.unshift({
        id: randomUUID(),
        month: monthNumber,
        amount: payout,
        status: 'paid',
        transactionReference,
        paymentDate: now,
        paidAt: now,
      });
      journey.totalRoiPaid = Number((journey.totalRoiPaid + payout).toFixed(2));
      journey.monthsElapsed = Math.min(journey.termMonths, journey.monthsElapsed + 1);
      user.wallet.cash = Number((user.wallet.cash + payout).toFixed(2));
      journey.status = journey.monthsElapsed >= journey.termMonths ? 'matured' : 'active';
      journey.maturityStatus = journey.monthsElapsed >= journey.termMonths ? 'matured' : 'in_progress';
      journey.nextRoiPaymentDate = journey.monthsElapsed >= journey.termMonths ? null : addMonthsIso(journey.investmentStartDate ?? now, journey.monthsElapsed + 1);
      if (journey.monthsElapsed >= journey.termMonths) {
        journey.maturityReachedAt = now;
      }
      addNotification(user, 'investment', `Monthly ROI payment of GHS ${payout.toFixed(2)} recorded.`);
      appendTransaction(user, {
        id: randomUUID(),
        type: 'deposit',
        amount: payout,
        status: 'completed',
        method: 'monthly-roi',
        reference: transactionReference,
        createdAt: now,
      });
      await writeDb(db);
      return res.json(success('Monthly ROI recorded.'));
    }

    case 'maturity': {
      if (journey.monthsElapsed < journey.termMonths) {
        return res.status(400).json({ message: 'The investment has not reached maturity yet.' });
      }

      const decision = String(payload.decision ?? '').trim().toLowerCase();

      if (decision === 'reinvest') {
        const roiBonus = Number(journey.totalRoiPaid.toFixed(2));
        journey.principal = Number((journey.principal + roiBonus).toFixed(2));
        journey.maturityDecision = 'reinvest';
        journey.maturityDecidedAt = now;
        journey.maturityStatus = 'pending';
        journey.status = 'reinvested';
        journey.monthsElapsed = 0;
        journey.maturityReachedAt = null;
        journey.investmentStartDate = now;
        journey.nextRoiPaymentDate = addMonthsIso(now, 1);
        journey.investmentMaturityDate = addDaysIso(addMonthsIso(now, journey.contractDurationMonths), 1);
        journey.monthlyDistribution = Number((journey.principal * journey.monthlyRoiRate).toFixed(2));
        addNotification(user, 'investment', 'Maturity reached and funds reinvested.');
        await writeDb(db);
        return res.json(success('Principal reinvested.'));
      }

      if (decision === 'redeem') {
        const redeemedPrincipal = journey.principal;
        user.wallet.cash = Number((user.wallet.cash + journey.principal).toFixed(2));
        journey.maturityDecision = 'redeem';
        journey.maturityDecidedAt = now;
        journey.maturityStatus = 'closed';
        journey.status = 'redeemed';
        journey.principal = 0;
        addNotification(user, 'investment', 'Maturity reached and principal redeemed.');
        appendTransaction(user, {
          id: randomUUID(),
          type: 'deposit',
          amount: redeemedPrincipal,
          status: 'completed',
          method: 'principal-redemption',
          reference: `REDEEM-${journey.id.slice(0, 8).toUpperCase()}`,
          createdAt: now,
        });
        await writeDb(db);
        return res.json(success('Principal redeemed.'));
      }

      return res.status(400).json({ message: 'Choose either reinvest or redeem.' });
    }

    default:
      return res.status(400).json({ message: 'Unknown investment action.' });
  }
});

app.get('/api/transactions', authRequired, async (req, res) => {
  return res.json({ transactions: req.user.transactions });
});

app.get('/api/orders', authRequired, async (req, res) => {
  return res.json({ orders: req.user.orders });
});

app.delete('/api/account', authRequired, async (req, res) => {
  const db = await readDb();
  db.users = db.users.filter((user) => user.id !== req.user.id);
  db.sessions = db.sessions.filter((session) => session.userId !== req.user.id);
  db.notifications = db.notifications.filter((item) => item.userId !== req.user.id);
  db.emailOutbox = db.emailOutbox.filter((item) => item.userId !== req.user.id);
  db.smsOutbox = db.smsOutbox.filter((item) => item.userId !== req.user.id);
  await writeDb(db);
  clearSessionCookie(res);
  return res.json({ message: 'Account deleted.' });
});

app.post('/api/wallet/deposit/initiate', authRequired, transactionRateLimit, async (req, res) => {
  const { amount, method, channel = method, customer = {} } = req.body ?? {};
  const numericAmount = Number(amount);
  const safeMethod = sanitizeText(method ?? 'Mobile Money', 32);
  const safeChannel = sanitizeText(channel ?? method ?? 'Mobile Money', 32);
  const allowedMethods = new Set(['Mobile Money', 'Visa Card', 'Mastercard']);

  if (!Number.isFinite(numericAmount) || numericAmount <= 0 || numericAmount > 5_000_000) {
    return res.status(400).json({ message: 'A valid deposit amount is required.' });
  }

  if (!allowedMethods.has(safeMethod)) {
    return res.status(400).json({ message: 'Unsupported payment method.' });
  }

  const db = await readDb();
  const reference = randomUUID();
  const request = {
    id: randomUUID(),
    reference,
    userId: req.user.id,
    amount: Number(numericAmount.toFixed(2)),
    method: safeMethod,
    channel: safeChannel,
    status: 'pending',
    customer,
    createdAt: nowIso(),
  };
  db.depositRequests.unshift(request);
  addNotification(req.user, 'wallet', `Deposit request for ${request.amount.toFixed(2)} GHS created.`);
  addOutbox(db, {
    to: req.user.email,
    subject: 'Deposit request created',
    body: `A deposit request for GHS ${request.amount.toFixed(2)} was created with reference ${reference}.`,
    type: 'deposit-request',
    userId: req.user.id,
  });

  const gatewayBase = String(process.env.PAYMENT_GATEWAY_BASE_URL ?? '').trim();
  const checkoutUrl = gatewayBase ? `${gatewayBase.replace(/\/$/, '')}/checkout/${reference}` : `${publicBaseUrl}/api/wallet/deposit/checkout/${reference}`;
  const smsMessage = `HKKO deposit request ${reference} initiated for GHS ${request.amount.toFixed(2)}.`;
  addSmsOutbox(db, { to: req.user.profile.phone, message: smsMessage, userId: req.user.id });
  await writeDb(db);

  return res.json({
    reference,
    checkoutUrl,
    status: 'pending',
  });
});

app.get('/api/wallet/deposit/checkout/:reference', authRequired, async (req, res) => {
  const reference = String(req.params.reference ?? '');
  const db = await readDb();
  const request = db.depositRequests.find((entry) => entry.reference === reference && entry.userId === req.user.id);

  if (!request) {
    return res.status(404).send('Deposit request not found.');
  }

  res.type('html').send(localCheckoutPage(reference, request.amount, req.user.email));
});

app.post('/api/wallet/deposit/confirm', authRequired, transactionRateLimit, async (req, res) => {
  const { reference, status = 'success', providerRef = '' } = req.body ?? {};
  const safeReference = sanitizeText(reference, 128);
  const safeStatus = sanitizeText(status, 24).toLowerCase();
  const safeProviderRef = sanitizeText(providerRef, 128);
  const db = await readDb();
  const request = db.depositRequests.find((entry) => entry.reference === safeReference && entry.userId === req.user.id);
  if (!request) {
    return res.status(404).json({ message: 'Deposit request not found.' });
  }

  if (safeStatus !== 'success') {
    request.status = 'failed';
    await writeDb(db);
    return res.status(400).json({ message: 'Deposit was not confirmed.' });
  }

  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  user.wallet.cash = Number((user.wallet.cash + request.amount).toFixed(2));
  request.status = 'confirmed';
  request.providerRef = safeProviderRef;
  appendTransaction(user, {
    id: randomUUID(),
    type: 'deposit',
    amount: request.amount,
    status: 'completed',
    method: request.method,
    channel: request.channel,
    reference: safeReference,
    createdAt: nowIso(),
  });
  user.orders.unshift({
    id: randomUUID(),
    type: 'wallet-funding',
    status: 'completed',
    amount: request.amount,
    reference: safeReference,
    createdAt: nowIso(),
  });
  addNotification(user, 'wallet', `Deposit confirmed for GHS ${request.amount.toFixed(2)}.`);
  addOutbox(db, {
    to: user.email,
    subject: 'Deposit confirmed',
    body: `Your deposit of GHS ${request.amount.toFixed(2)} has been confirmed.`,
    type: 'deposit-confirmed',
    userId: user.id,
  });
  addSmsOutbox(db, { to: user.profile.phone, message: `Deposit confirmed: GHS ${request.amount.toFixed(2)}.`, userId: user.id });
  await writeDb(db);
  return res.json({ message: 'Deposit confirmed.' });
});

app.post('/api/wallet/withdrawal/request', authRequired, transactionRateLimit, async (req, res) => {
  const { amount, destination = 'Mobile Money', account = '' } = req.body ?? {};
  const numericAmount = Number(amount);
  const safeDestination = sanitizeText(destination, 60) || 'Mobile Money';
  const safeAccount = sanitizeText(account, 120);

  if (!Number.isFinite(numericAmount) || numericAmount <= 0 || numericAmount > 5_000_000) {
    return res.status(400).json({ message: 'A valid withdrawal amount is required.' });
  }

  const db = await readDb();
  const user = db.users.find((entry) => entry.id === req.user.id);
  if (!user) {
    return res.status(404).json({ message: 'Account not found.' });
  }

  if (user.wallet.cash < numericAmount) {
    return res.status(400).json({ message: 'Insufficient wallet balance.' });
  }

  user.wallet.cash = Number((user.wallet.cash - numericAmount).toFixed(2));
  user.wallet.pending = Number((user.wallet.pending + numericAmount).toFixed(2));
  const request = {
    id: randomUUID(),
    userId: user.id,
    amount: Number(numericAmount.toFixed(2)),
    destination: safeDestination,
    account: safeAccount,
    status: 'pending',
    createdAt: nowIso(),
  };
  db.withdrawalRequests.unshift(request);
  appendTransaction(user, {
    id: randomUUID(),
    type: 'withdrawal',
    amount: numericAmount,
    status: 'pending',
    destination: safeDestination,
    account: safeAccount,
    createdAt: nowIso(),
  });
  addNotification(user, 'wallet', `Withdrawal request pending for GHS ${numericAmount.toFixed(2)}.`);
  addOutbox(db, {
    to: user.email,
    subject: 'Withdrawal request received',
    body: `Your withdrawal request for GHS ${numericAmount.toFixed(2)} is pending review.`,
    type: 'withdrawal-request',
    userId: user.id,
  });
  addSmsOutbox(db, { to: user.profile.phone, message: `Withdrawal request pending for GHS ${numericAmount.toFixed(2)}.`, userId: user.id });
  await writeDb(db);
  return res.json({ message: 'Withdrawal request created.', request });
});

app.get('/api/pricing/latest', async (_req, res) => {
  const price = await getLiveGoldPrice();
  res.json({
    price,
    margins: {
      retailPremiumPercent: 2.5,
      buyBackDiscountPercent: 1.8,
    },
    synchronizedAt: price.updatedAt,
  });
});

app.use((error, req, res, _next) => {
  console.error(`[${nowIso()}] Unhandled error on ${req.method} ${req.originalUrl}`, error);
  if (res.headersSent) {
    return;
  }
  res.status(500).json({ message: 'Internal server error.' });
});

await ensureDb();
const clientBundleAvailable = await hasClientBundle();

if (clientBundleAvailable) {
  app.use(express.static(clientDistDir));
}

app.use('/api', (req, res) => {
  res.status(404).json({ message: `Route not found: ${req.method} ${req.path}` });
});

if (clientBundleAvailable) {
  app.get('/{*splat}', (_req, res) => {
    res.sendFile(join(clientDistDir, 'index.html'));
  });
} else {
  app.use((req, res) => {
    res.status(404).json({
      message: `Route not found: ${req.method} ${req.path}. Build the frontend with \"npm run build\" to serve the web app from this server.`,
    });
  });
}

app.listen(port, () => {
  console.log(`HKKO API running on ${publicBaseUrl}`);
});
