const std = @import("std");
const ziggity = @import("ziggity");
const build_options = @import("build_options");

const help_text =
    \\ziggity — a terminal UI for Git
    \\
    \\Usage:
    \\  ziggity              Launch the TUI in the current git repository
    \\  ziggity --help       Show this help and exit
    \\  ziggity --version    Show the version and exit
    \\
    \\Run it from inside a git repository; press ? in the app for keybindings.
    \\Requires the `git` command to be installed.
    \\
    \\Homepage: https://github.com/simoarpe/ziggity
    \\
;

/// A TUI owns the terminal, so `std.log` output must never reach stderr — it
/// lands on the alt-screen and corrupts it. vaxis logs capability detection at
/// `.info` during startup (e.g. "kitty keyboard capability detected"), which is
/// what produced the garbled text on launch. Route all logging to a no-op sink;
/// app errors surface through the UI, and the pre-TUI startup error path in
/// `main` writes to stderr directly (before the alt-screen is entered).
pub const std_options: std.Options = .{
    .logFn = discardLog,
};

fn discardLog(
    comptime _: std.log.Level,
    comptime _: @EnumLiteral(),
    comptime _: []const u8,
    _: anytype,
) void {}

pub fn main(init: std.process.Init) !void {
    // When git runs us as its GIT_ASKPASS/SSH_ASKPASS program, the marker env
    // var is set: echo the requested credential and exit before touching the
    // terminal or requiring a git repository.
    if (init.environ_map.get(ziggity.app.askpass_env_marker) != null) {
        return ziggity.app.runAskpassHelper(init);
    }

    // `--help` / `--version`: print and exit before touching the terminal.
    if (try handleCliFlags(init)) return;

    var app = ziggity.app.App.init(init.gpa, init.io, init.environ_map) catch |err| blk: {
        // Not in a repo: offer to create one here and continue; otherwise exit
        // cleanly. Any other startup error is reported and aborts.
        if (err == error.NotGitRepository) {
            if (!offerCreateRepo(init)) std.process.exit(0);
            break :blk ziggity.app.App.init(init.gpa, init.io, init.environ_map) catch |e2| {
                reportStartupError(init, e2);
                std.process.exit(1);
            };
        }
        reportStartupError(init, err);
        std.process.exit(1);
    };
    defer app.deinit();

    ziggity.tui.run(init, &app) catch |err| {
        var buf: [1024]u8 = undefined;
        var w: std.Io.File.Writer = .init(.stderr(), init.io, &buf);
        const writer = &w.interface;
        // Coerce to `anyerror` so the switch is independent of run()'s exact
        // inferred error set.
        switch (@as(anyerror, err)) {
            error.NoTerminal => writer.writeAll("ziggity: no interactive terminal available — run it from a terminal (not with input/output redirected, or in a non-interactive environment)\n") catch {},
            else => writer.print("ziggity: terminal UI error: {s}\n", .{@errorName(err)}) catch {},
        }
        writer.flush() catch {};
        std.process.exit(1);
    };
}

/// Handle `--help` / `--version` (and short forms) from the first argument.
/// Prints to stdout and returns true when a flag was handled (the caller then
/// returns without starting the TUI). An unknown `-…` option is reported on
/// stderr and exits with code 2. Uses `initAllocator` for cross-platform args.
fn handleCliFlags(init: std.process.Init) !bool {
    var it = try std.process.Args.Iterator.initAllocator(init.minimal.args, init.gpa);
    defer it.deinit();
    _ = it.next(); // argv[0]
    const arg = it.next() orelse return false;

    if (eqAny(arg, &.{ "-h", "--help" })) {
        try writeStdout(init, help_text);
        return true;
    }
    if (eqAny(arg, &.{ "-v", "-V", "--version" })) {
        try writeStdout(init, "ziggity " ++ build_options.version ++ "\n");
        return true;
    }
    // Ziggity takes no options; flag it so a typo (e.g. `--verison`) isn't
    // silently swallowed into launching the TUI. A non-`-` argument is ignored.
    if (arg.len > 0 and arg[0] == '-') {
        var buf: [256]u8 = undefined;
        var w: std.Io.File.Writer = .init(.stderr(), init.io, &buf);
        w.interface.print("ziggity: unknown option '{s}'\nTry 'ziggity --help'.\n", .{arg}) catch {};
        w.interface.flush() catch {};
        std.process.exit(2);
    }
    return false;
}

fn eqAny(arg: []const u8, options: []const []const u8) bool {
    for (options) |o| {
        if (std.mem.eql(u8, arg, o)) return true;
    }
    return false;
}

fn writeStdout(init: std.process.Init, text: []const u8) !void {
    var buf: [512]u8 = undefined;
    var w: std.Io.File.Writer = .init(.stdout(), init.io, &buf);
    try w.interface.writeAll(text);
    try w.interface.flush();
}

/// Prompt (on the cooked terminal, before the TUI starts) to create a git repo
/// in the current directory. Returns true if one was created and we should
/// continue, false if the user declined. A failed `git init` reports and exits.
fn offerCreateRepo(init: std.process.Init) bool {
    var out_buf: [256]u8 = undefined;
    var out_w: std.Io.File.Writer = .init(.stderr(), init.io, &out_buf);
    const out = &out_w.interface;
    out.writeAll("Not in a git repository. Create one here? (y/N) ") catch {};
    out.flush() catch {};

    var in_buf: [128]u8 = undefined;
    var in_r: std.Io.File.Reader = .init(.stdin(), init.io, &in_buf);
    // No newline (EOF / non-interactive stdin) counts as "no".
    const line = in_r.interface.takeDelimiterExclusive('\n') catch "";
    const answer = std.mem.trim(u8, line, " \t\r");
    if (!(answer.len > 0 and (answer[0] == 'y' or answer[0] == 'Y'))) return false;

    const result = std.process.run(init.gpa, init.io, .{
        .argv = &.{ "git", "init" },
        .environ_map = init.environ_map,
        .stdout_limit = .limited(64 * 1024),
        .stderr_limit = .limited(64 * 1024),
    }) catch {
        out.writeAll("ziggity: failed to run git init\n") catch {};
        out.flush() catch {};
        std.process.exit(1);
    };
    const ok = switch (result.term) {
        .exited => |code| code == 0,
        else => false,
    };
    if (!ok) {
        out.print("ziggity: git init failed: {s}\n", .{result.stderr}) catch {};
        out.flush() catch {};
        std.process.exit(1);
    }
    // Echo git's confirmation (e.g. "Initialized empty Git repository in …").
    out.writeAll(result.stdout) catch {};
    out.flush() catch {};
    init.gpa.free(result.stdout);
    init.gpa.free(result.stderr);
    return true;
}

/// Report a non-repo startup failure on stderr (no error-return trace).
fn reportStartupError(init: std.process.Init, err: anyerror) void {
    var buf: [1024]u8 = undefined;
    var w: std.Io.File.Writer = .init(.stderr(), init.io, &buf);
    const writer = &w.interface;
    writer.print("ziggity: startup failed: {s}\n", .{@errorName(err)}) catch {};
    writer.flush() catch {};
}

test {
    std.testing.refAllDecls(@This());
}
