import axios from "axios";
import { ZodError } from "zod";
import { TransientError, BusinessError, PermissionError, ValidationError } from "../utils/errors.js";
import { extractReason } from "../utils/errors.js";
/** Default timeout for provider API calls (15 seconds) */
export const API_TIMEOUT_MS = 15_000;
/** Pre-configured axios instance with timeout for all provider API calls */
export const apiClient = axios.create({
    timeout: API_TIMEOUT_MS,
});
/**
 * Sanitize axios response data — whitelist-only approach.
 * Preserves known error message fields, strips everything else
 * to prevent API tokens or sensitive data from leaking via error cause chains.
 */
export function sanitizeResponseData(data) {
    if (data === null || data === undefined)
        return undefined;
    if (typeof data === "string")
        return data;
    if (typeof data !== "object")
        return undefined;
    const d = data;
    const clean = {};
    // Hetzner: { error: { message, code } }
    if (d.error && typeof d.error === "object") {
        const err = d.error;
        const errClean = {};
        if (typeof err.message === "string")
            errClean.message = err.message;
        if (typeof err.code === "string")
            errClean.code = err.code;
        if (Object.keys(errClean).length > 0)
            clean.error = errClean;
    }
    // Vultr: { error: "string" }
    else if (typeof d.error === "string") {
        clean.error = d.error;
    }
    // DigitalOcean: { message: "..." }
    if (typeof d.message === "string")
        clean.message = d.message;
    // Linode: { errors: [{ reason: "..." }] }
    if (Array.isArray(d.errors)) {
        clean.errors = d.errors
            .filter((e) => e !== null && typeof e === "object")
            .map((e) => ({
            ...(typeof e.reason === "string" && { reason: e.reason }),
        }));
    }
    return Object.keys(clean).length > 0 ? clean : undefined;
}
/**
 * Higher-order function for standard provider error handling.
 * Wraps an async operation with stripSensitiveData + consistent error formatting.
 * Use for methods with the standard try/catch pattern. Methods with custom error
 * handling (e.g., uploadSshKey with 409) should NOT use this.
 *
 * @param extractApiMessage - Optional function to extract provider-specific error
 *   messages from axios response data (e.g., Hetzner's `data.error.message`,
 *   Linode's `data.errors[].reason`). When provided and the error is an AxiosError,
 *   the extracted message takes priority over `error.message`.
 */
export async function withProviderErrorHandling(operation, fn, extractApiMessage) {
    try {
        return await fn();
    }
    catch (error) {
        stripSensitiveData(error);
        const errRecord = error;
        let message = error instanceof Error
            ? error.message
            : typeof errRecord?.message === "string"
                ? errRecord.message
                : String(error);
        if (extractApiMessage && axios.isAxiosError(error) && error.response?.data) {
            message = extractApiMessage(error.response.data) || message;
        }
        if (error instanceof ZodError) {
            throw new ValidationError(`Failed to ${operation}: unexpected API response shape — ${error.issues.map((i) => i.message).join(", ")}`, { cause: error });
        }
        const fullMessage = `Failed to ${operation}: ${message}`;
        if (axios.isAxiosError(error)) {
            const status = error.response?.status ?? 0;
            if (status === 401 || status === 403) {
                throw new PermissionError(fullMessage, { cause: error });
            }
            if (status === 404 || status === 409) {
                throw new BusinessError(fullMessage, { cause: error });
            }
            // status >= 500, network error (status === 0), or other non-auth/non-client errors
            throw new TransientError(fullMessage, { cause: error });
        }
        // Non-axios errors — safe default: retry-eligible
        throw new TransientError(fullMessage, { cause: error });
    }
}
/** Assert serverId is safe for API URLs (alphanumeric, hyphens, slashes for Linode image IDs) */
export function assertValidServerId(serverId) {
    if (!/^[a-zA-Z0-9-]+(\/[a-zA-Z0-9.-]+)?$/.test(serverId)) {
        throw new ValidationError(`Invalid server ID format: ${serverId}`, { hint: "Server ID must be alphanumeric with optional hyphens" });
    }
}
export async function uploadSshKeyWithConflict(name, publicKey, config) {
    const headers = {
        Authorization: `Bearer ${config.apiToken}`,
        "Content-Type": "application/json",
    };
    try {
        const body = {
            [config.nameField ?? "name"]: name,
            [config.bodyKeyField]: publicKey,
        };
        const response = await apiClient.post(`${config.baseUrl}${config.createPath}`, body, { headers });
        const key = response.data.ssh_key ?? response.data;
        const id = key.id;
        return config.idToString !== false ? id.toString() : id;
    }
    catch (error) {
        stripSensitiveData(error);
        if (axios.isAxiosError(error) && config.conflictStatuses.includes(error.response?.status ?? 0)) {
            const listResponse = await apiClient.get(`${config.baseUrl}${config.listPath}`, { headers });
            const items = listResponse.data[config.listArrayField] ?? listResponse.data;
            const existing = items.find((k) => k[config.listKeyField].trim() === publicKey.trim());
            if (existing) {
                const id = existing.id;
                return config.idToString !== false ? String(id) : id;
            }
        }
        throw new TransientError(`Failed to upload SSH key: ${extractReason(error)}`, { cause: error });
    }
}
export function stripSensitiveData(error) {
    if (axios.isAxiosError(error)) {
        if (error.config) {
            error.config.headers = undefined;
            error.config.data = undefined;
        }
        error.request = undefined;
        if (error.response) {
            error.response.data = sanitizeResponseData(error.response.data);
            error.response.headers = {};
        }
    }
}
//# sourceMappingURL=base.js.map