use serde::{Deserialize, Serialize};

use crate::{
    bare::GitBare,
    cmd::{command::GitCommandParams, oid::ObjectId},
    errors::{GitError, GitResult},
};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileChange {
    pub path: String,
    pub content: Vec<u8>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateCommitParams {
    pub branch: String,
    pub message: String,
    pub author_name: String,
    pub author_email: String,
    pub committer_name: String,
    pub committer_email: String,
    pub files: Vec<FileChange>,
}

#[derive(Debug, Clone)]
struct TreeEntry {
    mode: String, // e.g. "100644"
    kind: String, // "blob" or "tree"
    oid: ObjectId,
    path: String,
}

impl GitBare {
    /// Create a commit with the given file changes on the specified branch.
    ///
    /// Returns the OID of the newly created commit.
    pub fn commit_create(
        &self,
        params: CreateCommitParams,
    ) -> GitResult<ObjectId> {
        let repo = self.gix_repo()?;

        // 1. Write each file as a blob and collect entries
        let mut new_entries: Vec<TreeEntry> =
            Vec::with_capacity(params.files.len());
        for fc in &params.files {
            let blob_upload_result =
                self.blob_upload(crate::cmd::blob::BlobUploadParams {
                    blob: fc.content.clone(),
                    path: fc.path.clone(),
                })?;
            new_entries.push(TreeEntry {
                mode: "100644".to_string(),
                kind: "blob".to_string(),
                oid: blob_upload_result.id,
                path: fc.path.clone(),
            });
        }

        // 2. Get the parent commit (HEAD of the branch, or initial commit)
        let parent_oid: Option<ObjectId>;
        let existing_tree_entries: Vec<TreeEntry>;

        let branch_ref = format!("refs/heads/{}", params.branch);

        match repo.find_reference(&branch_ref) {
            Ok(r) => {
                let oid = r
                    .into_fully_peeled_id()
                    .map_err(|e| GitError::Gix(e.to_string()))?
                    .detach();
                let oid_str = oid.to_hex().to_string();
                let commit_oid = ObjectId::new(oid_str);
                let commit = self.commit_info(commit_oid.clone())?;

                let tree_entries = self._ls_tree(commit.tree_id)?;
                parent_oid = Some(commit_oid);
                existing_tree_entries = tree_entries;
            }
            Err(_) => {
                parent_oid = None;
                existing_tree_entries = Vec::new();
            }
        }

        // 3. Merge new entries into existing tree (replace by path)
        let mut merged: Vec<TreeEntry> = Vec::new();
        let mut seen_paths: std::collections::HashSet<String> =
            std::collections::HashSet::new();

        for entry in new_entries {
            seen_paths.insert(entry.path.clone());
            merged.push(entry);
        }
        for entry in existing_tree_entries {
            if !seen_paths.contains(&entry.path) {
                merged.push(entry);
            }
        }

        // 4. Create tree with git mktree
        let tree_oid = self._mktree(&merged)?;

        // 5. Create commit with git commit-tree
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs() as i64;
        let timestamp = crate::cmd::parse::format_git_timestamp(now, 0);

        let mut commit_tree_args =
            vec!["commit-tree".to_string(), tree_oid.as_str().to_string()];
        if let Some(parent) = &parent_oid {
            commit_tree_args.push("-p".to_string());
            commit_tree_args.push(parent.as_str().to_string());
        }
        commit_tree_args.push("-F".to_string());
        commit_tree_args.push("-".to_string());

        let commit_output = self.git_command_with(
            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(), timestamp.clone())
                .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(), timestamp),
        )?;

        if !commit_output.success {
            return Err(GitError::CommandFailed {
                status_code: commit_output.status_code,
                stderr: commit_output.stderr_lossy(),
            });
        }

        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: "no commit OID produced".to_string(),
            });
        }

        let commit_oid = ObjectId::new(commit_oid_str);

        // 6. Update the branch ref
        let update_ref_args = vec![
            "update-ref".to_string(),
            branch_ref,
            commit_oid.as_str().to_string(),
        ];
        let _update_output = self.git_command_with(
            GitCommandParams::new(update_ref_args).trusted(),
        )?;

        Ok(commit_oid)
    }

    /// List tree entries (mode, type, oid, path) using git ls-tree.
    fn _ls_tree(&self, tree_oid: ObjectId) -> GitResult<Vec<TreeEntry>> {
        let output = self.git_command_with(GitCommandParams::new(vec![
            "ls-tree".to_string(),
            tree_oid.as_str().to_string(),
        ]))?;

        if !output.success {
            return Err(GitError::CommandFailed {
                status_code: output.status_code,
                stderr: output.stderr_lossy(),
            });
        }

        let stdout = output.stdout_lossy();
        let mut entries = Vec::new();

        for line in stdout.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            // Output format: <mode> <type> <oid>\t<path>
            let parts: Vec<&str> = line.splitn(2, '\t').collect();
            if parts.len() < 2 {
                continue;
            }
            let meta: Vec<&str> = parts[0].split_whitespace().collect();
            if meta.len() < 3 {
                continue;
            }
            entries.push(TreeEntry {
                mode: meta[0].to_string(),
                kind: meta[1].to_string(),
                oid: ObjectId::new(meta[2]),
                path: parts[1].to_string(),
            });
        }

        Ok(entries)
    }

    /// Create a tree object from entries using git mktree.
    fn _mktree(&self, entries: &[TreeEntry]) -> GitResult<ObjectId> {
        // Build input for git mktree:
        // Each line: <mode> <type> <oid>\t<path>
        let mut input = String::new();
        for entry in entries {
            input.push_str(&format!(
                "{} {} {}\t{}\n",
                entry.mode,
                entry.kind,
                entry.oid.as_str(),
                entry.path
            ));
        }

        let output = self.git_command_with(
            GitCommandParams::new(vec!["mktree".to_string()])
                .with_stdin(input.into_bytes()),
        )?;

        if !output.success {
            return Err(GitError::CommandFailed {
                status_code: output.status_code,
                stderr: output.stderr_lossy(),
            });
        }

        let stdout = output.stdout_lossy();
        let oid_str = stdout.trim();
        Ok(ObjectId::new(oid_str))
    }
}
