use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;

use chrono::Utc;
use git2::{Repository, Signature, StatusOptions};
use serde::Serialize;
use tokio::time::interval;

use crate::config;

/// Git status for a file
#[derive(Debug, Clone, Serialize)]
pub struct FileStatus {
    pub path: String,
    pub status: String, // "new", "modified", "deleted", "renamed", "untracked"
}

/// Overall repository status
#[derive(Debug, Serialize)]
pub struct RepoStatus {
    pub is_repo: bool,
    pub branch: Option<String>,
    pub files: Vec<FileStatus>,
    pub has_changes: bool,
    pub last_commit: Option<CommitInfo>,
}

/// Commit information
#[derive(Debug, Clone, Serialize)]
pub struct CommitInfo {
    pub id: String,
    pub message: String,
    pub timestamp: String,
}

/// Extended commit info for history
#[derive(Debug, Serialize)]
pub struct CommitDetail {
    pub id: String,
    pub short_id: String,
    pub message: String,
    pub author: String,
    pub timestamp: String,
    pub files_changed: usize,
}

/// Diff information
#[derive(Debug, Serialize)]
pub struct DiffInfo {
    pub files: Vec<FileDiff>,
    pub stats: DiffStats,
}

/// File diff
#[derive(Debug, Serialize)]
pub struct FileDiff {
    pub path: String,
    pub status: String,
    pub additions: usize,
    pub deletions: usize,
    pub hunks: Vec<DiffHunk>,
}

/// Diff hunk (section of changes)
#[derive(Debug, Serialize)]
pub struct DiffHunk {
    pub header: String,
    pub lines: Vec<DiffLine>,
}

/// Single diff line
#[derive(Debug, Serialize)]
pub struct DiffLine {
    pub origin: char,
    pub content: String,
}

/// Diff statistics
#[derive(Debug, Serialize)]
pub struct DiffStats {
    pub files_changed: usize,
    pub insertions: usize,
    pub deletions: usize,
}

/// Remote repository information
#[derive(Debug, Serialize)]
pub struct RemoteInfo {
    pub name: String,
    pub url: String,
    pub has_upstream: bool,
    pub ahead: usize,
    pub behind: usize,
}

/// Auto-commit is enabled by default.
/// The background task simply tries to commit every interval;
/// commit_all() already handles "no changes" gracefully.

#[derive(Debug, Clone)]
struct GitAuthConfig {
    username: String,
    private_key: Option<PathBuf>,
    public_key: Option<PathBuf>,
    passphrase: Option<String>,
}

fn git_auth_config() -> GitAuthConfig {
    let username = std::env::var("IRONPAD_GIT_SSH_USERNAME").unwrap_or_else(|_| "git".to_string());
    let private_key = std::env::var("IRONPAD_GIT_SSH_PRIVATE_KEY")
        .ok()
        .map(PathBuf::from)
        .filter(|p| p.exists());
    let public_key = std::env::var("IRONPAD_GIT_SSH_PUBLIC_KEY")
        .ok()
        .map(PathBuf::from)
        .filter(|p| p.exists());
    let passphrase = std::env::var("IRONPAD_GIT_SSH_PASSPHRASE")
        .ok()
        .and_then(|s| {
            let trimmed = s.trim().to_string();
            if trimmed.is_empty() {
                None
            } else {
                Some(trimmed)
            }
        });

    GitAuthConfig {
        username,
        private_key,
        public_key,
        passphrase,
    }
}

fn remote_callbacks() -> git2::RemoteCallbacks<'static> {
    let auth = git_auth_config();
    let mut callbacks = git2::RemoteCallbacks::new();

    callbacks.credentials(move |_url, username_from_url, _allowed_types| {
        // Always prefer configured username (env-driven).
        // Some remotes embed a username in URL; that can cause auth mismatches.
        let username = auth.username.as_str();
        if let Some(url_user) = username_from_url {
            if url_user != username {
                tracing::warn!(
                    "Remote URL username '{}' differs from IRONPAD_GIT_SSH_USERNAME '{}'; using env value",
                    url_user,
                    username
                );
            }
        }

        if let Some(private_key) = auth.private_key.as_deref() {
            let public_key: Option<&Path> = auth.public_key.as_deref();
            let passphrase = auth.passphrase.as_deref();

            // First try with configured public key path (if provided),
            // then retry without public key file to avoid mismatch issues.
            if let Some(pub_key_path) = public_key {
                match git2::Cred::ssh_key(username, Some(pub_key_path), private_key, passphrase) {
                    Ok(cred) => return Ok(cred),
                    Err(e) => {
                        tracing::warn!(
                            "SSH key auth with explicit public key failed for user '{}', private '{}', public '{}': {}",
                            username,
                            private_key.display(),
                            pub_key_path.display(),
                            e
                        );
                    }
                }
            }

            match git2::Cred::ssh_key(username, None, private_key, passphrase) {
                Ok(cred) => return Ok(cred),
                Err(e) => {
                    tracing::warn!(
                        "SSH key auth from private key failed for user '{}', key '{}': {}",
                        username,
                        private_key.display(),
                        e
                    );
                }
            }
        } else {
            tracing::warn!(
                "IRONPAD_GIT_SSH_PRIVATE_KEY not set or file missing; falling back to SSH agent"
            );
        }

        git2::Cred::ssh_key_from_agent(username)
    });

    callbacks
}

fn use_git_cli_fallback() -> bool {
    std::env::var("IRONPAD_GIT_USE_CLI_FALLBACK")
        .map(|v| !matches!(v.to_ascii_lowercase().as_str(), "0" | "false" | "no" | "off"))
        .unwrap_or(true)
}

fn known_hosts_path() -> Option<PathBuf> {
    if let Ok(path) = std::env::var("IRONPAD_GIT_SSH_KNOWN_HOSTS") {
        let p = PathBuf::from(path);
        if p.exists() {
            return Some(p);
        }
    }

    let default = PathBuf::from("/root/.ssh/known_hosts");
    if default.exists() {
        Some(default)
    } else {
        None
    }
}

fn git_ssh_command(auth: &GitAuthConfig) -> Option<String> {
    let private_key = auth.private_key.as_ref()?;

    let mut cmd = format!(
        "ssh -i {} -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes",
        private_key.display()
    );
    if let Some(known_hosts) = known_hosts_path() {
        cmd.push_str(&format!(" -o UserKnownHostsFile={}", known_hosts.display()));
    }
    Some(cmd)
}

fn run_git_cli(args: &[&str]) -> Result<(), String> {
    let auth = git_auth_config();
    let data_path = config::data_dir();
    let mut cmd = Command::new("git");
    cmd.args(args)
        .current_dir(data_path)
        .env("GIT_TERMINAL_PROMPT", "0");

    if let Some(ssh_cmd) = git_ssh_command(&auth) {
        cmd.env("GIT_SSH_COMMAND", ssh_cmd);
    }

    let output = cmd
        .output()
        .map_err(|e| format!("Failed to run git CLI: {}", e))?;

    if output.status.success() {
        Ok(())
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
        let msg = if !stderr.is_empty() { stderr } else { stdout };
        Err(format!(
            "git {} failed (exit {}): {}",
            args.join(" "),
            output.status.code().unwrap_or(-1),
            msg
        ))
    }
}

/// Get repository status
pub fn get_status() -> Result<RepoStatus, String> {
    let data_path = config::data_dir();

    // Try to open as git repo
    let repo = match Repository::open(data_path) {
        Ok(r) => r,
        Err(_) => {
            return Ok(RepoStatus {
                is_repo: false,
                branch: None,
                files: Vec::new(),
                has_changes: false,
                last_commit: None,
            });
        }
    };

    // Get current branch
    let branch = repo
        .head()
        .ok()
        .and_then(|h| h.shorthand().map(String::from));

    // Get file statuses
    let mut opts = StatusOptions::new();
    opts.include_untracked(true)
        .recurse_untracked_dirs(true)
        .exclude_submodules(true);

    let statuses = repo.statuses(Some(&mut opts)).map_err(|e| e.to_string())?;

    let files: Vec<FileStatus> = statuses
        .iter()
        .filter_map(|entry| {
            let path = entry.path()?.to_string();
            let status = entry.status();

            let status_str = if status.is_index_new() || status.is_wt_new() {
                "new"
            } else if status.is_index_modified() || status.is_wt_modified() {
                "modified"
            } else if status.is_index_deleted() || status.is_wt_deleted() {
                "deleted"
            } else if status.is_index_renamed() || status.is_wt_renamed() {
                "renamed"
            } else {
                return None;
            };

            Some(FileStatus {
                path,
                status: status_str.to_string(),
            })
        })
        .collect();

    let has_changes = !files.is_empty();

    // Get last commit info
    let last_commit = repo.head().ok().and_then(|head| {
        let commit = head.peel_to_commit().ok()?;
        Some(CommitInfo {
            id: commit.id().to_string()[..8].to_string(),
            message: commit.message()?.trim().to_string(),
            timestamp: chrono::DateTime::from_timestamp(commit.time().seconds(), 0)?.to_rfc3339(),
        })
    });

    Ok(RepoStatus {
        is_repo: true,
        branch,
        files,
        has_changes,
        last_commit,
    })
}

/// Create a commit with all changes
pub fn commit_all(message: Option<&str>) -> Result<CommitInfo, String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    // Stage all changes
    let mut index = repo.index().map_err(|e| e.to_string())?;
    index
        .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
        .map_err(|e| e.to_string())?;
    index.write().map_err(|e| e.to_string())?;

    // Check if there are changes to commit
    let tree_id = index.write_tree().map_err(|e| e.to_string())?;
    let tree = repo.find_tree(tree_id).map_err(|e| e.to_string())?;

    // Get parent commit (if any)
    let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());

    // Check if tree is different from parent
    if let Some(ref p) = parent {
        if p.tree().map(|t| t.id()) == Ok(tree_id) {
            return Err("No changes to commit".to_string());
        }
    }

    // Create signature
    let sig = Signature::now("Ironpad", "ironpad@local").map_err(|e| e.to_string())?;

    // Generate commit message
    let msg = message.unwrap_or_else(|| "Auto-save");
    let timestamp = Utc::now().format("%Y-%m-%d %H:%M");
    let full_message = format!("{} ({})", msg, timestamp);

    // Create commit
    let parents: Vec<&git2::Commit> = parent.as_ref().map(|p| vec![p]).unwrap_or_default();
    let commit_id = repo
        .commit(Some("HEAD"), &sig, &sig, &full_message, &tree, &parents)
        .map_err(|e| e.to_string())?;

    Ok(CommitInfo {
        id: commit_id.to_string()[..8].to_string(),
        message: full_message,
        timestamp: Utc::now().to_rfc3339(),
    })
}

/// Initialize data directory as a git repository if not already
pub fn init_repo() -> Result<(), String> {
    let data_path = config::data_dir();

    if Repository::open(data_path).is_ok() {
        return Ok(()); // Already a repo
    }

    Repository::init(data_path).map_err(|e| format!("Failed to init repo: {}", e))?;

    // Create initial .gitignore
    let gitignore_path = data_path.join(".gitignore");
    if !gitignore_path.exists() {
        std::fs::write(&gitignore_path, "*.tmp\n.DS_Store\n")
            .map_err(|e| format!("Failed to create .gitignore: {}", e))?;
    }

    // Initial commit
    commit_all(Some("Initial commit"))?;

    Ok(())
}

/// Check for merge conflicts
pub fn check_conflicts() -> Result<Vec<String>, String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    let mut conflicts = Vec::new();

    // Check for .git/index.lock (another git operation in progress)
    let lock_path = data_path.join(".git").join("index.lock");
    if lock_path.exists() {
        // This isn't a conflict per se, but indicates git is busy
        tracing::warn!("Git index.lock exists - another operation may be in progress");
    }

    // Check status for conflicted files
    let mut opts = StatusOptions::new();
    opts.include_untracked(false);

    let statuses = repo.statuses(Some(&mut opts)).map_err(|e| e.to_string())?;

    for entry in statuses.iter() {
        let status = entry.status();
        // Check for conflict status flags
        if status.is_conflicted() {
            if let Some(path) = entry.path() {
                conflicts.push(path.to_string());
            }
        }
    }

    // Also check the index for conflicts
    let index = repo.index().map_err(|e| e.to_string())?;
    if index.has_conflicts() {
        for conflict in index.conflicts().map_err(|e| e.to_string())? {
            if let Ok(conflict) = conflict {
                if let Some(ancestor) = conflict.ancestor {
                    if let Some(path) = std::str::from_utf8(&ancestor.path).ok() {
                        if !conflicts.contains(&path.to_string()) {
                            conflicts.push(path.to_string());
                        }
                    }
                }
            }
        }
    }

    Ok(conflicts)
}

/// Push to remote repository
pub fn push_to_remote() -> Result<(), String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    // Get the current branch
    let head = repo.head().map_err(|e| e.to_string())?;
    let branch_name = head
        .shorthand()
        .ok_or_else(|| "Could not get branch name".to_string())?;

    // Find the remote (default to "origin")
    let mut remote = repo
        .find_remote("origin")
        .map_err(|e| format!("Remote 'origin' not found: {}", e))?;

    // Check if remote URL is configured
    let remote_url = remote
        .url()
        .ok_or_else(|| "No remote URL configured".to_string())?;
    if remote_url.is_empty() {
        return Err("No remote URL configured".to_string());
    }

    // Set up push options
    let mut push_options = git2::PushOptions::new();
    push_options.remote_callbacks(remote_callbacks());

    // Push the current branch
    let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
    match remote.push(&[&refspec], Some(&mut push_options)) {
        Ok(_) => {
            tracing::info!("Successfully pushed to origin/{}", branch_name);
            Ok(())
        }
        Err(e) => {
            let err_text = e.to_string();
            if !use_git_cli_fallback() {
                return Err(format!(
                    "Push failed: {}. Make sure SSH keys are configured.",
                    err_text
                ));
            }
            tracing::warn!("libgit2 push failed, trying git CLI fallback: {}", err_text);
            run_git_cli(&["push", "origin", branch_name]).map_err(|cli_err| {
                format!("Push failed: {} (libgit2) / {} (git CLI)", err_text, cli_err)
            })?;
            tracing::info!("Successfully pushed to origin/{} (git CLI fallback)", branch_name);
            Ok(())
        }
    }
}

/// Check if remote is configured
pub fn has_remote() -> bool {
    let data_path = config::data_dir();
    if let Ok(repo) = Repository::open(data_path) {
        if let Ok(remote) = repo.find_remote("origin") {
            return remote.url().is_some();
        }
    }
    false
}

/// Configure `origin` from IRONPAD_GIT_REMOTE_URL.
/// If `origin` exists, updates its URL. Otherwise creates it.
pub fn configure_remote_from_env() -> Result<(), String> {
    let remote_url = match std::env::var("IRONPAD_GIT_REMOTE_URL") {
        Ok(url) if !url.trim().is_empty() => url,
        _ => return Ok(()),
    };

    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    if repo.find_remote("origin").is_ok() {
        repo.remote_set_url("origin", &remote_url)
            .map_err(|e| format!("Failed to update origin URL: {}", e))?;
        tracing::info!("Updated git remote origin from IRONPAD_GIT_REMOTE_URL");
    } else {
        repo.remote("origin", &remote_url)
            .map_err(|e| format!("Failed to create origin remote: {}", e))?;
        tracing::info!("Configured git remote origin from IRONPAD_GIT_REMOTE_URL");
    }

    Ok(())
}

/// Start auto-commit background task.
/// Tries to commit every 60 seconds; commit_all() already handles "no changes" gracefully.
pub fn start_auto_commit() {
    tokio::spawn(async move {
        let mut interval = interval(Duration::from_secs(60));

        loop {
            interval.tick().await;

            match commit_all(Some("Auto-save")) {
                Ok(info) => {
                    tracing::info!("Auto-commit: {} - {}", info.id, info.message);
                }
                Err(e) => {
                    if !e.contains("No changes") {
                        tracing::warn!("Auto-commit failed: {}", e);
                    }
                }
            }
        }
    });
}

fn fast_forward_to_upstream(repo: &Repository, branch_name: &str) -> Result<(), String> {
    let local_branch = repo
        .find_branch(branch_name, git2::BranchType::Local)
        .map_err(|e| format!("Local branch not found: {}", e))?;
    let upstream_branch = match local_branch.upstream() {
        Ok(upstream) => upstream,
        Err(_) => return Ok(()),
    };

    let local_oid = match local_branch.get().target() {
        Some(oid) => oid,
        None => return Ok(()),
    };
    let upstream_oid = match upstream_branch.get().target() {
        Some(oid) => oid,
        None => return Ok(()),
    };

    let (ahead, behind) = repo
        .graph_ahead_behind(local_oid, upstream_oid)
        .map_err(|e| e.to_string())?;

    if behind == 0 {
        return Ok(());
    }

    if ahead > 0 {
        tracing::warn!(
            "Remote is ahead by {} and local is ahead by {}; skipping auto fast-forward",
            behind,
            ahead
        );
        return Ok(());
    }

    let refname = format!("refs/heads/{}", branch_name);
    repo.reference(&refname, upstream_oid, true, "ironpad auto fast-forward")
        .map_err(|e| format!("Failed to move local branch ref: {}", e))?;
    repo.set_head(&refname)
        .map_err(|e| format!("Failed to set HEAD: {}", e))?;

    let mut checkout = git2::build::CheckoutBuilder::new();
    checkout.force();
    repo.checkout_head(Some(&mut checkout))
        .map_err(|e| format!("Failed to checkout fast-forwarded HEAD: {}", e))?;

    tracing::info!("Fast-forwarded local branch {} by {} commit(s)", branch_name, behind);
    Ok(())
}

fn sync_once() -> Result<(), String> {
    configure_remote_from_env()?;
    if !has_remote() {
        return Ok(());
    }

    fetch_from_remote()?;

    // Try to fast-forward local branch to upstream when safe (no divergence).
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;
    if let Ok(head) = repo.head() {
        if let Some(branch_name) = head.shorthand() {
            let _ = fast_forward_to_upstream(&repo, branch_name);
        }
    }

    // Push local commits (if any) after fetching.
    if let Err(e) = push_to_remote() {
        if !(e.contains("non-fast-forward") || e.contains("rejected")) {
            return Err(e);
        }
        tracing::warn!("Auto-sync push skipped: {}", e);
    }

    Ok(())
}

/// Start periodic remote sync.
/// Controlled via env vars:
/// - IRONPAD_GIT_SYNC_INTERVAL_SECS: sync interval in seconds (default 0 = disabled)
/// - IRONPAD_GIT_REMOTE_URL: optional remote URL used to create/update `origin`
pub fn start_auto_sync() {
    let interval_secs = std::env::var("IRONPAD_GIT_SYNC_INTERVAL_SECS")
        .ok()
        .and_then(|s| s.parse::<u64>().ok())
        .unwrap_or(0);

    if interval_secs == 0 {
        tracing::info!("Git auto-sync disabled (set IRONPAD_GIT_SYNC_INTERVAL_SECS > 0 to enable)");
        return;
    }

    tokio::spawn(async move {
        let mut ticker = interval(Duration::from_secs(interval_secs));

        loop {
            ticker.tick().await;
            if let Err(e) = sync_once() {
                tracing::warn!("Git auto-sync failed: {}", e);
            }
        }
    });
}

/// Get commit history (most recent first)
pub fn get_log(limit: Option<usize>) -> Result<Vec<CommitDetail>, String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    let mut revwalk = repo.revwalk().map_err(|e| e.to_string())?;
    revwalk.push_head().map_err(|e| e.to_string())?;
    revwalk
        .set_sorting(git2::Sort::TIME)
        .map_err(|e| e.to_string())?;

    let max_commits = limit.unwrap_or(50);
    let mut commits = Vec::new();

    for (i, oid_result) in revwalk.enumerate() {
        if i >= max_commits {
            break;
        }

        let oid = oid_result.map_err(|e| e.to_string())?;
        let commit = repo.find_commit(oid).map_err(|e| e.to_string())?;

        // Count files changed in this commit
        let files_changed = if commit.parent_count() > 0 {
            let parent = commit.parent(0).ok();
            let parent_tree = parent.as_ref().and_then(|p| p.tree().ok());
            let commit_tree = commit.tree().ok();

            if let (Some(pt), Some(ct)) = (parent_tree, commit_tree) {
                let diff = repo.diff_tree_to_tree(Some(&pt), Some(&ct), None).ok();
                diff.map(|d| d.deltas().count()).unwrap_or(0)
            } else {
                0
            }
        } else {
            // Initial commit - count all files
            commit
                .tree()
                .ok()
                .map(|t| count_tree_entries(&t))
                .unwrap_or(0)
        };

        let timestamp = chrono::DateTime::from_timestamp(commit.time().seconds(), 0)
            .map(|dt| dt.to_rfc3339())
            .unwrap_or_else(|| "Unknown".to_string());

        commits.push(CommitDetail {
            id: oid.to_string(),
            short_id: oid.to_string()[..8].to_string(),
            message: commit.message().unwrap_or("").trim().to_string(),
            author: commit.author().name().unwrap_or("Unknown").to_string(),
            timestamp,
            files_changed,
        });
    }

    Ok(commits)
}

/// Helper to count entries in a tree recursively
fn count_tree_entries(tree: &git2::Tree) -> usize {
    tree.iter()
        .filter(|entry| entry.kind() == Some(git2::ObjectType::Blob))
        .count()
}

/// Get working directory diff (uncommitted changes)
pub fn get_working_diff() -> Result<DiffInfo, String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    // Get HEAD tree (or empty tree if no commits)
    let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());

    // Diff against working directory
    let diff = repo
        .diff_tree_to_workdir_with_index(head_tree.as_ref(), None)
        .map_err(|e| e.to_string())?;

    parse_diff(&diff)
}

/// Get diff for a specific commit
pub fn get_commit_diff(commit_id: &str) -> Result<DiffInfo, String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    let oid = git2::Oid::from_str(commit_id).map_err(|e| format!("Invalid commit ID: {}", e))?;
    let commit = repo
        .find_commit(oid)
        .map_err(|e| format!("Commit not found: {}", e))?;

    let commit_tree = commit.tree().map_err(|e| e.to_string())?;

    let parent_tree = if commit.parent_count() > 0 {
        commit.parent(0).ok().and_then(|p| p.tree().ok())
    } else {
        None
    };

    let diff = repo
        .diff_tree_to_tree(parent_tree.as_ref(), Some(&commit_tree), None)
        .map_err(|e| e.to_string())?;

    parse_diff(&diff)
}

/// Parse a git2::Diff into our DiffInfo structure
fn parse_diff(diff: &git2::Diff) -> Result<DiffInfo, String> {
    let stats = diff.stats().map_err(|e| e.to_string())?;

    let mut files = Vec::new();

    for delta_idx in 0..diff.deltas().count() {
        let delta = diff.get_delta(delta_idx).ok_or("Missing delta")?;

        let path = delta
            .new_file()
            .path()
            .or_else(|| delta.old_file().path())
            .map(|p| p.to_string_lossy().to_string())
            .unwrap_or_else(|| "Unknown".to_string());

        let status = match delta.status() {
            git2::Delta::Added => "added",
            git2::Delta::Deleted => "deleted",
            git2::Delta::Modified => "modified",
            git2::Delta::Renamed => "renamed",
            git2::Delta::Copied => "copied",
            _ => "unknown",
        };

        let mut hunks = Vec::new();
        let mut additions = 0;
        let mut deletions = 0;

        // Get patch for this file
        if let Ok(patch) = git2::Patch::from_diff(diff, delta_idx) {
            if let Some(p) = patch {
                for hunk_idx in 0..p.num_hunks() {
                    if let Ok((hunk, _)) = p.hunk(hunk_idx) {
                        let mut lines = Vec::new();

                        for line_idx in 0..p.num_lines_in_hunk(hunk_idx).unwrap_or(0) {
                            if let Ok(line) = p.line_in_hunk(hunk_idx, line_idx) {
                                let origin = line.origin();
                                let content = std::str::from_utf8(line.content())
                                    .unwrap_or("")
                                    .to_string();

                                match origin {
                                    '+' => additions += 1,
                                    '-' => deletions += 1,
                                    _ => {}
                                }

                                lines.push(DiffLine { origin, content });
                            }
                        }

                        hunks.push(DiffHunk {
                            header: std::str::from_utf8(hunk.header())
                                .unwrap_or("")
                                .trim()
                                .to_string(),
                            lines,
                        });
                    }
                }
            }
        }

        files.push(FileDiff {
            path,
            status: status.to_string(),
            additions,
            deletions,
            hunks,
        });
    }

    Ok(DiffInfo {
        files,
        stats: DiffStats {
            files_changed: stats.files_changed(),
            insertions: stats.insertions(),
            deletions: stats.deletions(),
        },
    })
}

/// Get remote repository information
pub fn get_remote_info() -> Result<Option<RemoteInfo>, String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    let remote = match repo.find_remote("origin") {
        Ok(r) => r,
        Err(_) => return Ok(None),
    };

    let url = remote.url().unwrap_or("").to_string();
    if url.is_empty() {
        return Ok(None);
    }

    // Get current branch
    let head = match repo.head() {
        Ok(h) => h,
        Err(_) => {
            return Ok(Some(RemoteInfo {
                name: "origin".to_string(),
                url,
                has_upstream: false,
                ahead: 0,
                behind: 0,
            }));
        }
    };

    let branch_name = head.shorthand().unwrap_or("HEAD");

    // Try to find upstream branch
    let local_branch = repo.find_branch(branch_name, git2::BranchType::Local).ok();
    let upstream = local_branch.as_ref().and_then(|b| b.upstream().ok());

    let (ahead, behind) = if let Some(ref up) = upstream {
        // Calculate ahead/behind
        let local_oid = head.target().unwrap_or_else(git2::Oid::zero);
        let upstream_oid = up.get().target().unwrap_or_else(git2::Oid::zero);

        repo.graph_ahead_behind(local_oid, upstream_oid)
            .unwrap_or((0, 0))
    } else {
        (0, 0)
    };

    Ok(Some(RemoteInfo {
        name: "origin".to_string(),
        url,
        has_upstream: upstream.is_some(),
        ahead,
        behind,
    }))
}

/// Fetch from remote
pub fn fetch_from_remote() -> Result<(), String> {
    let data_path = config::data_dir();
    let repo = Repository::open(data_path).map_err(|e| format!("Not a git repository: {}", e))?;

    let mut remote = repo
        .find_remote("origin")
        .map_err(|e| format!("Remote 'origin' not found: {}", e))?;

    let mut fetch_options = git2::FetchOptions::new();
    fetch_options.remote_callbacks(remote_callbacks());

    match remote.fetch(&[] as &[&str], Some(&mut fetch_options), None) {
        Ok(_) => Ok(()),
        Err(e) => {
            let err_text = e.to_string();
            if !use_git_cli_fallback() {
                return Err(format!("Fetch failed: {}", err_text));
            }
            tracing::warn!("libgit2 fetch failed, trying git CLI fallback: {}", err_text);
            run_git_cli(&["fetch", "origin", "--prune"]).map_err(|cli_err| {
                format!("Fetch failed: {} (libgit2) / {} (git CLI)", err_text, cli_err)
            })
        }
    }
}
