use anyhow::Result;
use std::process::{Command, Output};

/// 現在の変更をコミットした上でタグ付けし、タグ名を返す
pub(super) fn commit(tag_name: Option<String>) -> Result<String> {
    // gitリポジトリが存在しない場合は初期化
    if !is_git_repository()? {
        println!("Initializing git repository to tag the current changes...");
        git_init()?;
        git_add_all()?;
        git_commit("Initial commit")?;
    }

    git_add_all()?;
    let has_diff = git_diff()?;

    if has_diff {
        git_commit("automatically generated by pahcer")?;
    }

    let tag_name = generate_tag_name(tag_name)?;
    git_tag(&tag_name)?;

    if has_diff {
        git_reset()?;
    }

    Ok(tag_name)
}

/// pahcer関連のタグを削除する
pub(super) fn prune_tags() -> Result<()> {
    let tags = list_tags("pahcer/*")?;

    for tag in tags.iter() {
        check_return_code(Command::new("git").args(["tag", "-d", tag]).output()?)?;

        println!("Deleted tag: {tag}");
    }

    Ok(())
}

/// タグ名を生成する
fn generate_tag_name(tag_name: Option<String>) -> Result<String> {
    let tag_suffix = match tag_name {
        Some(name) => name,
        None => {
            // 現在のコミットハッシュの8桁を取得
            get_current_commit_hash()?
        }
    };

    Ok(format!("pahcer/{tag_suffix}"))
}

/// 現在のコミットハッシュの最初の8桁を取得する
fn get_current_commit_hash() -> Result<String> {
    let output = Command::new("git")
        .args(["rev-parse", "--short=8", "HEAD"])
        .output()?;

    let hash = read_stdout(output)?;
    Ok(hash.trim().to_string())
}

/// タグを生成する
fn git_tag(tag_name: &str) -> Result<()> {
    // 既に同じタグが存在する場合は何もしない
    if tag_exists(tag_name)? {
        println!("Tag already exists: {tag_name}. Skipping tag creation.");
        return Ok(());
    }

    check_return_code(
        Command::new("git")
            .args([
                "tag",
                "-a",
                tag_name,
                "-m",
                "automatically generated by pahcer",
            ])
            .output()?,
    )
}

/// 指定されたタグが存在するかどうかをチェックする
fn tag_exists(tag_name: &str) -> Result<bool> {
    let output = Command::new("git")
        .args(["tag", "--list", tag_name])
        .output()?;

    let tags = read_stdout(output)?;
    Ok(!tags.trim().is_empty())
}

/// 直前のコミットを取り消す
fn git_reset() -> Result<()> {
    check_return_code(
        Command::new("git")
            .args(["reset", "--mixed", "HEAD^"])
            .output()?,
    )
}

/// 全てのファイルをステージングする
fn git_add_all() -> Result<()> {
    check_return_code(Command::new("git").args(["add", "--all"]).output()?)?;
    Ok(())
}

/// 変更があるかどうかを判定する
fn git_diff() -> Result<bool> {
    let diffs = read_stdout(
        Command::new("git")
            .args(["diff", "--cached", "--name-only"])
            .output()?,
    )?;

    Ok(!diffs.is_empty())
}

/// 変更をコミットする
fn git_commit(message: &str) -> Result<()> {
    check_return_code(
        Command::new("git")
            .args(["commit", "-m", message])
            .output()?,
    )
}

/// タグ名のリストを取得する
fn list_tags(pattern: &str) -> Result<Vec<String>, anyhow::Error> {
    let tags = read_stdout(
        Command::new("git")
            .args(["tag", "--list", pattern])
            .output()?,
    )?;

    Ok(tags.lines().map(|s| s.to_string()).collect())
}

/// コマンドの実行結果を文字列として取得する
fn read_stdout(output: Output) -> Result<String> {
    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
        Ok(stdout)
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        Err(anyhow::anyhow!(stderr))
    }
}

/// コマンドが正常終了したかどうかをチェックする
fn check_return_code(output: Output) -> Result<()> {
    if output.status.success() {
        Ok(())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        Err(anyhow::anyhow!(stderr))
    }
}

/// gitリポジトリが存在するかどうかをチェックする
fn is_git_repository() -> Result<bool> {
    let output = Command::new("git")
        .args(["rev-parse", "--git-dir"])
        .output()?;

    Ok(output.status.success())
}

/// gitリポジトリを初期化する
fn git_init() -> Result<()> {
    check_return_code(Command::new("git").args(["init"]).output()?)
}
