import { Router, type Request, type Response } from 'express';
import bcrypt from 'bcrypt';
import { DatabaseService, type UserRole, type ResourceType } from '../services/DatabaseService';
import { authMiddleware } from '../middleware/auth';
import { requirePermission } from '../middleware/permissions';
import { rejectApiTokenScope } from '../middleware/apiTokenScope';
import { BCRYPT_SALT_ROUNDS, MIN_PASSWORD_LENGTH } from '../helpers/constants';
import { isDebugEnabled } from '../utils/debug';
import { getErrorMessage, isSqliteUniqueViolation } from '../utils/errors';
import { parseIntParam } from '../utils/parseIntParam';
import { sanitizeForLog } from '../utils/safeLog';
import { validateUsername } from '../helpers/validateUsername';
import { assertStackExistsOnNode } from '../helpers/assertStackExistsOnNode';
import { isValidStackName } from '../utils/validation';

const USERS_SCOPE_MESSAGE = 'API tokens cannot access user management.';
const VALID_USER_ROLES: UserRole[] = ['admin', 'viewer', 'deployer', 'node-admin', 'auditor'];
const VALID_ASSIGNMENT_ROLES: UserRole[] = ['admin', 'viewer', 'deployer', 'node-admin'];
const VALID_RESOURCE_TYPES: ResourceType[] = ['stack', 'node'];

export const usersRouter = Router();

usersRouter.get('/', authMiddleware, async (req: Request, res: Response): Promise<void> => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const db = DatabaseService.getInstance();
    const users = db.getUsers();
    const mfaUserIds = db.getUsersWithMfaEnabled();
    const enriched = users.map((u) => ({
      ...u,
      mfaEnabled: mfaUserIds.has(u.id),
    }));
    res.json(enriched);
  } catch (error) {
    console.error('[Users] List error:', error);
    res.status(500).json({ error: 'Failed to fetch users' });
  }
});

usersRouter.post('/', authMiddleware, async (req: Request, res: Response): Promise<void> => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const { username, password, role } = req.body;

    if (!username || !password || !role) {
      res.status(400).json({ error: 'Username, password, and role are required' });
      return;
    }
    const usernameError = validateUsername(username);
    if (usernameError) {
      res.status(400).json({ error: usernameError });
      return;
    }
    if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) {
      res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` });
      return;
    }
    if (!VALID_USER_ROLES.includes(role)) {
      res.status(400).json({ error: 'Role must be "admin", "viewer", "deployer", "node-admin", or "auditor"' });
      return;
    }

    const db = DatabaseService.getInstance();
    const existing = db.getUserByUsername(username);
    if (existing) {
      res.status(409).json({ error: 'A user with this username already exists' });
      return;
    }

    const passwordHash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
    const id = db.addUser({ username, password_hash: passwordHash, role });
    console.log('[Users] Created:', sanitizeForLog(username), 'role:', sanitizeForLog(role), 'by:', sanitizeForLog(req.user!.username));
    res.status(201).json({ id, username, role });
  } catch (error) {
    console.error('[Users] Create error:', error);
    res.status(500).json({ error: 'Failed to create user' });
  }
});

usersRouter.put('/:id', authMiddleware, async (req: Request, res: Response): Promise<void> => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const id = parseInt(req.params.id as string, 10);
    const db = DatabaseService.getInstance();
    const user = db.getUser(id);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    const { username, password, role } = req.body;
    const updates: Partial<{ username: string; password_hash: string; role: string }> = {};

    if (username !== undefined) {
      const usernameError = validateUsername(username);
      if (usernameError) {
        res.status(400).json({ error: usernameError });
        return;
      }
      const existing = db.getUserByUsername(username);
      if (existing && existing.id !== id) {
        res.status(409).json({ error: 'A user with this username already exists' });
        return;
      }
      updates.username = username;
    }

    if (role !== undefined) {
      if (!VALID_USER_ROLES.includes(role)) {
        res.status(400).json({ error: 'Role must be "admin", "viewer", "deployer", "node-admin", or "auditor"' });
        return;
      }
      if (user.username === req.user!.username && role !== user.role) {
        res.status(400).json({ error: 'Cannot change your own role' });
        return;
      }
      updates.role = role;
    }

    if (password !== undefined) {
      // Prevent setting passwords on SSO-provisioned users (would enable a
      // local-login bypass).
      if (user.auth_provider !== 'local') {
        res.status(400).json({ error: 'Cannot set a password on an SSO-provisioned user.' });
        return;
      }
      if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) {
        res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` });
        return;
      }
      updates.password_hash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
    }

    // updateUserIfNotLastAdmin returns false only when this update would demote
    // the last remaining admin; map that single case to the guard message.
    const applied = db.updateUserIfNotLastAdmin(id, updates);
    if (!applied) {
      res.status(400).json({ error: 'Cannot demote the only admin user' });
      return;
    }
    // Invalidate the user's active sessions when their role or password changes.
    if (updates.role || updates.password_hash) {
      db.bumpTokenVersion(id);
    }
    console.log('[Users] Updated user', id, 'fields:', Object.keys(updates).join(', '), 'by:', req.user!.username);
    res.json({ success: true });
  } catch (error) {
    console.error('[Users] Update error:', error);
    res.status(500).json({ error: 'Failed to update user' });
  }
});

usersRouter.delete('/:id', authMiddleware, async (req: Request, res: Response): Promise<void> => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const id = parseInt(req.params.id as string, 10);
    const db = DatabaseService.getInstance();
    const user = db.getUser(id);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    if (user.username === req.user!.username) {
      res.status(400).json({ error: 'Cannot delete your own account' });
      return;
    }

    const deleted = db.deleteUserIfNotLastAdmin(id);
    if (!deleted) {
      res.status(400).json({ error: 'Cannot delete the only admin user' });
      return;
    }
    console.log('[Users] Deleted:', user.username, '(id:', id, ') by:', req.user!.username);
    res.json({ success: true });
  } catch (error) {
    console.error('[Users] Delete error:', error);
    res.status(500).json({ error: 'Failed to delete user' });
  }
});

/**
 * Admin reset: clear a target user's MFA enrolment and force re-auth. Used
 * when a user has lost their authenticator AND exhausted their backup codes,
 * and another admin is available. For total lockout (including sole admin),
 * see the CLI `reset-mfa` command.
 */
usersRouter.post('/:id/mfa/reset', authMiddleware, (req: Request, res: Response): void => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const id = parseIntParam(req, res, 'id', 'user id');
    if (id === null) return;
    const db = DatabaseService.getInstance();
    const target = db.getUser(id);
    if (!target) {
      res.status(404).json({ error: 'User not found' });
      return;
    }
    db.deleteUserMfa(id);
    db.bumpTokenVersion(id);
    // The audit-log middleware records this POST automatically (summary
    // "Reset two-factor authentication: <id>", keyed on the target user id);
    // no explicit write is needed here.
    console.log('[MFA] Admin reset: target=', target.username, 'by=', req.user!.username);
    if (isDebugEnabled()) {
      console.log('[MFA:diag] admin-reset target=', target.username, 'actor=', req.user!.username);
    }
    res.json({ success: true });
  } catch (error) {
    console.error('[MFA] Admin reset error:', getErrorMessage(error, 'unknown'));
    res.status(500).json({ error: 'Failed to reset two-factor authentication' });
  }
});

// --- Scoped Role Assignments ---

usersRouter.get('/:id/roles', authMiddleware, (req: Request, res: Response): void => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const userId = parseInt(req.params.id as string, 10);
    const db = DatabaseService.getInstance();
    if (!db.getUser(userId)) {
      res.status(404).json({ error: 'User not found' });
      return;
    }
    const assignments = db.getAllRoleAssignments(userId);
    res.json(assignments);
  } catch (error) {
    console.error('[Roles] List error:', error);
    res.status(500).json({ error: 'Failed to fetch role assignments' });
  }
});

usersRouter.post('/:id/roles', authMiddleware, async (req: Request, res: Response): Promise<void> => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const userId = parseInt(req.params.id as string, 10);
    const { role, resource_type, resource_id, node_id: rawNodeId } = req.body;

    if (!VALID_ASSIGNMENT_ROLES.includes(role)) {
      res.status(400).json({ error: 'Invalid role' });
      return;
    }
    if (!VALID_RESOURCE_TYPES.includes(resource_type)) {
      res.status(400).json({ error: 'Invalid resource type' });
      return;
    }
    if (!resource_id || typeof resource_id !== 'string') {
      res.status(400).json({ error: 'resource_id is required' });
      return;
    }

    const db = DatabaseService.getInstance();
    if (!db.getUser(userId)) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    let nodeId: number | null = null;

    if (resource_type === 'stack') {
      if (typeof rawNodeId !== 'number' || !Number.isInteger(rawNodeId)) {
        res.status(400).json({ error: 'node_id is required for stack role assignments' });
        return;
      }
      if (!isValidStackName(resource_id)) {
        res.status(400).json({ error: 'Invalid stack name' });
        return;
      }
      const exists = await assertStackExistsOnNode(rawNodeId, resource_id);
      if (!exists.ok) {
        res.status(400).json({ error: exists.error });
        return;
      }
      nodeId = rawNodeId;
    } else {
      // node resource: reject a stack-style node_id qualifier
      if (rawNodeId !== undefined && rawNodeId !== null) {
        res.status(400).json({ error: 'node_id must not be set for node role assignments' });
        return;
      }
      if (!/^\d+$/.test(resource_id)) {
        res.status(400).json({ error: 'resource_id must be a numeric node id' });
        return;
      }
      const parsedNodeId = parseInt(resource_id, 10);
      if (String(parsedNodeId) !== resource_id) {
        res.status(400).json({ error: 'resource_id must be a canonical node id' });
        return;
      }
      if (!db.getNode(parsedNodeId)) {
        res.status(400).json({ error: 'Node not found' });
        return;
      }
    }

    try {
      const id = db.addRoleAssignment({
        user_id: userId,
        role,
        resource_type,
        resource_id,
        node_id: nodeId,
      });
      console.log(
        '[Roles] Assigned',
        sanitizeForLog(role),
        'on',
        sanitizeForLog(resource_type),
        sanitizeForLog(resource_id),
        sanitizeForLog(nodeId != null ? `node ${nodeId}` : ''),
        'to user',
        userId,
        'by:',
        sanitizeForLog(req.user!.username),
      );
      res.status(201).json({
        id,
        user_id: userId,
        role,
        resource_type,
        resource_id,
        node_id: nodeId,
      });
    } catch (err: unknown) {
      if (isSqliteUniqueViolation(err)) {
        res.status(409).json({ error: 'This role assignment already exists' });
        return;
      }
      throw err;
    }
  } catch (error) {
    console.error('[Roles] Create error:', error);
    res.status(500).json({ error: 'Failed to add role assignment' });
  }
});

usersRouter.delete('/:id/roles/:assignId', authMiddleware, (req: Request, res: Response): void => {
  if (rejectApiTokenScope(req, res, USERS_SCOPE_MESSAGE)) return;
  if (!requirePermission(req, res, 'system:users')) return;
  try {
    const userId = parseInt(req.params.id as string, 10);
    const assignId = parseInt(req.params.assignId as string, 10);
    const db = DatabaseService.getInstance();

    const assignment = db.getRoleAssignmentById(assignId);
    if (!assignment || assignment.user_id !== userId) {
      res.status(404).json({ error: 'Role assignment not found' });
      return;
    }

    db.deleteRoleAssignment(assignId);
    console.log('[Roles] Removed assignment', assignId, 'from user', userId, 'by:', req.user!.username);
    res.json({ success: true });
  } catch (error) {
    console.error('[Roles] Delete error:', error);
    res.status(500).json({ error: 'Failed to delete role assignment' });
  }
});
