/* git/commit.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use std::path::Path;

use super::diff::file_diffs_from_diff;
use super::types::*;
use super::{git_err, open_repo};

pub fn create_commit(repo_path: &Path, message: &str) -> Result<(), String> {
    let repo = open_repo(repo_path)?;

    let sig = repo.signature().map_err(git_err("Failed to get signature"))?;

    let mut index = repo.index().map_err(git_err("Failed to open index"))?;

    let tree_oid = index.write_tree().map_err(git_err("Failed to write tree"))?;

    let tree = repo.find_tree(tree_oid).map_err(git_err("Failed to find tree"))?;

    let parents: Vec<git2::Commit> = match repo.head() {
        Ok(head) => {
            let commit = head
                .peel_to_commit()
                .map_err(git_err("Failed to peel HEAD to commit"))?;

            vec![commit]
        }

        Err(_) => vec![],
    };

    let parent_refs: Vec<&git2::Commit> = parents.iter().collect();

    repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parent_refs)
        .map_err(git_err("Failed to create commit"))?;

    Ok(())
}

pub fn load_commit_message(repo_path: &Path, oid_str: &str) -> Result<(String, String), String> {
    let repo = open_repo(repo_path)?;
    let oid = git2::Oid::from_str(oid_str).map_err(git_err("Invalid OID"))?;
    let commit = repo.find_commit(oid).map_err(git_err("Failed to find commit"))?;

    let message = commit.message().unwrap_or("");

    if let Some((subject, body)) = message.split_once("\n\n") {
        Ok((subject.to_string(), body.trim_end().to_string()))
    } else {
        Ok((message.trim_end().to_string(), String::new()))
    }
}

pub fn get_head_commit_message(repo_path: &Path) -> Result<(String, String), String> {
    let repo = open_repo(repo_path)?;

    let head_oid = repo
        .head()
        .map_err(git_err("Failed to get HEAD"))?
        .peel_to_commit()
        .map_err(git_err("Failed to peel HEAD to commit"))?
        .id();

    load_commit_message(repo_path, &head_oid.to_string())
}

pub fn amend_commit(repo_path: &Path, message: &str) -> Result<(), String> {
    let repo = open_repo(repo_path)?;

    let sig = repo.signature().map_err(git_err("Failed to get signature"))?;

    let mut index = repo.index().map_err(git_err("Failed to open index"))?;

    let tree_oid = index.write_tree().map_err(git_err("Failed to write tree"))?;

    let tree = repo.find_tree(tree_oid).map_err(git_err("Failed to find tree"))?;

    let head_commit = repo
        .head()
        .map_err(git_err("Failed to get HEAD"))?
        .peel_to_commit()
        .map_err(git_err("Failed to peel HEAD to commit"))?;

    head_commit
        .amend(Some("HEAD"), None, Some(&sig), None, Some(message), Some(&tree))
        .map_err(git_err("Failed to amend commit"))?;

    Ok(())
}

pub fn load_commits_page(
    repo_path: &Path,
    ref_name: &str,
    start_after: Option<&str>,
    page_size: usize,
) -> Result<CommitPage, String> {
    let repo = open_repo(repo_path)?;

    let start_oid = match start_after {
        Some(oid_str) => git2::Oid::from_str(oid_str).map_err(git_err("Invalid OID"))?,
        None => {
            let obj = repo
                .revparse_single(ref_name)
                .map_err(git_err("Failed to resolve ref"))?;

            obj.peel_to_commit().map_err(git_err("Failed to peel to commit"))?.id()
        }
    };

    let mut revwalk = repo.revwalk().map_err(git_err("Failed to create revwalk"))?;

    revwalk
        .set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME)
        .map_err(git_err("Failed to set sorting"))?;

    revwalk.push(start_oid).map_err(git_err("Failed to push to revwalk"))?;

    // skip the first commit, it is the same as the last of the previous page
    if start_after.is_some()
        && let Some(first) = revwalk.next()
    {
        first.map_err(git_err("Revwalk error"))?;
    }

    let mut commits = Vec::with_capacity(page_size);
    let mut has_more = false;
    for oid_result in revwalk.take(page_size + 1) {
        let oid = oid_result.map_err(git_err("Revwalk error"))?;

        if commits.len() == page_size {
            has_more = true;
            break;
        }

        let commit = repo.find_commit(oid).map_err(git_err("Failed to find commit"))?;

        let message = commit.message().unwrap_or("");
        let (summary, body) = match message.split_once('\n') {
            Some((s, b)) => (s.to_string(), b.trim_start_matches('\n').to_string()),
            None => (message.to_string(), String::new()),
        };

        let author_sig = commit.author();
        let committer_sig = commit.committer();

        commits.push(CommitInfo {
            oid: oid.to_string(),
            summary,
            body,
            author: author_sig.name().unwrap_or("Unknown").to_string(),
            author_email: author_sig.email().unwrap_or("").to_string(),
            time: author_sig.when().seconds(),
            committer: committer_sig.name().unwrap_or("Unknown").to_string(),
            committer_email: committer_sig.email().unwrap_or("").to_string(),
            committer_time: committer_sig.when().seconds(),
            parents: commit.parent_ids().map(|id| id.to_string()).collect(),
            is_merge: commit.parent_count() > 1,
        });
    }

    Ok(CommitPage { commits, has_more })
}

pub fn load_commit_diff(repo_path: &Path, oid_str: &str) -> Result<Vec<FileDiff>, String> {
    let repo = open_repo(repo_path)?;
    let oid = git2::Oid::from_str(oid_str).map_err(git_err("Invalid OID"))?;
    let commit = repo.find_commit(oid).map_err(git_err("Failed to find commit"))?;

    let commit_tree = commit.tree().map_err(git_err("Failed to get commit tree"))?;

    let parent_tree = if commit.parent_count() > 0 {
        Some(
            commit
                .parent(0)
                .map_err(git_err("Failed to get parent commit"))?
                .tree()
                .map_err(git_err("Failed to get parent tree"))?,
        )
    } else {
        None
    };

    let diff = repo
        .diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), None)
        .map_err(git_err("Failed to generate diff"))?;

    file_diffs_from_diff(&diff)
}
