mod ci;
mod cli;
mod config;
mod gh;
mod git;
mod graph;
mod lock;
mod ops;
mod remote;
mod submodule;
mod tasks;
mod ui;
mod workspace;

use clap::error::ErrorKind;
use clap::Parser;
use std::ffi::{OsStr, OsString};
use std::process::ExitCode;

fn main() -> ExitCode {
    let raw_args: Vec<OsString> = std::env::args_os().collect();
    match cli::Cli::try_parse_from(&raw_args) {
        Ok(args) => {
            let fallback_args = git_fallback_args_for_parsed(&args, &raw_args);
            if no_repoverse_config() {
                if let Some(fallback_args) = fallback_args {
                    return run_git_fallback(fallback_args);
                }
            }
            match cli::run(args) {
                Ok(()) => ExitCode::SUCCESS,
                Err(e) => {
                    if no_repoverse_config() && error_mentions_missing_config(&e) {
                        if let Some(fallback_args) = fallback_args {
                            return run_git_fallback(fallback_args);
                        }
                    }
                    eprintln!("error: {e:#}");
                    ExitCode::FAILURE
                }
            }
        }
        Err(e) => {
            if no_repoverse_config() && should_fallback_parse_error(e.kind()) {
                let git_args = raw_args.get(1..).unwrap_or(&[]);
                if !git_args.is_empty() {
                    return run_git_fallback(git_args);
                }
            }
            let code = e.exit_code();
            let _ = e.print();
            exit_code(code)
        }
    }
}

fn no_repoverse_config() -> bool {
    let Ok(cwd) = std::env::current_dir() else {
        return false;
    };
    config::Config::discover(&cwd).is_none()
}

fn error_mentions_missing_config(e: &anyhow::Error) -> bool {
    format!("{e:#}").contains(config::CONFIG_FILE)
}

fn should_fallback_parse_error(kind: ErrorKind) -> bool {
    !matches!(kind, ErrorKind::DisplayHelp | ErrorKind::DisplayVersion)
}

fn git_fallback_args_for_parsed<'a>(
    args: &cli::Cli,
    raw_args: &'a [OsString],
) -> Option<&'a [OsString]> {
    if let cli::Cmd::Git { .. } = args.command {
        let git_args = git_args_after_rv_git(raw_args);
        return (!git_args.is_empty()).then_some(git_args);
    }

    let git_args = raw_args.get(1..).unwrap_or(&[]);
    let command = git_command_from_args(git_args)?;
    is_plain_git_command(command).then_some(git_args)
}

fn git_args_after_rv_git(raw_args: &[OsString]) -> &[OsString] {
    let Some(mut args) = raw_args.get(2..) else {
        return &[];
    };
    if args.first().is_some_and(|arg| arg == OsStr::new("--")) {
        args = &args[1..];
    }
    args
}

fn git_command_from_args(args: &[OsString]) -> Option<&str> {
    let mut i = 0;
    while i < args.len() {
        let arg = args[i].to_string_lossy();
        match arg.as_ref() {
            "-C" | "-c" | "--git-dir" | "--work-tree" | "--namespace" | "--exec-path"
            | "--config-env" => i += 2,
            "--no-pager"
            | "--paginate"
            | "-p"
            | "--bare"
            | "--literal-pathspecs"
            | "--no-literal-pathspecs"
            | "--glob-pathspecs"
            | "--noglob-pathspecs"
            | "--icase-pathspecs" => i += 1,
            _ if arg.starts_with("--git-dir=")
                || arg.starts_with("--work-tree=")
                || arg.starts_with("--namespace=")
                || arg.starts_with("--exec-path=")
                || arg.starts_with("--config-env=") =>
            {
                i += 1
            }
            _ if arg.starts_with('-') => return None,
            _ => return args[i].to_str(),
        }
    }
    None
}

fn is_plain_git_command(command: &str) -> bool {
    matches!(
        command,
        "add"
            | "am"
            | "apply"
            | "archive"
            | "bisect"
            | "blame"
            | "branch"
            | "bundle"
            | "checkout"
            | "cherry-pick"
            | "clean"
            | "clone"
            | "commit"
            | "config"
            | "describe"
            | "diff"
            | "fetch"
            | "format-patch"
            | "fsck"
            | "gc"
            | "grep"
            | "init"
            | "log"
            | "merge"
            | "mv"
            | "pull"
            | "push"
            | "rebase"
            | "remote"
            | "reset"
            | "restore"
            | "revert"
            | "rm"
            | "show"
            | "stash"
            | "status"
            | "submodule"
            | "switch"
            | "tag"
            | "worktree"
    )
}

fn run_git_fallback(args: &[OsString]) -> ExitCode {
    eprintln!(
        "warning: no {} found; running git {}",
        config::CONFIG_FILE,
        git::display_args(args)
    );
    match git::passthrough(args) {
        Ok(code) => exit_code(code),
        Err(e) => {
            eprintln!("error: {e:#}");
            ExitCode::FAILURE
        }
    }
}

fn exit_code(code: i32) -> ExitCode {
    ExitCode::from(u8::try_from(code).unwrap_or(1))
}
