import HTTPErrorResponse from "../Types/API/HTTPErrorResponse";
import HTTPMethod from "../Types/API/HTTPMethod";
import HTTPResponse from "../Types/API/HTTPResponse";
import Protocol from "../Types/API/Protocol";
import Route from "../Types/API/Route";
import URL from "../Types/API/URL";
import APIException from "../Types/Exception/ApiException";
import { RequestFailedPhase, } from "../Types/Probe/RequestFailedDetails";
import axios from "axios";
import Sleep from "../Types/Sleep";
export default class API {
    get protocol() {
        return this._protocol;
    }
    set protocol(v) {
        this._protocol = v;
    }
    get hostname() {
        return this._hostname;
    }
    set hostname(v) {
        this._hostname = v;
    }
    get baseRoute() {
        return this._baseRoute;
    }
    set baseRoute(v) {
        this._baseRoute = v;
    }
    constructor(protocol, hostname, baseRoute) {
        this._protocol = Protocol.HTTPS;
        this.protocol = protocol;
        this.hostname = hostname;
        if (baseRoute) {
            this.baseRoute = baseRoute;
        }
        else {
            this.baseRoute = new Route("/");
        }
    }
    static async tryRefreshAuth(_context) {
        return false;
    }
    async get(options) {
        return await API.get(options);
    }
    async delete(options) {
        return await API.delete(options);
    }
    async head(options) {
        return await API.head(options);
    }
    async put(options) {
        return await API.put(options);
    }
    async patch(options) {
        return await API.patch(options);
    }
    static handleError(error) {
        return error;
    }
    static async onResponseSuccessHeaders(headers) {
        return Promise.resolve(headers);
    }
    static getDefaultHeaders(_props) {
        const defaultHeaders = {
            "Access-Control-Allow-Origin": "*",
            Accept: "application/json",
            "Content-Type": "application/json;charset=UTF-8",
        };
        return defaultHeaders;
    }
    static getHeaders(headers) {
        let defaultHeaders = this.getDefaultHeaders();
        if (headers) {
            defaultHeaders = Object.assign(Object.assign({}, defaultHeaders), headers);
        }
        return defaultHeaders;
    }
    static async get(options) {
        const { url, data, headers, params, options: newOptions } = options;
        if (!url) {
            throw new APIException("URL is required for static method");
        }
        const fetchOptions = Object.assign(Object.assign(Object.assign(Object.assign({ method: HTTPMethod.GET, url }, (data && { data })), (headers && { headers })), (params && { params })), (newOptions && { options: newOptions }));
        return await this.fetch(fetchOptions);
    }
    static async delete(options) {
        const { url, data, headers, params, options: newOptions } = options;
        if (!url) {
            throw new APIException("URL is required for static method");
        }
        const fetchOptions = Object.assign(Object.assign(Object.assign(Object.assign({ method: HTTPMethod.DELETE, url }, (data && { data })), (headers && { headers })), (params && { params })), (newOptions && { options: newOptions }));
        return await this.fetch(fetchOptions);
    }
    static async head(options) {
        const { url, data, headers, params, options: newOptions } = options;
        if (!url) {
            throw new APIException("URL is required for static method");
        }
        const fetchOptions = Object.assign(Object.assign(Object.assign(Object.assign({ method: HTTPMethod.HEAD, url }, (data && { data })), (headers && { headers })), (params && { params })), (newOptions && { options: newOptions }));
        return await this.fetch(fetchOptions);
    }
    static async put(options) {
        const { url, data, headers, params, options: newOptions } = options;
        if (!url) {
            throw new APIException("URL is required for static method");
        }
        const fetchOptions = Object.assign(Object.assign(Object.assign(Object.assign({ method: HTTPMethod.PUT, url }, (data && { data })), (headers && { headers })), (params && { params })), (newOptions && { options: newOptions }));
        return await this.fetch(fetchOptions);
    }
    static async patch(options) {
        const { url, data, headers, params, options: newOptions } = options;
        if (!url) {
            throw new APIException("URL is required for static method");
        }
        const fetchOptions = Object.assign(Object.assign(Object.assign(Object.assign({ method: HTTPMethod.PATCH, url }, (data && { data })), (headers && { headers })), (params && { params })), (newOptions && { options: newOptions }));
        return await this.fetch(fetchOptions);
    }
    static async post(options) {
        const { url, data, headers, params, options: newOptions } = options;
        if (!url) {
            throw new APIException("URL is required for static method");
        }
        const fetchOptions = Object.assign(Object.assign(Object.assign(Object.assign({ method: HTTPMethod.POST, url }, (data && { data })), (headers && { headers })), (params && { params })), (newOptions && { options: newOptions }));
        return await this.fetch(fetchOptions);
    }
    static async fetch(options) {
        const { method, url, data, headers, params, options: newOptions } = options;
        if (!url) {
            throw new APIException("URL is required for static method");
        }
        return await this.fetchInternal(method, url, data, headers, params, newOptions);
    }
    static async fetchInternal(method, url, data, headers, params, options) {
        var _a;
        const apiHeaders = this.getHeaders(headers);
        if (params) {
            url.addQueryParams(params);
        }
        try {
            const finalHeaders = Object.assign(Object.assign({}, apiHeaders), headers);
            let finalBody = data;
            // if content-type is form-url-encoded, then stringify the data
            if (finalHeaders["Content-Type"] === "application/x-www-form-urlencoded" &&
                data) {
                finalBody = new URLSearchParams(data);
            }
            let currentRetry = 0;
            const maxRetries = (options === null || options === void 0 ? void 0 : options.retries) || 0;
            const exponentialBackoff = (options === null || options === void 0 ? void 0 : options.exponentialBackoff) || false;
            let result = null;
            while (currentRetry <= maxRetries) {
                currentRetry++;
                try {
                    const axiosOptions = {
                        method: method,
                        url: url.toString(),
                        headers: finalHeaders,
                        data: finalBody,
                    };
                    if (options === null || options === void 0 ? void 0 : options.timeout) {
                        axiosOptions.timeout = options.timeout;
                    }
                    if (options === null || options === void 0 ? void 0 : options.doNotFollowRedirects) {
                        axiosOptions.maxRedirects = 0;
                    }
                    // Attach proxy agents per request if provided (avoids global side-effects)
                    if (options === null || options === void 0 ? void 0 : options.httpAgent) {
                        axiosOptions.httpAgent = options.httpAgent;
                    }
                    if (options === null || options === void 0 ? void 0 : options.httpsAgent) {
                        axiosOptions.httpsAgent =
                            options.httpsAgent;
                    }
                    if (options === null || options === void 0 ? void 0 : options.onUploadProgress) {
                        axiosOptions.onUploadProgress = options.onUploadProgress;
                    }
                    result = await axios(axiosOptions);
                    break;
                }
                catch (e) {
                    if (currentRetry <= maxRetries) {
                        if (exponentialBackoff) {
                            await Sleep.sleep(2 ** currentRetry * 1000);
                        }
                        continue;
                    }
                    else {
                        throw e;
                    }
                }
            }
            if (!result) {
                throw new APIException("No response received from server.");
            }
            result.headers = await this.onResponseSuccessHeaders(result.headers);
            const response = new HTTPResponse(result.status, result.data, result.headers);
            return response;
        }
        catch (e) {
            const error = e;
            if (!axios.isAxiosError(error)) {
                throw new APIException(error.message);
            }
            const errorResponse = this.getErrorResponse(error);
            if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401 &&
                !(options === null || options === void 0 ? void 0 : options.skipAuthRefresh) &&
                !(options === null || options === void 0 ? void 0 : options.hasAttemptedAuthRefresh)) {
                const retryUrl = URL.fromString(url.toString());
                const requestContext = {
                    method,
                    url: retryUrl,
                };
                if (data) {
                    requestContext.data = data;
                }
                if (headers) {
                    requestContext.headers = headers;
                }
                if (params) {
                    requestContext.params = params;
                }
                if (options) {
                    requestContext.options = options;
                }
                const refreshed = await this.tryRefreshAuth({
                    error: errorResponse,
                    request: requestContext,
                });
                if (refreshed) {
                    const nextOptions = Object.assign(Object.assign({}, (options || {})), { hasAttemptedAuthRefresh: true });
                    return await this.fetchInternal(method, retryUrl, data, headers, params, nextOptions);
                }
            }
            this.handleError(errorResponse);
            return errorResponse;
        }
    }
    static getErrorResponse(error) {
        var _a;
        if (error.response) {
            return new HTTPErrorResponse(error.response.status, error.response.data, error.response.headers);
        }
        // get url from error
        const url = ((_a = error === null || error === void 0 ? void 0 : error.config) === null || _a === void 0 ? void 0 : _a.url) || "";
        // Get a meaningful error message, avoiding generic "Error" strings
        let errorMessage = error.message || "";
        // If error message is empty or just "Error", try to get more details from the error
        if (!errorMessage ||
            errorMessage.toLowerCase() === "error" ||
            errorMessage.trim() === "") {
            // Check for common axios error codes
            if (error.code) {
                switch (error.code) {
                    case "ECONNREFUSED":
                        errorMessage = "Connection refused";
                        break;
                    case "ECONNRESET":
                        errorMessage = "Connection reset";
                        break;
                    case "ETIMEDOUT":
                        errorMessage = "Connection timed out";
                        break;
                    case "ENOTFOUND":
                        errorMessage = "Host not found";
                        break;
                    case "ECONNABORTED":
                        errorMessage = "Connection aborted";
                        break;
                    case "ERR_NETWORK":
                        errorMessage = "Network error";
                        break;
                    case "ERR_BAD_REQUEST":
                        errorMessage = "Bad request";
                        break;
                    case "ERR_BAD_RESPONSE":
                        errorMessage = "Bad response from server";
                        break;
                    default:
                        errorMessage = error.code || "Unknown error";
                }
            }
            else {
                errorMessage = "Request failed";
            }
        }
        throw new APIException(`Request failed to ${url}. ${errorMessage}`, error);
    }
    static getFriendlyErrorMessage(error) {
        var _a, _b, _c, _d;
        let errorString = error.message || error.toString();
        if (error instanceof APIException) {
            // Get the nested error message, but avoid duplicating or adding empty/generic messages
            let nestedErrorMessage = "";
            if (error.error) {
                // Get the error message, avoiding generic "Error" or empty strings
                const errMsg = error.error.message || "";
                const errStr = error.error.toString() || "";
                // Check if the error message is meaningful (not just "Error" or empty)
                if (errMsg &&
                    errMsg.trim().toLowerCase() !== "error" &&
                    errMsg.trim() !== "") {
                    nestedErrorMessage = errMsg;
                }
                else if (errStr &&
                    errStr.trim().toLowerCase() !== "error" &&
                    errStr.trim().toLowerCase() !== "error:" &&
                    errStr.trim() !== "" &&
                    !errStr.toLowerCase().startsWith("error:")) {
                    nestedErrorMessage = errStr;
                }
            }
            // Only append nested error if it's meaningful and not already in the main message
            if (nestedErrorMessage && !((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes(nestedErrorMessage))) {
                errorString = `${(_b = error.message) === null || _b === void 0 ? void 0 : _b.toString()} ${nestedErrorMessage}`;
            }
            else {
                errorString = ((_c = error.message) === null || _c === void 0 ? void 0 : _c.toString()) || "";
            }
        }
        // Handle AggregateError by extracting the underlying error messages
        if (error &&
            error.name === "AggregateError" &&
            error.errors) {
            const aggregateErrors = error.errors;
            const errorMessages = aggregateErrors
                .map((err) => {
                return err.message || err.toString();
            })
                .filter((msg) => {
                return msg && msg.trim().length > 0;
            });
            if (errorMessages.length > 0) {
                errorString = errorMessages.join("; ");
            }
        }
        const status = (_d = error === null || error === void 0 ? void 0 : error.response) === null || _d === void 0 ? void 0 : _d.status;
        const lowerErr = errorString.toLocaleLowerCase();
        if (status !== 400 && lowerErr.includes("network error")) {
            return "Network Error.";
        }
        if (status !== 400 && lowerErr.includes("timeout")) {
            return "Timeout Error.";
        }
        if (status !== 400 && lowerErr.includes("request aborted")) {
            return "Request Aborted.";
        }
        if (status !== 400 && lowerErr.includes("canceled")) {
            return "Request Canceled.";
        }
        if (status !== 400 && lowerErr.includes("connection refused")) {
            return "Connection Refused.";
        }
        if (status !== 400 && lowerErr.includes("connection reset")) {
            return "Connection Reset.";
        }
        if (status !== 400 && lowerErr.includes("connection closed")) {
            return "Connection Closed.";
        }
        if (status !== 400 && lowerErr.includes("connection failed")) {
            return "Connection Failed.";
        }
        if (status !== 400 && lowerErr.includes("enotfound")) {
            return "Cannot Find Host.";
        }
        if (status !== 400 && lowerErr.includes("econnreset")) {
            return "Connection Reset.";
        }
        if (status !== 400 && lowerErr.includes("econnrefused")) {
            return "Connection Refused.";
        }
        if (status !== 400 && lowerErr.includes("econnaborted")) {
            return "Connection Aborted.";
        }
        if (status !== 400 && lowerErr.includes("certificate has expired")) {
            return "SSL Certificate Expired.";
        }
        if (errorString
            .toLocaleLowerCase()
            .includes("certificate signed by unknown authority")) {
            return "SSL Certificate Signed By Unknown Authority.";
        }
        if (errorString.toLocaleLowerCase().includes("self-signed certificate")) {
            return "Self Signed Certificate.";
        }
        return errorString;
    }
    /**
     * Extracts detailed error information from an axios error or generic error.
     * This provides more context about where and why a request failed.
     */
    static getRequestFailedDetails(error) {
        const axiosError = axios.isAxiosError(error)
            ? error
            : null;
        const errorCode = axiosError === null || axiosError === void 0 ? void 0 : axiosError.code;
        const rawErrorMessage = (error === null || error === void 0 ? void 0 : error.message) || String(error) || "Unknown error";
        // Helper to determine the phase and description based on error code/message
        const lowerMessage = rawErrorMessage.toLowerCase();
        // DNS resolution failures
        if (errorCode === "ENOTFOUND" || lowerMessage.includes("enotfound")) {
            return {
                failedPhase: RequestFailedPhase.DNSResolution,
                errorCode: errorCode || "ENOTFOUND",
                errorDescription: "DNS resolution failed. The hostname could not be resolved to an IP address. Please verify the hostname is correct and that DNS is working properly.",
                rawErrorMessage,
            };
        }
        // Connection refused
        if (errorCode === "ECONNREFUSED" || lowerMessage.includes("econnrefused")) {
            return {
                failedPhase: RequestFailedPhase.TCPConnection,
                errorCode: errorCode || "ECONNREFUSED",
                errorDescription: "Connection refused. The server actively refused the connection. This usually means no service is listening on the specified port, or a firewall is blocking the connection.",
                rawErrorMessage,
            };
        }
        // Connection reset
        if (errorCode === "ECONNRESET" ||
            lowerMessage.includes("econnreset") ||
            lowerMessage.includes("connection reset")) {
            return {
                failedPhase: RequestFailedPhase.TCPConnection,
                errorCode: errorCode || "ECONNRESET",
                errorDescription: "Connection reset. The connection was forcibly closed by the server or a network device. This can happen due to server restarts, load balancer timeouts, or network issues.",
                rawErrorMessage,
            };
        }
        // Connection aborted
        if (errorCode === "ECONNABORTED" ||
            lowerMessage.includes("econnaborted") ||
            lowerMessage.includes("connection aborted")) {
            return {
                failedPhase: RequestFailedPhase.RequestAborted,
                errorCode: errorCode || "ECONNABORTED",
                errorDescription: "Connection aborted. The request was aborted, possibly due to a timeout or the connection being closed unexpectedly.",
                rawErrorMessage,
            };
        }
        // Timeout errors
        if (errorCode === "ETIMEDOUT" ||
            errorCode === "ESOCKETTIMEDOUT" ||
            lowerMessage.includes("timeout") ||
            lowerMessage.includes("exceeded")) {
            return {
                failedPhase: RequestFailedPhase.RequestTimeout,
                errorCode: errorCode || "TIMEOUT",
                errorDescription: "Request timed out. The server did not respond within the allowed time. This could be due to network latency, server overload, or the server being unresponsive.",
                rawErrorMessage,
            };
        }
        // SSL/TLS Certificate errors
        if (lowerMessage.includes("certificate has expired") ||
            lowerMessage.includes("cert_has_expired")) {
            return {
                failedPhase: RequestFailedPhase.CertificateError,
                errorCode: "CERT_HAS_EXPIRED",
                errorDescription: "SSL certificate has expired. The server's SSL certificate is no longer valid. The certificate needs to be renewed.",
                rawErrorMessage,
            };
        }
        if (lowerMessage.includes("self-signed certificate") ||
            lowerMessage.includes("self signed certificate") ||
            lowerMessage.includes("depth_zero_self_signed_cert")) {
            return {
                failedPhase: RequestFailedPhase.CertificateError,
                errorCode: "SELF_SIGNED_CERT",
                errorDescription: "Self-signed certificate detected. The server is using a self-signed SSL certificate that is not trusted by default. Consider using a certificate from a trusted Certificate Authority.",
                rawErrorMessage,
            };
        }
        if (lowerMessage.includes("certificate signed by unknown authority") ||
            lowerMessage.includes("unable_to_verify_leaf_signature") ||
            lowerMessage.includes("unable to verify")) {
            return {
                failedPhase: RequestFailedPhase.CertificateError,
                errorCode: "CERT_UNKNOWN_AUTHORITY",
                errorDescription: "SSL certificate signed by unknown authority. The certificate chain could not be verified against known Certificate Authorities.",
                rawErrorMessage,
            };
        }
        if (lowerMessage.includes("ssl") ||
            lowerMessage.includes("tls") ||
            lowerMessage.includes("certificate") ||
            lowerMessage.includes("handshake")) {
            return {
                failedPhase: RequestFailedPhase.TLSHandshake,
                errorCode: errorCode || "TLS_ERROR",
                errorDescription: "TLS/SSL handshake failed. There was an error establishing a secure connection to the server. This could be due to certificate issues, protocol mismatches, or the server not supporting HTTPS.",
                rawErrorMessage,
            };
        }
        // Network errors
        if (lowerMessage.includes("network error") || errorCode === "ERR_NETWORK") {
            return {
                failedPhase: RequestFailedPhase.NetworkError,
                errorCode: errorCode || "NETWORK_ERROR",
                errorDescription: "Network error occurred. Unable to reach the server due to network connectivity issues. Please check your network connection and ensure the server is accessible.",
                rawErrorMessage,
            };
        }
        // Host unreachable
        if (errorCode === "EHOSTUNREACH" ||
            lowerMessage.includes("host unreachable")) {
            return {
                failedPhase: RequestFailedPhase.TCPConnection,
                errorCode: errorCode || "EHOSTUNREACH",
                errorDescription: "Host unreachable. The network path to the server could not be found. This may be due to routing issues or the host being offline.",
                rawErrorMessage,
            };
        }
        // Network unreachable
        if (errorCode === "ENETUNREACH" ||
            lowerMessage.includes("network unreachable")) {
            return {
                failedPhase: RequestFailedPhase.TCPConnection,
                errorCode: errorCode || "ENETUNREACH",
                errorDescription: "Network unreachable. There is no route to the network where the server resides. This is typically a routing or connectivity issue.",
                rawErrorMessage,
            };
        }
        // Server responded with error status
        if (axiosError === null || axiosError === void 0 ? void 0 : axiosError.response) {
            const status = axiosError.response.status;
            let description = `Server responded with HTTP status ${status}.`;
            if (status >= 500) {
                description += " This indicates a server-side error.";
            }
            else if (status === 404) {
                description += " The requested resource was not found.";
            }
            else if (status === 403) {
                description += " Access to the resource is forbidden.";
            }
            else if (status === 401) {
                description += " Authentication is required or has failed.";
            }
            else if (status === 400) {
                description += " The request was malformed or invalid.";
            }
            else if (status >= 400) {
                description += " This indicates a client-side error.";
            }
            return {
                failedPhase: RequestFailedPhase.ServerResponse,
                errorCode: `HTTP_${status}`,
                errorDescription: description,
                rawErrorMessage,
            };
        }
        // Request was made but no response received
        if ((axiosError === null || axiosError === void 0 ? void 0 : axiosError.request) && !(axiosError === null || axiosError === void 0 ? void 0 : axiosError.response)) {
            return {
                failedPhase: RequestFailedPhase.NetworkError,
                errorCode: errorCode || "NO_RESPONSE",
                errorDescription: "No response received from the server. The request was sent but no response was returned. This could indicate the server is down, unreachable, or the request timed out.",
                rawErrorMessage,
            };
        }
        // Default/Unknown error
        return {
            failedPhase: RequestFailedPhase.Unknown,
            errorCode: errorCode,
            errorDescription: `Request failed: ${API.getFriendlyErrorMessage(error)}`,
            rawErrorMessage,
        };
    }
}
//# sourceMappingURL=API.js.map