import type { AuthenticatedRequest } from "../../../types/index.js";
import type { RequestHandler, Router } from "express";
import { authLogger } from "../../utils/logger.js";
import { DatabaseSaveTrigger } from "../../utils/database-save-trigger.js";
import { logAudit, getRequestMeta } from "../../utils/audit-logger.js";
import bcrypt from "bcryptjs";
import { nanoid } from "nanoid";
import { AuthManager } from "../../utils/auth-manager.js";
import { DataCrypto } from "../../utils/data-crypto.js";
import {
  createCurrentRoleRepository,
  createCurrentUserRepository,
} from "../repositories/factory.js";
import type {
  UserRecord,
  UserRepository,
} from "../repositories/user-repository.js";

function isNonEmptyString(val: unknown): val is string {
  return typeof val === "string" && val.trim().length > 0;
}

async function getUserByPreferredIdentifier(
  userRepository: UserRepository,
  userId: string | null,
  username: string | null,
): Promise<UserRecord | null> {
  return userId
    ? userRepository.findById(userId)
    : userRepository.findByUsername(username!);
}

export function registerUserAdminRoutes(
  router: Router,
  authenticateJWT: RequestHandler,
): void {
  /**
   * @openapi
   * /users/list:
   *   get:
   *     summary: List users
   *     description: >
   *       Retrieves users in the system. Without `limit` the full list is
   *       returned, which is what the sharing pickers rely on. Pass `limit`
   *       (and optionally `offset`/`search`) to page through large directories.
   *     tags:
   *       - Users
   *     parameters:
   *       - in: query
   *         name: search
   *         required: false
   *         schema:
   *           type: string
   *         description: Case-insensitive username substring filter.
   *       - in: query
   *         name: limit
   *         required: false
   *         schema:
   *           type: integer
   *           maximum: 500
   *         description: Page size. Omit to return every user.
   *       - in: query
   *         name: offset
   *         required: false
   *         schema:
   *           type: integer
   *         description: Number of users to skip. Requires `limit`.
   *     responses:
   *       200:
   *         description: A list of users, with the total matching count.
   *       403:
   *         description: Not authorized.
   *       500:
   *         description: Failed to list users.
   */
  router.get("/list", authenticateJWT, async (req, res) => {
    try {
      const userRepository = createCurrentUserRepository();
      const requester = await userRepository.findById(
        (req as AuthenticatedRequest).userId,
      );

      const query = req.query ?? {};
      const search =
        typeof query.search === "string" ? query.search : undefined;
      const rawLimit = Number(query.limit);
      // Paging is opt-in: the share pickers fetch the whole list and filter it
      // client-side, so a request without ?limit keeps the original behaviour.
      const paginated = Number.isFinite(rawLimit) && rawLimit > 0;
      const limit = paginated ? Math.min(Math.floor(rawLimit), 500) : 0;
      const rawOffset = Number(query.offset);
      const offset =
        Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : 0;

      let pageUsers: UserRecord[];
      let total: number;
      if (paginated) {
        const page = await userRepository.listPage({ search, limit, offset });
        pageUsers = page.users;
        total = page.total;
      } else {
        const all = await userRepository.listAll();
        const term = search?.trim().toLowerCase();
        pageUsers = term
          ? all.filter((u) => u.username?.toLowerCase().includes(term))
          : all;
        total = pageUsers.length;
      }

      res.json({
        users: pageUsers.map((u) => ({
          userId: u.id,
          username: u.username,
          is_admin: u.isAdmin,
          is_oidc: u.isOidc,
          password_hash: u.passwordHash ? "set" : null,
          // Management-only details stay admin-eyes-only; regular users hit
          // this route to pick sharing targets.
          ...(requester?.isAdmin
            ? {
                data_unlocked: DataCrypto.canUserAccessData(u.id),
                totp_enabled: !!u.totpEnabled,
              }
            : {}),
        })),
        total,
        ...(paginated ? { limit, offset } : {}),
      });
    } catch (err) {
      authLogger.error("Failed to list users", err);
      res.status(500).json({ error: "Failed to list users" });
    }
  });

  /**
   * @openapi
   * /users/make-admin:
   *   post:
   *     summary: Make user admin
   *     description: Grants admin privileges to a user.
   *     tags:
   *       - Users
   *     requestBody:
   *       required: true
   *       content:
   *         application/json:
   *           schema:
   *             type: object
   *             properties:
   *               userId:
   *                 type: string
   *                 description: Preferred unique user identifier.
   *               username:
   *                 type: string
   *                 description: Legacy fallback identifier.
   *     responses:
   *       200:
   *         description: User is now an admin.
   *       400:
   *         description: User ID or username is required, or the user is already an admin.
   *       403:
   *         description: Not authorized.
   *       404:
   *         description: User not found.
   *       500:
   *         description: Failed to make user admin.
   */
  router.post("/make-admin", authenticateJWT, async (req, res) => {
    const userId = (req as AuthenticatedRequest).userId;
    const { userId: targetUserId, username } = req.body;
    const resolvedUserId = isNonEmptyString(targetUserId)
      ? targetUserId.trim()
      : null;
    const resolvedUsername = isNonEmptyString(username)
      ? username.trim()
      : null;

    if (!resolvedUserId && !resolvedUsername) {
      return res.status(400).json({ error: "User ID or username is required" });
    }

    try {
      const userRepository = createCurrentUserRepository();
      const adminUser = await userRepository.findById(userId);
      if (!adminUser?.isAdmin) {
        return res.status(403).json({ error: "Not authorized" });
      }

      const targetUser = await getUserByPreferredIdentifier(
        userRepository,
        resolvedUserId,
        resolvedUsername,
      );
      if (!targetUser) {
        return res.status(404).json({ error: "User not found" });
      }

      if (targetUser.isAdmin) {
        return res.status(400).json({ error: "User is already an admin" });
      }

      await userRepository.update(targetUser.id, { isAdmin: true });

      try {
        await createCurrentRoleRepository().switchUserRoleName({
          userId: targetUser.id,
          addRoleName: "admin",
          removeRoleName: "user",
          grantedBy: userId,
        });
      } catch (roleError) {
        authLogger.error("Failed to sync admin role on make-admin", roleError, {
          operation: "make_admin_role_sync",
          userId: targetUser.id,
        });
      }

      try {
        await DatabaseSaveTrigger.forceSave("make_admin_explicit_save");
      } catch (saveError) {
        authLogger.error(
          "Failed to persist admin promotion to disk",
          saveError,
          {
            operation: "make_admin_save_failed",
            userId: targetUser.id,
            username: targetUser.username,
          },
        );
      }

      authLogger.info("Admin privileges granted", {
        operation: "admin_grant",
        adminId: userId,
        targetUserId: targetUser.id,
        targetUsername: targetUser.username,
      });

      const { ipAddress, userAgent } = getRequestMeta(req);
      await logAudit({
        userId,
        username: adminUser.username ?? userId,
        action: "make_admin",
        resourceType: "user",
        resourceId: targetUser.id,
        resourceName: targetUser.username,
        ipAddress,
        userAgent,
        success: true,
      });

      res.json({ message: `User ${targetUser.username} is now an admin` });
    } catch (err) {
      authLogger.error("Failed to make user admin", err);
      res.status(500).json({ error: "Failed to make user admin" });
    }
  });

  /**
   * @openapi
   * /users/remove-admin:
   *   post:
   *     summary: Remove admin status
   *     description: Revokes admin privileges from a user.
   *     tags:
   *       - Users
   *     requestBody:
   *       required: true
   *       content:
   *         application/json:
   *           schema:
   *             type: object
   *             properties:
   *               userId:
   *                 type: string
   *                 description: Preferred unique user identifier.
   *               username:
   *                 type: string
   *                 description: Legacy fallback identifier.
   *     responses:
   *       200:
   *         description: Admin status removed from user.
   *       400:
   *         description: User ID or username is required, or cannot remove your own admin status.
   *       403:
   *         description: Not authorized.
   *       404:
   *         description: User not found.
   *       500:
   *         description: Failed to remove admin status.
   */
  router.post("/remove-admin", authenticateJWT, async (req, res) => {
    const userId = (req as AuthenticatedRequest).userId;
    const { userId: targetUserId, username } = req.body;
    const resolvedUserId = isNonEmptyString(targetUserId)
      ? targetUserId.trim()
      : null;
    const resolvedUsername = isNonEmptyString(username)
      ? username.trim()
      : null;

    if (!resolvedUserId && !resolvedUsername) {
      return res.status(400).json({ error: "User ID or username is required" });
    }

    try {
      const userRepository = createCurrentUserRepository();
      const adminUser = await userRepository.findById(userId);
      if (!adminUser?.isAdmin) {
        return res.status(403).json({ error: "Not authorized" });
      }

      if (
        (resolvedUserId && adminUser.id === resolvedUserId) ||
        (resolvedUsername && adminUser.username === resolvedUsername)
      ) {
        return res
          .status(400)
          .json({ error: "Cannot remove your own admin status" });
      }

      const targetUser = await getUserByPreferredIdentifier(
        userRepository,
        resolvedUserId,
        resolvedUsername,
      );
      if (!targetUser) {
        return res.status(404).json({ error: "User not found" });
      }

      if (!targetUser.isAdmin) {
        return res.status(400).json({ error: "User is not an admin" });
      }

      await userRepository.update(targetUser.id, { isAdmin: false });

      try {
        await createCurrentRoleRepository().switchUserRoleName({
          userId: targetUser.id,
          addRoleName: "user",
          removeRoleName: "admin",
          grantedBy: userId,
        });
      } catch (roleError) {
        authLogger.error(
          "Failed to sync user role on remove-admin",
          roleError,
          {
            operation: "remove_admin_role_sync",
            userId: targetUser.id,
          },
        );
      }

      try {
        await DatabaseSaveTrigger.forceSave("remove_admin_explicit_save");
      } catch (saveError) {
        authLogger.error("Failed to persist admin removal to disk", saveError, {
          operation: "remove_admin_save_failed",
          userId: targetUser.id,
          username: targetUser.username,
        });
      }

      authLogger.info("Admin privileges revoked", {
        operation: "admin_revoke",
        adminId: userId,
        targetUserId: targetUser.id,
        targetUsername: targetUser.username,
      });

      const { ipAddress, userAgent } = getRequestMeta(req);
      await logAudit({
        userId,
        username: adminUser.username ?? userId,
        action: "remove_admin",
        resourceType: "user",
        resourceId: targetUser.id,
        resourceName: targetUser.username,
        ipAddress,
        userAgent,
        success: true,
      });

      res.json({
        message: `Admin status removed from ${targetUser.username}`,
      });
    } catch (err) {
      authLogger.error("Failed to remove admin status", err);
      res.status(500).json({ error: "Failed to remove admin status" });
    }
  });

  /**
   * @openapi
   * /users/admin-create:
   *   post:
   *     summary: Admin create user
   *     description: Allows an admin to create a new user regardless of whether public registration is enabled.
   *     tags:
   *       - Users
   *     requestBody:
   *       required: true
   *       content:
   *         application/json:
   *           schema:
   *             type: object
   *             properties:
   *               username:
   *                 type: string
   *               password:
   *                 type: string
   *     responses:
   *       200:
   *         description: User created successfully.
   *       400:
   *         description: Username and password are required.
   *       403:
   *         description: Not authorized.
   *       409:
   *         description: Username already exists.
   *       500:
   *         description: Failed to create user.
   */
  router.post("/admin-create", authenticateJWT, async (req, res) => {
    const adminId = (req as AuthenticatedRequest).userId;
    const userRepository = createCurrentUserRepository();
    let adminUser: UserRecord | null = null;

    try {
      adminUser = await userRepository.findById(adminId);
      if (!adminUser?.isAdmin) {
        return res.status(403).json({ error: "Not authorized" });
      }
    } catch (err) {
      authLogger.error("Failed to verify admin status", err);
      return res.status(500).json({ error: "Failed to verify admin status" });
    }

    const { username, password } = req.body;

    if (!isNonEmptyString(username) || !isNonEmptyString(password)) {
      return res
        .status(400)
        .json({ error: "Username and password are required" });
    }

    try {
      const existing = await userRepository.findByUsername(username);
      if (existing) {
        return res.status(409).json({ error: "Username already exists" });
      }

      const password_hash = await bcrypt.hash(password, 10);
      const id = nanoid();

      await userRepository.create({
        id,
        username,
        passwordHash: password_hash,
        isAdmin: false,
        isOidc: false,
        clientId: "",
        clientSecret: "",
        issuerUrl: "",
        authorizationUrl: "",
        tokenUrl: "",
        identifierPath: "",
        namePath: "",
        scopes: "openid email profile",
        totpSecret: null,
        totpEnabled: false,
        totpBackupCodes: null,
      });

      try {
        await createCurrentRoleRepository().assignRoleNameToUser({
          userId: id,
          roleName: "user",
          grantedBy: adminId,
        });
      } catch (roleError) {
        authLogger.error(
          "Failed to assign default role during admin create",
          roleError,
          {
            operation: "admin_create_user_role",
            userId: id,
          },
        );
      }

      const authManager = AuthManager.getInstance();
      try {
        await authManager.registerUser(id, password);
      } catch (encryptionError) {
        await userRepository.delete(id);
        authLogger.error(
          "Failed to setup user encryption during admin create, rolled back",
          encryptionError,
          { operation: "admin_create_user_encryption_failed", userId: id },
        );
        return res.status(500).json({
          error: "Failed to setup user security - user creation cancelled",
        });
      }

      try {
        await DatabaseSaveTrigger.forceSave("admin_create_user_explicit_save");
      } catch (saveError) {
        authLogger.error(
          "Failed to persist admin-created user to disk",
          saveError,
          {
            operation: "admin_create_user_save_failed",
            userId: id,
          },
        );
      }

      authLogger.success("User created by admin", {
        operation: "admin_create_user_success",
        adminId,
        userId: id,
        username,
      });

      const { ipAddress, userAgent } = getRequestMeta(req);
      await logAudit({
        userId: adminId,
        username: adminUser.username ?? adminId,
        action: "create_user",
        resourceType: "user",
        resourceId: id,
        resourceName: username,
        ipAddress,
        userAgent,
        success: true,
      });

      res.json({
        message: "User created",
        toast: { type: "success", message: `User created: ${username}` },
      });
    } catch (err) {
      authLogger.error("Failed to admin-create user", err);
      res.status(500).json({ error: "Failed to create user" });
    }
  });

  /**
   * @openapi
   * /users/admin/reset-password:
   *   post:
   *     summary: Reset a user's password (admin only)
   *     description: >
   *       Resets another user's password. Data is preserved for users whose
   *       encryption key has been migrated to the system wrap. Users who never
   *       logged in since the encryption upgrade require confirmDataWipe,
   *       which deletes their encrypted data.
   *     tags:
   *       - Users
   *     requestBody:
   *       required: true
   *       content:
   *         application/json:
   *           schema:
   *             type: object
   *             required:
   *               - newPassword
   *             properties:
   *               userId:
   *                 type: string
   *               username:
   *                 type: string
   *               newPassword:
   *                 type: string
   *               confirmDataWipe:
   *                 type: boolean
   *     responses:
   *       200:
   *         description: Password reset; dataWiped indicates whether encrypted data was deleted.
   *       400:
   *         description: Missing or invalid parameters.
   *       403:
   *         description: Admin access required.
   *       404:
   *         description: User not found.
   *       409:
   *         description: Reset would wipe the user's data and confirmDataWipe was not set.
   *       500:
   *         description: Failed to reset password.
   */
  router.post("/admin/reset-password", authenticateJWT, async (req, res) => {
    const adminId = (req as AuthenticatedRequest).userId;
    const { userId: targetUserId, username, newPassword } = req.body;
    const resolvedUserId = isNonEmptyString(targetUserId)
      ? targetUserId.trim()
      : null;
    const resolvedUsername = isNonEmptyString(username)
      ? username.trim()
      : null;

    if (!resolvedUserId && !resolvedUsername) {
      return res.status(400).json({ error: "User ID or username is required" });
    }
    if (!isNonEmptyString(newPassword)) {
      return res.status(400).json({ error: "New password is required" });
    }

    try {
      const userRepository = createCurrentUserRepository();
      const adminUser = await userRepository.findById(adminId);
      if (!adminUser?.isAdmin) {
        return res.status(403).json({ error: "Admin access required" });
      }

      const targetUser = await getUserByPreferredIdentifier(
        userRepository,
        resolvedUserId,
        resolvedUsername,
      );
      if (!targetUser) {
        return res.status(404).json({ error: "User not found" });
      }
      if (targetUser.isOidc && !targetUser.passwordHash) {
        return res.status(400).json({
          error: "This user authenticates through an external provider",
        });
      }

      const { resetUserPassword } =
        await import("./user-password-reset-routes.js");
      const outcome = await resetUserPassword(AuthManager.getInstance(), {
        userId: targetUser.id,
        username: targetUser.username,
        newPassword,
        confirmDataWipe: req.body?.confirmDataWipe === true,
      });

      if (outcome.status === "wipe_confirmation_required") {
        return res.status(409).json({
          error:
            "This user has not logged in since the encryption upgrade, so their data cannot be recovered. Set confirmDataWipe to reset anyway and delete their hosts, credentials and snippets.",
          code: "DATA_WIPE_REQUIRED",
        });
      }

      const { ipAddress, userAgent } = getRequestMeta(req);
      await logAudit({
        userId: adminId,
        username: adminUser.username ?? adminId,
        action: "admin_reset_password",
        resourceType: "user",
        resourceId: targetUser.id,
        resourceName: targetUser.username,
        ipAddress,
        userAgent,
        success: true,
      });

      await DatabaseSaveTrigger.forceSave("admin_password_reset");

      res.json({
        message: "Password reset",
        dataWiped: outcome.dataWiped,
      });
    } catch (err) {
      authLogger.error("Failed to reset user password", err);
      res.status(500).json({ error: "Failed to reset password" });
    }
  });

  /**
   * @openapi
   * /users/admin/totp/disable:
   *   post:
   *     summary: Disable a user's TOTP (admin only)
   *     description: Clears another user's TOTP secret, enabled flag and backup codes so they can log in without 2FA.
   *     tags:
   *       - Users
   *     requestBody:
   *       required: true
   *       content:
   *         application/json:
   *           schema:
   *             type: object
   *             properties:
   *               userId:
   *                 type: string
   *     responses:
   *       200:
   *         description: TOTP disabled for the user.
   *       400:
   *         description: User ID is required or TOTP is not enabled.
   *       403:
   *         description: Admin access required.
   *       404:
   *         description: User not found.
   *       500:
   *         description: Failed to disable TOTP.
   */
  router.post("/admin/totp/disable", authenticateJWT, async (req, res) => {
    const adminId = (req as AuthenticatedRequest).userId;
    const { userId: targetUserId } = req.body;

    if (!isNonEmptyString(targetUserId)) {
      return res.status(400).json({ error: "User ID is required" });
    }

    try {
      const userRepository = createCurrentUserRepository();
      const adminUser = await userRepository.findById(adminId);
      if (!adminUser?.isAdmin) {
        return res.status(403).json({ error: "Admin access required" });
      }

      const targetUser = await userRepository.findById(targetUserId.trim());
      if (!targetUser) {
        return res.status(404).json({ error: "User not found" });
      }
      if (!targetUser.totpEnabled) {
        return res
          .status(400)
          .json({ error: "TOTP is not enabled for this user" });
      }

      await userRepository.update(targetUser.id, {
        totpSecret: null,
        totpEnabled: false,
        totpBackupCodes: null,
      });

      try {
        await DatabaseSaveTrigger.forceSave("admin_disable_totp");
      } catch (saveError) {
        authLogger.error("Failed to persist TOTP disable to disk", saveError, {
          operation: "admin_disable_totp_save_failed",
          userId: targetUser.id,
        });
      }

      const { ipAddress, userAgent } = getRequestMeta(req);
      await logAudit({
        userId: adminId,
        username: adminUser.username ?? adminId,
        action: "admin_disable_totp",
        resourceType: "user",
        resourceId: targetUser.id,
        resourceName: targetUser.username,
        ipAddress,
        userAgent,
        success: true,
      });

      res.json({ message: "TOTP disabled" });
    } catch (err) {
      authLogger.error("Failed to disable TOTP for user", err);
      res.status(500).json({ error: "Failed to disable TOTP" });
    }
  });

  /**
   * @openapi
   * /users/admin/export/{userId}:
   *   get:
   *     summary: Export a user's data (admin only)
   *     description: Downloads a JSON export of another user's data (hosts, credentials, file manager bookmarks). Secrets are decrypted server-side, so handle the file carefully.
   *     tags:
   *       - Users
   *     parameters:
   *       - in: path
   *         name: userId
   *         required: true
   *         schema:
   *           type: string
   *     responses:
   *       200:
   *         description: JSON export of the user's data.
   *       403:
   *         description: Admin access required.
   *       404:
   *         description: User not found.
   *       423:
   *         description: The user's data stays locked until their next login.
   *       500:
   *         description: Failed to export user data.
   */
  router.get("/admin/export/:userId", authenticateJWT, async (req, res) => {
    const adminId = (req as AuthenticatedRequest).userId;
    const targetUserId = Array.isArray(req.params.userId)
      ? req.params.userId[0]
      : req.params.userId;

    try {
      const userRepository = createCurrentUserRepository();
      const adminUser = await userRepository.findById(adminId);
      if (!adminUser?.isAdmin) {
        return res.status(403).json({ error: "Admin access required" });
      }

      const targetUser = await userRepository.findById(targetUserId);
      if (!targetUser) {
        return res.status(404).json({ error: "User not found" });
      }

      if (!DataCrypto.canUserAccessData(targetUser.id)) {
        return res.status(423).json({
          error: "Target user's data stays locked until their next login",
          code: "TARGET_DATA_LOCKED",
        });
      }

      const { UserDataExport } =
        await import("../../utils/user-data-export.js");
      const exportData = await UserDataExport.exportUserData(targetUser.id, {
        format: "plaintext",
        includeCredentials: true,
      });

      const { ipAddress, userAgent } = getRequestMeta(req);
      await logAudit({
        userId: adminId,
        username: adminUser.username ?? adminId,
        action: "admin_export_user_data",
        resourceType: "user",
        resourceId: targetUser.id,
        resourceName: targetUser.username,
        ipAddress,
        userAgent,
        success: true,
      });

      res.setHeader("Content-Type", "application/json");
      res.setHeader(
        "Content-Disposition",
        `attachment; filename="termix-user-${targetUser.username}-export.json"`,
      );
      res.json(exportData);
    } catch (err) {
      authLogger.error("Failed to export user data", err);
      res.status(500).json({ error: "Failed to export user data" });
    }
  });
}
