// Copyright (C) 2023-2026  Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const builtin = @import("builtin");

const c = @cImport({
    @cInclude("curl/curl.h");
});

const IS_DEBUG = builtin.mode == .Debug;

pub const Curl = c.CURL;
pub const CurlM = c.CURLM;
pub const CurlCode = c.CURLcode;
pub const CurlMCode = c.CURLMcode;
pub const CurlSList = c.curl_slist;
pub const CurlHeader = c.curl_header;
pub const CurlHttpPost = c.curl_httppost;
pub const CurlSocket = c.curl_socket_t;
pub const CurlBlob = c.curl_blob;
pub const CurlOffT = c.curl_off_t;

pub const CurlDebugFunction = fn (*Curl, CurlInfoType, [*c]u8, usize, *anyopaque) c_int;
pub const CurlHeaderFunction = fn ([*]const u8, usize, usize, *anyopaque) usize;
pub const CurlWriteFunction = fn ([*]const u8, usize, usize, *anyopaque) usize;
pub const curl_writefunc_error: usize = c.CURL_WRITEFUNC_ERROR;
pub const curl_readfunc_pause: usize = c.CURL_READFUNC_PAUSE;
pub const CurlReadFunction = fn ([*]u8, usize, usize, *anyopaque) usize;

pub const CurlSockType = enum(c.curlsocktype) {
    ipcxn = c.CURLSOCKTYPE_IPCXN,
    accept = c.CURLSOCKTYPE_ACCEPT,
};

/// Mirror of curl's struct curl_sockaddr. The addr field is a struct sockaddr
/// inline (not a pointer), so addrlen tells you how many bytes of addr are valid.
pub const CurlSockAddr = extern struct {
    family: c_int,
    socktype: c_int,
    protocol: c_int,
    addrlen: c_uint,
    addr: std.posix.sockaddr,
};

pub const CURL_SOCKET_BAD: c.curl_socket_t = c.CURL_SOCKET_BAD;

pub const FreeCallback = fn (ptr: ?*anyopaque) void;
pub const StrdupCallback = fn (str: [*:0]const u8) ?[*:0]u8;
pub const MallocCallback = fn (size: usize) ?*anyopaque;
pub const CallocCallback = fn (nmemb: usize, size: usize) ?*anyopaque;
pub const ReallocCallback = fn (ptr: ?*anyopaque, size: usize) ?*anyopaque;

pub const CurlAllocator = struct {
    free: FreeCallback,
    strdup: StrdupCallback,
    malloc: MallocCallback,
    calloc: CallocCallback,
    realloc: ReallocCallback,
};

pub const CurlGlobalFlags = packed struct(u8) {
    ssl: bool = false,
    _reserved: u7 = 0,

    pub fn to_c(self: @This()) c_long {
        var flags: c_long = 0;
        if (self.ssl) flags |= c.CURL_GLOBAL_SSL;
        return flags;
    }
};

pub const CurlHeaderOrigin = enum(c_uint) {
    header = c.CURLH_HEADER,
    trailer = c.CURLH_TRAILER,
    connect = c.CURLH_CONNECT,
    @"1xx" = c.CURLH_1XX,
    pseudo = c.CURLH_PSEUDO,
};

pub const CurlWaitEvents = packed struct(c_short) {
    pollin: bool = false,
    pollpri: bool = false,
    pollout: bool = false,
    _reserved: u13 = 0,
};

pub const CurlInfoType = enum(c.curl_infotype) {
    text = c.CURLINFO_TEXT,
    header_in = c.CURLINFO_HEADER_IN,
    header_out = c.CURLINFO_HEADER_OUT,
    data_in = c.CURLINFO_DATA_IN,
    data_out = c.CURLINFO_DATA_OUT,
    ssl_data_in = c.CURLINFO_SSL_DATA_IN,
    ssl_data_out = c.CURLINFO_SSL_DATA_OUT,
    end = c.CURLINFO_END,
};

pub const CurlWaitFd = extern struct {
    fd: CurlSocket,
    events: CurlWaitEvents,
    revents: CurlWaitEvents,
};

pub const CurlPauseFlags = packed struct(c_short) {
    recv: bool = false,
    send: bool = false,
    all: bool = false,
    cont: bool = false,
    _reserved: u12 = 0,

    pub fn to_c(self: @This()) c_int {
        var flags: c_int = 0;
        if (self.recv) flags |= c.CURLPAUSE_RECV;
        if (self.send) flags |= c.CURLPAUSE_SEND;
        if (self.all) flags |= c.CURLPAUSE_ALL;
        if (self.cont) flags |= c.CURLPAUSE_CONT;
        return flags;
    }
};

comptime {
    const debug_cb_check: c.curl_debug_callback = struct {
        fn cb(handle: ?*Curl, msg_type: c.curl_infotype, raw: [*c]u8, len: usize, user: ?*anyopaque) callconv(.c) c_int {
            _ = handle;
            _ = msg_type;
            _ = raw;
            _ = len;
            _ = user;
            return 0;
        }
    }.cb;
    const write_cb_check: c.curl_write_callback = struct {
        fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {
            _ = buffer;
            _ = count;
            _ = len;
            _ = user;
            return 0;
        }
    }.cb;
    const opensocket_cb_check: c.curl_opensocket_callback = struct {
        fn cb(clientp: ?*anyopaque, purpose: c.curlsocktype, address: [*c]c.curl_sockaddr) callconv(.c) c.curl_socket_t {
            _ = clientp;
            _ = purpose;
            _ = address;
            return CURL_SOCKET_BAD;
        }
    }.cb;
    _ = debug_cb_check;
    _ = write_cb_check;
    _ = opensocket_cb_check;

    if (@sizeOf(CurlWaitFd) != @sizeOf(c.curl_waitfd)) {
        @compileError("CurlWaitFd size mismatch");
    }
    if (@offsetOf(CurlWaitFd, "fd") != @offsetOf(c.curl_waitfd, "fd") or
        @offsetOf(CurlWaitFd, "events") != @offsetOf(c.curl_waitfd, "events") or
        @offsetOf(CurlWaitFd, "revents") != @offsetOf(c.curl_waitfd, "revents"))
    {
        @compileError("CurlWaitFd layout mismatch");
    }
    if (c.CURL_WAIT_POLLIN != 1 or c.CURL_WAIT_POLLPRI != 2 or c.CURL_WAIT_POLLOUT != 4) {
        @compileError("CURL_WAIT_* flag values don't match CurlWaitEvents packed struct bit layout");
    }
    if (@sizeOf(CurlSockAddr) != @sizeOf(c.curl_sockaddr)) {
        @compileError("CurlSockAddr size mismatch with curl_sockaddr");
    }
    if (@offsetOf(CurlSockAddr, "family") != @offsetOf(c.curl_sockaddr, "family") or
        @offsetOf(CurlSockAddr, "socktype") != @offsetOf(c.curl_sockaddr, "socktype") or
        @offsetOf(CurlSockAddr, "protocol") != @offsetOf(c.curl_sockaddr, "protocol") or
        @offsetOf(CurlSockAddr, "addrlen") != @offsetOf(c.curl_sockaddr, "addrlen") or
        @offsetOf(CurlSockAddr, "addr") != @offsetOf(c.curl_sockaddr, "addr"))
    {
        @compileError("CurlSockAddr layout mismatch with curl_sockaddr");
    }
}

pub const CurlOption = enum(c.CURLoption) {
    url = c.CURLOPT_URL,
    timeout_ms = c.CURLOPT_TIMEOUT_MS,
    connect_timeout_ms = c.CURLOPT_CONNECTTIMEOUT_MS,
    max_redirs = c.CURLOPT_MAXREDIRS,
    follow_location = c.CURLOPT_FOLLOWLOCATION,
    redir_protocols_str = c.CURLOPT_REDIR_PROTOCOLS_STR,
    proxy = c.CURLOPT_PROXY,
    ca_info_blob = c.CURLOPT_CAINFO_BLOB,
    proxy_ca_info_blob = c.CURLOPT_PROXY_CAINFO_BLOB,
    ssl_verify_host = c.CURLOPT_SSL_VERIFYHOST,
    ssl_verify_peer = c.CURLOPT_SSL_VERIFYPEER,
    proxy_ssl_verify_host = c.CURLOPT_PROXY_SSL_VERIFYHOST,
    proxy_ssl_verify_peer = c.CURLOPT_PROXY_SSL_VERIFYPEER,
    accept_encoding = c.CURLOPT_ACCEPT_ENCODING,
    verbose = c.CURLOPT_VERBOSE,
    debug_function = c.CURLOPT_DEBUGFUNCTION,
    custom_request = c.CURLOPT_CUSTOMREQUEST,
    post = c.CURLOPT_POST,
    http_post = c.CURLOPT_HTTPPOST,
    post_field_size = c.CURLOPT_POSTFIELDSIZE,
    copy_post_fields = c.CURLOPT_COPYPOSTFIELDS,
    http_get = c.CURLOPT_HTTPGET,
    http_header = c.CURLOPT_HTTPHEADER,
    cookie = c.CURLOPT_COOKIE,
    private = c.CURLOPT_PRIVATE,
    proxy_user_pwd = c.CURLOPT_PROXYUSERPWD,
    user_pwd = c.CURLOPT_USERPWD,
    header_data = c.CURLOPT_HEADERDATA,
    header_function = c.CURLOPT_HEADERFUNCTION,
    write_data = c.CURLOPT_WRITEDATA,
    write_function = c.CURLOPT_WRITEFUNCTION,
    read_data = c.CURLOPT_READDATA,
    read_function = c.CURLOPT_READFUNCTION,
    connect_only = c.CURLOPT_CONNECT_ONLY,
    upload = c.CURLOPT_UPLOAD,
    opensocket_function = c.CURLOPT_OPENSOCKETFUNCTION,
    opensocket_data = c.CURLOPT_OPENSOCKETDATA,
};

pub const CurlMOption = enum(c.CURLMoption) {
    max_host_connections = c.CURLMOPT_MAX_HOST_CONNECTIONS,
};

pub const CurlInfo = enum(c.CURLINFO) {
    effective_url = c.CURLINFO_EFFECTIVE_URL,
    private = c.CURLINFO_PRIVATE,
    redirect_count = c.CURLINFO_REDIRECT_COUNT,
    response_code = c.CURLINFO_RESPONSE_CODE,
    connect_code = c.CURLINFO_HTTP_CONNECTCODE,
};

pub const Error = error{
    UnsupportedProtocol,
    FailedInit,
    UrlMalformat,
    NotBuiltIn,
    CouldntResolveProxy,
    CouldntResolveHost,
    CouldntConnect,
    WeirdServerReply,
    RemoteAccessDenied,
    FtpAcceptFailed,
    FtpWeirdPassReply,
    FtpAcceptTimeout,
    FtpWeirdPasvReply,
    FtpWeird227Format,
    FtpCantGetHost,
    Http2,
    FtpCouldntSetType,
    PartialFile,
    FtpCouldntRetrFile,
    QuoteError,
    HttpReturnedError,
    WriteError,
    UploadFailed,
    ReadError,
    OutOfMemory,
    OperationTimedout,
    FtpPortFailed,
    FtpCouldntUseRest,
    RangeError,
    SslConnectError,
    BadDownloadResume,
    FileCouldntReadFile,
    LdapCannotBind,
    LdapSearchFailed,
    AbortedByCallback,
    BadFunctionArgument,
    InterfaceFailed,
    TooManyRedirects,
    UnknownOption,
    SetoptOptionSyntax,
    GotNothing,
    SslEngineNotfound,
    SslEngineSetfailed,
    SendError,
    RecvError,
    SslCertproblem,
    SslCipher,
    PeerFailedVerification,
    BadContentEncoding,
    FilesizeExceeded,
    UseSslFailed,
    SendFailRewind,
    SslEngineInitfailed,
    LoginDenied,
    TftpNotfound,
    TftpPerm,
    RemoteDiskFull,
    TftpIllegal,
    TftpUnknownid,
    RemoteFileExists,
    TftpNosuchuser,
    SslCacertBadfile,
    RemoteFileNotFound,
    Ssh,
    SslShutdownFailed,
    Again,
    SslCrlBadfile,
    SslIssuerError,
    FtpPretFailed,
    RtspCseqError,
    RtspSessionError,
    FtpBadFileList,
    ChunkFailed,
    NoConnectionAvailable,
    SslPinnedpubkeynotmatch,
    SslInvalidcertstatus,
    Http2Stream,
    RecursiveApiCall,
    AuthError,
    Http3,
    QuicConnectError,
    Proxy,
    SslClientcert,
    UnrecoverablePoll,
    TooLarge,
    Unknown,
};

pub fn errorFromCode(code: c.CURLcode) Error {
    if (comptime IS_DEBUG) {
        std.debug.assert(code != c.CURLE_OK);
    }

    return switch (code) {
        c.CURLE_UNSUPPORTED_PROTOCOL => Error.UnsupportedProtocol,
        c.CURLE_FAILED_INIT => Error.FailedInit,
        c.CURLE_URL_MALFORMAT => Error.UrlMalformat,
        c.CURLE_NOT_BUILT_IN => Error.NotBuiltIn,
        c.CURLE_COULDNT_RESOLVE_PROXY => Error.CouldntResolveProxy,
        c.CURLE_COULDNT_RESOLVE_HOST => Error.CouldntResolveHost,
        c.CURLE_COULDNT_CONNECT => Error.CouldntConnect,
        c.CURLE_WEIRD_SERVER_REPLY => Error.WeirdServerReply,
        c.CURLE_REMOTE_ACCESS_DENIED => Error.RemoteAccessDenied,
        c.CURLE_FTP_ACCEPT_FAILED => Error.FtpAcceptFailed,
        c.CURLE_FTP_WEIRD_PASS_REPLY => Error.FtpWeirdPassReply,
        c.CURLE_FTP_ACCEPT_TIMEOUT => Error.FtpAcceptTimeout,
        c.CURLE_FTP_WEIRD_PASV_REPLY => Error.FtpWeirdPasvReply,
        c.CURLE_FTP_WEIRD_227_FORMAT => Error.FtpWeird227Format,
        c.CURLE_FTP_CANT_GET_HOST => Error.FtpCantGetHost,
        c.CURLE_HTTP2 => Error.Http2,
        c.CURLE_FTP_COULDNT_SET_TYPE => Error.FtpCouldntSetType,
        c.CURLE_PARTIAL_FILE => Error.PartialFile,
        c.CURLE_FTP_COULDNT_RETR_FILE => Error.FtpCouldntRetrFile,
        c.CURLE_QUOTE_ERROR => Error.QuoteError,
        c.CURLE_HTTP_RETURNED_ERROR => Error.HttpReturnedError,
        c.CURLE_WRITE_ERROR => Error.WriteError,
        c.CURLE_UPLOAD_FAILED => Error.UploadFailed,
        c.CURLE_READ_ERROR => Error.ReadError,
        c.CURLE_OUT_OF_MEMORY => Error.OutOfMemory,
        c.CURLE_OPERATION_TIMEDOUT => Error.OperationTimedout,
        c.CURLE_FTP_PORT_FAILED => Error.FtpPortFailed,
        c.CURLE_FTP_COULDNT_USE_REST => Error.FtpCouldntUseRest,
        c.CURLE_RANGE_ERROR => Error.RangeError,
        c.CURLE_SSL_CONNECT_ERROR => Error.SslConnectError,
        c.CURLE_BAD_DOWNLOAD_RESUME => Error.BadDownloadResume,
        c.CURLE_FILE_COULDNT_READ_FILE => Error.FileCouldntReadFile,
        c.CURLE_LDAP_CANNOT_BIND => Error.LdapCannotBind,
        c.CURLE_LDAP_SEARCH_FAILED => Error.LdapSearchFailed,
        c.CURLE_ABORTED_BY_CALLBACK => Error.AbortedByCallback,
        c.CURLE_BAD_FUNCTION_ARGUMENT => Error.BadFunctionArgument,
        c.CURLE_INTERFACE_FAILED => Error.InterfaceFailed,
        c.CURLE_TOO_MANY_REDIRECTS => Error.TooManyRedirects,
        c.CURLE_UNKNOWN_OPTION => Error.UnknownOption,
        c.CURLE_SETOPT_OPTION_SYNTAX => Error.SetoptOptionSyntax,
        c.CURLE_GOT_NOTHING => Error.GotNothing,
        c.CURLE_SSL_ENGINE_NOTFOUND => Error.SslEngineNotfound,
        c.CURLE_SSL_ENGINE_SETFAILED => Error.SslEngineSetfailed,
        c.CURLE_SEND_ERROR => Error.SendError,
        c.CURLE_RECV_ERROR => Error.RecvError,
        c.CURLE_SSL_CERTPROBLEM => Error.SslCertproblem,
        c.CURLE_SSL_CIPHER => Error.SslCipher,
        c.CURLE_PEER_FAILED_VERIFICATION => Error.PeerFailedVerification,
        c.CURLE_BAD_CONTENT_ENCODING => Error.BadContentEncoding,
        c.CURLE_FILESIZE_EXCEEDED => Error.FilesizeExceeded,
        c.CURLE_USE_SSL_FAILED => Error.UseSslFailed,
        c.CURLE_SEND_FAIL_REWIND => Error.SendFailRewind,
        c.CURLE_SSL_ENGINE_INITFAILED => Error.SslEngineInitfailed,
        c.CURLE_LOGIN_DENIED => Error.LoginDenied,
        c.CURLE_TFTP_NOTFOUND => Error.TftpNotfound,
        c.CURLE_TFTP_PERM => Error.TftpPerm,
        c.CURLE_REMOTE_DISK_FULL => Error.RemoteDiskFull,
        c.CURLE_TFTP_ILLEGAL => Error.TftpIllegal,
        c.CURLE_TFTP_UNKNOWNID => Error.TftpUnknownid,
        c.CURLE_REMOTE_FILE_EXISTS => Error.RemoteFileExists,
        c.CURLE_TFTP_NOSUCHUSER => Error.TftpNosuchuser,
        c.CURLE_SSL_CACERT_BADFILE => Error.SslCacertBadfile,
        c.CURLE_REMOTE_FILE_NOT_FOUND => Error.RemoteFileNotFound,
        c.CURLE_SSH => Error.Ssh,
        c.CURLE_SSL_SHUTDOWN_FAILED => Error.SslShutdownFailed,
        c.CURLE_AGAIN => Error.Again,
        c.CURLE_SSL_CRL_BADFILE => Error.SslCrlBadfile,
        c.CURLE_SSL_ISSUER_ERROR => Error.SslIssuerError,
        c.CURLE_FTP_PRET_FAILED => Error.FtpPretFailed,
        c.CURLE_RTSP_CSEQ_ERROR => Error.RtspCseqError,
        c.CURLE_RTSP_SESSION_ERROR => Error.RtspSessionError,
        c.CURLE_FTP_BAD_FILE_LIST => Error.FtpBadFileList,
        c.CURLE_CHUNK_FAILED => Error.ChunkFailed,
        c.CURLE_NO_CONNECTION_AVAILABLE => Error.NoConnectionAvailable,
        c.CURLE_SSL_PINNEDPUBKEYNOTMATCH => Error.SslPinnedpubkeynotmatch,
        c.CURLE_SSL_INVALIDCERTSTATUS => Error.SslInvalidcertstatus,
        c.CURLE_HTTP2_STREAM => Error.Http2Stream,
        c.CURLE_RECURSIVE_API_CALL => Error.RecursiveApiCall,
        c.CURLE_AUTH_ERROR => Error.AuthError,
        c.CURLE_HTTP3 => Error.Http3,
        c.CURLE_QUIC_CONNECT_ERROR => Error.QuicConnectError,
        c.CURLE_PROXY => Error.Proxy,
        c.CURLE_SSL_CLIENTCERT => Error.SslClientcert,
        c.CURLE_UNRECOVERABLE_POLL => Error.UnrecoverablePoll,
        c.CURLE_TOO_LARGE => Error.TooLarge,
        else => Error.Unknown,
    };
}

pub const ErrorMulti = error{
    BadHandle,
    BadEasyHandle,
    OutOfMemory,
    InternalError,
    BadSocket,
    UnknownOption,
    AddedAlready,
    RecursiveApiCall,
    WakeupFailure,
    BadFunctionArgument,
    AbortedByCallback,
    UnrecoverablePoll,
    Unknown,
};

pub const ErrorHeader = error{
    OutOfMemory,
    BadArgument,
    NotBuiltIn,
    Unknown,
};

pub fn errorMFromCode(code: c.CURLMcode) ErrorMulti {
    if (comptime IS_DEBUG) {
        std.debug.assert(code != c.CURLM_OK);
    }

    return switch (code) {
        c.CURLM_BAD_HANDLE => ErrorMulti.BadHandle,
        c.CURLM_BAD_EASY_HANDLE => ErrorMulti.BadEasyHandle,
        c.CURLM_OUT_OF_MEMORY => ErrorMulti.OutOfMemory,
        c.CURLM_INTERNAL_ERROR => ErrorMulti.InternalError,
        c.CURLM_BAD_SOCKET => ErrorMulti.BadSocket,
        c.CURLM_UNKNOWN_OPTION => ErrorMulti.UnknownOption,
        c.CURLM_ADDED_ALREADY => ErrorMulti.AddedAlready,
        c.CURLM_RECURSIVE_API_CALL => ErrorMulti.RecursiveApiCall,
        c.CURLM_WAKEUP_FAILURE => ErrorMulti.WakeupFailure,
        c.CURLM_BAD_FUNCTION_ARGUMENT => ErrorMulti.BadFunctionArgument,
        c.CURLM_ABORTED_BY_CALLBACK => ErrorMulti.AbortedByCallback,
        c.CURLM_UNRECOVERABLE_POLL => ErrorMulti.UnrecoverablePoll,
        else => ErrorMulti.Unknown,
    };
}

pub fn errorHFromCode(code: c.CURLHcode) ErrorHeader {
    if (comptime IS_DEBUG) {
        std.debug.assert(code != c.CURLHE_OK);
    }

    return switch (code) {
        c.CURLHE_OUT_OF_MEMORY => ErrorHeader.OutOfMemory,
        c.CURLHE_BAD_ARGUMENT => ErrorHeader.BadArgument,
        c.CURLHE_NOT_BUILT_IN => ErrorHeader.NotBuiltIn,
        else => ErrorHeader.Unknown,
    };
}

pub fn errorCheck(code: c.CURLcode) Error!void {
    if (code == c.CURLE_OK) {
        return;
    }
    return errorFromCode(code);
}

pub fn errorMCheck(code: c.CURLMcode) ErrorMulti!void {
    if (code == c.CURLM_OK) {
        return;
    }
    if (code == c.CURLM_CALL_MULTI_PERFORM) {
        return;
    }
    return errorMFromCode(code);
}

pub fn errorHCheck(code: c.CURLHcode) ErrorHeader!void {
    if (code == c.CURLHE_OK) {
        return;
    }
    return errorHFromCode(code);
}

pub const CurlMsgType = enum(c.CURLMSG) {
    none = c.CURLMSG_NONE,
    done = c.CURLMSG_DONE,
    last = c.CURLMSG_LAST,
};

pub const CurlMsgData = union(CurlMsgType) {
    none: ?*anyopaque,
    done: ?Error,
    last: ?*anyopaque,
};

pub const CurlMsg = struct {
    easy_handle: *Curl,
    data: CurlMsgData,
};

pub fn curl_global_init(flags: CurlGlobalFlags, comptime curl_allocator: ?CurlAllocator) Error!void {
    const alloc = curl_allocator orelse {
        return errorCheck(c.curl_global_init(flags.to_c()));
    };

    // The purpose of these wrappers is to hide callconv
    // and provide an easy place to add logging when debugging.
    const free = struct {
        fn cb(ptr: ?*anyopaque) callconv(.c) void {
            alloc.free(ptr);
        }
    }.cb;
    const strdup = struct {
        fn cb(str: [*c]const u8) callconv(.c) [*c]u8 {
            const s: [*:0]const u8 = @ptrCast(str orelse return null);
            return @ptrCast(alloc.strdup(s));
        }
    }.cb;
    const malloc = struct {
        fn cb(size: usize) callconv(.c) ?*anyopaque {
            return alloc.malloc(size);
        }
    }.cb;
    const calloc = struct {
        fn cb(nmemb: usize, size: usize) callconv(.c) ?*anyopaque {
            return alloc.calloc(nmemb, size);
        }
    }.cb;
    const realloc = struct {
        fn cb(ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque {
            return alloc.realloc(ptr, size);
        }
    }.cb;

    try errorCheck(c.curl_global_init_mem(flags.to_c(), malloc, free, realloc, strdup, calloc));
}

pub fn curl_global_cleanup() void {
    c.curl_global_cleanup();
}

pub fn curl_version() [*c]const u8 {
    return c.curl_version();
}

pub fn curl_easy_init() ?*Curl {
    return c.curl_easy_init();
}

pub fn curl_easy_cleanup(easy: *Curl) void {
    c.curl_easy_cleanup(easy);
}

pub fn curl_easy_reset(easy: *Curl) void {
    c.curl_easy_reset(easy);
}

pub fn curl_easy_perform(easy: *Curl) Error!void {
    try errorCheck(c.curl_easy_perform(easy));
}

pub fn curl_easy_setopt(easy: *Curl, comptime option: CurlOption, value: anytype) Error!void {
    const opt: c.CURLoption = @intFromEnum(option);
    const code = switch (option) {
        .verbose,
        .post,
        .upload,
        .http_get,
        .ssl_verify_host,
        .ssl_verify_peer,
        .proxy_ssl_verify_host,
        .proxy_ssl_verify_peer,
        => blk: {
            const n: c_long = switch (@typeInfo(@TypeOf(value))) {
                .bool => switch (option) {
                    .ssl_verify_host, .proxy_ssl_verify_host => if (value) 2 else 0,
                    else => if (value) 1 else 0,
                },
                else => @compileError("expected bool|integer for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_easy_setopt(easy, opt, n);
        },

        .timeout_ms,
        .connect_timeout_ms,
        .max_redirs,
        .follow_location,
        .post_field_size,
        .connect_only,
        => blk: {
            const n: c_long = switch (@typeInfo(@TypeOf(value))) {
                .comptime_int, .int => @intCast(value),
                else => @compileError("expected integer for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_easy_setopt(easy, opt, n);
        },

        .url,
        .redir_protocols_str,
        .proxy,
        .accept_encoding,
        .custom_request,
        .cookie,
        .user_pwd,
        .proxy_user_pwd,
        .copy_post_fields,
        => blk: {
            const s: ?[*]const u8 = value;
            break :blk c.curl_easy_setopt(easy, opt, s);
        },

        .ca_info_blob,
        .proxy_ca_info_blob,
        => blk: {
            const blob: CurlBlob = value;
            break :blk c.curl_easy_setopt(easy, opt, blob);
        },

        .http_post => blk: {
            // CURLOPT_HTTPPOST expects ?*curl_httppost (multipart formdata)
            const ptr: ?*CurlHttpPost = value;
            break :blk c.curl_easy_setopt(easy, opt, ptr);
        },

        .http_header => blk: {
            const list: ?*CurlSList = value;
            break :blk c.curl_easy_setopt(easy, opt, list);
        },

        .private,
        .header_data,
        .read_data,
        .write_data,
        .opensocket_data,
        => blk: {
            const ptr: ?*anyopaque = switch (@typeInfo(@TypeOf(value))) {
                .null => null,
                else => @ptrCast(value),
            };
            break :blk c.curl_easy_setopt(easy, opt, ptr);
        },

        .debug_function => blk: {
            const cb: c.curl_debug_callback = switch (@typeInfo(@TypeOf(value))) {
                .null => null,
                .@"fn" => struct {
                    fn cb(handle: ?*Curl, msg_type: c.curl_infotype, raw: [*c]u8, len: usize, user: ?*anyopaque) callconv(.c) c_int {
                        const h = handle orelse unreachable;
                        const u = user orelse unreachable;
                        return value(h, @enumFromInt(@intFromEnum(msg_type)), raw, len, u);
                    }
                }.cb,
                else => @compileError("expected Zig function or null for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_easy_setopt(easy, opt, cb);
        },

        .opensocket_function => blk: {
            const cb: c.curl_opensocket_callback = switch (@typeInfo(@TypeOf(value))) {
                .null => null,
                .@"fn" => struct {
                    fn cb(clientp: ?*anyopaque, purpose: c.curlsocktype, address: [*c]c.curl_sockaddr) callconv(.c) c.curl_socket_t {
                        const addr: *CurlSockAddr = @ptrCast(address orelse return CURL_SOCKET_BAD);
                        return value(@enumFromInt(purpose), addr, clientp);
                    }
                }.cb,
                else => @compileError("expected Zig function or null for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_easy_setopt(easy, opt, cb);
        },

        .header_function => blk: {
            const cb: c.curl_write_callback = switch (@typeInfo(@TypeOf(value))) {
                .null => null,
                .@"fn" => struct {
                    fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {
                        const u = user orelse unreachable;
                        return value(@ptrCast(buffer), count, len, u);
                    }
                }.cb,
                else => @compileError("expected Zig function or null for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_easy_setopt(easy, opt, cb);
        },

        .read_function => blk: {
            const cb: c.curl_write_callback = switch (@typeInfo(@TypeOf(value))) {
                .null => null,
                .@"fn" => |info| struct {
                    fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {
                        const user_arg = if (@typeInfo(info.params[3].type.?) == .optional)
                            user
                        else
                            user orelse unreachable;
                        return value(@ptrCast(buffer), count, len, user_arg);
                    }
                }.cb,
                else => @compileError("expected Zig function or null for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_easy_setopt(easy, opt, cb);
        },
        .write_function => blk: {
            const cb: c.curl_write_callback = switch (@typeInfo(@TypeOf(value))) {
                .null => null,
                .@"fn" => |info| struct {
                    fn cb(buffer: [*c]u8, count: usize, len: usize, user: ?*anyopaque) callconv(.c) usize {
                        const user_arg = if (@typeInfo(info.params[3].type.?) == .optional)
                            user
                        else
                            user orelse unreachable;
                        return value(@ptrCast(buffer), count, len, user_arg);
                    }
                }.cb,
                else => @compileError("expected Zig function or null for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_easy_setopt(easy, opt, cb);
        },
    };
    try errorCheck(code);
}

pub fn curl_easy_getinfo(easy: *Curl, comptime info: CurlInfo, out: anytype) Error!void {
    if (@typeInfo(@TypeOf(out)) != .pointer) {
        @compileError("curl_easy_getinfo out must be a pointer, got " ++ @typeName(@TypeOf(out)));
    }

    const inf: c.CURLINFO = @intFromEnum(info);
    const code = switch (info) {
        .effective_url => blk: {
            const p: *[*c]u8 = out;
            break :blk c.curl_easy_getinfo(easy, inf, p);
        },
        .response_code,
        .connect_code,
        .redirect_count,
        => blk: {
            const p: *c_long = out;
            break :blk c.curl_easy_getinfo(easy, inf, p);
        },
        .private => blk: {
            const p: **anyopaque = out;
            break :blk c.curl_easy_getinfo(easy, inf, p);
        },
    };
    try errorCheck(code);
}

pub fn curl_easy_pause(easy: *Curl, flags: CurlPauseFlags) Error!void {
    try errorCheck(c.curl_easy_pause(easy, flags.to_c()));
}

pub fn curl_easy_header(
    easy: *Curl,
    name: [*:0]const u8,
    index: usize,
    comptime origin: CurlHeaderOrigin,
    request: c_int,
    hout: *?*CurlHeader,
) ErrorHeader!void {
    var c_hout: [*c]CurlHeader = null;
    const code = c.curl_easy_header(easy, name, index, @intFromEnum(origin), request, &c_hout);
    switch (code) {
        c.CURLHE_OK => {
            hout.* = @ptrCast(c_hout);
            return;
        },
        c.CURLHE_BADINDEX,
        c.CURLHE_MISSING,
        c.CURLHE_NOHEADERS,
        c.CURLHE_NOREQUEST,
        => {
            hout.* = null;
            return;
        },
        else => {
            hout.* = null;
            return errorHFromCode(code);
        },
    }
}

pub fn curl_easy_nextheader(
    easy: *Curl,
    comptime origin: CurlHeaderOrigin,
    request: c_int,
    prev: ?*CurlHeader,
) ?*CurlHeader {
    const ptr = c.curl_easy_nextheader(easy, @intFromEnum(origin), request, prev);
    if (ptr == null) return null;
    return @ptrCast(ptr);
}

pub fn curl_multi_init() ?*CurlM {
    return c.curl_multi_init();
}

pub fn curl_multi_cleanup(multi: *CurlM) ErrorMulti!void {
    try errorMCheck(c.curl_multi_cleanup(multi));
}

pub fn curl_multi_setopt(multi: *CurlM, comptime option: CurlMOption, value: anytype) ErrorMulti!void {
    const opt: c.CURLMoption = @intFromEnum(option);
    const code = switch (option) {
        .max_host_connections => blk: {
            const n: c_long = switch (@typeInfo(@TypeOf(value))) {
                .comptime_int, .int => @intCast(value),
                else => @compileError("expected integer for " ++ @tagName(option) ++ ", got " ++ @typeName(@TypeOf(value))),
            };
            break :blk c.curl_multi_setopt(multi, opt, n);
        },
    };
    try errorMCheck(code);
}

pub fn curl_multi_add_handle(multi: *CurlM, easy: *Curl) ErrorMulti!void {
    try errorMCheck(c.curl_multi_add_handle(multi, easy));
}

pub fn curl_multi_remove_handle(multi: *CurlM, easy: *Curl) ErrorMulti!void {
    try errorMCheck(c.curl_multi_remove_handle(multi, easy));
}

pub fn curl_multi_perform(multi: *CurlM, running_handles: *c_int) ErrorMulti!void {
    try errorMCheck(c.curl_multi_perform(multi, running_handles));
}

pub fn curl_multi_poll(
    multi: *CurlM,
    extra_fds: []CurlWaitFd,
    timeout_ms: c_int,
    numfds: ?*c_int,
) ErrorMulti!void {
    const raw_fds: [*c]c.curl_waitfd = if (extra_fds.len == 0) null else @ptrCast(extra_fds.ptr);
    try errorMCheck(c.curl_multi_poll(multi, raw_fds, @intCast(extra_fds.len), timeout_ms, numfds));
}

pub fn curl_multi_waitfds(multi: *CurlM, ufds: []CurlWaitFd, fd_count: *c_uint) ErrorMulti!void {
    const raw_fds: [*c]c.curl_waitfd = if (ufds.len == 0) null else @ptrCast(ufds.ptr);
    try errorMCheck(c.curl_multi_waitfds(multi, raw_fds, @intCast(ufds.len), fd_count));
}

pub fn curl_multi_timeout(multi: *CurlM, timeout_ms: *c_long) ErrorMulti!void {
    try errorMCheck(c.curl_multi_timeout(multi, timeout_ms));
}

pub fn curl_multi_info_read(multi: *CurlM, msgs_in_queue: *c_int) ?CurlMsg {
    const ptr = c.curl_multi_info_read(multi, msgs_in_queue);
    if (ptr == null) return null;

    const msg: *const c.CURLMsg = @ptrCast(ptr);
    const easy_handle = msg.easy_handle orelse unreachable;

    return switch (msg.msg) {
        c.CURLMSG_NONE => .{
            .easy_handle = easy_handle,
            .data = .{ .none = msg.data.whatever },
        },
        c.CURLMSG_DONE => .{
            .easy_handle = easy_handle,
            .data = .{ .done = if (errorCheck(msg.data.result)) |_| null else |err| err },
        },
        c.CURLMSG_LAST => .{
            .easy_handle = easy_handle,
            .data = .{ .last = msg.data.whatever },
        },
        else => unreachable,
    };
}

pub fn curl_slist_append(list: ?*CurlSList, header: [*:0]const u8) ?*CurlSList {
    return c.curl_slist_append(list, header);
}

pub fn curl_slist_free_all(list: ?*CurlSList) void {
    if (list) |ptr| {
        c.curl_slist_free_all(ptr);
    }
}

// WebSocket support (requires libcurl 7.86.0+)
pub const WsFrameType = enum {
    text,
    binary,
    cont,
    close,
    ping,
    pong,

    fn toInt(self: WsFrameType) c_uint {
        return switch (self) {
            .text => c.CURLWS_TEXT,
            .binary => c.CURLWS_BINARY,
            .cont => c.CURLWS_CONT,
            .close => c.CURLWS_CLOSE,
            .ping => c.CURLWS_PING,
            .pong => c.CURLWS_PONG,
        };
    }

    fn fromFlags(flags: c_int) WsFrameType {
        const f: c_uint = @bitCast(flags);
        if (f & c.CURLWS_TEXT != 0) return .text;
        if (f & c.CURLWS_BINARY != 0) return .binary;
        if (f & c.CURLWS_CLOSE != 0) return .close;
        if (f & c.CURLWS_PING != 0) return .ping;
        if (f & c.CURLWS_PONG != 0) return .pong;
        if (f & c.CURLWS_CONT != 0) return .cont;
        return .binary; // default fallback
    }
};

pub const WsFrameMeta = struct {
    frame_type: WsFrameType,
    offset: usize,
    bytes_left: usize,
    len: usize,

    fn from(frame: *const c.curl_ws_frame) WsFrameMeta {
        return .{
            .frame_type = WsFrameType.fromFlags(frame.flags),
            .offset = @intCast(frame.offset),
            .bytes_left = @intCast(frame.bytesleft),
            .len = if (frame.len < 0)
                std.math.maxInt(usize)
            else
                @intCast(frame.len),
        };
    }
};

pub fn curl_ws_send(easy: *Curl, buffer: []const u8, sent: *usize, fragsize: CurlOffT, frame_type: WsFrameType) Error!void {
    try errorCheck(c.curl_ws_send(easy, buffer.ptr, buffer.len, sent, fragsize, frame_type.toInt()));
}

pub fn curl_ws_recv(easy: *Curl, buffer: []u8, recv: *usize, meta: *?WsFrameMeta) Error!void {
    var c_meta: [*c]const c.curl_ws_frame = null;
    const code = c.curl_ws_recv(easy, buffer.ptr, buffer.len, recv, &c_meta);
    if (c_meta) |m| {
        meta.* = WsFrameMeta.from(m);
    } else {
        meta.* = null;
    }
    try errorCheck(code);
}

pub fn curl_ws_meta(easy: *Curl) ?WsFrameMeta {
    const ptr = c.curl_ws_meta(easy);
    if (ptr == null) return null;
    return WsFrameMeta.from(ptr);
}

pub fn curl_ws_start_frame(easy: *Curl, frame_type: WsFrameType, size: CurlOffT) Error!void {
    try errorCheck(c.curl_ws_start_frame(easy, frame_type.toInt(), size));
}
