const std = @import("std");
const clap = @import("clap");
const curl = @cImport({
    @cInclude("curl/curl.h");
});

const uri = @import("uri");
const json = @import("json");

const log = std.log.scoped(.b2dlkeygen);

const params = clap.parseParamsComptime(
    \\-h, --help                   Display this help and exit.
    \\-c, --config <FILE>          Read application config from FILE.
);

fn printFullUsage(w: anytype) !void {
    _ = try w.print("{s} ", .{std.os.argv[0]});
    try clap.usage(w, clap.Help, &params);
    _ = try w.writeByte('\n');
    try clap.help(w, clap.Help, &params, .{});
    return;
}

var curlerr = [_:0]u8{0} ** (curl.CURL_ERROR_SIZE);
fn curlErrorReport(str: []const u8, code: curl.CURLcode) void {
    log.err("{s}: {s} {s}", .{ str, curl.curl_easy_strerror(code), curlerr[0.. :0] });
}

const strList = std.ArrayList([]const u8);
const str0List = std.ArrayList([:0]const u8);

fn Deoptional(comptime t: type) type {
    const ti = @typeInfo(t);
    return switch (ti) {
        .Optional => |o| o.child,
        else => t,
    };
}

pub fn unwrap(
    un: anytype,
    comptime tag: std.meta.Tag(Deoptional(@TypeOf(un))),
) ?std.meta.TagPayload(Deoptional(@TypeOf(un)), tag) {
    if (@typeInfo(@TypeOf(un)) == .Optional) {
        if (un == null) return null;
        if (un.? != tag) return null;
        return @field(un.?, @tagName(tag));
    } else {
        if (un != tag) return null;
        return @field(un, @tagName(tag));
    }
}

const Config = struct {
    key: [*:0]const u8,
    pre: []const u8,
    post: []const u8,
    output: []const u8,

    pub fn newFromFile(path: []const u8, alloc: std.mem.Allocator) !Config {
        const file = std.fs.cwd().openFile(path, .{ .mode = .read_only }) catch |e| {
            log.err("Error opening file: {s}\n", .{e});
            std.os.exit(2);
        };
        const buf = try file.readToEndAlloc(alloc, 100000);
        defer alloc.free(buf);

        var config: Config = undefined;

        var obj = try json.parse(alloc, buf);
        defer {
            if (unwrap(obj, .Object)) |o| alloc.free(o);
        }

        if (unwrap(obj.get(.{"key"}), .String)) |key| {
            config.key = try alloc.dupeZ(u8, key);
        } else {
            log.err("No key in config, or not a string.", .{});
            return error.ConfigError;
        }
        if (unwrap(obj.get(.{"pre"}), .String)) |pre| {
            config.pre = try alloc.dupe(u8, pre);
        } else {
            log.err("No pre in config, or not a string.", .{});
            return error.ConfigError;
        }
        if (unwrap(obj.get(.{"post"}), .String)) |post| {
            config.post = try alloc.dupe(u8, post);
        } else {
            log.err("No post in config, or not a string.", .{});
            return error.ConfigError;
        }
        if (unwrap(obj.get(.{"output"}), .String)) |output| {
            config.output = try alloc.dupe(u8, output);
        } else {
            log.err("No output in config, or not a string.", .{});
            return error.ConfigError;
        }
        return config;
    }
};

pub fn main() anyerror!void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const alloc = gpa.allocator();
    std.log.info("All your codebase are belong to us.", .{});

    const parsers = comptime .{
        .PATH = clap.parsers.string,
        .FILE = clap.parsers.string,
        .NAME = clap.parsers.string,
        .REGEX = clap.parsers.string,
        .ID = clap.parsers.int(i64, 10),
    };

    var diag = clap.Diagnostic{};
    var clap_res = clap.parse(clap.Help, &params, parsers, .{
        .diagnostic = &diag,
        .allocator = alloc,
    }) catch |err| {
        diag.report(std.io.getStdErr().writer(), err) catch {};
        return err;
    };
    defer clap_res.deinit();
    var args = clap_res.args;

    if (args.help) {
        var w = std.io.getStdOut().writer();
        try printFullUsage(w);
        return;
    }

    if (args.config == null) {
        log.err("Please specify a config file with --config.", .{});
        std.os.exit(3);
    }
    const cfile = args.config.?;

    var ret = curl.curl_global_init(curl.CURL_GLOBAL_ALL);
    if (ret != curl.CURLE_OK) {
        log.err("cURL global init failure: {s}", .{curl.curl_easy_strerror(ret)});
        return;
    }
    defer curl.curl_global_cleanup();
    const handle = curl.curl_easy_init() orelse return error.CURLHandleInitFailed;
    defer curl.curl_easy_cleanup(handle);
    var response_buffer = std.ArrayList(u8).init(alloc);
    defer response_buffer.deinit();

    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_ERRORBUFFER, &curlerr);

    //    try easyFetch(handle, "http://cloud/foo.jpeg", &response_buffer);
    //    var dir = try std.fs.cwd().makeOpenPath(".", .{
    //        .access_sub_paths = true,
    //    });
    //    var file = try dir.createFile(
    //        "test.foo",
    //        .{
    //            .read = false,
    //            .truncate = true,
    //        },
    //    );
    //    defer file.close();
    //    try file.writeAll(response_buffer.items);

    const conf = try Config.newFromFile(cfile, alloc);

    var api = try getAuthToken(handle, conf.key, alloc);
    try getDlToken(handle, &api, alloc);

    var file: std.fs.File = try std.fs.cwd().createFile(conf.output, .{ .read = false, .truncate = true });
    defer file.close();
    var w = file.writer();
    try w.print("{s}{s}{s}\n", .{ conf.pre, api.dl_auth.?, conf.post });
}

const B2API = struct {
    auth: [*:0]const u8,
    url: [*:0]const u8,
    dl_url: [*:0]const u8,
    dl_auth: ?[]const u8,
};

fn getAuthToken(handle: *curl.CURL, key: [*:0]const u8, alloc: std.mem.Allocator) !B2API {
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_URL, "https://api.backblazeb2.com/b2api/v2/b2_authorize_account");
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_USERPWD, key);
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEFUNCTION, writeToArrayListCallback);

    var resp = std.ArrayList(u8).init(alloc);
    defer resp.deinit();

    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEDATA, resp);
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_USERAGENT, "b2keygen 0.1 (linux)");
    _ = curl.curl_easy_perform(handle);

    var val = try json.parse(alloc, resp.items);

    var autho = val.get(.{"authorizationToken"});
    var urlo = val.get(.{"apiUrl"});
    var urlo2 = val.get(.{"downloadUrl"});

    if (autho == null or urlo == null or urlo2 == null) {
        return error.ShitsFucked;
    }

    var foo: B2API = undefined;
    foo.auth = try std.fmt.allocPrintZ(alloc, "Authorization: {s}", .{autho.?.String});
    foo.url = try alloc.dupeZ(u8, urlo.?.String);
    foo.dl_url = try alloc.dupeZ(u8, urlo2.?.String);
    foo.dl_auth = null;

    return foo;
}

fn getDlToken(handle: *curl.CURL, api: *B2API, alloc: std.mem.Allocator) !void {
    const url = try std.fmt.allocPrintZ(alloc, "{s}/b2api/v2/b2_get_download_authorization", .{api.url});
    //const url: [:0]const u8 = "http://localhost:6666";
    defer alloc.free(url);

    var resp = std.ArrayList(u8).init(alloc);
    defer resp.deinit();

    var bar = curl.curl_slist_append(null, api.auth);
    bar = curl.curl_slist_append(bar, "Content-Type: application/json");
    defer curl.curl_slist_free_all(bar);

    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_URL, url.ptr);
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEFUNCTION, writeToArrayListCallback);
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEDATA, resp);
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_HTTPHEADER, bar);
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_USERAGENT, "b2keygen 0.1 (linux)");
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_POST, @as(u32, 1));

    var buffer: [1024]u8 = undefined;
    var aaa = std.io.fixedBufferStream(buffer[0..]).writer();
    var asd = [_]json.Member{
        .{ .key = "bucketId", .value = json.Value{ .String = "1b191ebe58f4fa1876bc031d" } },
        .{ .key = "fileNamePrefix", .value = json.Value{ .String = "" } },
        .{ .key = "validDurationInSeconds", .value = json.Value{ .Int = 259200 } },
    };
    const data = json.Value{
        .Object = asd[0..],
    };
    try data.format("", .{}, aaa);
    buffer[aaa.context.pos] = 0;
    const jason = @ptrCast([*:0]const u8, buffer[0..aaa.context.pos].ptr);

    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_POSTFIELDS, jason);
    _ = curl.curl_easy_setopt(handle, curl.CURLOPT_POSTFIELDSIZE, aaa.context.pos);
    _ = curl.curl_easy_perform(handle);

    var val = try json.parse(alloc, resp.items);

    var autho = val.get(.{"authorizationToken"});

    if (autho == null) {
        return error.ShitsFucked2;
    }

    api.dl_auth = try alloc.dupe(u8, autho.?.String);
}

fn easyFetch(handle: *curl.CURL, url: [*:0]const u8, resp: *std.ArrayList(u8)) !void {
    //    if (fetch_wait > 0) {
    //        if (fetch_timer) |*timer| {
    //            const cur = timer.read() / (1000 * 1000);
    //            if (cur < fetch_wait) {
    //                std.time.sleep((fetch_wait - cur) * 1000 * 1000);
    //            }
    //            timer.reset();
    //        } else {
    //            fetch_timer = try std.time.Timer.start();
    //        }
    //    }
    var ret = curl.curl_easy_setopt(handle, curl.CURLOPT_URL, url);
    if (ret != curl.CURLE_OK) {
        curlErrorReport("cURL set url:", ret);
        return error.CurlError;
    }
    ret = curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEFUNCTION, writeToArrayListCallback);
    if (ret != curl.CURLE_OK) {
        curlErrorReport("cURL set writefunction:", ret);
        return error.CurlError;
    }
    ret = curl.curl_easy_setopt(handle, curl.CURLOPT_WRITEDATA, resp);
    if (ret != curl.CURLE_OK) {
        curlErrorReport("cURL set writedata:", ret);
        return error.CurlError;
    }
    ret = curl.curl_easy_setopt(handle, curl.CURLOPT_USERAGENT, "b2backup 0.1 (linux)");
    if (ret != curl.CURLE_OK) {
        curlErrorReport("cURL set user agent:", ret);
        return error.CurlError;
    }
    ret = curl.curl_easy_perform(handle);
    if (ret != curl.CURLE_OK) {
        curlErrorReport("cURL perform:", ret);
        return error.CurlError;
    }

    log.info("Got {d} bytes", .{resp.items.len});
}

fn writeToArrayListCallback(
    data: *anyopaque,
    size: c_uint,
    nmemb: c_uint,
    user_data: *anyopaque,
) callconv(.C) c_uint {
    var buffer = @intToPtr(*std.ArrayList(u8), @ptrToInt(user_data));
    var typed_data = @ptrCast([*]u8, data);
    buffer.appendSlice(typed_data[0 .. nmemb * size]) catch return 0;
    return nmemb * size;
}