const prisma = require("../utils/prisma");
const { v4: uuidv4 } = require("uuid");
const ip = require("ip");

/**
 * @typedef {Object} TemporaryMobileDeviceRequest
 * @property {number|null} userId - User id to associate creation of key with.
 * @property {number} createdAt - Timestamp of when the token was created.
 * @property {number} expiresAt - Timestamp of when the token expires.
 */

/**
 * Temporary map to store mobile device requests
 * that are not yet approved. Generates a simple JWT
 * that expires and is tied to the user (if provided)
 * This token must be provided during /register event.
 * @type {Map<string, TemporaryMobileDeviceRequest>}
 */
const TemporaryMobileDeviceRequests = new Map();

const MobileDevice = {
  platform: "server",
  validDeviceOs: ["android"],
  tablename: "desktop_mobile_devices",
  writable: ["approved"],
  validators: {
    approved: (value) => {
      if (typeof value !== "boolean") return "Must be a boolean";
      return null;
    },
  },

  /**
   * Looks up and consumes a temporary token that was registered
   * Will return null if the token is not found or expired.
   * @param {string} token - The temporary token to lookup
   * @returns {TemporaryMobileDeviceRequest|null} Temp token details
   */
  tempToken: (token = null) => {
    try {
      if (!token || !TemporaryMobileDeviceRequests.has(token)) return null;
      const tokenData = TemporaryMobileDeviceRequests.get(token);
      if (tokenData.expiresAt < Date.now()) return null;
      return tokenData;
    } catch {
      return null;
    } finally {
      TemporaryMobileDeviceRequests.delete(token);
    }
  },

  /**
   * Registers a temporary token for a mobile device request
   * This is just using a random token to identify the request
   * @security Note: If we use a JWT the QR code that encodes it becomes extremely complex
   * and noisy as QR codes have byte limits that could be exceeded with JWTs. Since this is
   * a temporary token that is only used to register a device and is short lived we can use UUIDs.
   * @param {import("@prisma/client").users|null} user - User to get connection URL for in Multi-User Mode
   * @returns {string} The temporary token
   */
  registerTempToken: function (user = null) {
    let tokenData = {};
    if (user) tokenData.userId = user.id;
    else tokenData.userId = null;

    // Set short lived expiry to this mapping
    const createdAt = Date.now();
    tokenData.createdAt = createdAt;
    tokenData.expiresAt = createdAt + 3 * 60_000;

    const tempToken = uuidv4().split("-").slice(0, 3).join("");
    TemporaryMobileDeviceRequests.set(tempToken, tokenData);

    // Run this on register since there is no BG task to do this.
    this.cleanupExpiredTokens();
    return tempToken;
  },

  /**
   * Cleans up expired temporary registration tokens
   * Should run quick since this mapping is wiped often
   * and does not live past restarts.
   */
  cleanupExpiredTokens: function () {
    const now = Date.now();
    for (const [token, data] of TemporaryMobileDeviceRequests.entries()) {
      if (data.expiresAt < now) TemporaryMobileDeviceRequests.delete(token);
    }
  },

  /**
   * Returns the connection URL for the mobile app to use to connect to the backend.
   * Since you have to have a valid session to call /mobile/connect-info we can pre-register
   * a temporary token for the user that is passed back to /mobile/register and can lookup
   * who a device belongs to so we can scope it's access token.
   * @param {import("@prisma/client").users|null} user - User to get connection URL for in Multi-User Mode
   * @returns {string}
   */
  connectionURL: function (user = null) {
    let baseUrl = "/api/mobile";
    if (process.env.NODE_ENV === "production") baseUrl = "/api/mobile";
    else
      baseUrl = `http://${ip.address()}:${process.env.SERVER_PORT || 3001}/api/mobile`;

    const tempToken = this.registerTempToken(user);
    baseUrl = `${baseUrl}?t=${tempToken}`;
    return baseUrl;
  },

  /**
   * Creates a new device for the mobile app
   * @param {object} params - The params to create the device with.
   * @param {string} params.deviceOs - Device os to associate creation of key with.
   * @param {string} params.deviceName - Device name to associate creation of key with.
   * @param {number|null} params.userId - User id to associate creation of key with.
   * @returns {Promise<{device: import("@prisma/client").desktop_mobile_devices|null, error:string|null}>}
   */
  create: async function ({ deviceOs, deviceName, userId = null }) {
    try {
      if (!deviceOs || !deviceName)
        return { device: null, error: "Device OS and name are required" };
      if (!this.validDeviceOs.includes(deviceOs))
        return { device: null, error: `Invalid device OS - ${deviceOs}` };

      const device = await prisma.desktop_mobile_devices.create({
        data: {
          deviceName: String(deviceName),
          deviceOs: String(deviceOs).toLowerCase(),
          token: uuidv4(),
          userId: userId ? Number(userId) : null,
        },
      });
      return { device, error: null };
    } catch (error) {
      console.error("Failed to create mobile device", error);
      return { device: null, error: error.message };
    }
  },

  /**
   * Validated existing API key
   * @param {string} id - Device id (db id)
   * @param {object} updates - Updates to apply to device
   * @returns {Promise<{device: import("@prisma/client").desktop_mobile_devices|null, error:string|null}>}
   */
  update: async function (id, updates = {}) {
    const device = await this.get({ id: parseInt(id) });
    if (!device) return { device: null, error: "Device not found" };

    const validUpdates = {};
    for (const [key, value] of Object.entries(updates)) {
      if (!this.writable.includes(key)) continue;
      const validation = this.validators[key](value);
      if (validation !== null) return { device: null, error: validation };
      validUpdates[key] = value;
    }
    // If no updates, return the device.
    if (Object.keys(validUpdates).length === 0) return { device, error: null };

    const updatedDevice = await prisma.desktop_mobile_devices.update({
      where: { id: device.id },
      data: validUpdates,
    });
    return { device: updatedDevice, error: null };
  },

  /**
   * Fetches mobile device by params.
   * @param {object} clause - Prisma props for search
   * @returns {Promise<import("@prisma/client").desktop_mobile_devices[]>}
   */
  get: async function (clause = {}, include = null) {
    try {
      const device = await prisma.desktop_mobile_devices.findFirst({
        where: clause,
        ...(include !== null ? { include } : {}),
      });
      return device;
    } catch (error) {
      console.error("FAILED TO GET MOBILE DEVICE.", error);
      return [];
    }
  },

  /**
   * Deletes mobile device by db id.
   * @param {number} id - database id of mobile device
   * @returns {Promise<{success: boolean, error:string|null}>}
   */
  delete: async function (id) {
    try {
      await prisma.desktop_mobile_devices.delete({
        where: { id: parseInt(id) },
      });
      return { success: true, error: null };
    } catch (error) {
      console.error("Failed to delete mobile device", error);
      return { success: false, error: error.message };
    }
  },

  /**
   * Gets mobile devices by params
   * @param {object} clause
   * @param {number|null} limit
   * @param {object|null} orderBy
   * @returns {Promise<import("@prisma/client").desktop_mobile_devices[]>}
   */
  where: async function (
    clause = {},
    limit = null,
    orderBy = null,
    include = null
  ) {
    try {
      const devices = await prisma.desktop_mobile_devices.findMany({
        where: clause,
        ...(limit !== null ? { take: limit } : {}),
        ...(orderBy !== null ? { orderBy } : {}),
        ...(include !== null ? { include } : {}),
      });
      return devices;
    } catch (error) {
      console.error("FAILED TO GET MOBILE DEVICES.", error.message);
      return [];
    }
  },

  /**
   * Migrates all mobile devices with null userId to the specified user.
   * Called during multi-user mode enablement to assign orphaned devices to the new admin.
   * @param {number} userId - The admin user ID to assign devices to
   * @returns {Promise<void>}
   */
  migrateDevicesToMultiUser: async function (userId) {
    try {
      await prisma.desktop_mobile_devices.updateMany({
        where: { userId: null },
        data: { userId: userId },
      });
      console.log("Successfully migrated mobile devices to multi-user mode");
    } catch (error) {
      console.error(
        "Error migrating mobile devices to multi-user mode:",
        error
      );
    }
  },
};

module.exports = { MobileDevice };
