import envPaths from "env-paths";
import { exec } from "node:child_process";
import fs from "node:fs/promises";
import http from "node:http";
import https from "node:https";
import os from "node:os";
import { URL } from "node:url";
import path from "pathe";
import pkg from "../../package.json" with { type: "json" };
import { Scope } from "../scope.js";
import { parseOption } from "./cli-args.js";
import { isTransientNetworkError } from "./is-transient-error.js";
import { logger } from "./logger.js";
import { memoize } from "./memoize.js";
const ALCHEMY_DIR = path.join(os.homedir(), ".alchemy");
const ID_PATH = path.join(ALCHEMY_DIR, "id");
const ID_PATH_LEGACY = path.join(envPaths("alchemy", { suffix: "" }).config, "id");
const DISABLED_PATH = path.join(ALCHEMY_DIR, "telemetry-disabled");
export const TELEMETRY_DISABLED = !!process.env.ALCHEMY_TELEMETRY_DISABLED || !!process.env.DO_NOT_TRACK;
export const TELEMETRY_API_URL = process.env.ALCHEMY_TELEMETRY_API_URL ?? "https://telemetry.alchemy.run";
export const SUPPRESS_TELEMETRY_ERRORS = !!process.env.ALCHEMY_TELEMETRY_SUPPRESS_ERRORS;
export const getGlobalTelemetryDisabled = memoize(async () => {
    const disabled = await fs
        .readFile(DISABLED_PATH, "utf-8")
        .then((data) => data.trim() === "true")
        .catch(() => false);
    return disabled;
});
export async function setGlobalTelemetryDisabled() {
    await fs.mkdir(ALCHEMY_DIR, { recursive: true });
    await fs.writeFile(DISABLED_PATH, "true");
}
export async function setGlobalTelemetryEnabled() {
    await fs.rm(DISABLED_PATH, { force: true });
}
async function getOrCreateUserId() {
    async function readUserId(path) {
        try {
            return (await fs.readFile(path, "utf-8")).trim();
        }
        catch {
            return null;
        }
    }
    const id = await readUserId(ID_PATH);
    if (id) {
        return id;
    }
    const legacyId = await readUserId(ID_PATH_LEGACY);
    try {
        const id = legacyId ?? crypto.randomUUID();
        await fs.mkdir(ALCHEMY_DIR, { recursive: true });
        await fs.writeFile(ID_PATH, id);
        if (!legacyId) {
            console.warn([
                "Attention: To help improve Alchemy, we collect anonymous usage, performance, and error data.",
                "You can opt out by setting the ALCHEMY_TELEMETRY_DISABLED or DO_NOT_TRACK environment variable to a truthy value.",
            ].join("\n"));
        }
        return id;
    }
    catch {
        return null;
    }
}
async function getRootCommitHash() {
    return new Promise((resolve) => {
        const command = process.platform === "win32"
            ? `git rev-list --max-parents=0 HEAD | ForEach-Object { if (-not (git cat-file -p $_ | Select-String "^parent ")) { $_ } }`
            : `git rev-list --max-parents=0 HEAD | xargs -r -I{} sh -c 'git cat-file -p {} | grep -q "^parent " || echo {}'`;
        exec(command, (err, stdout) => {
            if (err) {
                resolve(null);
                return;
            }
            resolve(stdout.trim());
        });
    });
}
async function getGitOriginUrlHash() {
    return new Promise((resolve) => {
        exec("git config --get remote.origin.url", (err, stdout) => {
            if (err) {
                resolve(null);
                return;
            }
            resolve(hashString(stdout.trim()));
        });
    });
}
async function getBranchName() {
    return new Promise((resolve) => {
        exec("git rev-parse --abbrev-ref HEAD", (err, stdout) => {
            if (err) {
                resolve(null);
                return;
            }
            resolve(stdout.trim());
        });
    });
}
function getRuntime() {
    if (globalThis.Bun)
        return { name: "bun", version: Bun.version };
    // @ts-expect-error
    if (globalThis.Deno)
        // @ts-expect-error
        return { name: "deno", version: Deno.version?.deno ?? null };
    // @ts-expect-error
    if (globalThis.EdgeRuntime)
        return { name: "workerd", version: null };
    if (globalThis.process?.versions?.node)
        return { name: "node", version: process.versions.node };
    return { name: null, version: null };
}
const PROVIDERS = [
    { env: "GITHUB_ACTIONS", provider: "GitHub Actions", isCI: true },
    { env: "GITLAB_CI", provider: "GitLab CI", isCI: true },
    { env: "CIRCLECI", provider: "CircleCI", isCI: true },
    { env: "JENKINS_URL", provider: "Jenkins", isCI: true },
    { env: "TRAVIS", provider: "Travis CI", isCI: true },
    { env: "NOW_BUILDER", provider: "Vercel", isCI: true },
    { env: "VERCEL", provider: "Vercel", isCI: false },
];
function getEnvironment() {
    for (const provider of PROVIDERS) {
        if (process.env[provider.env]) {
            return {
                provider: provider.provider,
                isCI: provider.isCI,
            };
        }
    }
    return {
        provider: null,
        isCI: !!process.env.CI,
    };
}
export const collectData = memoize(async () => {
    const [userId, rootCommitHash, gitOriginUrlHash, branchHash, runtime, environment,] = await Promise.all([
        getOrCreateUserId(),
        getRootCommitHash(),
        getGitOriginUrlHash(),
        getBranchName().then(hashString),
        getRuntime(),
        getEnvironment(),
    ]);
    const sessionId = parseOption("--telemetry-session-id") ??
        process.env.ALCHEMY_TELEMETRY_SESSION_ID ??
        //@ts-expect-error
        globalThis.ALCHEMY_TELEMETRY_SESSION_ID ??
        crypto.randomUUID();
    const referrer = parseOption("--telemetry-ref") ??
        process.env.ALCHEMY_TELEMETRY_REF ??
        //@ts-expect-error
        globalThis.ALCHEMY_TELEMETRY_REF ??
        "";
    return {
        userId: userId ?? "",
        sessionId,
        referrer,
        platform: os.platform(),
        osVersion: os.release(),
        arch: os.arch(),
        cpus: os.cpus().length,
        memory: Math.round(os.totalmem() / 1024 / 1024),
        rootCommitHash: rootCommitHash ?? "",
        gitOriginUrl: gitOriginUrlHash ?? "",
        gitBranchHash: branchHash ?? "",
        runtime: runtime.name ?? "",
        runtimeVersion: runtime.version ?? "",
        ciProvider: environment.provider ?? "",
        isCI: environment.isCI,
        alchemyVersion: pkg.version,
    };
});
async function isTelemetryDisabled() {
    return (Scope.getScope()?.noTrack ||
        TELEMETRY_DISABLED ||
        (await getGlobalTelemetryDisabled()));
}
export async function createAndSendEvent(data, error) {
    if (await isTelemetryDisabled()) {
        return;
    }
    const maxRetries = 3;
    let telemetryErrors = [];
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            const eventData = {
                ...data,
                ...("duration" in data
                    ? { duration: Math.round(data.duration * 1000) }
                    : {}),
                ...(await collectData()),
                ...serializeError(error),
                telemetryErrors: JSON.stringify(telemetryErrors.map((e) => serializeError(e))),
            };
            await fetchNoResponse(TELEMETRY_API_URL, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(eventData),
            });
            return;
        }
        catch (error) {
            telemetryErrors.push(error);
            const shouldRetry = isTransientNetworkError(error) || isTransientNetworkError(error.cause);
            if (!shouldRetry || attempt === maxRetries - 1) {
                if (!SUPPRESS_TELEMETRY_ERRORS) {
                    logger.warn("Failed to send telemetry event:", error);
                }
                return;
            }
        }
    }
}
async function hashString(input) {
    if (input == null) {
        return null;
    }
    const data = new TextEncoder().encode(input);
    const hashBuffer = await crypto.subtle.digest("SHA-256", data);
    return Array.from(new Uint8Array(hashBuffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
function serializeError(error) {
    if (error instanceof Error) {
        return {
            errorTag: error.name ?? "",
            errorMessage: error.message?.replaceAll(os.homedir(), "~") ?? "", // redact home directory
            errorStack: error.stack?.replaceAll(os.homedir(), "~") ?? "",
        };
    }
    return {
        errorTag: "",
        errorMessage: "",
        errorStack: "",
    };
}
async function fetchNoResponse(url, options = {}) {
    const parsedUrl = new URL(url);
    const isHttps = parsedUrl.protocol === "https:";
    const client = isHttps ? https : http;
    const body = options.body || "";
    const headers = {
        ...options.headers,
        "Content-Length": Buffer.byteLength(body),
    };
    const requestOptions = {
        hostname: parsedUrl.hostname,
        port: parsedUrl.port || (isHttps ? 443 : 80),
        path: parsedUrl.pathname + parsedUrl.search,
        method: options.method || "GET",
        headers,
        signal: AbortSignal.timeout(options?.timeout ?? 3000),
    };
    await new Promise((resolve, reject) => {
        const req = client.request(requestOptions);
        req.on("error", (error) => {
            if (error.name === "AbortError" && !SUPPRESS_TELEMETRY_ERRORS) {
                logger.warn("Telemetry request timed out");
            }
            reject(error);
        });
        if (body) {
            req.write(body);
        }
        req.once("finish", () => {
            resolve(void 0);
        });
        req.end();
    });
}
//# sourceMappingURL=telemetry.js.map