"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.noop = exports.isArguments = exports.defaults = exports.Debug = exports.getPackageMeta = exports.zipMap = exports.CONNECTION_CLOSED_ERROR_MSG = exports.shuffle = exports.sample = exports.resolveTLSProfile = exports.parseURL = exports.optimizeErrorStack = exports.toArg = exports.convertMapToArray = exports.convertObjectToArray = exports.timeout = exports.packObject = exports.isInt = exports.wrapMultiResult = exports.convertBufferToString = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const lodash_1 = require("./lodash");
Object.defineProperty(exports, "defaults", { enumerable: true, get: function () { return lodash_1.defaults; } });
Object.defineProperty(exports, "isArguments", { enumerable: true, get: function () { return lodash_1.isArguments; } });
Object.defineProperty(exports, "noop", { enumerable: true, get: function () { return lodash_1.noop; } });
const debug_1 = require("./debug");
exports.Debug = debug_1.default;
const TLSProfiles_1 = require("../constants/TLSProfiles");
/**
 * Convert a buffer to string, supports buffer array
 *
 * @example
 * ```js
 * const input = [Buffer.from('foo'), [Buffer.from('bar')]]
 * const res = convertBufferToString(input, 'utf8')
 * expect(res).to.eql(['foo', ['bar']])
 * ```
 */
function convertBufferToString(value, encoding) {
    if (value instanceof Buffer) {
        return value.toString(encoding);
    }
    if (Array.isArray(value)) {
        const length = value.length;
        const res = Array(length);
        for (let i = 0; i < length; ++i) {
            res[i] =
                value[i] instanceof Buffer && encoding === "utf8"
                    ? value[i].toString()
                    : convertBufferToString(value[i], encoding);
        }
        return res;
    }
    return value;
}
exports.convertBufferToString = convertBufferToString;
/**
 * Convert a list of results to node-style
 *
 * @example
 * ```js
 * const input = ['a', 'b', new Error('c'), 'd']
 * const output = exports.wrapMultiResult(input)
 * expect(output).to.eql([[null, 'a'], [null, 'b'], [new Error('c')], [null, 'd'])
 * ```
 */
function wrapMultiResult(arr) {
    // When using WATCH/EXEC transactions, the EXEC will return
    // a null instead of an array
    if (!arr) {
        return null;
    }
    const result = [];
    const length = arr.length;
    for (let i = 0; i < length; ++i) {
        const item = arr[i];
        if (item instanceof Error) {
            result.push([item]);
        }
        else {
            result.push([null, item]);
        }
    }
    return result;
}
exports.wrapMultiResult = wrapMultiResult;
/**
 * Detect if the argument is a int
 * @example
 * ```js
 * > isInt('123')
 * true
 * > isInt('123.3')
 * false
 * > isInt('1x')
 * false
 * > isInt(123)
 * true
 * > isInt(true)
 * false
 * ```
 */
function isInt(value) {
    const x = parseFloat(value);
    return !isNaN(value) && (x | 0) === x;
}
exports.isInt = isInt;
/**
 * Pack an array to an Object
 *
 * @example
 * ```js
 * > packObject(['a', 'b', 'c', 'd'])
 * { a: 'b', c: 'd' }
 * ```
 */
function packObject(array) {
    const result = {};
    const length = array.length;
    for (let i = 1; i < length; i += 2) {
        result[array[i - 1]] = array[i];
    }
    return result;
}
exports.packObject = packObject;
/**
 * Return a callback with timeout
 */
function timeout(callback, timeout) {
    let timer = null;
    const run = function () {
        if (timer) {
            clearTimeout(timer);
            timer = null;
            callback.apply(this, arguments);
        }
    };
    timer = setTimeout(run, timeout, new Error("timeout"));
    return run;
}
exports.timeout = timeout;
/**
 * Convert an object to an array
 * @example
 * ```js
 * > convertObjectToArray({ a: '1' })
 * ['a', '1']
 * ```
 */
function convertObjectToArray(obj) {
    const result = [];
    const keys = Object.keys(obj); // Object.entries requires node 7+
    for (let i = 0, l = keys.length; i < l; i++) {
        result.push(keys[i], obj[keys[i]]);
    }
    return result;
}
exports.convertObjectToArray = convertObjectToArray;
/**
 * Convert a map to an array
 * @example
 * ```js
 * > convertMapToArray(new Map([[1, '2']]))
 * [1, '2']
 * ```
 */
function convertMapToArray(map) {
    const result = [];
    let pos = 0;
    map.forEach(function (value, key) {
        result[pos] = key;
        result[pos + 1] = value;
        pos += 2;
    });
    return result;
}
exports.convertMapToArray = convertMapToArray;
/**
 * Convert a non-string arg to a string
 */
function toArg(arg) {
    if (arg === null || typeof arg === "undefined") {
        return "";
    }
    return String(arg);
}
exports.toArg = toArg;
/**
 * Optimize error stack
 *
 * @param error actually error
 * @param friendlyStack the stack that more meaningful
 * @param filterPath only show stacks with the specified path
 */
function optimizeErrorStack(error, friendlyStack, filterPath) {
    const stacks = friendlyStack.split("\n");
    let lines = "";
    let i;
    for (i = 1; i < stacks.length; ++i) {
        if (stacks[i].indexOf(filterPath) === -1) {
            break;
        }
    }
    for (let j = i; j < stacks.length; ++j) {
        lines += "\n" + stacks[j];
    }
    if (error.stack) {
        const pos = error.stack.indexOf("\n");
        error.stack = error.stack.slice(0, pos) + lines;
    }
    return error;
}
exports.optimizeErrorStack = optimizeErrorStack;
/**
 * Parse the redis protocol url
 */
function parseURL(url) {
    if (isInt(url)) {
        return { port: url };
    }
    // Re-assert string type: isInt's type guard (value is string) incorrectly
    // narrows url to 'never' in the false branch since url is already string.
    const rawUrl = url;
    // Determine if the URL has a known protocol (redis:// or rediss://)
    const hasProtocol = /^rediss?:\/\//i.test(rawUrl);
    // Unix socket paths (starting with "/") are not valid URLs — handle separately.
    // Preserve query params (e.g., /tmp/redis.sock?db=2)
    if (rawUrl[0] === "/") {
        const qIdx = rawUrl.indexOf("?");
        const result = {
            path: qIdx === -1 ? rawUrl : rawUrl.slice(0, qIdx),
        };
        if (qIdx !== -1) {
            const options = {};
            const params = new URLSearchParams(rawUrl.slice(qIdx + 1));
            params.forEach((value, key) => {
                options[key] = parseURLQueryItem(key, value);
            });
            (0, lodash_1.defaults)(result, options);
        }
        return result;
    }
    let parsed;
    if (hasProtocol) {
        parsed = new URL(rawUrl);
    }
    else {
        // Prepend a dummy protocol so new URL() can parse "host:port?query" correctly
        parsed = new URL("redis://" + rawUrl);
    }
    // Convert searchParams to a plain object (equivalent to url.parse(url, true).query)
    const options = {};
    parsed.searchParams.forEach((value, key) => {
        options[key] = parseURLQueryItem(key, value);
    });
    const result = {};
    // Extract auth — WHATWG URL percent-encodes special chars, so decode them.
    // Check both username AND password: redis://:password@host has empty username but valid password.
    if (parsed.username || parsed.password) {
        result.username = decodeURIComponent(parsed.username);
        result.password = decodeURIComponent(parsed.password);
    }
    if (parsed.pathname && parsed.pathname !== "/") {
        if (hasProtocol) {
            // For redis:// and rediss://, the path after / is the db number
            if (parsed.pathname.length > 1) {
                result.db = parsed.pathname.slice(1);
            }
        }
        else {
            result.path = parsed.pathname;
        }
    }
    if (parsed.hostname) {
        // WHATWG URL keeps brackets around IPv6 addresses (e.g., "[::1]").
        // Strip them to match the old url.parse() behavior.
        result.host = parsed.hostname.replace(/^\[|\]$/g, "");
    }
    if (parsed.port) {
        result.port = parsed.port;
    }
    (0, lodash_1.defaults)(result, options);
    return result;
}
exports.parseURL = parseURL;
function parseURLQueryItem(key, value) {
    if (key === "family") {
        const intFamily = Number.parseInt(value, 10);
        if (!Number.isNaN(intFamily)) {
            return intFamily;
        }
    }
    return value;
}
/**
 * Resolve TLS profile shortcut in connection options
 */
function resolveTLSProfile(options) {
    let tls = options === null || options === void 0 ? void 0 : options.tls;
    if (typeof tls === "string")
        tls = { profile: tls };
    const profile = TLSProfiles_1.default[tls === null || tls === void 0 ? void 0 : tls.profile];
    if (profile) {
        tls = Object.assign({}, profile, tls);
        delete tls.profile;
        options = Object.assign({}, options, { tls });
    }
    return options;
}
exports.resolveTLSProfile = resolveTLSProfile;
/**
 * Get a random element from `array`
 */
function sample(array, from = 0) {
    const length = array.length;
    if (from >= length) {
        return null;
    }
    return array[from + Math.floor(Math.random() * (length - from))];
}
exports.sample = sample;
/**
 * Shuffle the array using the Fisher-Yates Shuffle.
 * This method will mutate the original array.
 */
function shuffle(array) {
    let counter = array.length;
    // While there are elements in the array
    while (counter > 0) {
        // Pick a random index
        const index = Math.floor(Math.random() * counter);
        // Decrease counter by 1
        counter--;
        // And swap the last element with it
        [array[counter], array[index]] = [array[index], array[counter]];
    }
    return array;
}
exports.shuffle = shuffle;
/**
 * Error message for connection being disconnected
 */
exports.CONNECTION_CLOSED_ERROR_MSG = "Connection is closed.";
function zipMap(keys, values) {
    const map = new Map();
    keys.forEach((key, index) => {
        map.set(key, values[index]);
    });
    return map;
}
exports.zipMap = zipMap;
/**
 * Memoized package metadata to avoid repeated file system reads.
 *
 * @internal
 */
let cachedPackageMeta = null;
/**
 * Retrieves cached package metadata from package.json.
 *
 * @internal
 * @returns {Promise<{version: string} | null>} Package metadata or null if unavailable
 */
async function getPackageMeta() {
    if (cachedPackageMeta) {
        return cachedPackageMeta;
    }
    try {
        const filePath = (0, path_1.resolve)(__dirname, "..", "..", "package.json");
        const data = await fs_1.promises.readFile(filePath, "utf8");
        const parsed = JSON.parse(data);
        cachedPackageMeta = {
            version: parsed.version,
        };
        return cachedPackageMeta;
    }
    catch (err) {
        cachedPackageMeta = {
            version: "error-fetching-version",
        };
        return cachedPackageMeta;
    }
}
exports.getPackageMeta = getPackageMeta;
