import { Env, ErrorFactory, SignedRequest, ZintrustLang } from '../../../src/index.js';
const resolveSigningPrefix = (baseUrl) => {
    try {
        const parsed = new URL(baseUrl);
        const path = parsed.pathname.endsWith('/') ? parsed.pathname.slice(0, -1) : parsed.pathname;
        if (path === '' || path === '/')
            return undefined;
        return path;
    }
    catch {
        return undefined;
    }
};
const buildRequestUrl = (baseUrl, path) => {
    const url = new URL(baseUrl);
    const basePath = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname;
    const requestPath = path.startsWith('/') ? path : `/${path}`;
    url.pathname = `${basePath}${requestPath}`;
    return url;
};
const buildSigningUrl = (requestUrl, baseUrl) => {
    const prefix = resolveSigningPrefix(baseUrl);
    if (!prefix)
        return requestUrl;
    if (requestUrl.pathname === prefix || requestUrl.pathname.startsWith(`${prefix}/`)) {
        const signingUrl = new URL(requestUrl.toString());
        const stripped = requestUrl.pathname.slice(prefix.length);
        signingUrl.pathname = stripped.startsWith('/') ? stripped : `/${stripped}`;
        return signingUrl;
    }
    return requestUrl;
};
let publishClientInstance = null;
let publishClientConnected = false;
const resolveProxyBaseUrl = () => {
    const explicit = Env.REDIS_PROXY_URL.trim();
    if (explicit !== '')
        return explicit;
    if (Env.USE_REDIS_PROXY === false)
        return '';
    const host = Env.REDIS_PROXY_HOST || '127.0.0.1';
    const port = Env.REDIS_PROXY_PORT;
    return `http://${host}:${port}`;
};
const buildProxySettings = () => {
    const baseUrl = resolveProxyBaseUrl();
    const keyId = Env.REDIS_PROXY_KEY_ID || undefined;
    const secret = Env.REDIS_PROXY_SECRET || Env.APP_KEY || undefined;
    const timeoutMs = Env.REDIS_PROXY_TIMEOUT_MS;
    return { baseUrl, keyId, secret, timeoutMs };
};
const buildHeaders = async (settings, requestUrl, body) => {
    const headers = {
        'Content-Type': 'application/json',
    };
    if (settings.keyId && settings.secret) {
        const signingUrl = buildSigningUrl(requestUrl, settings.baseUrl);
        const signed = await SignedRequest.createHeaders({
            method: 'POST',
            url: signingUrl,
            body,
            keyId: settings.keyId,
            secret: settings.secret,
        });
        Object.assign(headers, signed);
    }
    return headers;
};
const requestProxy = async (settings, path, payload) => {
    if (settings.baseUrl.trim() === '') {
        throw ErrorFactory.createConfigError('Redis proxy URL is missing (REDIS_PROXY_URL)');
    }
    const body = JSON.stringify(payload);
    const url = buildRequestUrl(settings.baseUrl, path);
    const headers = await buildHeaders(settings, url, body);
    const timeoutSignal = typeof AbortSignal !== 'undefined' && 'timeout' in AbortSignal;
    const signal = timeoutSignal ? AbortSignal.timeout(settings.timeoutMs) : undefined;
    const response = await fetch(url.toString(), {
        method: 'POST',
        headers,
        body,
        signal,
    });
    if (!response.ok) {
        const text = await response.text();
        throw ErrorFactory.createTryCatchError(`Redis proxy request failed (${response.status})`, text);
    }
    return (await response.json());
};
const toNumber = (value) => {
    if (typeof value === 'number')
        return value;
    if (typeof value === 'string') {
        const parsed = Number(value);
        return Number.isFinite(parsed) ? parsed : 0;
    }
    return 0;
};
const tryCreateProxyPublishClient = async () => {
    const settings = buildProxySettings();
    if (settings.baseUrl.trim() === '')
        return null;
    return {
        publish: async (channel, message) => {
            const response = await requestProxy(settings, '/zin/redis/command', {
                command: 'PUBLISH',
                args: [channel, message],
            });
            return toNumber(response.result);
        },
    };
};
/**
 * Build Redis URL from environment variables
 */
const buildRedisUrl = () => {
    // Get REDIS_URL from environment
    const redisUrl = getRedisUrlFromEnv();
    if (redisUrl)
        return redisUrl;
    // Build URL from individual components
    return buildRedisUrlFromComponents();
};
/**
 * Get REDIS_URL from environment variables
 */
const getRedisUrlFromEnv = () => {
    const anyEnv = process.env;
    const fromEnv = typeof anyEnv.get === 'function' ? anyEnv.get('REDIS_URL', '') : '';
    const hasProcess = typeof process === 'object' && process !== null;
    const fallback = hasProcess ? (process.env?.['REDIS_URL'] ?? '') : '';
    const trimmed = fromEnv.trim();
    const url = (trimmed.length > 0 ? fromEnv : String(fallback)).trim();
    return url.length > 0 ? url : null;
};
/**
 * Build Redis URL from individual environment components
 */
const buildRedisUrlFromComponents = () => {
    const host = process.env?.['REDIS_HOST'] ?? 'localhost';
    const port = Number(process.env?.['REDIS_PORT'] ?? ZintrustLang.REDIS_DEFAULT_PORT);
    const password = process.env?.['REDIS_PASSWORD'];
    const database = Number(process.env?.['REDIS_QUEUE_DB'] ?? ZintrustLang.REDIS_DEFAULT_DB);
    let redisUrl = `redis://`;
    if (password)
        redisUrl += `:${password}@`;
    redisUrl += `${host}:${port}`;
    if (database > 0)
        redisUrl += `/${database}`;
    return redisUrl;
};
/**
 * Singleton Redis publish client factory
 * Creates and caches a Redis publish client for broadcasting
 */
export const createRedisPublishClient = async () => {
    // Return cached instance if available
    if (publishClientConnected && publishClientInstance !== null) {
        return publishClientInstance;
    }
    const proxyClient = await tryCreateProxyPublishClient();
    if (proxyClient) {
        return cacheAndReturnClient(proxyClient);
    }
    const url = buildRedisUrl();
    if (url === null)
        throw ErrorFactory.createConfigError('Redis publish client requires REDIS_URL');
    // Try different Redis clients in order of preference
    const redisClient = (await tryCreateRedisClient(url)) || (await tryCreateIoRedisClient(url)) || getFallbackClient();
    return redisClient;
};
/**
 * Try to create client using 'redis' package
 */
const tryCreateRedisClient = async (url) => {
    try {
        const mod = (await import('redis'));
        const client = mod.createClient({ url });
        if (typeof client.connect === 'function') {
            await connectClient(client, 'Redis publish client failed to connect');
        }
        return cacheAndReturnClient(client);
    }
    catch {
        return null;
    }
};
/**
 * Try to create client using 'ioredis' package
 */
const tryCreateIoRedisClient = async (url) => {
    try {
        const mod = (await import('ioredis'));
        const redis = mod.default(url);
        const client = {
            publish: (channel, message) => redis.publish(channel, message),
            connect: redis.connect
                ? async () => {
                    const connectFn = redis.connect;
                    await connectFn();
                }
                : undefined,
        };
        if (typeof client.connect === 'function') {
            await connectClient(client, 'Redis publish client (ioredis) failed to connect');
        }
        return cacheAndReturnClient(client);
    }
    catch {
        return null;
    }
};
/**
 * Get fallback client from global or throw error
 */
const getFallbackClient = () => {
    const globalFake = globalThis
        .__fakeRedisClient;
    if (globalFake === undefined) {
        throw ErrorFactory.createConfigError("Redis publish client requires the 'redis' or 'ioredis' package (run `zin add broadcast:redis' / `zin plugin install broadcast:redis`, or `npm install redis` / `npm install ioredis`) or a test fake client set in globalThis.__fakeRedisClient");
    }
    return cacheAndReturnClient(globalFake);
};
/**
 * Connect to Redis client with error handling
 */
const connectClient = async (client, errorMessage) => {
    try {
        if (client.connect) {
            await client.connect();
        }
    }
    catch (err) {
        throw ErrorFactory.createTryCatchError(errorMessage, err);
    }
};
/**
 * Cache and return the client instance
 */
const cacheAndReturnClient = (client) => {
    publishClientInstance = client;
    publishClientConnected = true;
    return client;
};
/**
 * Reset the singleton publish client (useful for testing)
 */
export const resetPublishClient = () => {
    publishClientInstance = null;
    publishClientConnected = false;
};
