/**
 * models/user.js
 */

'use strict';

const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const { db } = require('../config/db');
const { getPlan } = require('../config/plans');
const sec = require('../config/security');

const BCRYPT_ROUNDS = sec.BCRYPT_COST;
const VERIFY_TTL    = 1000 * 60 * 60 * 24;     // 24h
const RESET_TTL     = 1000 * 60 * 60 * 2;      // 2h
const LOCK_MS       = sec.LOGIN_LOCKOUT_MIN * 60 * 1000;

const insert = db.prepare(`
  INSERT INTO users
    (email, password_hash, name, role, plan, email_verified, verify_token, verify_expires, created_at, updated_at)
  VALUES
    (@email, @password_hash, @name, @role, @plan, 0, @verify_token, @verify_expires, @created_at, @updated_at)
`);

const findById = db.prepare('SELECT * FROM users WHERE id = ?');
const findByEmail = db.prepare('SELECT * FROM users WHERE email = ?');
const findByVerifyToken = db.prepare('SELECT * FROM users WHERE verify_token = ?');
const findByResetToken  = db.prepare('SELECT * FROM users WHERE reset_token = ?');
const findByStripeCustomer = db.prepare('SELECT * FROM users WHERE stripe_customer_id = ?');

const markVerified = db.prepare(`
  UPDATE users SET email_verified = 1, verify_token = NULL, verify_expires = NULL, updated_at = ?
  WHERE id = ?
`);

const setPasswordHash = db.prepare(`
  UPDATE users SET password_hash = ?, password_changed_at = ?, reset_token = NULL, reset_expires = NULL, updated_at = ? WHERE id = ?
`);

const bumpPasswordChangedAt = db.prepare(`
  UPDATE users SET password_changed_at = ?, updated_at = ? WHERE id = ?
`);

const setEmailStmt = db.prepare(`
  UPDATE users SET email = ?, email_changed_at = ?, email_verified = 0, verify_token = ?, verify_expires = ?, updated_at = ? WHERE id = ?
`);

const setResetToken = db.prepare(`
  UPDATE users SET reset_token = ?, reset_expires = ?, updated_at = ? WHERE id = ?
`);

const bumpFailedLogin = db.prepare(`
  UPDATE users
     SET failed_logins = failed_logins + 1,
         locked_until = CASE WHEN failed_logins + 1 >= ? THEN ? ELSE locked_until END,
         updated_at = ?
   WHERE id = ?
`);

const resetFailedLogins = db.prepare(`
  UPDATE users SET failed_logins = 0, locked_until = NULL, last_login_at = ?, updated_at = ? WHERE id = ?
`);

const updateProfile = db.prepare(`
  UPDATE users SET name = COALESCE(?, name), plan = COALESCE(?, plan),
                   plan_override = COALESCE(?, plan_override),
                   plan_override_reason = COALESCE(?, plan_override_reason),
                   suspended = COALESCE(?, suspended),
                   updated_at = ?
  WHERE id = ?
`);

const setStripeCustomer = db.prepare(`
  UPDATE users SET stripe_customer_id = ?, updated_at = ? WHERE id = ?
`);

const setStripeSubscription = db.prepare(`
  UPDATE users SET stripe_subscription_id = ?, plan = ?, billing_interval = ?, seats = ?, updated_at = ? WHERE id = ?
`);

const markPaymentFailed = db.prepare(`
  UPDATE users SET payment_failed = 1, updated_at = ? WHERE id = ?
`);
const clearPaymentFailed = db.prepare(`
  UPDATE users SET payment_failed = 0, updated_at = ? WHERE id = ?
`);

const deleteById = db.prepare('DELETE FROM users WHERE id = ?');
const listAll = db.prepare(`
  SELECT id, email, name, role, plan, suspended, payment_failed, last_login_at, created_at
    FROM users
   ORDER BY created_at DESC
   LIMIT ? OFFSET ?
`);
const countAll = db.prepare('SELECT COUNT(*) AS n FROM users');
const countByPlan = db.prepare(`
  SELECT plan, COUNT(*) AS n FROM users WHERE role != 'superadmin' GROUP BY plan
`);

const recentRegistrations = db.prepare(`
  SELECT DATE(created_at/1000, 'unixepoch') AS day, COUNT(*) AS n
    FROM users
   WHERE created_at > ?
   GROUP BY day
   ORDER BY day ASC
`);

function validatePassword(pw) {
  if (typeof pw !== 'string' || pw.length < 10) return 'Password must be at least 10 characters';
  if (!/[0-9]/.test(pw))                       return 'Password must include a number';
  if (!/[^A-Za-z0-9]/.test(pw))                 return 'Password must include a symbol';
  return null;
}

function create({ email, password, name, role = 'user', plan = 'free' }) {
  if (!email || !password || !name) throw new Error('Missing required fields');
  const err = validatePassword(password);
  if (err) throw new Error(err);

  const password_hash = bcrypt.hashSync(password, BCRYPT_ROUNDS);
  const now = Date.now();
  const verify_token   = crypto.randomBytes(32).toString('hex');
  const verify_expires = now + VERIFY_TTL;

  const info = insert.run({
    email: email.toLowerCase().trim(),
    password_hash,
    name: name.trim(),
    role,
    plan,
    verify_token,
    verify_expires,
    created_at: now,
    updated_at: now,
  });
  const u = getById(info.lastInsertRowid);
  // Stamp password_changed_at on creation so the loadUser session check
  // has a baseline to compare against.
  bumpPasswordChangedAt.run(now, now, u.id);
  return getById(u.id);
}

function getById(id) { return findById.get(id); }
function getByEmail(email) { return findByEmail.get(String(email).toLowerCase().trim()); }
function getByVerifyToken(token) { return findByVerifyToken.get(token); }
function getByResetToken(token) { return findByResetToken.get(token); }
function getByStripeCustomer(id) { return findByStripeCustomer.get(id); }

function verifyEmail(userId) { markVerified.run(Date.now(), userId); }
function setPassword(userId, newPassword) {
  const err = validatePassword(newPassword);
  if (err) throw new Error(err);
  const now = Date.now();
  setPasswordHash.run(bcrypt.hashSync(newPassword, BCRYPT_ROUNDS), now, now, userId);
  // The bump in password_changed_at is what loadUser uses to invalidate
  // other sessions — setPasswordHash already sets it, but be explicit.
  bumpPasswordChangedAt.run(now, now, userId);
}

function setEmail(userId, newEmail) {
  const verify_token   = crypto.randomBytes(32).toString('hex');
  const verify_expires = Date.now() + VERIFY_TTL;
  const now = Date.now();
  setEmailStmt.run(newEmail.toLowerCase().trim(), now, verify_token, verify_expires, now, userId);
  return { verify_token, verify_expires };
}

function issueResetToken(userId) {
  const token = crypto.randomBytes(32).toString('hex');
  setResetToken.run(token, Date.now() + RESET_TTL, Date.now(), userId);
  return token;
}

function comparePassword(plain, hash) {
  try { return bcrypt.compareSync(plain, hash); }
  catch { return false; }
}

function registerFailedLogin(userId) {
  const max = parseInt(process.env.LOGIN_MAX_ATTEMPTS, 10) || 5;
  bumpFailedLogin.run(max, Date.now() + LOCK_MS, Date.now(), userId);
}

function clearFailedLogins(userId) {
  resetFailedLogins.run(Date.now(), Date.now(), userId);
}

function isLocked(user) {
  return user && user.locked_until && user.locked_until > Date.now();
}

function safeUser(user) {
  if (!user) return null;
  const { password_hash, verify_token, reset_token, ...safe } = user;
  safe.plan_details = getPlan(user.plan, user.plan_override);
  return safe;
}

function update(id, { name, plan, plan_override, plan_override_reason, suspended }) {
  updateProfile.run(
    name ?? null,
    plan ?? null,
    plan_override !== undefined ? (plan_override ? JSON.stringify(plan_override) : null) : null,
    plan_override_reason ?? null,
    suspended === undefined ? null : (suspended ? 1 : 0),
    Date.now(),
    id
  );
}

function setStripeCustomerId(id, customerId) {
  setStripeCustomer.run(customerId, Date.now(), id);
}
function setStripeSubscriptionId(id, subscriptionId, plan, interval, seats) {
  // Coerce + validate — if interval or seats are bogus, fall back to
  // month / 1 rather than write garbage to the DB.
  const iv = (interval === 'year') ? 'year' : 'month';
  const sq = Math.max(1, parseInt(seats, 10) || 1);
  setStripeSubscription.run(subscriptionId, plan, iv, sq, Date.now(), id);
}

// Add non-expiring credits from a one-time credit-pack purchase.
// Both grants are optional; pass 0 to skip a column. Used by the
// webhook when a mode='payment' checkout completes.
const addCredits = db.prepare(`
  UPDATE users SET extra_scans = extra_scans + ?, extra_takedowns = extra_takedowns + ?,
                   updated_at = ? WHERE id = ?
`);
function grantCredits(id, scans, takedowns) {
  const s = Math.max(0, parseInt(scans, 10) || 0);
  const t = Math.max(0, parseInt(takedowns, 10) || 0);
  addCredits.run(s, t, Date.now(), id);
}

// Spend one credit of a given type ('scan' | 'takedown'). Returns
// true if a credit was consumed, false if no credits available
// (caller should fall back to the monthly quota).
const spendCredit = db.prepare(`
  UPDATE users SET extra_scans = extra_scans - 1, updated_at = ?
   WHERE id = ? AND extra_scans > 0
`);
const spendTakedownCredit = db.prepare(`
  UPDATE users SET extra_takedowns = extra_takedowns - 1, updated_at = ?
   WHERE id = ? AND extra_takedowns > 0
`);
function consumeCredit(id, type) {
  if (type === 'scan')     return spendCredit.run(Date.now(), id).changes > 0;
  if (type === 'takedown') return spendTakedownCredit.run(Date.now(), id).changes > 0;
  return false;
}
function markFailedPayment(id) { markPaymentFailed.run(Date.now(), id); }
function clearFailedPayment(id) { clearPaymentFailed.run(Date.now(), id); }
function remove(id) { deleteById.run(id); }

function list({ limit = 50, offset = 0, search = '', plan = '', status = '', include_test = '0' } = {}) {
  // dynamic filters — build prepared query
  const where = [];
  const params = [];
  if (search) {
    where.push('(email LIKE ? OR name LIKE ?)');
    params.push(`%${search}%`, `%${search}%`);
  }
  if (plan)    { where.push('plan = ?');    params.push(plan); }
  if (status === 'suspended')  { where.push('suspended = 1'); }
  if (status === 'active')     { where.push('suspended = 0'); }
  if (status === 'payment_failed') { where.push('payment_failed = 1'); }
  // Default: hide test data (636 @test.local users drown the table).
  // Pass include_test=1 to see them (for cleanup / debugging).
  if (include_test !== '1') where.push('is_test_data = 0');
  const sql = `
    SELECT id, email, name, role, plan, suspended, payment_failed, last_login_at, created_at, is_test_data
      FROM users
     ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
     ORDER BY is_test_data ASC, created_at DESC
     LIMIT ? OFFSET ?
  `;
  params.push(limit, offset);
  return db.prepare(sql).all(...params);
}

function counts() {
  return {
    total: countAll.get().n,
    by_plan: countByPlan.all(),
  };
}

function registrationsLast30Days() {
  const since = Date.now() - 30 * 24 * 60 * 60 * 1000;
  return recentRegistrations.all(since);
}

module.exports = {
  create, getById, getByEmail, getByVerifyToken, getByResetToken, getByStripeCustomer,
  verifyEmail, setPassword, setEmail, issueResetToken, comparePassword,
  registerFailedLogin, clearFailedLogins, isLocked, safeUser,
  update, setStripeCustomerId, setStripeSubscriptionId,
  grantCredits, consumeCredit,
  markFailedPayment, clearFailedPayment, remove, list, counts, registrationsLast30Days,
  validatePassword,
};
