"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticsSchema = exports.DIAGNOSTICS_SCHEMA_VERSION = void 0;
exports.redactHomePath = redactHomePath;
exports.collectDiagnostics = collectDiagnostics;
/**
 * Diagnostics collector — gathers a structured snapshot of the user's
 * system, codebase, and wizard session state for attachment to /feedback
 * submissions. Opt-in: callers must obtain explicit user consent before
 * invoking collectDiagnostics().
 *
 * Design:
 * - Deterministic, in-process collection (no LLM). Schema-stable so
 *   downstream analytics can aggregate across submissions.
 * - Every field is individually try/catched so one failing probe doesn't
 *   drop the whole payload.
 * - Home directory paths are stripped from any string field that may
 *   leak them. No file contents, no env var values, no git remotes.
 */
const os_1 = require("os");
const promises_1 = require("fs/promises");
const fs_1 = require("fs");
const zod_1 = require("zod");
const package_manager_detection_js_1 = require("./package-manager-detection.js");
const wizard_session_js_1 = require("./wizard-session.js");
/** Bump when the schema changes in a way downstream consumers care about. */
exports.DIAGNOSTICS_SCHEMA_VERSION = 1;
/** Default budget for the full collection to keep /feedback responsive. */
const DEFAULT_TIMEOUT_MS = 3000;
/** Repo size bucket boundaries (top-level entries in installDir). */
const REPO_SIZE_BUCKETS = [
    { label: '<20', max: 20 },
    { label: '20-100', max: 100 },
    { label: '100-500', max: 500 },
    { label: '500+', max: Number.POSITIVE_INFINITY },
];
/**
 * Build a path to a known project file under installDir.
 * Uses manual concat with a closed set of basenames so no attacker-
 * controlled component can ever appear in the suffix. Trailing slash
 * on installDir is normalized. Node accepts forward slashes on Windows.
 */
function projectPath(installDir, file) {
    const base = installDir.replace(/[/\\]+$/, '');
    return `${base}/${file}`;
}
// ── Schema ─────────────────────────────────────────────────────────────
const SystemSchema = zod_1.z.object({
    os: zod_1.z.string(),
    os_release: zod_1.z.string().nullable(),
    arch: zod_1.z.string(),
    node_version: zod_1.z.string(),
    shell: zod_1.z.string().nullable(),
    term_program: zod_1.z.string().nullable(),
    locale: zod_1.z.string().nullable(),
    wizard_version: zod_1.z.string(),
});
const CodebaseSchema = zod_1.z.object({
    detected_frameworks: zod_1.z.array(zod_1.z.string()),
    package_managers: zod_1.z.array(zod_1.z.string()),
    has_typescript: zod_1.z.boolean(),
    has_package_json: zod_1.z.boolean(),
    amplitude_sdk_packages: zod_1.z.array(zod_1.z.object({
        name: zod_1.z.string(),
        version: zod_1.z.string(),
    })),
    dependency_counts: zod_1.z.object({
        dependencies: zod_1.z.number().int().nonnegative(),
        dev_dependencies: zod_1.z.number().int().nonnegative(),
    }),
    monorepo: zod_1.z.boolean(),
    repo_size_bucket: zod_1.z.string(),
});
const SessionSchema = zod_1.z.object({
    run_phase: zod_1.z.string(),
    integration: zod_1.z.string().nullable(),
    region: zod_1.z.enum(['us', 'eu']).nullable(),
    intro_concluded: zod_1.z.boolean(),
    setup_confirmed: zod_1.z.boolean(),
    detection_complete: zod_1.z.boolean(),
    has_credentials: zod_1.z.boolean(),
});
exports.DiagnosticsSchema = zod_1.z.object({
    schema_version: zod_1.z.literal(exports.DIAGNOSTICS_SCHEMA_VERSION),
    collected_at: zod_1.z.string(),
    system: SystemSchema,
    codebase: CodebaseSchema,
    session: SessionSchema,
    collection_errors: zod_1.z.array(zod_1.z.string()),
});
// ── Redaction ──────────────────────────────────────────────────────────
/**
 * Replace the user's home directory (and /Users/<name> style paths) with
 * a tilde so the payload doesn't leak usernames.
 */
function redactHomePath(input) {
    const home = (0, os_1.homedir)();
    let out = input;
    if (home) {
        out = out.split(home).join('~');
    }
    // Catch /Users/<name>, /home/<name>, and /root style prefixes that slip
    // past homedir() (e.g. paths collected from a different shell context).
    out = out.replace(/(?:\/Users\/|\/home\/)[^/\s:]+/g, (match) => match.split('/').slice(0, 2).join('/') + '/~');
    return out;
}
// ── Helpers ────────────────────────────────────────────────────────────
function bucketize(count) {
    for (const b of REPO_SIZE_BUCKETS) {
        if (count < b.max)
            return b.label;
    }
    return REPO_SIZE_BUCKETS[REPO_SIZE_BUCKETS.length - 1].label;
}
async function projectFileExists(installDir, file) {
    try {
        await (0, promises_1.access)(projectPath(installDir, file), fs_1.constants.R_OK);
        return true;
    }
    catch {
        return false;
    }
}
async function readProjectJson(installDir, file) {
    try {
        const raw = await (0, promises_1.readFile)(projectPath(installDir, file), 'utf8');
        return JSON.parse(raw);
    }
    catch {
        return null;
    }
}
async function isMonorepo(installDir) {
    const markers = [
        'pnpm-workspace.yaml',
        'lerna.json',
        'turbo.json',
        'nx.json',
        'rush.json',
    ];
    for (const marker of markers) {
        if (await projectFileExists(installDir, marker))
            return true;
    }
    // yarn/npm workspaces in package.json
    const pkg = await readProjectJson(installDir, 'package.json');
    return Boolean(pkg?.workspaces);
}
async function countTopLevelEntries(installDir) {
    try {
        const { readdir } = await import('fs/promises');
        const entries = await readdir(installDir, { withFileTypes: true });
        // Skip hidden entries and node_modules to get a "project complexity" signal
        return entries.filter((e) => !e.name.startsWith('.') && e.name !== 'node_modules').length;
    }
    catch {
        return 0;
    }
}
async function hasTypescript(installDir) {
    if (await projectFileExists(installDir, 'tsconfig.json'))
        return true;
    const pkg = await readProjectJson(installDir, 'package.json');
    const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
    return 'typescript' in deps;
}
async function readAmplitudeSdkPackages(installDir) {
    const pkg = await readProjectJson(installDir, 'package.json');
    if (!pkg)
        return [];
    const all = { ...pkg.dependencies, ...pkg.devDependencies };
    return Object.entries(all)
        .filter(([name]) => name.startsWith('@amplitude/') || name === 'amplitude')
        .map(([name, version]) => ({ name, version: String(version) }))
        .sort((a, b) => a.name.localeCompare(b.name));
}
async function dependencyCounts(installDir) {
    const pkg = await readProjectJson(installDir, 'package.json');
    return {
        dependencies: Object.keys(pkg?.dependencies ?? {}).length,
        dev_dependencies: Object.keys(pkg?.devDependencies ?? {}).length,
    };
}
function withTimeout(promise, ms, fallback, onTimeout) {
    let timerId;
    const timeoutPromise = new Promise((resolve) => {
        timerId = setTimeout(() => {
            timerId = undefined;
            onTimeout();
            resolve(fallback);
        }, ms);
    });
    return Promise.race([
        promise.then((result) => {
            if (timerId !== undefined) {
                clearTimeout(timerId);
            }
            return result;
        }),
        timeoutPromise,
    ]);
}
/**
 * Gather a validated diagnostics payload. Never throws — on any failure
 * the offending field is set to its safe default and the error message
 * is pushed onto `collection_errors`.
 */
async function collectDiagnostics(opts) {
    const { session, wizardVersion, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
    const errors = [];
    const track = (label, err) => {
        const msg = err instanceof Error ? err.message : String(err);
        errors.push(`${label}: ${redactHomePath(msg)}`);
    };
    const safe = async (label, fn, fallback) => {
        try {
            return await fn();
        }
        catch (err) {
            track(label, err);
            return fallback;
        }
    };
    // Collection runs under a shared timeout. Individual probes run in
    // parallel so a slow one doesn't serialize the whole collection.
    const collection = Promise.all([
        safe('package_managers', () => (0, package_manager_detection_js_1.detectNodePackageManagers)(session.installDir), {
            detected: [],
            primary: null,
            recommendation: '',
        }),
        safe('has_typescript', () => hasTypescript(session.installDir), false),
        safe('has_package_json', () => projectFileExists(session.installDir, 'package.json'), false),
        safe('amplitude_sdk_packages', () => readAmplitudeSdkPackages(session.installDir), []),
        safe('dependency_counts', () => dependencyCounts(session.installDir), {
            dependencies: 0,
            dev_dependencies: 0,
        }),
        safe('monorepo', () => isMonorepo(session.installDir), false),
        safe('repo_size', () => countTopLevelEntries(session.installDir), 0),
    ]);
    const [pm, hasTs, hasPkg, amplitudePkgs, depCounts, monorepo, topLevelCount] = await withTimeout(collection, timeoutMs, [
        {
            detected: [],
            primary: null,
            recommendation: '',
        },
        false,
        false,
        [],
        { dependencies: 0, dev_dependencies: 0 },
        false,
        0,
    ], () => errors.push(`collection: timed out after ${timeoutMs}ms`));
    const detectedFrameworks = (opts.detectedFrameworks ?? [])
        .filter((f) => f.detected)
        .map((f) => f.integration);
    const payload = {
        schema_version: exports.DIAGNOSTICS_SCHEMA_VERSION,
        collected_at: new Date().toISOString(),
        system: {
            os: (0, os_1.platform)(),
            os_release: safeString(() => `${(0, os_1.type)()} ${(0, os_1.release)()}`),
            arch: (0, os_1.arch)(),
            node_version: process.version,
            shell: safeEnv('SHELL'),
            term_program: safeEnv('TERM_PROGRAM') ?? safeEnv('TERM'),
            locale: safeEnv('LANG') ?? safeEnv('LC_ALL'),
            wizard_version: wizardVersion,
        },
        codebase: {
            detected_frameworks: detectedFrameworks,
            package_managers: pm.detected.map((d) => d.name),
            has_typescript: hasTs,
            has_package_json: hasPkg,
            amplitude_sdk_packages: amplitudePkgs,
            dependency_counts: depCounts,
            monorepo,
            repo_size_bucket: bucketize(topLevelCount),
        },
        session: {
            run_phase: session.runPhase ?? wizard_session_js_1.RunPhase.Idle,
            integration: session.integration ?? null,
            region: session.region ?? null,
            intro_concluded: Boolean(session.introConcluded),
            setup_confirmed: Boolean(session.setupConfirmed),
            detection_complete: Boolean(session.detectionComplete),
            has_credentials: Boolean(session.credentials),
        },
        collection_errors: errors,
    };
    // Validate + strip unknown keys. Throw here would surface a bug in the
    // collector itself, not a runtime failure, so we let it bubble.
    return exports.DiagnosticsSchema.parse(payload);
}
function safeString(fn) {
    try {
        return fn();
    }
    catch {
        return null;
    }
}
function safeEnv(key) {
    const v = process.env[key];
    return v && v.length > 0 ? v : null;
}
