require('dotenv').config({ path: require('path').join(__dirname, '.env') });

const express = require('express');
const db = require('./db');
const auth = require('./auth');
const rateLimit = require('./rate_limit');

const path = require('path');
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

// Init DB
db.initDB();

// GET /api/status
app.get('/api/status', (req, res) => {
  res.json({ status: 'ok', server_time: new Date().toISOString() });
});

// POST /api/login
app.post('/api/login', async (req, res) => {
  const { username, password_hash, hwid_components } = req.body;
  const ip = req.ip || req.socket.remoteAddress || 'unknown';
  const userAgent = req.headers['user-agent'] || '';

  if (!username || !password_hash || !hwid_components) {
    return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
  }
  const { cpu, board, disk, mac } = hwid_components;
  if (!cpu || !board || !disk || !mac) {
    return res.status(400).json({ success: false, error: 'MISSING_HWID_COMPONENTS' });
  }

  const hwidCombined = auth.combinedHwidHash(hwid_components);

  // Rate limit
  const rl = rateLimit.checkLoginRateLimit(ip, username, hwidCombined);
  if (!rl.allowed) {
    return res.status(429).json({
      success: false, error: 'RATE_LIMITED',
      message: 'Too many attempts', retry_after: rl.retryAfter
    });
  }

  // User lookup
  const user = db.getUserByUsername(username);
  if (!user) {
    db.logLogin(null, null, ip, userAgent, 0, false, 'USER_NOT_FOUND');
    return res.json({ success: false, error: 'INVALID_CREDENTIALS' });
  }

  if (!user.is_active) return res.json({ success: false, error: 'ACCOUNT_DISABLED' });
  if (user.expires_at && new Date(user.expires_at) < new Date())
    return res.json({ success: false, error: 'ACCOUNT_EXPIRED' });

  // Verify password (Argon2id)
  let passwordValid = false;
  try {
    passwordValid = await auth.verifyPassword(user.password_hash, password_hash);
  } catch (err) {
    console.error('[login] argon2 error:', err.message);
    return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' });
  }
  if (!passwordValid) {
    db.logLogin(user.id, null, ip, userAgent, 0, false, 'WRONG_PASSWORD');
    return res.json({ success: false, error: 'INVALID_CREDENTIALS' });
  }

  // Admin accounts skip HWID check (for web panel access)
  if (user.role === 'admin') {
    const accessToken = auth.signAccessToken(user, 0);
    const refreshToken = auth.generateRefreshToken();
    const tokenHash = auth.hashToken(refreshToken);
    const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
    db.saveRefreshToken(tokenHash, user.id, 0, 'admin', expiresAt);
    db.logLogin(user.id, 0, ip, userAgent, 1.0, true, null);
    return res.json({
      success: true,
      access_token: accessToken,
      refresh_token: refreshToken,
      username: user.username,
      match_rate: 1.0
    });
  }

  // HWID matching
  const userDevices = db.findMatchingDevice(user.id, cpu, board, disk, mac);
  let matchedDevice = null;
  let bestRate = 0;

  for (const device of userDevices) {
    const result = auth.hwidMatchRate(device, hwid_components);
    if (result.match && result.rate > bestRate) {
      matchedDevice = device;
      bestRate = result.rate;
    }
  }

  if (!matchedDevice) {
    if (userDevices.length === 0) {
      // First login — auto-create device
      const displayName = req.headers['x-device-name'] || 'Unknown Device';
      const result = db.createDevice(user.id, displayName, cpu, board, disk, mac, ip);
      matchedDevice = { id: result.lastInsertRowid, cpu_hash: cpu, board_hash: board, disk_hash: disk, mac_hash: mac };
      bestRate = 1.0;
    } else {
      // HWID threshold failed
      db.logLogin(user.id, null, ip, userAgent, bestRate, false, 'HWID_THRESHOLD');
      return res.json({
        success: false, error: 'HWID_THRESHOLD',
        message: 'Hardware does not match any bound device', match_rate: bestRate
      });
    }
  } else {
    db.updateDeviceLogin(matchedDevice.id, ip);
  }

  // Generate tokens
  const accessToken = auth.signAccessToken(user, matchedDevice.id);
  const refreshToken = auth.generateRefreshToken();
  const tokenHash = auth.hashToken(refreshToken);
  const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
  db.saveRefreshToken(tokenHash, user.id, matchedDevice.id, hwidCombined, expiresAt);

  db.logLogin(user.id, matchedDevice.id, ip, userAgent, bestRate, true, null);

  res.json({
    success: true,
    access_token: accessToken,
    refresh_token: refreshToken,
    username: user.username,
    match_rate: bestRate
  });
});

// POST /api/refresh
app.post('/api/refresh', (req, res) => {
  const { refresh_token, hwid_components } = req.body;
  if (!refresh_token || !hwid_components) {
    return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
  }

  const tokenHash = auth.hashToken(refresh_token);
  const stored = db.findRefreshToken(tokenHash);
  if (!stored || stored.revoked) return res.json({ success: false, error: 'TOKEN_EXPIRED' });
  if (new Date(stored.expires_at) < new Date()) return res.json({ success: false, error: 'TOKEN_EXPIRED' });

  // Verify HWID
  const hwidCombined = auth.combinedHwidHash(hwid_components);
  if (stored.hwid_hash !== hwidCombined) {
    const user = db.getUserById(stored.user_id);
    if (!user) return res.json({ success: false, error: 'TOKEN_EXPIRED' });

    const userDevices = db.findMatchingDevice(user.id,
      hwid_components.cpu, hwid_components.board,
      hwid_components.disk, hwid_components.mac);

    let matched = false;
    for (const device of userDevices) {
      if (auth.hwidMatchRate(device, hwid_components).match) { matched = true; break; }
    }
    if (!matched) return res.json({ success: false, error: 'HWID_THRESHOLD' });
  }

  // Rotate
  db.revokeRefreshToken(tokenHash);
  const user = db.getUserById(stored.user_id);
  const newAccessToken = auth.signAccessToken(user, stored.device_id);
  const newRefreshToken = auth.generateRefreshToken();
  const newTokenHash = auth.hashToken(newRefreshToken);
  const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
  db.saveRefreshToken(newTokenHash, user.id, stored.device_id,
    auth.combinedHwidHash(hwid_components), expiresAt);

  if (stored.device_id) db.updateDeviceLogin(stored.device_id, req.ip || req.socket.remoteAddress);

  res.json({ success: true, access_token: newAccessToken, refresh_token: newRefreshToken });
});

// POST /api/logout
app.post('/api/logout', (req, res) => {
  const { refresh_token } = req.body;
  if (refresh_token) db.revokeRefreshToken(auth.hashToken(refresh_token));
  res.json({ success: true });
});

// POST /api/register (public self-registration)
app.post('/api/register', async (req, res) => {
  const { username, password_hash } = req.body;
  const ip = req.ip || req.socket.remoteAddress || 'unknown';

  if (!username || !password_hash) {
    return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
  }

  // Username rules
  if (username.length < 3 || username.length > 32) {
    return res.json({ success: false, error: 'INVALID_USERNAME', message: 'Username must be 3-32 characters' });
  }
  if (!/^[a-zA-Z0-9_]+$/.test(username)) {
    return res.json({ success: false, error: 'INVALID_USERNAME', message: 'Only letters, numbers, underscores' });
  }

  // Rate limit registration per IP (5 per hour)
  const rl = db.checkRateLimit(`register:${ip}`, 'register', 5, 3600);
  if (!rl.allowed) {
    return res.status(429).json({
      success: false, error: 'RATE_LIMITED',
      message: 'Too many registrations. Try again later.', retry_after: rl.retryAfter
    });
  }

  // Check if username taken
  if (db.getUserByUsername(username)) {
    return res.json({ success: false, error: 'USERNAME_TAKEN' });
  }

  // password_hash comes as SHA-256 from client — verify it's 64-char hex
  if (password_hash.length !== 64 || !/^[a-f0-9]{64}$/.test(password_hash)) {
    return res.json({ success: false, error: 'INVALID_PASSWORD_HASH' });
  }

  // password_hash is already SHA-256(password) — Argon2id it directly
  let storedHash;
  try {
    storedHash = await auth.hashPasswordFromHash(password_hash);
  } catch (err) {
    return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' });
  }

  db.createUser(username, storedHash, 'user', null);

  console.log(`[register] New user: ${username} (ip: ${ip})`);
  res.json({ success: true, message: 'Registration successful. You can now login.' });
});

// Admin: POST /api/admin/create-user
app.post('/api/admin/create-user', auth.requireAdmin, async (req, res) => {
  const { username, password, role, expires_at } = req.body;
  if (!username || !password) {
    return res.status(400).json({ success: false, error: 'MISSING_FIELDS' });
  }

  if (db.getUserByUsername(username)) {
    return res.json({ success: false, error: 'USERNAME_TAKEN' });
  }

  let passwordHash;
  try { passwordHash = await auth.hashPassword(password); }
  catch (err) { return res.status(500).json({ success: false, error: 'INTERNAL_ERROR' }); }

  db.createUser(username, passwordHash, role || 'user', expires_at || null);
  res.json({ success: true, message: `User '${username}' created` });
});

// Admin: GET /api/admin/users (list all users)
app.get('/api/admin/users', auth.requireAdmin, (req, res) => {
  const db = require('./db');
  const users = db.getDB().prepare(
    `SELECT u.id, u.username, u.role, u.is_active, u.created_at, u.expires_at,
            (SELECT MIN(d.created_at) FROM devices d WHERE d.user_id = u.id) as hwid_bound_at
     FROM users u ORDER BY u.id DESC`
  ).all();
  res.json({ users });
});

// Admin: GET /api/admin/logs (recent login logs, last 100)
app.get('/api/admin/logs', auth.requireAdmin, (req, res) => {
  const db = require('./db');
  const logs = db.getDB().prepare(
    `SELECT login_log.*, users.username FROM login_log
     LEFT JOIN users ON login_log.user_id = users.id
     ORDER BY login_log.id DESC LIMIT 100`
  ).all();
  res.json({ logs });
});

// Admin: GET /api/admin/stats
app.get('/api/admin/stats', auth.requireAdmin, (req, res) => {
  const db = require('./db');
  const totalUsers = db.getDB().prepare('SELECT COUNT(*) as cnt FROM users').get().cnt;
  const activeUsers = db.getDB().prepare('SELECT COUNT(*) as cnt FROM users WHERE is_active = 1').get().cnt;
  const logins24h = db.getDB().prepare(
    "SELECT COUNT(*) as cnt FROM login_log WHERE success = 1 AND attempted_at > datetime('now', '-1 day')"
  ).get().cnt;
  const failures24h = db.getDB().prepare(
    "SELECT COUNT(*) as cnt FROM login_log WHERE success = 0 AND attempted_at > datetime('now', '-1 day')"
  ).get().cnt;
  res.json({ totalUsers, activeUsers, logins24h, failures24h });
});

// Admin: POST /api/admin/user/:id/toggle (ban/unban)
app.post('/api/admin/user/:id/toggle', auth.requireAdmin, (req, res) => {
  const db = require('./db');
  const user = db.getUserById(req.params.id);
  if (!user) return res.json({ success: false, error: 'NOT_FOUND' });
  const newStatus = user.is_active ? 0 : 1;
  db.getDB().prepare('UPDATE users SET is_active = ? WHERE id = ?').run(newStatus, req.params.id);
  res.json({ success: true, is_active: newStatus });
});

// Admin: POST /api/admin/user/:id/reset-hwid
app.post('/api/admin/user/:id/reset-hwid', auth.requireAdmin, (req, res) => {
  const db = require('./db');
  db.getDB().prepare('DELETE FROM devices WHERE user_id = ?').run(req.params.id);
  db.getDB().prepare(
    'UPDATE refresh_tokens SET revoked = 1 WHERE user_id = ?'
  ).run(req.params.id);
  res.json({ success: true, message: 'HWID reset. User can bind to new machine on next login.' });
});

// Start
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`[server] running on port ${PORT}`);
  console.log(`  GET  /api/status`);
  console.log(`  POST /api/login`);
  console.log(`  POST /api/refresh`);
  console.log(`  POST /api/logout`);
  console.log(`  POST /api/admin/create-user  (admin)`);
});
