use anyhow::{bail, Context, Result};
use git2::{Repository, Signature, StatusOptions, Config, Oid, ObjectType};
use std::path::{Path, PathBuf};
use std::collections::HashMap;

pub mod changelog;
pub mod commit;
pub mod tag;

pub use changelog::ChangelogGenerator;
pub use commit::CommitBuilder;
pub use tag::TagBuilder;

/// Git repository wrapper with enhanced cross-platform support
pub struct GitRepo {
    repo: Repository,
    path: PathBuf,
    config: Option<Config>,
}

impl GitRepo {
    /// Open a git repository
    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref();
        let absolute_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());

        let repo = Repository::discover(&absolute_path)
            .or_else(|_| Repository::open(&absolute_path))
            .with_context(|| {
                format!(
                    "Failed to open git repository at '{:?}'. Please ensure:\n\
                    1. The directory is set as safe (run: git config --global --add safe.directory \"{}\")\n\
                    2. The path is correct and contains a valid '.git' folder.",
                    absolute_path,
                    absolute_path.display()
                )
            })?;

        let config = repo.config().ok();

        Ok(Self {
            repo,
            path: absolute_path,
            config,
        })
    }

    /// Get repository path
    pub fn path(&self) -> &Path {
        &self.path
    }

    /// Get internal git2 repository
    pub fn inner(&self) -> &Repository {
        &self.repo
    }

    /// Get repository configuration
    pub fn config(&self) -> Option<&Config> {
        self.config.as_ref()
    }

    /// Get a configuration value
    pub fn get_config(&self, key: &str) -> Result<Option<String>> {
        if let Some(ref config) = self.config {
            config.get_string(key).map(Some).map_err(Into::into)
        } else {
            Ok(None)
        }
    }

    /// Get all configuration values matching a pattern
    pub fn get_config_regex(&self, _pattern: &str) -> Result<HashMap<String, String>> {
        Ok(HashMap::new())
    }

    /// Get the configured user name
    pub fn get_user_name(&self) -> Result<String> {
        self.get_config("user.name")?
            .or_else(|| std::env::var("GIT_AUTHOR_NAME").ok())
            .ok_or_else(|| anyhow::anyhow!("User name not configured. Set it with: git config user.name \"Your Name\""))
    }

    /// Get the configured user email
    pub fn get_user_email(&self) -> Result<String> {
        self.get_config("user.email")?
            .or_else(|| std::env::var("GIT_AUTHOR_EMAIL").ok())
            .ok_or_else(|| anyhow::anyhow!("User email not configured. Set it with: git config user.email \"your.email@example.com\""))
    }

    /// Get the configured GPG signing key
    pub fn get_signing_key(&self) -> Result<Option<String>> {
        Ok(self.get_config("user.signingkey")?
            .or_else(|| std::env::var("GIT_SIGNING_KEY").ok()))
    }

    /// Check if commits should be signed by default
    pub fn should_sign_commits(&self) -> bool {
        self.get_config("commit.gpgsign")
            .ok()
            .flatten()
            .and_then(|v| v.parse::<bool>().ok())
            .unwrap_or(false)
    }

    /// Check if tags should be signed by default
    pub fn should_sign_tags(&self) -> bool {
        self.get_config("tag.gpgsign")
            .ok()
            .flatten()
            .and_then(|v| v.parse::<bool>().ok())
            .unwrap_or(false)
    }

    /// Get the GPG program to use
    pub fn get_gpg_program(&self) -> Result<String> {
        if let Some(program) = self.get_config("gpg.program")? {
            return Ok(program);
        }
        
        let default_gpg = if cfg!(windows) {
            "gpg.exe"
        } else {
            "gpg"
        };
        
        Ok(default_gpg.to_string())
    }

    /// Create a signature using repository configuration
    pub fn create_signature(&self) -> Result<Signature<'_>> {
        let name = self.get_user_name()?;
        let email = self.get_user_email()?;
        let time = git2::Time::new(std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64, 0);
        Signature::new(&name, &email, &time).map_err(Into::into)
    }

    /// Check if this is a valid git repository
    pub fn is_valid(&self) -> bool {
        !self.repo.is_bare()
    }

    /// Check if there are uncommitted changes
    pub fn has_changes(&self) -> Result<bool> {
        let statuses = self.repo.statuses(Some(
            StatusOptions::new()
                .include_untracked(true)
                .renames_head_to_index(true)
                .renames_index_to_workdir(true),
        ))?;

        Ok(!statuses.is_empty())
    }

    /// Get staged diff
    pub fn get_staged_diff(&self) -> Result<String> {
        let head = self.repo.head().ok();
        let head_tree = head.as_ref()
            .and_then(|h| h.peel_to_tree().ok());

        let mut index = self.repo.index()?;
        let index_tree = index.write_tree()?;
        let index_tree = self.repo.find_tree(index_tree)?;

        let diff = if let Some(head) = head_tree {
            self.repo.diff_tree_to_index(Some(&head), Some(&index), None)?
        } else {
            self.repo.diff_tree_to_index(None, Some(&index), None)?
        };

        let mut diff_text = String::new();
        diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
            if let Ok(content) = std::str::from_utf8(line.content()) {
                diff_text.push_str(content);
            }
            true
        })?;

        Ok(diff_text)
    }

    /// Get unstaged diff
    pub fn get_unstaged_diff(&self) -> Result<String> {
        let diff = self.repo.diff_index_to_workdir(None, None)?;

        let mut diff_text = String::new();
        diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
            if let Ok(content) = std::str::from_utf8(line.content()) {
                diff_text.push_str(content);
            }
            true
        })?;

        Ok(diff_text)
    }

    /// Get complete diff (staged + unstaged)
    pub fn get_full_diff(&self) -> Result<String> {
        let staged = self.get_staged_diff().unwrap_or_default();
        let unstaged = self.get_unstaged_diff().unwrap_or_default();

        Ok(format!("{}{}", staged, unstaged))
    }

    /// Get list of changed files
    pub fn get_changed_files(&self) -> Result<Vec<String>> {
        let statuses = self.repo.statuses(Some(
            StatusOptions::new()
                .include_untracked(true)
                .renames_head_to_index(true)
                .renames_index_to_workdir(true),
        ))?;

        let mut files = vec![];
        for entry in statuses.iter() {
            if let Some(path) = entry.path() {
                files.push(path.to_string());
            }
        }

        Ok(files)
    }

    /// Get list of staged files
    pub fn get_staged_files(&self) -> Result<Vec<String>> {
        let statuses = self.repo.statuses(Some(
            StatusOptions::new()
                .include_untracked(false),
        ))?;

        let mut files = vec![];
        for entry in statuses.iter() {
            let status = entry.status();
            if status.is_index_new() || status.is_index_modified() || status.is_index_deleted() || status.is_index_renamed() || status.is_index_typechange() {
                if let Some(path) = entry.path() {
                    files.push(path.to_string());
                }
            }
        }

        Ok(files)
    }

    /// Stage files
    pub fn stage_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<()> {
        let mut index = self.repo.index()?;

        for path in paths {
            let path = path.as_ref();
            if path.is_absolute() {
                if let Ok(rel_path) = path.strip_prefix(&self.path) {
                    index.add_path(rel_path)?;
                }
            } else {
                index.add_path(path)?;
            }
        }

        index.write()?;
        Ok(())
    }

    /// Stage all changes including subdirectories
    pub fn stage_all(&self) -> Result<()> {
        let mut index = self.repo.index()?;

        fn add_directory_recursive(index: &mut git2::Index, base_dir: &Path, current_dir: &Path) -> Result<()> {
            for entry in std::fs::read_dir(current_dir)
                .with_context(|| format!("Failed to read directory: {:?}", current_dir))?
            {
                let entry = entry?;
                let path = entry.path();

                if path.is_file() {
                    if let Ok(rel_path) = path.strip_prefix(base_dir) {
                        let _ = index.add_path(rel_path);
                    }
                } else if path.is_dir() {
                    add_directory_recursive(index, base_dir, &path)?;
                }
            }
            Ok(())
        }

        add_directory_recursive(&mut index, &self.path, &self.path)?;
        index.write()?;
        Ok(())
    }

    /// Unstage files
    pub fn unstage_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<()> {
        let head = self.repo.head()?;
        let head_commit = head.peel_to_commit()?;
        let head_tree = head_commit.tree()?;

        let mut index = self.repo.index()?;

        for path in paths {
            let path = path.as_ref();
            let rel_path = if path.is_absolute() {
                path.strip_prefix(&self.path)?
            } else {
                path
            };

            if let Ok(_tree_entry) = head_tree.get_path(rel_path) {
                let tree_id = head_tree.id();
                let tree_obj = self.repo.find_object(tree_id, Some(ObjectType::Tree))?;
                let tree = tree_obj.peel_to_tree()?;
                index.read_tree(&tree)?;
            } else {
                index.remove_path(rel_path)?;
            }
        }

        index.write()?;
        Ok(())
    }

    /// Create a commit
    pub fn commit(&self, message: &str, sign: bool) -> Result<Oid> {
        let signature = self.create_signature()?;
        let head = self.repo.head().ok();

        let parents = if let Some(ref head) = head {
            vec![head.peel_to_commit()?]
        } else {
            vec![]
        };

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

        let oid = if sign {
            self.commit_signed_with_git2(message, &signature)?
        } else {
            let tree_id = self.repo.index()?.write_tree()?;
            let tree = self.repo.find_tree(tree_id)?;

            self.repo.commit(
                Some("HEAD"),
                &signature,
                &signature,
                message,
                &tree,
                &parent_refs,
            )?
        };

        Ok(oid)
    }

    /// Create a signed commit using git CLI
    fn commit_signed_with_git2(&self, message: &str, _signature: &Signature) -> Result<Oid> {
        let temp_file = tempfile::NamedTempFile::new()?;
        std::fs::write(temp_file.path(), message)?;

        let mut cmd = std::process::Command::new("git");
        cmd.args(&["commit", "-S", "-F", temp_file.path().to_str().unwrap()])
            .current_dir(&self.path)
            .output()?;

        let output = std::process::Command::new("git")
            .args(&["commit", "-S", "-F", temp_file.path().to_str().unwrap()])
            .current_dir(&self.path)
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!("Failed to create signed commit: {}", stderr);
        }

        let head = self.repo.head()?;
        Ok(head.target().unwrap())
    }

    /// Get current branch name
    pub fn current_branch(&self) -> Result<String> {
        let head = self.repo.head()?;

        if head.is_branch() {
            let name = head.shorthand()
                .ok_or_else(|| anyhow::anyhow!("Invalid branch name"))?;
            Ok(name.to_string())
        } else {
            bail!("HEAD is not pointing to a branch")
        }
    }

    /// Get current commit hash (short)
    pub fn current_commit_short(&self) -> Result<String> {
        let head = self.repo.head()?;
        let oid = head.target()
            .ok_or_else(|| anyhow::anyhow!("No target for HEAD"))?;
        Ok(oid.to_string()[..8].to_string())
    }

    /// Get current commit hash (full)
    pub fn current_commit(&self) -> Result<String> {
        let head = self.repo.head()?;
        let oid = head.target()
            .ok_or_else(|| anyhow::anyhow!("No target for HEAD"))?;
        Ok(oid.to_string())
    }

    /// Get commit history
    pub fn get_commits(&self, count: usize) -> Result<Vec<CommitInfo>> {
        let mut revwalk = self.repo.revwalk()?;
        revwalk.push_head()?;

        let mut commits = vec![];
        for (i, oid) in revwalk.enumerate() {
            if i >= count {
                break;
            }

            let oid = oid?;
            let commit = self.repo.find_commit(oid)?;

            commits.push(CommitInfo {
                id: oid.to_string(),
                short_id: oid.to_string()[..8].to_string(),
                message: commit.message().unwrap_or("").to_string(),
                author: commit.author().name().unwrap_or("").to_string(),
                email: commit.author().email().unwrap_or("").to_string(),
                time: commit.time().seconds(),
            });
        }

        Ok(commits)
    }

    /// Get commits between two references
    pub fn get_commits_between(&self, from: &str, to: &str) -> Result<Vec<CommitInfo>> {
        let from_obj = self.repo.revparse_single(from)?;
        let to_obj = self.repo.revparse_single(to)?;

        let from_commit = from_obj.peel_to_commit()?;
        let to_commit = to_obj.peel_to_commit()?;

        let mut revwalk = self.repo.revwalk()?;
        revwalk.push(to_commit.id())?;
        revwalk.hide(from_commit.id())?;

        let mut commits = vec![];
        for oid in revwalk {
            let oid = oid?;
            let commit = self.repo.find_commit(oid)?;

            commits.push(CommitInfo {
                id: oid.to_string(),
                short_id: oid.to_string()[..8].to_string(),
                message: commit.message().unwrap_or("").to_string(),
                author: commit.author().name().unwrap_or("").to_string(),
                email: commit.author().email().unwrap_or("").to_string(),
                time: commit.time().seconds(),
            });
        }

        Ok(commits)
    }

    /// Get tags
    pub fn get_tags(&self) -> Result<Vec<TagInfo>> {
        let mut tags = vec![];

        self.repo.tag_foreach(|oid, name| {
            let name = String::from_utf8_lossy(name);
            let name = name.strip_prefix("refs/tags/").unwrap_or(&name);

            if let Ok(commit) = self.repo.find_commit(oid) {
                tags.push(TagInfo {
                    name: name.to_string(),
                    target: oid.to_string(),
                    message: commit.message().unwrap_or("").to_string(),
                });
            }

            true
        })?;

        Ok(tags)
    }

    /// Create a tag
    pub fn create_tag(&self, name: &str, message: Option<&str>, sign: bool) -> Result<()> {
        let head = self.repo.head()?;
        let target = head.peel_to_commit()?;

        if let Some(msg) = message {
            let sig = self.create_signature()?;

            if sign {
                self.create_signed_tag_with_git2(name, msg, &sig, target.id())?;
            } else {
                self.repo.tag(
                    name,
                    target.as_object(),
                    &sig,
                    msg,
                    false,
                )?;
            }
        } else {
            self.repo.tag(
                name,
                target.as_object(),
                &self.create_signature()?,
                "",
                false,
            )?;
        }

        Ok(())
    }

    /// Create signed tag using git CLI
    fn create_signed_tag_with_git2(&self, name: &str, message: &str, _signature: &Signature, _target_id: Oid) -> Result<()> {
        let output = std::process::Command::new("git")
            .args(&["tag", "-s", name, "-m", message])
            .current_dir(&self.path)
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!("Failed to create signed tag: {}", stderr);
        }

        Ok(())
    }

    /// Create GPG signature for arbitrary content
    fn create_gpg_signature_for_content(&self, _content: &str, _gpg_program: &str, _signing_key: &str) -> Result<String> {
        Ok(String::new())
    }

    /// Delete a tag
    pub fn delete_tag(&self, name: &str) -> Result<()> {
        self.repo.tag_delete(name)?;
        Ok(())
    }

    /// Push to remote
    pub fn push(&self, remote: &str, refspec: &str) -> Result<()> {
        let output = std::process::Command::new("git")
            .args(&["push", remote, refspec])
            .current_dir(&self.path)
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!("Push failed: {}", stderr);
        }

        Ok(())
    }

    /// Get remote URL
    pub fn get_remote_url(&self, remote: &str) -> Result<String> {
        let remote_obj = self.repo.find_remote(remote)?;
        let url = remote_obj.url()
            .ok_or_else(|| anyhow::anyhow!("Remote has no URL"))?;
        Ok(url.to_string())
    }

    /// Check if working directory is clean
    pub fn is_clean(&self) -> Result<bool> {
        Ok(!self.has_changes()?)
    }

    /// Get repository status summary
    pub fn status_summary(&self) -> Result<StatusSummary> {
        let statuses = self.repo.statuses(Some(StatusOptions::new().include_untracked(true)))?;

        let mut staged = 0;
        let mut unstaged = 0;
        let mut untracked = 0;
        let mut conflicted = 0;

        for entry in statuses.iter() {
            let status = entry.status();

            if status.is_index_new() || status.is_index_modified() ||
               status.is_index_deleted() || status.is_index_renamed() ||
               status.is_index_typechange() {
                staged += 1;
            }

            if status.is_wt_modified() || status.is_wt_deleted() ||
               status.is_wt_renamed() || status.is_wt_typechange() {
                unstaged += 1;
            }

            if status.is_wt_new() {
                untracked += 1;
            }

            if status.is_conflicted() {
                conflicted += 1;
            }
        }

        Ok(StatusSummary {
            staged,
            unstaged,
            untracked,
            conflicted,
            clean: staged == 0 && unstaged == 0 && untracked == 0 && conflicted == 0,
        })
    }
}

/// Commit information
#[derive(Debug, Clone)]
pub struct CommitInfo {
    pub id: String,
    pub short_id: String,
    pub message: String,
    pub author: String,
    pub email: String,
    pub time: i64,
}

impl CommitInfo {
    /// Get commit message subject (first line)
    pub fn subject(&self) -> &str {
        self.message.lines().next().unwrap_or("")
    }

    /// Get commit type from conventional commit
    pub fn commit_type(&self) -> Option<String> {
        let subject = self.subject();
        if let Some(colon) = subject.find(':') {
            let type_part = &subject[..colon];
            let type_name = type_part.split('(').next()?;
            Some(type_name.to_string())
        } else {
            None
        }
    }
}

/// Tag information
#[derive(Debug, Clone)]
pub struct TagInfo {
    pub name: String,
    pub target: String,
    pub message: String,
}

/// Repository status summary
#[derive(Debug, Clone)]
pub struct StatusSummary {
    pub staged: usize,
    pub unstaged: usize,
    pub untracked: usize,
    pub conflicted: usize,
    pub clean: bool,
}

impl StatusSummary {
    /// Format as human-readable string
    pub fn format(&self) -> String {
        if self.clean {
            "working tree clean".to_string()
        } else {
            let mut parts = vec![];
            if self.staged > 0 {
                parts.push(format!("{} staged", self.staged));
            }
            if self.unstaged > 0 {
                parts.push(format!("{} unstaged", self.unstaged));
            }
            if self.untracked > 0 {
                parts.push(format!("{} untracked", self.untracked));
            }
            if self.conflicted > 0 {
                parts.push(format!("{} conflicted", self.conflicted));
            }
            parts.join(", ")
        }
    }
}

/// Find git repository starting from path and walking up
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
    let start_path = start_path.as_ref();
    
    if let Ok(repo) = GitRepo::open(start_path) {
        return Ok(repo);
    }
    
    let mut current = start_path;
    while let Some(parent) = current.parent() {
        if let Ok(repo) = GitRepo::open(parent) {
            return Ok(repo);
        }
        current = parent;
    }
    
    bail!("No git repository found starting from {:?}", start_path)
}

/// Check if path is inside a git repository
pub fn is_git_repo<P: AsRef<Path>>(path: P) -> bool {
    find_repo(path).is_ok()
}

/// Git configuration helper for managing user settings
pub struct GitConfigHelper<'a> {
    repo: Option<&'a Repository>,
    global: bool,
}

impl<'a> GitConfigHelper<'a> {
    /// Create a helper for repository-level configuration
    pub fn for_repo(repo: &'a Repository) -> Self {
        Self {
            repo: Some(repo),
            global: false,
        }
    }

    /// Create a helper for global configuration
    pub fn for_global() -> Result<Self> {
        let _config = git2::Config::open_default()?;
        Ok(Self {
            repo: None,
            global: true,
        })
    }

    /// Get configuration value
    pub fn get(&self, key: &str) -> Result<Option<String>> {
        let config = if self.global {
            git2::Config::open_default()?
        } else if let Some(repo) = self.repo {
            repo.config()?
        } else {
            return Ok(None);
        };

        config.get_string(key).map(Some).map_err(Into::into)
    }

    /// Set configuration value
    pub fn set(&self, key: &str, value: &str) -> Result<()> {
        let mut config = if self.global {
            git2::Config::open_default()?
        } else if let Some(repo) = self.repo {
            repo.config()?
        } else {
            bail!("No configuration available");
        };

        config.set_str(key, value)?;
        Ok(())
    }

    /// Remove configuration value
    pub fn remove(&self, key: &str) -> Result<()> {
        let mut config = if self.global {
            git2::Config::open_default()?
        } else if let Some(repo) = self.repo {
            repo.config()?
        } else {
            bail!("No configuration available");
        };

        config.remove(key)?;
        Ok(())
    }

    /// Get all user configuration
    pub fn get_user_config(&self) -> Result<UserConfig> {
        Ok(UserConfig {
            name: self.get("user.name")?,
            email: self.get("user.email")?,
            signing_key: self.get("user.signingkey")?,
            ssh_command: self.get("core.sshCommand")?,
        })
    }

    /// Set all user configuration
    pub fn set_user_config(&self, config: &UserConfig) -> Result<()> {
        if let Some(ref name) = config.name {
            self.set("user.name", name)?;
        }
        if let Some(ref email) = config.email {
            self.set("user.email", email)?;
        }
        if let Some(ref key) = config.signing_key {
            self.set("user.signingkey", key)?;
        }
        if let Some(ref cmd) = config.ssh_command {
            self.set("core.sshCommand", cmd)?;
        }
        Ok(())
    }
}

/// User configuration for git
#[derive(Debug, Clone)]
pub struct UserConfig {
    pub name: Option<String>,
    pub email: Option<String>,
    pub signing_key: Option<String>,
    pub ssh_command: Option<String>,
}

impl UserConfig {
    /// Check if configuration is complete
    pub fn is_complete(&self) -> bool {
        self.name.is_some() && self.email.is_some()
    }

    /// Compare with another configuration
    pub fn compare(&self, other: &UserConfig) -> Vec<ConfigDiff> {
        let mut diffs = vec![];

        if self.name != other.name {
            diffs.push(ConfigDiff {
                key: "user.name".to_string(),
                left: self.name.clone().unwrap_or_else(|| "<not set>".to_string()),
                right: other.name.clone().unwrap_or_else(|| "<not set>".to_string()),
            });
        }

        if self.email != other.email {
            diffs.push(ConfigDiff {
                key: "user.email".to_string(),
                left: self.email.clone().unwrap_or_else(|| "<not set>".to_string()),
                right: other.email.clone().unwrap_or_else(|| "<not set>".to_string()),
            });
        }

        if self.signing_key != other.signing_key {
            diffs.push(ConfigDiff {
                key: "user.signingkey".to_string(),
                left: self.signing_key.clone().unwrap_or_else(|| "<not set>".to_string()),
                right: other.signing_key.clone().unwrap_or_else(|| "<not set>".to_string()),
            });
        }

        diffs
    }
}

/// Configuration difference
#[derive(Debug, Clone)]
pub struct ConfigDiff {
    pub key: String,
    pub left: String,
    pub right: String,
}
