use std::path::PathBuf;

use anyhow::Result;
use clap::{ArgGroup, Args as ClapArgs, ColorChoice, Parser, Subcommand, builder::styling};

use crate::remotes::RemoteSpec;

// Custom styles for clap help output
fn styles() -> styling::Styles {
    styling::Styles::styled()
        .header(styling::AnsiColor::Green.on_default().bold())
        .usage(styling::AnsiColor::Green.on_default().bold())
        .literal(styling::AnsiColor::Cyan.on_default())
        .placeholder(styling::AnsiColor::Blue.on_default())
        .error(styling::AnsiColor::Red.on_default().bold())
        .valid(styling::AnsiColor::Green.on_default())
        .invalid(styling::AnsiColor::Yellow.on_default())
}

#[derive(Parser, Debug)]
#[command(
    name = "madgit",
    version,
    about = "Mass run commands distributed across multiple Git repositories",
    color = ColorChoice::Auto,
    styles = styles(),
    after_help = "Unknown commands are forwarded to Git and executed in each repository, similar to 'exec'"
)]
pub struct Args {
    #[command(subcommand)]
    pub command: Commands,

    #[arg(long, value_name = "DIR", help = "Directory containing repositories")]
    pub dir: Option<PathBuf>,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    #[command(
        about = "Clone repositories from a file or remote owner",
        group = ArgGroup::new("clone_source")
            .required(true)
            .multiple(false)
            .args(["file", "github", "gitlab", "forgejo"])
    )]
    Clone {
        #[arg(short, long, help = "Read repository URLs from a file")]
        file: Option<PathBuf>,
        #[command(flatten)]
        remote: RemoteArgs,
    },
    #[command(
        about = "Print repository URLs discovered from a remote owner",
        group = ArgGroup::new("remote_provider")
            .required(true)
            .multiple(false)
            .args(["github", "gitlab", "forgejo"])
    )]
    Repos {
        #[command(flatten)]
        remote: RemoteArgs,
    },
    #[command(about = "Show Git status for every repository")]
    Status {
        #[arg(long, help = "Only show repositories with changes")]
        dirty: bool,
    },
    #[command(
        name = "checkout-main",
        about = "Checkout main or master branch in each repository"
    )]
    CheckoutMain,
    #[command(about = "Execute arbitrary command in each repository")]
    Exec {
        #[arg(last = true, help = "Command and arguments to execute")]
        command: Vec<String>,
    },
    #[command(about = "List subdirectories that are not Git repositories")]
    NoGit,
    #[command(external_subcommand)]
    External(Vec<String>),
}

#[derive(ClapArgs, Debug)]
pub struct RemoteArgs {
    #[arg(
        long,
        value_name = "OWNER",
        help = "GitHub user or organization whose repositories should be used"
    )]
    pub github: Option<String>,
    #[arg(
        long,
        value_name = "URL-OR-OWNER",
        help = "GitLab owner, may include a self-hosted URL"
    )]
    pub gitlab: Option<String>,
    #[arg(
        long,
        value_name = "URL-OR-OWNER",
        help = "Forgejo owner, may include a self-hosted URL"
    )]
    pub forgejo: Option<String>,
}

impl RemoteArgs {
    pub fn into_spec(self) -> Result<Option<RemoteSpec>> {
        match (self.gitlab, self.github, self.forgejo) {
            (Some(arg), _, _) => Ok(Some(RemoteSpec::gitlab(&arg)?)),
            (_, Some(arg), _) => Ok(Some(RemoteSpec::github(arg)?)),
            (_, _, Some(arg)) => Ok(Some(RemoteSpec::forgejo(&arg)?)),
            _ => Ok(None),
        }
    }
}

#[cfg(test)]
mod tests {
    use clap::Parser;

    use super::RemoteArgs;
    use super::{Args, Commands};
    use crate::remotes::{ProviderKind, RemoteSpec};

    #[test]
    fn into_spec_prefers_gitlab_when_multiple_values_are_present() {
        let spec = RemoteArgs {
            github: Some("github-owner".to_string()),
            gitlab: Some("gitlab.example.com/gitlab-owner".to_string()),
            forgejo: Some("forgejo-owner".to_string()),
        }
        .into_spec()
        .unwrap();

        assert_eq!(
            spec,
            Some(RemoteSpec {
                kind: ProviderKind::GitLab,
                base_url: Some("gitlab.example.com".to_string()),
                owner: "gitlab-owner".to_string(),
            })
        );
    }

    #[test]
    fn into_spec_returns_github_when_only_github_is_present() {
        let spec = RemoteArgs {
            github: Some("wykwit".to_string()),
            gitlab: None,
            forgejo: None,
        }
        .into_spec()
        .unwrap();

        assert_eq!(spec, Some(RemoteSpec::github("wykwit").unwrap()));
    }

    #[test]
    fn into_spec_returns_none_when_no_remote_is_selected() {
        let spec = RemoteArgs {
            github: None,
            gitlab: None,
            forgejo: None,
        }
        .into_spec()
        .unwrap();

        assert_eq!(spec, None);
    }

    #[test]
    fn parses_clone_command_with_github_and_directory() {
        let args =
            Args::try_parse_from(["madgit", "--dir", "./repos", "clone", "--github", "wykwit"])
                .unwrap();

        assert!(matches!(args.command, Commands::Clone { .. }));
        if let Commands::Clone { file, remote } = args.command {
            assert_eq!(file, None);
            assert_eq!(remote.github.as_deref(), Some("wykwit"));
            assert_eq!(args.dir.unwrap().to_string_lossy(), "./repos");
        }
    }

    #[test]
    fn parses_external_git_subcommand() {
        let args = Args::try_parse_from(["madgit", "rev-parse", "--show-toplevel"]).unwrap();

        assert!(matches!(
            args.command,
            Commands::External(ref command)
            if command == &["rev-parse".to_string(), "--show-toplevel".to_string()]
        ));
    }

    #[test]
    fn parses_checkout_main_command() {
        let args = Args::try_parse_from(["madgit", "checkout-main"]).unwrap();

        assert!(matches!(args.command, Commands::CheckoutMain));
    }

    #[test]
    fn into_spec_returns_forgejo_when_only_forgejo_is_present() {
        let spec = RemoteArgs {
            github: None,
            gitlab: None,
            forgejo: Some("codeberg.org/wykwit".to_string()),
        }
        .into_spec()
        .unwrap();

        assert_eq!(
            spec,
            Some(RemoteSpec::forgejo("codeberg.org/wykwit").unwrap())
        );
    }

    #[test]
    fn rejects_clone_without_source() {
        let error = Args::try_parse_from(["madgit", "clone"]).unwrap_err();

        assert_eq!(
            error.kind(),
            clap::error::ErrorKind::MissingRequiredArgument
        );
    }
}
