// @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';
('use strict');

const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const { passwordLimiter } = require('../middleware/rateLimiter.cts');

const { getDb, getSetting } = require('../db/database.cts');
const {
  hashPassword,
  invalidateOtherSessions,
  rotateSessionId,
  COOKIE_NAME,
  cookieOpts,
} = require('../services/authService.cts');
const { getImportHistory } = require('../services/spreadsheetImportService.cts');
const { logAudit } = require('../services/auditService.cts');
const { standardizeError } = require('../middleware/errorFormatter.cts');
const { encryptSecret, decryptSecret } = require('../services/encryptionService.cts');

// All profile routes require authentication — enforced in server.js.
// req.user is always the signed-in user; user_id is never accepted from the body.

function dataImportEnabled() {
  return String(process.env.DATA_IMPORT_ENABLED ?? 'true').toLowerCase() !== 'false';
}

function requireDataImportEnabled(req, res, next) {
  if (!dataImportEnabled()) {
    return res
      .status(403)
      .json(standardizeError('Data import is disabled by DATA_IMPORT_ENABLED=false', 'FORBIDDEN'));
  }
  next();
}

function profileResponse(user) {
  const displayName = user.display_name || null;
  return {
    id: user.id,
    username: user.username,
    display_name: displayName,
    displayName,
    name: displayName || user.username,
    role: user.role,
    active: !!user.active,
    is_default_admin: !!user.is_default_admin,
    created_at: user.created_at,
    updated_at: user.updated_at,
    last_password_change_at: user.last_password_change_at || null,
    first_login: !!user.first_login,
  };
}

// ── GET /api/profile ──────────────────────────────────────────────────────────
// Returns safe profile data for the signed-in user.
// Never returns password_hash, session tokens, or secrets.
router.get('/', (req: Req, res: Res) => {
  const db = getDb();
  const user = db
    .prepare(
      `
    SELECT id, username, display_name, role, active, is_default_admin,
           first_login, created_at, updated_at,
           last_password_change_at,
           notification_email, notifications_enabled,
           notify_3d, notify_1d, notify_due, notify_overdue
    FROM users WHERE id = ?
  `,
    )
    .get(req.user.id);

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

  res.json({
    ...profileResponse(user),
    notifications: {
      email: user.notification_email || null,
      enabled: !!user.notifications_enabled,
      notify_3d: !!user.notify_3d,
      notify_1d: !!user.notify_1d,
      notify_due: !!user.notify_due,
      notify_overdue: !!user.notify_overdue,
    },
    exports: {
      user_db: '/api/export/user-db',
      user_excel: '/api/export/user-excel',
    },
    import_history_url: '/api/profile/import-history',
  });
});

// ── PATCH /api/profile ────────────────────────────────────────────────────────
// Updates safe profile fields: username and display_name.
// Ignores any unknown or restricted fields.
router.patch('/', (req: Req, res: Res) => {
  const { username } = req.body;
  const displayNameInput =
    req.body.display_name !== undefined
      ? req.body.display_name
      : req.body.displayName !== undefined
        ? req.body.displayName
        : req.body.name;
  const db = getDb();

  if (username !== undefined) {
    if (typeof username !== 'string') {
      return res.status(400).json({ error: 'username must be a string' });
    }
    const trimmedUsername = username.trim();
    if (trimmedUsername.length < 3) {
      return res.status(400).json({ error: 'username must be at least 3 characters' });
    }
    if (trimmedUsername.length > 50) {
      return res.status(400).json({ error: 'username must be 50 characters or fewer' });
    }
    const taken = db
      .prepare('SELECT id FROM users WHERE username = ? COLLATE NOCASE AND id != ?')
      .get(trimmedUsername, req.user.id);
    if (taken) {
      return res.status(409).json({ error: 'Username already taken' });
    }
    db.prepare("UPDATE users SET username = ?, updated_at = datetime('now') WHERE id = ?").run(
      trimmedUsername,
      req.user.id,
    );
    logAudit({
      user_id: req.user.id,
      action: 'profile.username.change',
      ip_address: req.ip,
      user_agent: req.get('user-agent'),
    });
  }

  if (displayNameInput !== undefined) {
    if (typeof displayNameInput !== 'string') {
      return res.status(400).json({ error: 'display_name must be a string' });
    }
    const trimmed = displayNameInput.trim();
    if (trimmed.length > 100) {
      return res.status(400).json({ error: 'display_name must be 100 characters or fewer' });
    }

    db.prepare("UPDATE users SET display_name = ?, updated_at = datetime('now') WHERE id = ?").run(
      trimmed || null,
      req.user.id,
    );

    logAudit({
      user_id: req.user.id,
      action: 'profile.update',
      ip_address: req.ip,
      user_agent: req.get('user-agent'),
    });
  }

  const updated = getDb()
    .prepare(
      `
    SELECT id, username, display_name, role, active, is_default_admin,
           first_login, created_at, updated_at,
           last_password_change_at
    FROM users WHERE id = ?
  `,
    )
    .get(req.user.id);

  res.json({ success: true, profile: profileResponse(updated) });
});

// ── GET /api/profile/settings ─────────────────────────────────────────────────
// Returns user-owned notification preferences from the users table.
// Does not return admin/global/SMTP settings.
router.get('/settings', (req: Req, res: Res) => {
  const db = getDb();
  const user = db
    .prepare(
      `
    SELECT notification_email, notifications_enabled,
           notify_3d, notify_1d, notify_due, notify_overdue, notify_amount_change,
           notify_push_enabled, push_channel, push_url, push_token, push_chat_id,
           geolocation_enabled
    FROM users WHERE id = ?
  `,
    )
    .get(req.user.id);

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

  // Decrypt push secrets — return masked indicator for token (never the raw value)
  const pushUrlDecrypted = user.push_url
    ? (() => {
        try {
          return decryptSecret(user.push_url);
        } catch {
          return user.push_url;
        }
      })()
    : null;

  res.json({
    notification_email: user.notification_email || null,
    notifications_enabled: !!user.notifications_enabled,
    notify_3d: !!user.notify_3d,
    notify_1d: !!user.notify_1d,
    notify_due: !!user.notify_due,
    notify_overdue: !!user.notify_overdue,
    notify_amount_change: user.notify_amount_change !== 0,
    notify_push_enabled: !!user.notify_push_enabled,
    push_channel: user.push_channel || null,
    push_url: pushUrlDecrypted || null,
    push_token_set: !!user.push_token,
    push_chat_id: user.push_chat_id || null,
    geolocation_enabled: !!user.geolocation_enabled,
  });
});

// ── PATCH /api/profile/settings ───────────────────────────────────────────────
// Updates user-owned notification preferences only.
// Cannot modify global/admin/SMTP settings through this endpoint.
const VALID_PUSH_CHANNELS = new Set(['ntfy', 'gotify', 'discord', 'telegram']);

router.patch('/settings', (req: Req, res: Res) => {
  const db = getDb();
  const {
    notification_email,
    email,
    notifications_enabled,
    notify_3d,
    notify_1d,
    notify_due,
    notify_overdue,
    notify_amount_change,
    notify_push_enabled,
    push_channel,
    push_url,
    push_token,
    push_chat_id,
    geolocation_enabled,
  } = req.body;

  const nextEmail = notification_email !== undefined ? notification_email : email;

  if (nextEmail !== undefined && nextEmail !== null) {
    if (typeof nextEmail !== 'string') {
      return res.status(400).json({ error: 'notification_email must be a string' });
    }
    if (nextEmail.trim().length > 255) {
      return res.status(400).json({ error: 'notification_email is too long' });
    }
  }

  if (
    push_channel !== undefined &&
    push_channel !== null &&
    !VALID_PUSH_CHANNELS.has(push_channel)
  ) {
    return res
      .status(400)
      .json({ error: `push_channel must be one of: ${[...VALID_PUSH_CHANNELS].join(', ')}` });
  }

  const current = db
    .prepare(
      `
    SELECT notification_email, notifications_enabled,
           notify_3d, notify_1d, notify_due, notify_overdue, notify_amount_change,
           notify_push_enabled, push_channel, push_url, push_token, push_chat_id,
           geolocation_enabled
    FROM users WHERE id = ?
  `,
    )
    .get(req.user.id);

  const emailVal =
    nextEmail !== undefined
      ? nextEmail
        ? nextEmail.trim() || null
        : null
      : current.notification_email;

  const boolVal = (incoming, fallback) => (incoming !== undefined ? (incoming ? 1 : 0) : fallback);

  // Encrypt push_url and push_token before storing
  const encryptOrNull = (v, fallback) => {
    if (v === undefined) return fallback;
    if (!v) return null;
    try {
      return encryptSecret(String(v).trim());
    } catch {
      return String(v).trim();
    }
  };

  db.prepare(
    `
    UPDATE users SET
      notification_email    = ?,
      notifications_enabled = ?,
      notify_3d             = ?,
      notify_1d             = ?,
      notify_due            = ?,
      notify_overdue        = ?,
      notify_amount_change  = ?,
      notify_push_enabled   = ?,
      push_channel          = ?,
      push_url              = ?,
      push_token            = ?,
      push_chat_id          = ?,
      geolocation_enabled   = ?,
      updated_at            = datetime('now')
    WHERE id = ?
  `,
  ).run(
    emailVal,
    boolVal(notifications_enabled, current.notifications_enabled),
    boolVal(notify_3d, current.notify_3d),
    boolVal(notify_1d, current.notify_1d),
    boolVal(notify_due, current.notify_due),
    boolVal(notify_overdue, current.notify_overdue),
    boolVal(notify_amount_change, current.notify_amount_change),
    boolVal(notify_push_enabled, current.notify_push_enabled),
    push_channel !== undefined ? push_channel || null : current.push_channel,
    encryptOrNull(push_url, current.push_url),
    encryptOrNull(push_token, current.push_token),
    push_chat_id !== undefined ? push_chat_id?.trim() || null : current.push_chat_id,
    boolVal(geolocation_enabled, current.geolocation_enabled),
    req.user.id,
  );

  logAudit({
    user_id: req.user.id,
    action: 'profile.settings.update',
    ip_address: req.ip,
    user_agent: req.get('user-agent'),
  });

  res.json({ success: true });
});

// ── POST /api/profile/change-password ─────────────────────────────────────────
// Changes the signed-in user's password.
// Always requires: current_password, new_password, confirm_new_password.
// Never bypasses current_password verification regardless of must_change_password.
// Never accepts user_id from the request body.
router.post('/change-password', passwordLimiter, async (req: Req, res: Res) => {
  const { current_password, new_password, confirm_new_password } = req.body;

  if (!current_password) {
    return res.status(400).json({ error: 'current_password is required' });
  }
  if (!new_password) {
    return res.status(400).json({ error: 'new_password is required' });
  }
  if (!confirm_new_password) {
    return res.status(400).json({ error: 'confirm_new_password is required' });
  }
  if (new_password !== confirm_new_password) {
    return res.status(400).json({ error: 'new passwords do not match' });
  }
  if (new_password.length < 8) {
    return res.status(400).json({ error: 'new password must be at least 8 characters' });
  }

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

  try {
    const valid = await bcrypt.compare(current_password, user.password_hash);
    if (!valid) {
      return res.status(401).json({ error: 'current password is incorrect' });
    }

    const hash = await hashPassword(new_password);

    db.prepare(
      `
      UPDATE users
      SET password_hash = ?, must_change_password = 0,
          last_password_change_at = datetime('now'),
          updated_at = datetime('now')
      WHERE id = ?
    `,
    ).run(hash, req.user.id);

    // Invalidate all other sessions for this user
    const currentSessionId = req.cookies?.[COOKIE_NAME];
    if (currentSessionId) {
      invalidateOtherSessions(req.user.id, currentSessionId);

      // Rotate the current session ID for security
      const newSessionId = rotateSessionId(currentSessionId, req.user.id);
      if (newSessionId) {
        res.cookie(COOKIE_NAME, newSessionId, cookieOpts(req));
      }
    }

    logAudit({
      user_id: req.user.id,
      action: 'password.change',
      ip_address: req.ip,
      user_agent: req.get('user-agent'),
    });

    res.json({ success: true });
  } catch (err) {
    console.error('[profile] change-password error:', err.message);
    res.status(500).json({ error: 'Password change failed' });
  }
});

// ── GET /api/profile/exports ──────────────────────────────────────────────────
// Returns metadata about available user export capabilities.
// Does not trigger any download; links point to the actual export endpoints.
router.get('/exports', (req: Req, res: Res) => {
  res.json({
    exports: [
      {
        id: 'user_db',
        label: 'SQLite database export',
        description: 'Your bills, categories, payments, and notes as a portable SQLite file',
        url: '/api/export/user-db',
        method: 'GET',
      },
      {
        id: 'user_excel',
        label: 'Excel databook export',
        description: 'Your data as an Excel workbook with multiple sheets',
        url: '/api/export/user-excel',
        method: 'GET',
      },
    ],
  });
});

// ── GET /api/profile/import-history ──────────────────────────────────────────
// Returns the signed-in user's import history.
// Delegates to the same service as GET /api/import/history.
router.get('/import-history', requireDataImportEnabled, (req: Req, res: Res) => {
  try {
    const history = getImportHistory(req.user.id);
    res.json({ history });
  } catch (err) {
    console.error('[profile] import-history error:', err.message);
    res.status(500).json({ error: 'Failed to load import history' });
  }
});

module.exports = router;
