use serde::{Deserialize, Serialize};

use crate::{
    bare::GitBare,
    cmd::{
        commit::CommitSignature, merge::MergeOptions, oid::ObjectId,
        parse::format_git_timestamp,
    },
    errors::{GitError, GitResult},
};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MergeCommitParams {
    pub their_commit: ObjectId,
    pub author: CommitSignature,
    pub committer: CommitSignature,
    pub message: String,
    pub update_ref: Option<String>,
    pub options: Option<MergeOptions>,
}

impl GitBare {
    pub fn merge_commit(
        &self,
        params: MergeCommitParams,
    ) -> GitResult<ObjectId> {
        let repo = self.gix_repo()?;
        let head_id =
            repo.head_id().map_err(|e| GitError::Gix(e.to_string()))?;
        let head_oid = ObjectId::new(head_id.detach().to_hex().to_string());
        let mut merge_tree_args =
            vec!["merge-tree".to_string(), "--write-tree".to_string()];
        if let Some(opts) = &params.options {
            if opts.find_renames {
                merge_tree_args.push("--find-renames".to_string());
                if opts.rename_threshold > 0 {
                    merge_tree_args.push(format!(
                        "--rename-threshold={}",
                        opts.rename_threshold
                    ));
                }
            }
            if opts.fail_on_conflict {
                merge_tree_args.push("--fail-on-conflict".to_string());
            }
        }
        merge_tree_args.push(head_oid.as_str().to_string());
        merge_tree_args.push(params.their_commit.as_str().to_string());

        let merge_tree_output = self.git_command_stdout(merge_tree_args)?;
        let tree_oid_str =
            merge_tree_output.lines().next().unwrap_or("").trim();
        if tree_oid_str.is_empty() {
            return Err(GitError::CommandFailed {
                status_code: None,
                stderr: "merge-tree produced no output".to_string(),
            });
        }

        let tree_oid = ObjectId::new(tree_oid_str);
        let commit_tree_args = vec![
            "commit-tree".to_string(),
            tree_oid.as_str().to_string(),
            "-p".to_string(),
            head_oid.as_str().to_string(),
            "-p".to_string(),
            params.their_commit.as_str().to_string(),
            "-F".to_string(),
            "-".to_string(), // read message from stdin
        ];

        let author_timestamp = format_git_timestamp(
            params.author.time_secs,
            params.author.offset_minutes,
        );
        let committer_timestamp = format_git_timestamp(
            params.committer.time_secs,
            params.committer.offset_minutes,
        );

        let commit_output = self.git_command_with(
            crate::cmd::command::GitCommandParams::new(commit_tree_args)
                .with_stdin(params.message.as_bytes().to_vec())
                .with_env(
                    "GIT_AUTHOR_NAME".to_string(),
                    params.author.name.clone(),
                )
                .with_env(
                    "GIT_AUTHOR_EMAIL".to_string(),
                    params.author.email.clone(),
                )
                .with_env("GIT_AUTHOR_DATE".to_string(), author_timestamp)
                .with_env(
                    "GIT_COMMITTER_NAME".to_string(),
                    params.committer.name.clone(),
                )
                .with_env(
                    "GIT_COMMITTER_EMAIL".to_string(),
                    params.committer.email.clone(),
                )
                .with_env(
                    "GIT_COMMITTER_DATE".to_string(),
                    committer_timestamp,
                ),
        )?;

        let stdout_str = commit_output.stdout_lossy();
        let commit_oid_str = stdout_str.trim();
        if commit_oid_str.is_empty() {
            return Err(GitError::CommandFailed {
                status_code: commit_output.status_code,
                stderr: commit_output.stderr_lossy(),
            });
        }

        let result_oid = ObjectId::new(commit_oid_str);
        if let Some(ref_name) = &params.update_ref {
            self.git_command_trusted_stdout(vec![
                "update-ref".to_string(),
                ref_name.clone(),
                result_oid.as_str().to_string(),
            ])?;
        }

        Ok(result_oid)
    }
}
