//! Git branch tracking utilities for mi6.
//!
//! This module provides functionality to:
//! - Capture the current git branch from a working directory
//! - Detect git branch-changing commands in shell commands
//!
//! # Branch Info Capture
//!
//! The [`parse_branch_info`] function captures the git branch name.
//!
//! **Note:** This function does not extract issue/PR numbers from branch names.
//! Branch naming conventions are too inconsistent and led to false positives
//! (e.g., `upgrade-text-fix-2` being incorrectly parsed as issue #2).
//! Issue and PR numbers are now only extracted from explicit `gh` CLI commands
//! (see the [`github`](super::github) module).
//!
//! # Branch-Changing Command Detection
//!
//! The [`is_branch_changing_command`] function detects git commands that change the
//! current branch, triggering re-capture of branch info.
//!
//! ## Detected Commands
//!
//! | Command | Description |
//! |---------|-------------|
//! | `git checkout <branch>` | Switch branches |
//! | `git checkout -b <branch>` | Create and switch |
//! | `git switch <branch>` | Modern branch switch |
//! | `git worktree add <path>` | Create worktree |
//! | `git merge <branch>` | Merge (may fast-forward) |
//! | `git rebase <branch>` | Rebase onto branch |
//! | `git pull` | Pull (may merge/rebase) |
//!
//! ## Chained Command Support
//!
//! Commands are split on `&`, `|`, and `;` to detect branch changes in compound commands:
//!
//! ```text
//! echo foo && git checkout main   -> detected
//! git status; git checkout main   -> detected
//! cargo build && git switch dev   -> detected
//! ```
//!
//! # Limitations
//!
//! 1. **Chained commands** - Simple split on delimiters; doesn't handle quoted delimiters

use std::fs;
use std::path::Path;

use super::github::split_chained_commands;

/// Result of parsing a git branch name for PR/issue numbers.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GitBranchInfo {
    /// The branch name
    pub branch: String,
    /// PR number if detected in branch name
    pub pr_number: Option<i32>,
    /// Issue number if detected in branch name
    pub issue_number: Option<i32>,
}

/// Get the current git branch for a directory.
///
/// Uses direct file system access for speed (~30µs vs ~650µs with libgit2).
/// Reads the .git/HEAD file and parses the branch reference.
///
/// Returns `None` if:
/// - The directory is not a git repository
/// - HEAD is detached (not pointing to a branch)
/// - Any error occurs
pub fn get_current_branch(cwd: &Path) -> Option<String> {
    // Find the .git directory (handles worktrees)
    let git_dir = find_worktree_git_dir(cwd)?;

    // Read HEAD file
    let head_path = git_dir.join("HEAD");
    let head_content = fs::read_to_string(head_path).ok()?;
    let head_content = head_content.trim();

    // Parse HEAD - it's either "ref: refs/heads/branch" or a raw SHA (detached)
    if let Some(ref_path) = head_content.strip_prefix("ref: ") {
        // Extract branch name from refs/heads/branch
        ref_path.strip_prefix("refs/heads/").map(|s| s.to_string())
    } else {
        // Detached HEAD - no branch
        None
    }
}

/// Find the git directory for worktrees (used by [`get_current_branch`]).
///
/// For worktrees, this returns the **worktree-specific** .git directory
/// (e.g., `.git/worktrees/branch-name`) which contains the HEAD file
/// for that worktree.
///
/// # Why This Exists Separately from [`find_git_dir`]
///
/// This function and [`find_git_dir`] look similar but serve different purposes:
///
/// - **This function** (`find_worktree_git_dir`): Returns the worktree-specific
///   `.git/worktrees/<name>` directory, used to read the worktree's HEAD file.
///
/// - **[`find_git_dir`]**: Returns the **main** `.git` directory (going up two
///   levels from the worktree path), used to read the shared `config` file.
///
/// For a normal (non-worktree) repository, both return the same `.git` directory.
/// For worktrees, they intentionally return different paths. Do not consolidate
/// these functions without understanding this distinction.
fn find_worktree_git_dir(cwd: &Path) -> Option<std::path::PathBuf> {
    let mut current = cwd;
    loop {
        let git_path = current.join(".git");
        if git_path.exists() {
            if git_path.is_file() {
                // Worktree: .git file contains "gitdir: /path/to/.git/worktrees/name"
                let content = fs::read_to_string(&git_path).ok()?;
                let gitdir = content.strip_prefix("gitdir: ")?.trim();
                return Some(std::path::PathBuf::from(gitdir));
            }
            return Some(git_path);
        }
        current = current.parent()?;
    }
}

/// Create a GitBranchInfo from a branch name.
///
/// Note: This function no longer extracts issue/PR numbers from branch names.
/// Branch naming conventions are too inconsistent and lead to false positives
/// (e.g., `upgrade-text-fix-2` being parsed as issue #2). Issue and PR numbers
/// are now only extracted from explicit `gh` CLI commands.
pub fn parse_branch_info(branch: &str) -> GitBranchInfo {
    GitBranchInfo {
        branch: branch.to_string(),
        pr_number: None,
        issue_number: None,
    }
}

/// Get git branch info for a directory.
///
/// Combines `get_current_branch` and `parse_branch_info` into a single call.
pub fn get_branch_info(cwd: &Path) -> Option<GitBranchInfo> {
    get_current_branch(cwd).map(|branch| parse_branch_info(&branch))
}

/// Get the absolute path to the .git directory for a working directory.
///
/// Uses direct file system access for speed (~15µs vs ~640µs with libgit2).
/// Walks up the directory tree to find `.git`, handling both normal repos
/// and worktrees.
///
/// Returns `None` if:
/// - The directory is not a git repository
/// - Any error occurs
///
/// # Example
///
/// ```ignore
/// use std::path::Path;
/// use mi6_core::context::get_local_git_dir;
///
/// // In a normal repo: returns "/path/to/repo/.git"
/// let git_dir = get_local_git_dir(Path::new("/path/to/repo"));
///
/// // In a worktree: returns "/path/to/main/.git/worktrees/branch-name"
/// let git_dir = get_local_git_dir(Path::new("/path/to/worktree"));
/// ```
pub fn get_local_git_dir(cwd: &Path) -> Option<String> {
    // Walk up the directory tree to find .git
    let mut current = cwd;
    loop {
        let git_path = current.join(".git");
        if git_path.exists() {
            if git_path.is_file() {
                // Worktree: .git file contains "gitdir: /path/to/.git/worktrees/name"
                // The path may be relative, so resolve it against the .git file's directory
                let content = fs::read_to_string(&git_path).ok()?;
                let gitdir = content.strip_prefix("gitdir: ")?.trim();
                let gitdir_path = Path::new(gitdir);

                // Resolve relative paths against the directory containing the .git file
                let resolved = if gitdir_path.is_relative() {
                    current.join(gitdir_path)
                } else {
                    gitdir_path.to_path_buf()
                };

                // Canonicalize for consistency with normal repos
                return resolved
                    .canonicalize()
                    .ok()
                    .map(|p| p.to_string_lossy().to_string());
            }
            // Normal repo: .git is a directory
            return git_path
                .canonicalize()
                .ok()
                .map(|p| p.to_string_lossy().to_string());
        }
        current = current.parent()?;
    }
}

/// Get the worktree root path if the current directory is within a git worktree.
///
/// Returns the absolute path to the worktree root (the directory containing
/// the `.git` file that points to the worktree's git directory).
///
/// Returns `None` if:
/// - The directory is not in a git repository
/// - The directory is in a normal git repository (not a worktree)
///
/// # Example
///
/// ```ignore
/// use std::path::Path;
/// use mi6_core::context::get_worktree_root;
///
/// // In a worktree: returns "/path/to/worktree"
/// let root = get_worktree_root(Path::new("/path/to/worktree/subdir"));
///
/// // In a normal repo: returns None
/// let root = get_worktree_root(Path::new("/path/to/normal/repo"));
/// ```
pub fn get_worktree_root(cwd: &Path) -> Option<String> {
    let mut current = cwd;
    loop {
        let git_path = current.join(".git");
        if git_path.exists() {
            if git_path.is_file() {
                // Worktree: .git is a file, return the directory containing it
                return current
                    .canonicalize()
                    .ok()
                    .map(|p| p.to_string_lossy().to_string());
            }
            // Normal repo: .git is a directory, not a worktree
            return None;
        }
        current = current.parent()?;
    }
}

/// Get the GitHub repository from git remote origin.
///
/// Uses direct file system access for speed (~30µs vs ~1344µs with libgit2).
/// Reads the git config file and parses the origin remote URL.
///
/// Returns `None` if:
/// - The directory is not a git repository
/// - No origin remote is configured
/// - The origin remote is not a GitHub URL
///
/// # Supported URL formats
///
/// - SSH: `git@github.com:owner/repo.git`
/// - HTTPS: `https://github.com/owner/repo.git` or `https://github.com/owner/repo`
/// - SSH URL: `ssh://git@github.com/owner/repo.git`
///
/// # Example
///
/// ```ignore
/// use std::path::Path;
/// use mi6_core::context::get_github_repo;
///
/// let repo = get_github_repo(Path::new("/path/to/git/repo"));
/// // Returns Some("owner/repo") if valid GitHub remote
/// ```
pub fn get_github_repo(cwd: &Path) -> Option<String> {
    // Find the .git directory (handles worktrees)
    let git_dir = find_git_dir(cwd)?;

    // Read config file
    let config_path = git_dir.join("config");
    let config = fs::read_to_string(config_path).ok()?;

    // Parse for origin URL
    // Git config is case-insensitive for section/key names but case-sensitive for remote names
    let mut in_origin = false;
    for line in config.lines() {
        let trimmed = line.trim();
        // Case-insensitive match for section header, but "origin" remote name is case-sensitive
        if trimmed.eq_ignore_ascii_case("[remote \"origin\"]") {
            in_origin = true;
        } else if trimmed.starts_with('[') {
            in_origin = false;
        } else if in_origin {
            // Case-insensitive key match, handle both "url = value" and "url=value"
            let lower = trimmed.to_ascii_lowercase();
            if lower.starts_with("url") {
                // Find the value after "url" and optional whitespace/equals
                if let Some(eq_pos) = trimmed.find('=') {
                    let url = trimmed[eq_pos + 1..].trim();
                    // Strip surrounding quotes if present
                    let url = url
                        .strip_prefix('"')
                        .and_then(|s| s.strip_suffix('"'))
                        .unwrap_or(url);
                    return parse_github_repo_from_remote_url(url);
                }
            }
        }
    }

    None
}

/// Find the **main** .git directory for a working directory (used by [`get_github_repo`]).
///
/// This returns the path to the repository's main `.git` directory, where the
/// shared `config` file lives. For worktrees, this goes up from the worktree-specific
/// `.git/worktrees/<name>` to the parent `.git` directory.
///
/// # Why This Exists Separately from [`find_worktree_git_dir`]
///
/// This function and [`find_worktree_git_dir`] look similar but serve different purposes:
///
/// - **This function** (`find_git_dir`): Returns the **main** `.git` directory
///   (going up two levels from the worktree path), used to read the shared `config` file.
///
/// - **[`find_worktree_git_dir`]**: Returns the worktree-specific
///   `.git/worktrees/<name>` directory, used to read the worktree's HEAD file.
///
/// For a normal (non-worktree) repository, both return the same `.git` directory.
/// For worktrees, they intentionally return different paths. Do not consolidate
/// these functions without understanding this distinction.
fn find_git_dir(cwd: &Path) -> Option<std::path::PathBuf> {
    let mut current = cwd;
    loop {
        let git_path = current.join(".git");
        if git_path.exists() {
            if git_path.is_file() {
                // Worktree: .git file contains "gitdir: /path/to/.git/worktrees/name"
                // The path may be relative, so resolve it against the .git file's directory
                let content = fs::read_to_string(&git_path).ok()?;
                let gitdir = content.strip_prefix("gitdir: ")?.trim();
                let gitdir_path = Path::new(gitdir);

                // Resolve relative paths against the directory containing the .git file
                let resolved = if gitdir_path.is_relative() {
                    current.join(gitdir_path)
                } else {
                    gitdir_path.to_path_buf()
                };

                // For worktrees, config is in the main .git directory
                // The worktree .git points to .git/worktrees/name, we need .git
                // Go up two levels: .git/worktrees/name -> .git
                return resolved.parent()?.parent().map(|p| p.to_path_buf());
            }
            return Some(git_path);
        }
        current = current.parent()?;
    }
}

/// Parse GitHub repo from a git remote URL.
///
/// Extracts the `owner/repo` format from various GitHub URL formats.
/// Returns `None` if the URL is not a recognized GitHub format.
///
/// # Supported formats
///
/// - SSH: `git@github.com:owner/repo.git` -> `owner/repo`
/// - HTTPS: `https://github.com/owner/repo.git` -> `owner/repo`
/// - HTTPS (no .git): `https://github.com/owner/repo` -> `owner/repo`
/// - SSH URL: `ssh://git@github.com/owner/repo.git` -> `owner/repo`
///
/// # Example
///
/// ```
/// use mi6_core::context::parse_github_repo_from_remote_url;
///
/// assert_eq!(
///     parse_github_repo_from_remote_url("git@github.com:owner/repo.git"),
///     Some("owner/repo".to_string())
/// );
/// assert_eq!(
///     parse_github_repo_from_remote_url("https://github.com/owner/repo"),
///     Some("owner/repo".to_string())
/// );
/// ```
pub fn parse_github_repo_from_remote_url(url: &str) -> Option<String> {
    // SSH format: git@github.com:owner/repo.git
    if let Some(rest) = url.strip_prefix("git@github.com:") {
        let repo = rest.trim_end_matches(".git");
        return Some(repo.to_string());
    }

    // HTTPS format: https://github.com/owner/repo.git or https://github.com/owner/repo
    if let Some(rest) = url
        .strip_prefix("https://github.com/")
        .or_else(|| url.strip_prefix("http://github.com/"))
    {
        let repo = rest.trim_end_matches(".git");
        return Some(repo.to_string());
    }

    // SSH URL format: ssh://git@github.com/owner/repo.git
    if let Some(rest) = url.strip_prefix("ssh://git@github.com/") {
        let repo = rest.trim_end_matches(".git");
        return Some(repo.to_string());
    }

    None
}

/// Check if a shell command might change the git branch.
///
/// Returns `true` for commands like:
/// - `git checkout <branch>`
/// - `git switch <branch>`
/// - `git worktree add <path>`
/// - `git merge <branch>` (in case of fast-forward)
/// - `git rebase <branch>`
///
/// This is a heuristic and may have false positives/negatives.
pub fn is_branch_changing_command(command: &str) -> bool {
    let cmd_lower = command.to_lowercase();

    for part in split_chained_commands(&cmd_lower) {
        let trimmed = part.trim();

        // Must start with "git"
        if !trimmed.starts_with("git ") && !trimmed.starts_with("git\t") {
            continue;
        }

        // Check for branch-changing subcommands
        let git_args = trimmed.strip_prefix("git").unwrap_or("").trim();

        // git checkout (not -b which creates a branch without switching context significantly)
        // but git checkout -b still changes branch, so include it
        if git_args.starts_with("checkout ") || git_args.starts_with("checkout\t") {
            return true;
        }

        // git switch
        if git_args.starts_with("switch ") || git_args.starts_with("switch\t") {
            return true;
        }

        // git worktree add (changes branch in new worktree context)
        if git_args.starts_with("worktree ")
            && (git_args.contains("add ") || git_args.contains("add\t"))
        {
            return true;
        }

        // git merge (can change branch via fast-forward)
        if git_args.starts_with("merge ") || git_args.starts_with("merge\t") {
            return true;
        }

        // git rebase (can change branch)
        if git_args.starts_with("rebase ") || git_args.starts_with("rebase\t") {
            return true;
        }

        // git pull (can change branch via merge/rebase)
        if git_args.starts_with("pull ") || git_args.starts_with("pull\t") || git_args == "pull" {
            return true;
        }
    }

    false
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_branch_info_no_extraction() {
        // Branch parsing no longer extracts issue/PR numbers from branch names.
        // This avoids false positives like "upgrade-text-fix-2" being parsed as issue #2.

        let info = parse_branch_info("main");
        assert_eq!(info.branch, "main");
        assert_eq!(info.pr_number, None);
        assert_eq!(info.issue_number, None);

        let info = parse_branch_info("feature/issue-123");
        assert_eq!(info.branch, "feature/issue-123");
        assert_eq!(info.issue_number, None);

        let info = parse_branch_info("pr-100");
        assert_eq!(info.branch, "pr-100");
        assert_eq!(info.pr_number, None);

        let info = parse_branch_info("upgrade-text-fix-2");
        assert_eq!(info.branch, "upgrade-text-fix-2");
        assert_eq!(info.issue_number, None);
    }

    #[test]
    fn test_is_branch_changing_command() {
        // Should detect
        assert!(is_branch_changing_command("git checkout main"));
        assert!(is_branch_changing_command("git checkout -b feature"));
        assert!(is_branch_changing_command("git switch develop"));
        assert!(is_branch_changing_command("git worktree add ../wt main"));
        assert!(is_branch_changing_command("git merge feature"));
        assert!(is_branch_changing_command("git rebase main"));
        assert!(is_branch_changing_command("git pull"));
        assert!(is_branch_changing_command("git pull origin main"));

        // Chained commands
        assert!(is_branch_changing_command("echo foo && git checkout main"));
        assert!(is_branch_changing_command("git status; git checkout main"));

        // Should not detect
        assert!(!is_branch_changing_command("git status"));
        assert!(!is_branch_changing_command("git add ."));
        assert!(!is_branch_changing_command("git commit -m 'test'"));
        assert!(!is_branch_changing_command("git push"));
        assert!(!is_branch_changing_command("git log"));
        assert!(!is_branch_changing_command("git diff"));
        assert!(!is_branch_changing_command("git branch -a"));
        assert!(!is_branch_changing_command("echo checkout"));
        assert!(!is_branch_changing_command("cargo test"));
    }

    #[test]
    fn test_get_current_branch_in_git_repo() {
        // This test assumes we're running in a git repo
        let Ok(cwd) = std::env::current_dir() else {
            return; // Skip test if cwd is unavailable
        };
        let branch = get_current_branch(&cwd);

        // Should return Some branch name in a git repo
        // (may be None if running outside a git repo)
        if let Some(ref b) = branch {
            assert!(!b.is_empty());
            assert_ne!(b, "HEAD");
        }
    }

    #[test]
    fn test_get_current_branch_not_git_repo() {
        // Use a path that definitely doesn't exist to ensure it's not a git repo
        let branch = get_current_branch(Path::new("/nonexistent/path/that/should/not/exist"));
        assert!(branch.is_none());
    }

    #[test]
    fn test_parse_github_repo_ssh() {
        assert_eq!(
            parse_github_repo_from_remote_url("git@github.com:owner/repo.git"),
            Some("owner/repo".to_string())
        );
        assert_eq!(
            parse_github_repo_from_remote_url("git@github.com:paradigmxyz/mi6.git"),
            Some("paradigmxyz/mi6".to_string())
        );
    }

    #[test]
    fn test_parse_github_repo_https() {
        assert_eq!(
            parse_github_repo_from_remote_url("https://github.com/owner/repo.git"),
            Some("owner/repo".to_string())
        );
        assert_eq!(
            parse_github_repo_from_remote_url("https://github.com/owner/repo"),
            Some("owner/repo".to_string())
        );
        assert_eq!(
            parse_github_repo_from_remote_url("http://github.com/owner/repo.git"),
            Some("owner/repo".to_string())
        );
    }

    #[test]
    fn test_parse_github_repo_ssh_url() {
        assert_eq!(
            parse_github_repo_from_remote_url("ssh://git@github.com/owner/repo.git"),
            Some("owner/repo".to_string())
        );
        assert_eq!(
            parse_github_repo_from_remote_url("ssh://git@github.com/owner/repo"),
            Some("owner/repo".to_string())
        );
    }

    #[test]
    fn test_parse_github_repo_non_github() {
        // Non-GitHub URLs should return None
        assert_eq!(
            parse_github_repo_from_remote_url("git@gitlab.com:owner/repo.git"),
            None
        );
        assert_eq!(
            parse_github_repo_from_remote_url("https://gitlab.com/owner/repo"),
            None
        );
        assert_eq!(
            parse_github_repo_from_remote_url("git@bitbucket.org:owner/repo.git"),
            None
        );
    }

    #[test]
    fn test_get_github_repo_in_git_repo() {
        // This test assumes we're running in a git repo with GitHub origin
        let Ok(cwd) = std::env::current_dir() else {
            return; // Skip test if cwd is unavailable
        };
        let repo = get_github_repo(&cwd);

        // Should return Some repo name if in a GitHub repo
        // May return None if not in a git repo or no GitHub origin
        if let Some(ref r) = repo {
            assert!(!r.is_empty());
            assert!(r.contains('/'), "repo should be in owner/repo format");
        }
    }

    #[test]
    fn test_get_github_repo_not_git_repo() {
        // Use a path that definitely doesn't exist to ensure it's not a git repo
        let repo = get_github_repo(Path::new("/nonexistent/path/that/should/not/exist"));
        assert!(repo.is_none());
    }

    #[test]
    fn test_get_local_git_dir_in_git_repo() {
        // This test assumes we're running in a git repo
        let Ok(cwd) = std::env::current_dir() else {
            return; // Skip test if cwd is unavailable
        };
        let git_dir = get_local_git_dir(&cwd);

        // Should return Some path if in a git repo
        if let Some(ref path) = git_dir {
            assert!(!path.is_empty());
            // Should be an absolute path
            assert!(
                Path::new(path).is_absolute(),
                "git_dir should be absolute: {}",
                path
            );
            // Should contain ".git" in the path
            assert!(
                path.contains(".git"),
                "git_dir should contain .git: {}",
                path
            );
        }
    }

    #[test]
    fn test_get_local_git_dir_not_git_repo() {
        // Use a path that definitely doesn't exist to ensure it's not a git repo
        let git_dir = get_local_git_dir(Path::new("/nonexistent/path/that/should/not/exist"));
        assert!(git_dir.is_none());
    }

    #[test]
    fn test_get_worktree_root_not_git_repo() {
        // Use a path that definitely doesn't exist to ensure it's not a git repo
        let root = get_worktree_root(Path::new("/nonexistent/path/that/should/not/exist"));
        assert!(root.is_none());
    }

    #[test]
    fn test_get_worktree_root_normal_repo() {
        // In a normal git repo (not a worktree), should return None
        // This test assumes we're running in a git repo
        let Ok(cwd) = std::env::current_dir() else {
            return; // Skip test if cwd is unavailable
        };

        // First check if we're in a git repo at all
        let git_dir = get_local_git_dir(&cwd);
        if git_dir.is_none() {
            return; // Skip test if not in a git repo
        }

        // Check if cwd's .git is a directory (normal repo) or file (worktree)
        // We need to find the .git entry to determine which type we're in
        let mut current = cwd.as_path();
        loop {
            let git_path = current.join(".git");
            if git_path.exists() {
                if git_path.is_dir() {
                    // Normal repo - get_worktree_root should return None
                    assert!(
                        get_worktree_root(&cwd).is_none(),
                        "get_worktree_root should return None in normal repo"
                    );
                } else {
                    // Worktree - get_worktree_root should return Some
                    let root = get_worktree_root(&cwd);
                    assert!(
                        root.is_some(),
                        "get_worktree_root should return Some in worktree"
                    );
                    let root_path = root.unwrap();
                    assert!(
                        Path::new(&root_path).is_absolute(),
                        "worktree root should be absolute: {}",
                        root_path
                    );
                }
                break;
            }
            match current.parent() {
                Some(parent) => current = parent,
                None => break,
            }
        }
    }
}
