const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const db = require('../config/db');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const otplib = require('otplib');
const crypto = require('crypto');

const { logAudit, logAuditFromReq } = require('../middlewares/audit');
const sessionCtrl = require('./session.controller');

const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
  console.error('FATAL: JWT_SECRET environment variable is not set. Exiting.');
  process.exit(1);
}
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '15m';
const REFRESH_TOKEN_TTL_DAYS = parseInt(process.env.REFRESH_TOKEN_TTL_DAYS || '30', 10);
const REFRESH_COOKIE_NAME = process.env.REFRESH_COOKIE_NAME || 'refresh_token';
const COOKIE_SECURE = process.env.COOKIE_SECURE !== 'false';
const COOKIE_SAMESITE = process.env.COOKIE_SAMESITE || 'lax';
const COOKIE_PATH = process.env.COOKIE_PATH || '/api/auth';

function hashToken(token) {
  return crypto.createHash('sha256').update(token).digest('hex');
}

function getCookieOptions(expiresAt) {
  return {
    httpOnly: true,
    secure: COOKIE_SECURE,
    sameSite: COOKIE_SAMESITE,
    path: COOKIE_PATH,
    expires: expiresAt,
  };
}

function setRefreshCookie(res, token, expiresAt) {
  res.cookie(REFRESH_COOKIE_NAME, token, getCookieOptions(expiresAt));
}

function clearRefreshCookie(res) {
  res.clearCookie(REFRESH_COOKIE_NAME, {
    httpOnly: true,
    secure: COOKIE_SECURE,
    sameSite: COOKIE_SAMESITE,
    path: COOKIE_PATH,
  });
}

async function createRefreshToken(userId, req) {
  const token = crypto.randomBytes(48).toString('base64url');
  const tokenHash = hashToken(token);
  const expiresAt = new Date(Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);

  await db.query(
    `INSERT INTO refresh_tokens (user_id, token_hash, expires_at, created_by_ip, created_by_user_agent)
     VALUES (?, ?, ?, ?, ?)`,
    [
      userId,
      tokenHash,
      expiresAt,
      req.ip || req.connection?.remoteAddress || null,
      req.headers['user-agent'] || null,
    ]
  );

  return { token, tokenHash, expiresAt };
}

async function revokeRefreshToken(tokenHash, replacedByTokenHash = null) {
  await db.query(
    `UPDATE refresh_tokens
     SET revoked_at = NOW(), replaced_by_token_hash = COALESCE(?, replaced_by_token_hash)
     WHERE token_hash = ? AND revoked_at IS NULL`,
    [replacedByTokenHash, tokenHash]
  );
}

async function revokeAllRefreshTokens(userId) {
  await db.query(
    `UPDATE refresh_tokens
     SET revoked_at = NOW()
     WHERE user_id = ? AND revoked_at IS NULL`,
    [userId]
  );
}

async function issueAuthTokens(res, user, req) {
  const accessToken = jwt.sign(
    { id: user.id, username: user.username, role: user.role },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  );

  const refresh = await createRefreshToken(user.id, req);
  setRefreshCookie(res, refresh.token, refresh.expiresAt);

  return accessToken;
}

// ── Avatar upload config ──
// uploads are now inside backend/src/uploads
const avatarDir = path.join(__dirname, '..', 'uploads', 'avatars');
if (!fs.existsSync(avatarDir)) fs.mkdirSync(avatarDir, { recursive: true });

const avatarStorage = multer.diskStorage({
  destination: (_req, _file, cb) => cb(null, avatarDir),
  filename: (_req, file, cb) => {
    const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, unique + path.extname(file.originalname));
  },
});

const uploadAvatar = multer({
  storage: avatarStorage,
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: (_req, file, cb) => {
    const allowed = /jpeg|jpg|png|webp/;
    const extOk = allowed.test(path.extname(file.originalname).toLowerCase());
    const mimeOk = allowed.test(file.mimetype) || file.mimetype === 'application/octet-stream';
    const ok = extOk && mimeOk;
    cb(ok ? null : new Error('Only image files are allowed'), ok);
  },
});

exports.uploadAvatar = uploadAvatar;

exports.login = async (req, res, next) => {
  try {
    const { username, password } = req.body;
    if (!username || !password) {
      return res.status(400).json({ error: 'Username and password are required' });
    }

    const [rows] = await db.query(
      'SELECT * FROM users WHERE username = ? AND is_active = 1',
      [username]
    );
    if (rows.length === 0) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    const user = rows[0];
    const valid = await bcrypt.compare(password, user.password_hash);
    if (!valid) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // Check if TOTP MFA is enabled and verified for this user
    if (user.totp_enabled && user.totp_verified) {
      // Issue a short-lived MFA token instead of full JWT
      const mfaToken = jwt.sign(
        { id: user.id, purpose: 'mfa' },
        JWT_SECRET,
        { expiresIn: '5m' }
      );
      return res.json({
        requires_totp: true,
        mfa_token: mfaToken,
        user: {
          id: user.id,
          full_name: user.full_name,
          role: user.role,
        },
      });
    }

    // No MFA — revoke prior refresh tokens and issue full JWT directly
    await revokeAllRefreshTokens(user.id);
    const token = await issueAuthTokens(res, user, req);

    // Start session and log audit
    const ipAddress = req.ip || req.connection?.remoteAddress;
    const deviceInfo = req.headers['user-agent'] || null;
    const sessionId = await sessionCtrl.startSession(user.id, ipAddress, deviceInfo);

    logAudit({
      userId: user.id,
      action: 'login',
      entityType: 'auth',
      entityId: user.id,
      details: { method: 'password', session_id: sessionId },
      ipAddress,
    });

    // Parse permissions JSON
    let permissions = null;
    try { permissions = typeof user.permissions === 'string' ? JSON.parse(user.permissions) : user.permissions; } catch(_) {}

    res.json({
      token,
      session_id: sessionId,
      user: {
        id: user.id,
        username: user.username,
        full_name: user.full_name,
        role: user.role,
        permissions: permissions,
        avatar_url: user.avatar_url,
      },
    });
  } catch (err) {
    next(err);
  }
};

// ── TOTP verification (second step of MFA login) ──
exports.verifyTotp = async (req, res, next) => {
  try {
    const { mfa_token, totp_code } = req.body;
    if (!mfa_token || !totp_code) {
      return res.status(400).json({ error: 'mfa_token and totp_code are required' });
    }

    // Decode the short-lived MFA token
    let decoded;
    try {
      decoded = jwt.verify(mfa_token, JWT_SECRET);
    } catch (_) {
      return res.status(401).json({ error: 'MFA session expired. Please log in again.' });
    }
    if (decoded.purpose !== 'mfa') {
      return res.status(401).json({ error: 'Invalid MFA token' });
    }

    const [rows] = await db.query(
      'SELECT * FROM users WHERE id = ? AND is_active = 1',
      [decoded.id]
    );
    if (rows.length === 0) {
      return res.status(401).json({ error: 'User not found' });
    }

    const user = rows[0];
    if (!user.totp_secret) {
      return res.status(400).json({ error: 'TOTP not configured for this user' });
    }

    // Verify the TOTP code
    const result = otplib.verifySync({ token: totp_code, secret: user.totp_secret });
    if (!result.valid) {
      return res.status(401).json({ error: 'Invalid authentication code' });
    }

    // Issue the full JWT and revoke any existing refresh tokens
    await revokeAllRefreshTokens(user.id);
    const token = await issueAuthTokens(res, user, req);

    const ipAddress = req.ip || req.connection?.remoteAddress;
    const deviceInfo = req.headers['user-agent'] || null;
    const sessionId = await sessionCtrl.startSession(user.id, ipAddress, deviceInfo);

    logAudit({
      userId: user.id,
      action: 'login',
      entityType: 'auth',
      entityId: user.id,
      details: { method: 'password+totp', session_id: sessionId },
      ipAddress,
    });

    // Parse permissions JSON
    let perms2 = null;
    try { perms2 = typeof user.permissions === 'string' ? JSON.parse(user.permissions) : user.permissions; } catch(_) {}

    res.json({
      token,
      session_id: sessionId,
      user: {
        id: user.id,
        username: user.username,
        full_name: user.full_name,
        role: user.role,
        permissions: perms2,
        avatar_url: user.avatar_url,
      },
    });
  } catch (err) {
    next(err);
  }
};

// ── Refresh access token with refresh token rotation ──
exports.refresh = async (req, res, next) => {
  try {
    const refreshToken = req.cookies?.[REFRESH_COOKIE_NAME];
    if (!refreshToken) {
      return res.status(401).json({ error: 'Refresh token missing' });
    }

    const tokenHash = hashToken(refreshToken);
    const [rows] = await db.query(
      'SELECT * FROM refresh_tokens WHERE token_hash = ? LIMIT 1',
      [tokenHash]
    );
    if (rows.length === 0) {
      clearRefreshCookie(res);
      return res.status(401).json({ error: 'Invalid refresh token' });
    }

    const stored = rows[0];
    if (stored.revoked_at) {
      clearRefreshCookie(res);
      return res.status(401).json({ error: 'Refresh token revoked' });
    }

    if (new Date(stored.expires_at) < new Date()) {
      await revokeRefreshToken(tokenHash);
      clearRefreshCookie(res);
      return res.status(401).json({ error: 'Refresh token expired' });
    }

    const [users] = await db.query(
      'SELECT id, username, role, permissions, full_name, avatar_url, is_active FROM users WHERE id = ?',
      [stored.user_id]
    );
    if (users.length === 0 || users[0].is_active !== 1) {
      await revokeRefreshToken(tokenHash);
      clearRefreshCookie(res);
      return res.status(401).json({ error: 'User not found or inactive' });
    }

    const user = users[0];
    const accessToken = jwt.sign(
      { id: user.id, username: user.username, role: user.role },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );

    const rotated = await createRefreshToken(user.id, req);
    await revokeRefreshToken(tokenHash, rotated.tokenHash);
    setRefreshCookie(res, rotated.token, rotated.expiresAt);

    res.json({ token: accessToken });
  } catch (err) {
    next(err);
  }
};

// ── Logout and revoke current refresh token ──
exports.logout = async (req, res, next) => {
  try {
    const refreshToken = req.cookies?.[REFRESH_COOKIE_NAME];
    if (refreshToken) {
      const tokenHash = hashToken(refreshToken);
      await revokeRefreshToken(tokenHash);
    }
    clearRefreshCookie(res);
    res.json({ message: 'Logged out' });
  } catch (err) {
    next(err);
  }
};

// ── Verify PIN for session unlock ──
exports.verifyPin = async (req, res, next) => {
  try {
    const { pin } = req.body;
    if (!pin) return res.status(400).json({ error: 'pin is required' });

    const userId = req.user.id;
    const [rows] = await db.query(
      'SELECT id, pin_hash, is_active FROM users WHERE id = ?',
      [userId]
    );
    if (rows.length === 0 || rows[0].is_active !== 1) {
      return res.status(401).json({ error: 'User not found or inactive' });
    }

    const user = rows[0];
    if (!user.pin_hash) {
      return res.status(400).json({ error: 'PIN not set for this user' });
    }

    const validPin = await bcrypt.compare(String(pin), user.pin_hash);
    if (!validPin) {
      return res.status(401).json({ error: 'Invalid PIN' });
    }

    logAuditFromReq(req, 'pin_unlock', 'auth', userId);
    res.json({ message: 'PIN verified' });
  } catch (err) {
    next(err);
  }
};

// ── List active users (for login screen selection) ──
exports.listUsers = async (_req, res, next) => {
  try {
    const [rows] = await db.query(
      `SELECT id, username, full_name, role, avatar_url,
              (totp_enabled = 1 AND totp_verified = 1) as mfa_enabled
       FROM users WHERE is_active = 1 ORDER BY full_name`
    );
    res.json(rows);
  } catch (err) {
    next(err);
  }
};

exports.register = async (req, res, next) => {
  try {
    const { username, password, full_name, role, pin, permissions } = req.body;
    if (!username || !password || !full_name) {
      return res.status(400).json({ error: 'username, password and full_name are required' });
    }

    const salt = await bcrypt.genSalt(10);
    const password_hash = await bcrypt.hash(password, salt);

    let pin_hash = null;
    if (pin) {
      pin_hash = await bcrypt.hash(pin, salt);
    }

    // Handle avatar file path if uploaded via multipart
    let avatar_url = null;
    if (req.file) {
      avatar_url = '/api/uploads/avatars/' + req.file.filename;
    }

    // Serialize permissions JSON
    const permJson = permissions ? JSON.stringify(permissions) : null;

    const [result] = await db.query(
      'INSERT INTO users (username, password_hash, pin_hash, full_name, role, permissions, avatar_url) VALUES (?, ?, ?, ?, ?, ?, ?)',
      [username, password_hash, pin_hash, full_name, role || 'staff', permJson, avatar_url]
    );

    const [rows] = await db.query(
      'SELECT id, username, full_name, role, permissions, avatar_url, created_at FROM users WHERE id = ?',
      [result.insertId]
    );

    // Log audit for user registration
    if (req.user?.id) {
      logAudit({
        userId: req.user.id,
        action: 'create',
        entityType: 'user',
        entityId: result.insertId,
        details: { username, full_name, role: role || 'staff' },
        ipAddress: req.ip || req.connection?.remoteAddress,
      });
    }

    res.status(201).json(rows[0]);
  } catch (err) {
    next(err);
  }
};

exports.me = async (req, res, next) => {
  try {
    const [rows] = await db.query(
      'SELECT id, username, full_name, role, permissions, avatar_url, is_active, created_at FROM users WHERE id = ?',
      [req.user.id]
    );
    if (rows.length === 0) return res.status(404).json({ error: 'User not found' });
    const u = rows[0];
    try { u.permissions = typeof u.permissions === 'string' ? JSON.parse(u.permissions) : u.permissions; } catch(_) {}
    res.json(u);
  } catch (err) {
    next(err);
  }
};
