import { poll } from "../util/poll.js";
import { extractToken } from "./api.js";
/**
 * Base libraries that PlanetScale always includes in shared_preload_libraries.
 * These are managed internally and should not be toggled by users.
 * @internal
 */
const BASE_LIBRARIES = ["pg_pscale_utils", "pg_readonly", "pgextwlist"];
function mapPgCron(config) {
    return {
        library: "pg_cron",
        params: {
            "cron.database_name": config.databaseName ?? "postgres",
            "cron.launch_active_jobs": config.launchActiveJobs ?? "on",
            "cron.log_min_messages": config.logMinMessages ?? "warning",
            "cron.log_run": config.logRun ?? "on",
            "cron.log_statement": config.logStatement ?? "on",
            "cron.max_running_jobs": String(config.maxRunningJobs ?? 1),
        },
    };
}
function mapPgDuckdb(config) {
    return {
        library: "pg_duckdb",
        params: {
            "duckdb.postgres_role": config.postgresRole ?? "pscale_superuser",
            "duckdb.memory_limit": config.memoryLimit ?? "0",
        },
    };
}
function mapPgHintPlan(config) {
    return {
        library: "pg_hint_plan",
        params: {
            "pg_hint_plan.enable_hint": config.enableHint ?? "on",
            "pg_hint_plan.enable_hint_table": config.enableHintTable ?? "off",
            "pg_hint_plan.parse_messages": config.parseMessages ?? "info",
            "pg_hint_plan.debug_print": config.debugPrint ?? "off",
            "pg_hint_plan.message_level": config.messageLevel ?? "info",
        },
    };
}
function mapPgPartmanBgw(config) {
    return {
        library: "pg_partman_bgw",
        params: {
            "pg_partman_bgw.interval": String(config.interval ?? 3600),
            "pg_partman_bgw.dbname": config.dbname ?? "postgres",
            "pg_partman_bgw.role": config.role ?? "postgres",
            "pg_partman_bgw.analyze": config.analyze ?? "off",
            "pg_partman_bgw.jobmon": config.jobmon ?? "on",
        },
    };
}
function mapPgSqueeze(config) {
    return {
        library: "pg_squeeze",
        params: {
            "squeeze.max_xlock_time": String(config.maxXlockTime ?? 0),
            "squeeze.worker_autostart": config.workerAutostart ?? "postgres",
            "squeeze.workers_per_database": String(config.workersPerDatabase ?? 1),
        },
    };
}
function mapPgStatStatements(config) {
    return {
        library: "pg_stat_statements",
        params: {
            "pg_stat_statements.max": String(config.max ?? 5000),
            "pg_stat_statements.track": config.track ?? "top",
            "pg_stat_statements.track_utility": config.trackUtility ?? "on",
            "pg_stat_statements.track_planning": config.trackPlanning ?? "off",
            "pg_stat_statements.save": config.save ?? "on",
        },
    };
}
function mapTimescaledb(config) {
    return {
        library: "timescaledb",
        params: {
            "timescaledb.bgw_log_level": config.bgwLogLevel ?? "warning",
            "timescaledb.enable_chunk_append": config.enableChunkAppend ?? "on",
            "timescaledb.enable_chunk_skipping": config.enableChunkSkipping ?? "off",
            "timescaledb.enable_constraint_aware_append": config.enableConstraintAwareAppend ?? "on",
            "timescaledb.enable_constraint_exclusion": config.enableConstraintExclusion ?? "on",
            "timescaledb.enable_custom_hashagg": config.enableCustomHashagg ?? "off",
            "timescaledb.enable_deprecation_warnings": config.enableDeprecationWarnings ?? "on",
            "timescaledb.enable_event_triggers": config.enableEventTriggers ?? "off",
            "timescaledb.enable_foreign_key_propagation": config.enableForeignKeyPropagation ?? "on",
            "timescaledb.enable_job_execution_logging": config.enableJobExecutionLogging ?? "off",
            "timescaledb.enable_now_constify": config.enableNowConstify ?? "on",
            "timescaledb.enable_optimizations": config.enableOptimizations ?? "on",
            "timescaledb.enable_ordered_append": config.enableOrderedAppend ?? "on",
            "timescaledb.enable_parallel_chunk_append": config.enableParallelChunkAppend ?? "on",
            "timescaledb.enable_qual_propagation": config.enableQualPropagation ?? "on",
            "timescaledb.enable_runtime_exclusion": config.enableRuntimeExclusion ?? "on",
            "timescaledb.enable_tiered_reads": config.enableTieredReads ?? "on",
            "timescaledb.enable_tss_callbacks": config.enableTssCallbacks ?? "on",
            "timescaledb.max_cached_chunks_per_hypertable": String(config.maxCachedChunksPerHypertable ?? 1024),
            "timescaledb.max_open_chunks_per_insert": String(config.maxOpenChunksPerInsert ?? 1024),
            "timescaledb.restoring": config.restoring ?? "off",
        },
    };
}
function mapVector(config) {
    return {
        library: "vector",
        params: {
            "hnsw.ef_search": String(config.hnswEfSearch ?? 40),
            "hnsw.iterative_scan": config.hnswIterativeScan ?? "off",
            "hnsw.max_scan_tuples": String(config.hnswMaxScanTuples ?? 20000),
            "hnsw.scan_mem_multiplier": String(config.hnswScanMemMultiplier ?? 1),
            "ivfflat.probes": String(config.ivfflatProbes ?? 1),
            "ivfflat.iterative_scan": config.ivfflatIterativeScan ?? "off",
            "ivfflat.max_probes": String(config.ivfflatMaxProbes ?? 32768),
        },
    };
}
function mapPgInsights(config) {
    return {
        library: "pginsights",
        params: {
            "pginsights.raw_queries": config.rawQueries ?? "off",
            "pginsights.normalize_schema_names": config.normalizeSchemaNames ?? "off",
        },
    };
}
/**
 * Converts a DatabaseExtensions object into a map of library name -> pgconf params.
 * Only includes extensions that are present (enabled).
 * @internal
 */
export function extensionsToParamMappings(extensions) {
    const mappings = new Map();
    if (extensions.pgCron !== undefined) {
        const m = mapPgCron(extensions.pgCron);
        mappings.set(m.library, m.params);
    }
    if (extensions.pgDuckdb !== undefined) {
        const m = mapPgDuckdb(extensions.pgDuckdb);
        mappings.set(m.library, m.params);
    }
    if (extensions.pgHintPlan !== undefined) {
        const m = mapPgHintPlan(extensions.pgHintPlan);
        mappings.set(m.library, m.params);
    }
    if (extensions.pgPartmanBgw !== undefined) {
        const m = mapPgPartmanBgw(extensions.pgPartmanBgw);
        mappings.set(m.library, m.params);
    }
    if (extensions.pgSqueeze !== undefined) {
        const m = mapPgSqueeze(extensions.pgSqueeze);
        mappings.set(m.library, m.params);
    }
    if (extensions.pgStatStatements !== undefined) {
        const m = mapPgStatStatements(extensions.pgStatStatements);
        mappings.set(m.library, m.params);
    }
    if (extensions.pgStrict !== undefined) {
        mappings.set("pg_strict", {});
    }
    if (extensions.timescaledb !== undefined) {
        const m = mapTimescaledb(extensions.timescaledb);
        mappings.set(m.library, m.params);
    }
    if (extensions.vector !== undefined) {
        const m = mapVector(extensions.vector);
        mappings.set(m.library, m.params);
    }
    if (extensions.pgInsights !== undefined) {
        const m = mapPgInsights(extensions.pgInsights);
        mappings.set(m.library, m.params);
    }
    return mappings;
}
/**
 * Computes the diff between two extension configurations.
 *
 * @param current - The currently active extensions (or `undefined`/`{}` if none)
 * @param desired - The desired extensions configuration
 * @returns An ExtensionsDiff describing what needs to change
 *
 * @example
 * ```ts
 * const diff = diffExtensions(
 *   { pgCron: { maxRunningJobs: 1 } },
 *   { pgCron: { maxRunningJobs: 5 }, vector: {} },
 * );
 * // diff.changes = [
 * //   { library: "pg_cron", action: "update", params: { "cron.max_running_jobs": "5" } },
 * //   { library: "vector", action: "add", params: {} },
 * // ]
 * ```
 */
export function diffExtensions(current, desired) {
    const currentMappings = extensionsToParamMappings(current ?? {});
    const desiredMappings = extensionsToParamMappings(desired ?? {});
    const changes = [];
    // Find additions and updates
    for (const [library, desiredParams] of desiredMappings) {
        const currentParams = currentMappings.get(library);
        if (!currentParams) {
            // Extension is being added
            changes.push({ library, action: "add", params: desiredParams });
        }
        else {
            // Extension exists — check if params changed
            const paramsChanged = JSON.stringify(sortedEntries(currentParams)) !==
                JSON.stringify(sortedEntries(desiredParams));
            if (paramsChanged) {
                changes.push({ library, action: "update", params: desiredParams });
            }
        }
    }
    // Find removals
    for (const [library] of currentMappings) {
        if (!desiredMappings.has(library)) {
            changes.push({ library, action: "remove", params: {} });
        }
    }
    return {
        changes,
        hasChanges: changes.length > 0,
    };
}
function sortedEntries(obj) {
    return Object.entries(obj).sort(([a], [b]) => a.localeCompare(b));
}
function resolveOptions(opts) {
    const organization = opts.organization ??
        (typeof opts.database !== "string"
            ? opts.database.organization
            : undefined) ??
        process.env.PLANETSCALE_ORGANIZATION ??
        process.env.PLANETSCALE_ORG_ID;
    if (!organization) {
        throw new Error("PlanetScale organization is required. Please set the `organization` property or the `PLANETSCALE_ORGANIZATION` environment variable.");
    }
    const database = typeof opts.database === "string" ? opts.database : opts.database.name;
    const branch = !opts.branch
        ? "main"
        : typeof opts.branch === "string"
            ? opts.branch
            : opts.branch.name;
    const baseUrl = opts.baseUrl ?? "https://api.planetscale.com/v1";
    const token = extractToken(opts);
    return { organization, database, branch, baseUrl, token };
}
/**
 * Builds the multipart/form-data body for a change request that sets extensions.
 *
 * The API expects:
 * - `_method=PATCH`
 * - `queued=1` (for queue phase) or `update=1` (for apply phase)
 * - `parameters[pgconf][shared_preload_libraries][]` repeated for each library
 * - `parameters[pgconf][<param.name>]=<param.value>` for each config param
 *
 * @internal
 */
function buildExtensionFormData(desired, phase) {
    const form = new FormData();
    form.append("_method", "PATCH");
    if (phase === "apply") {
        form.append("update", "1");
        return form;
    }
    // Queue phase — include all libraries and params
    form.append("queued", "1");
    const mappings = extensionsToParamMappings(desired);
    // Always include base libraries
    for (const lib of BASE_LIBRARIES) {
        form.append("parameters[pgconf][shared_preload_libraries][]", lib);
    }
    // Include pginsights in base libraries if it has config, otherwise include as base
    const hasPgInsights = mappings.has("pginsights");
    if (!hasPgInsights) {
        // pginsights is always loaded; if user didn't explicitly configure it, include with defaults off
        form.append("parameters[pgconf][shared_preload_libraries][]", "pginsights");
        form.append("parameters[pgconf][pginsights.raw_queries]", "off");
        form.append("parameters[pgconf][pginsights.normalize_schema_names]", "off");
    }
    // Add user-requested extensions
    for (const [library, params] of mappings) {
        form.append("parameters[pgconf][shared_preload_libraries][]", library);
        for (const [key, value] of Object.entries(params)) {
            form.append(`parameters[pgconf][${key}]`, value);
        }
    }
    return form;
}
/**
 * Updates the PostgreSQL extensions on a PlanetScale database branch.
 *
 * Uses the reverse-engineered two-phase change request API:
 * 1. **Queue** — submits a change request with the desired extension config
 * 2. **Apply** — triggers execution of the queued change
 * 3. **Poll** — waits for the change to complete
 *
 * @param desired - The desired extension configuration. Extensions present are enabled;
 *                  extensions absent are disabled.
 * @param options - Connection and targeting options.
 *
 * @example
 * ## Enable pgvector and pg_cron
 *
 * ```ts
 * await updateExtensions(
 *   {
 *     vector: { hnswEfSearch: 100 },
 *     pgCron: {},
 *   },
 *   {
 *     organization: "my-org",
 *     database: "my-db",
 *     branch: "main",
 *   },
 * );
 * ```
 *
 * @example
 * ## Disable all user extensions
 *
 * ```ts
 * await updateExtensions({}, {
 *   organization: "my-org",
 *   database: "my-db",
 * });
 * ```
 */
export async function updateExtensions(desired, options) {
    const { organization, database, branch, baseUrl, token } = resolveOptions(options);
    const changesUrl = `${baseUrl}/organizations/${organization}/databases/${database}/branches/${branch}/changes`;
    // Wait for any in-flight changes to complete before submitting ours
    await waitForPendingChanges(changesUrl, token);
    // Phase 1: Queue the change
    const queueForm = buildExtensionFormData(desired, "queue");
    const queueResponse = await fetch(changesUrl, {
        method: "POST",
        headers: {
            Authorization: token,
        },
        body: queueForm,
    });
    if (!queueResponse.ok) {
        const errorBody = await queueResponse.text();
        throw new Error(`Failed to queue extension change (${queueResponse.status}): ${errorBody}`);
    }
    // Phase 2: Apply the change
    const applyForm = buildExtensionFormData(desired, "apply");
    const applyResponse = await fetch(changesUrl, {
        method: "POST",
        headers: {
            Authorization: token,
        },
        body: applyForm,
    });
    if (!applyResponse.ok) {
        const errorBody = await applyResponse.text();
        throw new Error(`Failed to apply extension change (${applyResponse.status}): ${errorBody}`);
    }
    // Phase 3: Poll until the change is completed
    await waitForPendingChanges(changesUrl, token);
}
/**
 * Polls the changes endpoint until all changes are completed or canceled.
 * @internal
 */
async function waitForPendingChanges(changesUrl, token) {
    await poll({
        description: "extension changes to complete",
        fn: async () => {
            const response = await fetch(`${changesUrl}?per_page=1`, {
                headers: { Authorization: token },
            });
            if (!response.ok) {
                throw new Error(`Failed to poll change status (${response.status}): ${await response.text()}`);
            }
            const data = (await response.json());
            return data;
        },
        predicate: (result) => {
            if (result.data.length === 0)
                return true;
            const latest = result.data[0];
            return latest.state === "completed" || latest.state === "canceled";
        },
    });
}
//# sourceMappingURL=database-extensions.js.map