/* eslint-disable no-console */
import { Agent, setGlobalDispatcher } from "undici";
import { createTokenBucket, TokenBucket } from "./ratelimit.js";

// Глобальный keep-alive агент (один на процесс)
const agent = new Agent({
    connections: 128,
    keepAliveTimeout: 10_000,
    keepAliveMaxTimeout: 60_000,
    // pipelining: 1,
});
setGlobalDispatcher(agent);

export type RpcClientOptions = {
    baseUrl: string;        // http(s)://host:26657
    timeoutMs: number;      // per-request timeout
    retries: number;        // max retries for transient/5xx
    backoffMs: number;      // base backoff
    backoffJitter: number;  // 0..1
    rps: number;            // target req/s (token bucket)
    log?: (msg: string, extra?: unknown) => void;
    headers?: Record<string, string>;
};

export type RpcClient = {
    getJson: <T = any>(path: string, params?: Record<string, string | number | boolean | undefined>) => Promise<T>;
    fetchBlock: (height: number) => Promise<any>;
    fetchBlockResults: (height: number) => Promise<any>;
    fetchStatus: () => Promise<any>;
};

function sleep(ms: number) {
    return new Promise((r) => setTimeout(r, ms));
}

function jitter(base: number, j: number) {
    if (j <= 0) return base;
    const delta = base * j;
    return base + (Math.random() * 2 - 1) * delta;
}

function buildUrl(base: string, path: string, params?: Record<string, string | number | boolean | undefined>) {
    const u = new URL(path.replace(/^\/+/, "/"), base.replace(/\/+$/, "/"));
    if (params) {
        for (const [k, v] of Object.entries(params)) {
            if (v === undefined) continue;
            u.searchParams.set(k, String(v));
        }
    }
    return u.toString();
}

export function createRpcClient(opts: RpcClientOptions): RpcClient {
    const bucket: TokenBucket = createTokenBucket(Math.max(1, Math.floor(opts.rps)), 2);
    const headers: Record<string, string> = {
        accept: "application/json",
        "accept-encoding": "gzip, br",
        connection: "keep-alive",
        ...(opts.headers ?? {}),
    };
    const log = opts.log;

    async function getJson<T = any>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T> {
        const url = buildUrl(opts.baseUrl, path, params);

        for (let attempt = 0; attempt <= opts.retries; attempt++) {
            await bucket.take(1);

            const ac = new AbortController();
            const t = setTimeout(() => ac.abort(), opts.timeoutMs);

            try {
                const res = await fetch(url, { method: "GET", headers, signal: ac.signal });
                clearTimeout(t);

                if (!res.ok) {
                    const text = await res.text().catch(() => "");
                    const err = new Error(`HTTP ${res.status} ${res.statusText} for ${url} :: ${text.slice(0, 200)}`);
                    if ((res.status >= 500 || res.status === 429) && attempt < opts.retries) {
                        const delay = jitter(opts.backoffMs * Math.pow(2, attempt), opts.backoffJitter);
                        log?.("retry http", { attempt, delay, status: res.status });
                        await sleep(delay);
                        continue;
                    }
                    throw err;
                }

                return (await res.json()) as T;

            } catch (e: any) {
                clearTimeout(t);
                const transient = e?.name === "AbortError" || e?.code === "ECONNRESET" || e?.code === "ETIMEDOUT";
                if (transient && attempt < opts.retries) {
                    const delay = jitter(opts.backoffMs * Math.pow(2, attempt), opts.backoffJitter);
                    log?.("retry net", { attempt, delay, error: String(e?.message ?? e) });
                    await sleep(delay);
                    continue;
                }
                throw e;
            }
        }
        // недостижимо
        throw new Error("unreachable");
    }

    async function fetchBlock(height: number): Promise<any> {
        const j = await getJson<any>("/block", { height });
        return j.result ?? j;
    }

    async function fetchBlockResults(height: number): Promise<any> {
        const j = await getJson<any>("/block_results", { height });
        return j.result ?? j;
    }

    async function fetchStatus(): Promise<any> {
        const j = await getJson<any>("/status");
        return j.result ?? j;
    }

    return { getJson, fetchBlock, fetchBlockResults, fetchStatus };
}

// Удобная обёртка поверх конфига шага 1
export function createRpcClientFromConfig(cfg: {
    rpcUrl: string;
    timeoutMs: number;
    retries: number;
    backoffMs: number;
    backoffJitter: number;
    rps: number;
    logLevel?: "info" | "debug";
}) {
    const log = cfg.logLevel === "debug" ? (msg: string, extra?: unknown) => console.log(`[rpc] ${msg}`, extra ?? "") : undefined;
    return createRpcClient({
        baseUrl: cfg.rpcUrl,
        timeoutMs: cfg.timeoutMs,
        retries: cfg.retries,
        backoffMs: cfg.backoffMs,
        backoffJitter: cfg.backoffJitter,
        rps: cfg.rps,
        log,
    });
}