const { GET_BODYSHOP_BY_ID } = require("../graphql-client/queries");
const devDebugLogger = require("./devDebugLogger");
const client = require("../graphql-client/graphql-client").client;

/**
 * Bodyshop cache TTL in seconds
 * @type {number}
 */
const BODYSHOP_CACHE_TTL = 3600; // 1 hour

/**
 * Chatter API token cache TTL in seconds
 * @type {number}
 */
const CHATTER_TOKEN_CACHE_TTL = 3600; // 1 hour

/**
 * Generate a cache key for a bodyshop
 * @param bodyshopId
 * @returns {`bodyshop-cache:${string}`}
 */
const getBodyshopCacheKey = (bodyshopId) => `bodyshop-cache:${bodyshopId}`;

/**
 * Generate a cache key for a Chatter API token
 * @param companyId
 * @returns {`chatter-token:${string}`}
 */
const getChatterTokenCacheKey = (companyId) => `chatter-token:${companyId}`;

/**
 * Generate a cache key for a user socket mapping
 * @param email
 * @returns {`user:${string}:${string}:socketMapping`}
 */
const getUserSocketMappingKey = (email) =>
  `user:${process.env?.NODE_ENV === "production" ? "prod" : "dev"}:${email}:socketMapping`;

const getSocketTransactionkey = ({ socketId, transactionType }) => `socket:${socketId}:${transactionType}`;
/**
 * Fetch bodyshop data from the database
 * @param bodyshopId
 * @param logger
 * @returns {Promise<*>}
 */
const fetchBodyshopFromDB = async (bodyshopId, logger) => {
  try {
    const response = await client.request(GET_BODYSHOP_BY_ID, { id: bodyshopId });
    const bodyshop = response.bodyshops_by_pk;
    if (!bodyshop) {
      throw new Error(`Bodyshop with ID ${bodyshopId} not found`);
    }
    return bodyshop; // Return the full object as-is
  } catch (error) {
    logger.log("fetch-bodyshop-from-db", "ERROR", "redis", null, {
      bodyshopId,
      error: error?.message,
      stack: error?.stack
    });
    throw error;
  }
};

/**
 * Apply Redis helper functions
 * @param pubClient
 * @param app
 * @param logger
 */
const applyRedisHelpers = ({ pubClient, app, logger }) => {
  const toRedisJson = (value) => JSON.stringify(value === undefined ? null : value);

  // Store session data in Redis
  const setSessionData = async (socketId, key, value, ttl) => {
    try {
      const sessionKey = `socket:${socketId}`;

      // Supports both forms:
      // 1) setSessionData(socketId, "field", value, ttl)
      // 2) setSessionData(socketId, { fieldA: valueA, fieldB: valueB }, ttl)
      if (key && typeof key === "object" && !Array.isArray(key)) {
        const entries = Object.entries(key).flatMap(([field, fieldValue]) => [field, toRedisJson(fieldValue)]);

        if (entries.length > 0) {
          await pubClient.hset(sessionKey, ...entries);
        }

        const objectTtl = typeof value === "number" ? value : typeof ttl === "number" ? ttl : null;
        if (objectTtl) {
          await pubClient.expire(sessionKey, objectTtl);
        }
        return;
      }

      await pubClient.hset(sessionKey, key, toRedisJson(value)); // Use Redis pubClient
      if (ttl && typeof ttl === "number") {
        await pubClient.expire(sessionKey, ttl);
      }
    } catch (error) {
      logger.log(`Error Setting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
    }
  };

  /**
   * Retrieve session data from Redis
   * @param socketId
   * @param key
   * @returns {Promise<any|null>}
   */
  const getSessionData = async (socketId, key) => {
    try {
      const sessionKey = `socket:${socketId}`;

      // Supports:
      // 1) getSessionData(socketId, "field") -> parsed field value
      // 2) getSessionData(socketId) -> parsed object of all fields
      if (typeof key === "undefined") {
        const raw = await pubClient.hgetall(sessionKey);
        if (!raw || Object.keys(raw).length === 0) return null;

        return Object.entries(raw).reduce((acc, [field, rawValue]) => {
          try {
            acc[field] = JSON.parse(rawValue);
          } catch {
            acc[field] = rawValue;
          }
          return acc;
        }, {});
      }

      const data = await pubClient.hget(sessionKey, key);
      return data ? JSON.parse(data) : null;
    } catch (error) {
      logger.log(`Error Getting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
    }
  };

  /**
   * Store session transaction data in Redis
   * @param socketId
   * @param transactionType
   * @param key
   * @param value
   * @param ttl
   * @returns {Promise<void>}
   */
  const setSessionTransactionData = async (socketId, transactionType, key, value, ttl) => {
    try {
      await pubClient.hset(getSocketTransactionkey({ socketId, transactionType }), key, toRedisJson(value)); // Use Redis pubClient
      if (ttl && typeof ttl === "number") {
        await pubClient.expire(getSocketTransactionkey({ socketId, transactionType }), ttl);
      }
    } catch (error) {
      logger.log(
        `Error Setting Session Data for socket transaction ${socketId}:${transactionType}: ${error}`,
        "ERROR",
        "redis"
      );
    }
  };

  /**
   * Retrieve session transaction data from Redis
   * @param socketId
   * @param transactionType
   * @param key
   * @returns {Promise<any|null>}
   */
  const getSessionTransactionData = async (socketId, transactionType, key) => {
    try {
      const data = await pubClient.hget(getSocketTransactionkey({ socketId, transactionType }), key);
      return data ? JSON.parse(data) : null;
    } catch (error) {
      logger.log(
        `Error Getting Session Data for socket transaction ${socketId}:${transactionType}: ${error}`,
        "ERROR",
        "redis"
      );
    }
  };

  /**
   * Clear session data from Redis
   * @param socketId
   * @returns {Promise<void>}
   */
  const clearSessionData = async (socketId) => {
    try {
      await pubClient.del(`socket:${socketId}`);
    } catch (error) {
      logger.log(`Error Clearing Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
    }
  };

  /**
   * Clear session transaction data from Redis
   * @param socketId
   * @param transactionType
   * @returns {Promise<void>}
   */
  const clearSessionTransactionData = async (socketId, transactionType) => {
    try {
      if (transactionType) {
        await pubClient.del(getSocketTransactionkey({ socketId, transactionType }));
        return;
      }

      // If no transactionType is provided, clear all transaction namespaces for this socket.
      const pattern = getSocketTransactionkey({ socketId, transactionType: "*" });
      const keys = await pubClient.keys(pattern);
      if (Array.isArray(keys) && keys.length > 0) {
        await pubClient.del(...keys);
      }
    } catch (error) {
      logger.log(
        `Error Clearing Session Transaction Data for socket ${socketId}:${transactionType}: ${error}`,
        "ERROR",
        "redis"
      );
    }
  };

  /**
   * Add a socket mapping for a user
   * @param email
   * @param socketId
   * @param bodyshopId
   * @returns {Promise<void>}
   */
  const addUserSocketMapping = async (email, socketId, bodyshopId) => {
    const socketMappingKey = getUserSocketMappingKey(email);
    try {
      devDebugLogger(`Adding socket ${socketId} to user ${email} for bodyshop ${bodyshopId}`);
      // Save the mapping: socketId -> bodyshopId
      await pubClient.hset(socketMappingKey, socketId, bodyshopId);
      // Set TTL (24 hours) for the mapping hash
      await pubClient.expire(socketMappingKey, 86400);
    } catch (error) {
      logger.log(`Error adding socket mapping for ${email} (bodyshop ${bodyshopId}): ${error}`, "ERROR", "redis");
    }
  };

  /**
   * Refresh the TTL for a user's socket mapping
   * @param email
   * @returns {Promise<void>}
   */
  const refreshUserSocketTTL = async (email) => {
    const socketMappingKey = getUserSocketMappingKey(email);

    try {
      const exists = await pubClient.exists(socketMappingKey);
      if (exists) {
        await pubClient.expire(socketMappingKey, 86400);
        devDebugLogger(`Refreshed TTL for ${email} socket mapping`);
      }
    } catch (error) {
      logger.log(`Error refreshing TTL for ${email}: ${error}`, "ERROR", "redis");
    }
  };

  /**
   * Remove a socket mapping for a user
   * @param email
   * @param socketId
   * @returns {Promise<void>}
   */
  const removeUserSocketMapping = async (email, socketId) => {
    const socketMappingKey = getUserSocketMappingKey(email);

    try {
      devDebugLogger(`Removing socket ${socketId} mapping for user ${email}`);
      // Look up the bodyshopId associated with this socket
      const bodyshopId = await pubClient.hget(socketMappingKey, socketId);
      if (!bodyshopId) {
        devDebugLogger(`Socket ${socketId} not found for user ${email}`);
        return;
      }
      // Remove the socket mapping
      await pubClient.hdel(socketMappingKey, socketId);
      devDebugLogger(`Removed socket ${socketId} (associated with bodyshop ${bodyshopId}) for user ${email}`);

      // Refresh TTL if any socket mappings remain
      const remainingSockets = await pubClient.hlen(socketMappingKey);
      if (remainingSockets > 0) {
        await pubClient.expire(socketMappingKey, 86400);
      }
    } catch (error) {
      logger.log(`Error removing socket mapping for ${email}: ${error}`, "ERROR", "redis");
    }
  };

  /**
   * Get all socket mappings for a user
   * @param email
   * @returns {Promise<{}>}
   */
  const getUserSocketMapping = async (email) => {
    const socketMappingKey = getUserSocketMappingKey(email);

    try {
      // Retrieve all socket mappings for the user
      const mapping = await pubClient.hgetall(socketMappingKey);
      const ttl = await pubClient.ttl(socketMappingKey);
      // Group socket IDs by bodyshopId
      const result = {};
      for (const [socketId, bodyshopId] of Object.entries(mapping)) {
        if (!result[bodyshopId]) {
          result[bodyshopId] = { socketIds: [], ttl };
        }
        result[bodyshopId].socketIds.push(socketId);
      }
      return result;
    } catch (error) {
      console.error(`Error retrieving socket mappings for ${email}:`, error);
      throw error;
    }
  };

  /**
   * Get socket IDs for a user by bodyshopId
   * @param email
   * @param bodyshopId
   * @returns {Promise<{socketIds: [string, string], ttl: *}>}
   */
  const getUserSocketMappingByBodyshop = async (email, bodyshopId) => {
    const socketMappingKey = getUserSocketMappingKey(email);

    try {
      // Retrieve all socket mappings for the user
      const mapping = await pubClient.hgetall(socketMappingKey);
      const ttl = await pubClient.ttl(socketMappingKey);
      // Filter socket IDs for the provided bodyshopId
      const socketIds = Object.entries(mapping).reduce((acc, [socketId, bId]) => {
        if (bId === bodyshopId) {
          acc.push(socketId);
        }
        return acc;
      }, []);
      return { socketIds, ttl };
    } catch (error) {
      logger.log(`Error retrieving socket mappings for ${email} by bodyshop ${bodyshopId}: ${error}`, "ERROR", "redis");
      throw error;
    }
  };

  /**
   * Get bodyshop data from Redis
   * @param bodyshopId
   * @returns {Promise<*>}
   */
  const getBodyshopFromRedis = async (bodyshopId) => {
    const key = getBodyshopCacheKey(bodyshopId);
    try {
      // Check if data exists in Redis
      const cachedData = await pubClient.get(key);
      if (cachedData) {
        return JSON.parse(cachedData); // Parse and return the full object
      }

      // Cache miss: fetch from DB
      const bodyshopData = await fetchBodyshopFromDB(bodyshopId, logger);

      // Store in Redis as a single JSON string
      const jsonData = JSON.stringify(bodyshopData);
      await pubClient.set(key, jsonData);
      await pubClient.expire(key, BODYSHOP_CACHE_TTL);

      devDebugLogger("bodyshop-cache-miss", {
        bodyshopId,
        action: "Fetched from DB and cached"
      });

      return bodyshopData; // Return the full object
    } catch (error) {
      logger.log("get-bodyshop-from-redis", "ERROR", "redis", null, {
        bodyshopId,
        error: error.message
      });
      throw error;
    }
  };

  /**
   * Update or invalidate bodyshop data in Redis
   * @param bodyshopId
   * @param values
   * @returns {Promise<void>}
   */
  const updateOrInvalidateBodyshopFromRedis = async (bodyshopId, values = null) => {
    const key = getBodyshopCacheKey(bodyshopId);
    try {
      if (!values) {
        // Invalidate cache by deleting the key
        await pubClient.del(key);
        devDebugLogger("bodyshop-cache-invalidate", {
          bodyshopId,
          action: "Cache invalidated"
        });
      } else {
        // Update cache with the full provided values
        const jsonData = JSON.stringify(values);
        await pubClient.set(key, jsonData);
        await pubClient.expire(key, BODYSHOP_CACHE_TTL);
        devDebugLogger("bodyshop-cache-update", {
          bodyshopId,
          action: "Cache updated",
          values
        });
      }
    } catch (error) {
      logger.log("update-or-invalidate-bodyshop-from-redis", "ERROR", "api", "redis", {
        bodyshopId,
        values,
        error: error.message
      });
      throw error;
    }
  };

  /**
   * Set provider cache data
   * @param ns
   * @param field
   * @param value
   * @param ttl
   * @returns {Promise<void>}
   */
  const setProviderCache = (ns, field, value, ttl) => setSessionData(`${ns}:provider`, field, value, ttl);

  /**
   * Get provider cache data
   * @param ns
   * @param field
   * @returns {Promise<any|null|undefined>}
   */
  const getProviderCache = (ns, field) => getSessionData(`${ns}:provider`, field);

  /**
   * Get Chatter API token from Redis cache
   * @param companyId
   * @returns {Promise<string|null>}
   */
  const getChatterToken = async (companyId) => {
    const key = getChatterTokenCacheKey(companyId);
    try {
      const token = await pubClient.get(key);
      return token;
    } catch (error) {
      logger.log("get-chatter-token-from-redis", "ERROR", "redis", null, {
        companyId,
        error: error.message
      });
      return null;
    }
  };

  /**
   * Set Chatter API token in Redis cache
   * @param companyId
   * @param token
   * @returns {Promise<void>}
   */
  const setChatterToken = async (companyId, token) => {
    const key = getChatterTokenCacheKey(companyId);
    try {
      await pubClient.set(key, token);
      await pubClient.expire(key, CHATTER_TOKEN_CACHE_TTL);
      devDebugLogger("chatter-token-cache-set", {
        companyId,
        action: "Token cached"
      });
    } catch (error) {
      logger.log("set-chatter-token-in-redis", "ERROR", "redis", null, {
        companyId,
        error: error.message
      });
      throw error;
    }
  };

  const api = {
    getUserSocketMappingKey,
    getBodyshopCacheKey,
    getChatterTokenCacheKey,
    setSessionData,
    getSessionData,
    clearSessionData,
    addUserSocketMapping,
    removeUserSocketMapping,
    getUserSocketMappingByBodyshop,
    getUserSocketMapping,
    refreshUserSocketTTL,
    getBodyshopFromRedis,
    updateOrInvalidateBodyshopFromRedis,
    setSessionTransactionData,
    getSessionTransactionData,
    clearSessionTransactionData,
    setProviderCache,
    getProviderCache,
    getChatterToken,
    setChatterToken
  };

  Object.assign(module.exports, api);

  app.use((req, res, next) => {
    req.sessionUtils = api;
    next();
  });

  return api;
};

module.exports = { applyRedisHelpers };
