use base64::{engine::general_purpose::STANDARD, Engine as _};
use reqwest::blocking::Client;
use reqwest::header::{ACCEPT, AUTHORIZATION};
use serde::Serialize;
use std::collections::HashSet;
use std::env;
use std::fs;
use std::io::Cursor;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;

#[derive(Serialize)]
struct GitCommandResult {
    command: String,
    stdout: String,
    stderr: String,
    success: bool,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ServerConnectionResult {
    ok: bool,
    message: String,
    api_base_url: String,
    version: Option<String>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalRepoBranch {
    name: String,
    current: bool,
    is_remote: bool,
    remote_name: Option<String>,
    local_name: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalRepoEntry {
    name: String,
    path: String,
    entry_type: String,
    size: Option<u64>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalRepoFile {
    path: String,
    size: u64,
    content: Option<String>,
    is_binary: bool,
    too_large: bool,
    preview_mime: Option<String>,
    preview_base64: Option<String>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalRepoCandidate {
    name: String,
    path: String,
    matched_remote_url: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitChangedFile {
    path: String,
    original_path: Option<String>,
    x: String,
    y: String,
    status: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitStatusSummary {
    branch: Option<String>,
    upstream: Option<String>,
    ahead: u32,
    behind: u32,
    files: Vec<GitChangedFile>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitRepositorySyncStatus {
    branch: Option<String>,
    upstream: Option<String>,
    upstream_remote: Option<String>,
    default_remote: Option<String>,
    ahead: u32,
    behind: u32,
    has_remote: bool,
    is_detached: bool,
    is_unpublished: bool,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitFileDiff {
    path: String,
    diff: String,
    is_binary: bool,
    is_deleted: bool,
    is_untracked: bool,
    preview_mime: Option<String>,
    preview_base64: Option<String>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitCommitSummary {
    hash: String,
    short_hash: String,
    title: String,
    author: String,
    date: String,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GitCommitDetail {
    hash: String,
    short_hash: String,
    title: String,
    message: String,
    author: String,
    date: String,
    files: Vec<GitChangedFile>,
    diff: String,
}

#[derive(Serialize, Clone)]
#[serde(rename_all = "camelCase")]
struct InstalledIde {
    id: String,
    name: String,
    executable_path: String,
}

fn normalize_api_base_url(server_url: &str) -> Result<String, String> {
    // Normalize user input so every backend consistently resolves to /api/v1.
    let trimmed = server_url.trim().trim_end_matches('/');
    if !(trimmed.starts_with("http://") || trimmed.starts_with("https://")) {
        return Err("Server URL must start with http:// or https://".to_string());
    }

    if trimmed.ends_with("/api/v1") {
        Ok(trimmed.to_string())
    } else {
        Ok(format!("{trimmed}/api/v1"))
    }
}

fn resolve_git_binary(git_path: Option<String>) -> String {
    git_path
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .unwrap_or("git")
        .to_string()
}

fn new_background_command(program: &str) -> Command {
    let mut command = Command::new(program);
    #[cfg(target_os = "windows")]
    command.creation_flags(CREATE_NO_WINDOW);
    command
}

fn run_git_command(
    repo_path: Option<&str>,
    git_path: Option<String>,
    args: Vec<String>,
) -> Result<GitCommandResult, String> {
    // Central command executor keeps Git command behavior consistent.
    let git_binary = resolve_git_binary(git_path);
    let mut command = new_background_command(&git_binary);

    if let Some(path) = repo_path {
        if !Path::new(path).exists() {
            return Err(format!("Repository path does not exist: {path}"));
        }
        command.current_dir(path);
    }

    command.args(&args);
    let output = command
        .output()
        .map_err(|err| format!("Failed to run git command: {err}"))?;

    Ok(GitCommandResult {
        command: format!("{git_binary} {}", args.join(" ")),
        stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(),
        stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(),
        success: output.status.success(),
    })
}

fn run_git_output(
    repo_path: &str,
    git_path: Option<String>,
    args: Vec<String>,
) -> Result<Vec<u8>, String> {
    let git_binary = resolve_git_binary(git_path);
    let path = repo_path.trim();
    if !Path::new(path).exists() {
        return Err(format!("Repository path does not exist: {path}"));
    }

    let mut command = new_background_command(&git_binary);
    let output = command
        .current_dir(path)
        .args(&args)
        .output()
        .map_err(|err| format!("Failed to run git command: {err}"))?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        return Err(if stderr.is_empty() {
            format!("{git_binary} {} failed", args.join(" "))
        } else {
            stderr
        });
    }

    Ok(output.stdout)
}

fn ensure_git_success(result: GitCommandResult) -> Result<GitCommandResult, String> {
    if result.success {
        Ok(result)
    } else if !result.stderr.is_empty() {
        Err(result.stderr)
    } else {
        Err(format!("{} failed", result.command))
    }
}

fn validate_repo_dir(repo_path: &str) -> Result<String, String> {
    let path = repo_path.trim();
    if path.is_empty() {
        return Err("Repository path is required.".to_string());
    }
    let path_buf = PathBuf::from(path);
    if !path_buf.exists() || !path_buf.is_dir() {
        return Err(format!("Repository path does not exist: {path}"));
    }
    if !path_buf.join(".git").exists() {
        return Err(format!("Selected path is not a Git repository: {path}"));
    }
    Ok(path.to_string())
}

#[cfg(target_os = "windows")]
fn normalize_windows_verbatim_path(path: &str) -> String {
    if let Some(stripped) = path.strip_prefix(r"\\?\UNC\") {
        format!(r"\\{stripped}")
    } else if let Some(stripped) = path.strip_prefix(r"\\?\") {
        stripped.to_string()
    } else {
        path.to_string()
    }
}

fn user_facing_path(path: &Path) -> String {
    let path = path.to_string_lossy().to_string();

    #[cfg(target_os = "windows")]
    {
        normalize_windows_verbatim_path(&path)
    }

    #[cfg(not(target_os = "windows"))]
    {
        path
    }
}

fn repo_display_path(path: &Path) -> String {
    let display_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
    user_facing_path(&display_path)
}

fn normalize_external_repo_path(path: &str) -> String {
    #[cfg(target_os = "windows")]
    {
        normalize_windows_verbatim_path(path)
    }

    #[cfg(not(target_os = "windows"))]
    {
        path.to_string()
    }
}

fn validate_git_path(path: &str) -> Result<String, String> {
    let normalized = normalize_repo_path(path)?;
    if normalized.is_empty() {
        return Err("File path is required.".to_string());
    }
    Ok(normalized)
}

fn add_installed_ide(
    ides: &mut Vec<InstalledIde>,
    seen: &mut HashSet<String>,
    name: &str,
    executable_path: PathBuf,
) {
    if !executable_path.is_file() {
        return;
    }

    let executable_path = executable_path.to_string_lossy().to_string();
    let key = executable_path.to_lowercase();
    if seen.insert(key) {
        ides.push(InstalledIde {
            id: format!(
                "{}:{}",
                name.to_lowercase().replace(' ', "-"),
                executable_path
            ),
            name: name.to_string(),
            executable_path,
        });
    }
}

fn env_path(name: &str) -> Option<PathBuf> {
    env::var_os(name).map(PathBuf::from)
}

fn add_relative_ide_candidate(
    ides: &mut Vec<InstalledIde>,
    seen: &mut HashSet<String>,
    env_name: &str,
    relative_path: &str,
    name: &str,
) {
    if let Some(base_path) = env_path(env_name) {
        add_installed_ide(ides, seen, name, base_path.join(relative_path));
    }
}

fn add_path_ide_candidate(
    ides: &mut Vec<InstalledIde>,
    seen: &mut HashSet<String>,
    command_names: &[&str],
    name: &str,
) {
    if let Some(executable_path) = find_executable_on_path(command_names) {
        add_installed_ide(ides, seen, name, executable_path);
    }
}

fn find_executable_on_path(command_names: &[&str]) -> Option<PathBuf> {
    let path_values = env::var_os("PATH")?;
    let extensions: Vec<String> = if cfg!(windows) {
        env::var("PATHEXT")
            .unwrap_or_else(|_| ".EXE;.CMD;.BAT".to_string())
            .split(';')
            .map(|value| value.trim().to_string())
            .filter(|value| !value.is_empty())
            .collect()
    } else {
        vec!["".to_string()]
    };

    for dir in env::split_paths(&path_values) {
        for command_name in command_names {
            let command_path = Path::new(command_name);
            let has_extension = command_path.extension().is_some();
            let candidates = if has_extension {
                vec![dir.join(command_name)]
            } else {
                extensions
                    .iter()
                    .map(|extension| dir.join(format!("{command_name}{extension}")))
                    .collect()
            };

            for candidate in candidates {
                if candidate.is_file() {
                    return Some(candidate);
                }
            }
        }
    }

    None
}

#[cfg(target_os = "windows")]
fn add_windows_ide_candidates(ides: &mut Vec<InstalledIde>, seen: &mut HashSet<String>) {
    let candidates = [
        (
            "LOCALAPPDATA",
            "Programs\\Microsoft VS Code\\Code.exe",
            "Visual Studio Code",
        ),
        (
            "LOCALAPPDATA",
            "Programs\\Microsoft VS Code Insiders\\Code - Insiders.exe",
            "Visual Studio Code Insiders",
        ),
        ("LOCALAPPDATA", "Programs\\Cursor\\Cursor.exe", "Cursor"),
        (
            "LOCALAPPDATA",
            "Programs\\Windsurf\\Windsurf.exe",
            "Windsurf",
        ),
        (
            "LOCALAPPDATA",
            "Programs\\VSCodium\\VSCodium.exe",
            "VSCodium",
        ),
        ("LOCALAPPDATA", "Programs\\Zed\\Zed.exe", "Zed"),
        (
            "ProgramFiles",
            "Microsoft VS Code\\Code.exe",
            "Visual Studio Code",
        ),
        (
            "ProgramFiles",
            "Microsoft VS Code Insiders\\Code - Insiders.exe",
            "Visual Studio Code Insiders",
        ),
        ("ProgramFiles", "Cursor\\Cursor.exe", "Cursor"),
        ("ProgramFiles", "Windsurf\\Windsurf.exe", "Windsurf"),
        ("ProgramFiles", "VSCodium\\VSCodium.exe", "VSCodium"),
        (
            "ProgramFiles",
            "Sublime Text\\sublime_text.exe",
            "Sublime Text",
        ),
        ("ProgramFiles", "Notepad++\\notepad++.exe", "Notepad++"),
        ("ProgramFiles(x86)", "Notepad++\\notepad++.exe", "Notepad++"),
    ];

    for (env_name, relative_path, name) in candidates {
        add_relative_ide_candidate(ides, seen, env_name, relative_path, name);
    }

    for env_name in ["ProgramFiles", "ProgramFiles(x86)"] {
        if let Some(jetbrains_root) = env_path(env_name).map(|path| path.join("JetBrains")) {
            add_jetbrains_candidates(ides, seen, &jetbrains_root);
        }
    }
}

#[cfg(not(target_os = "windows"))]
fn add_windows_ide_candidates(_ides: &mut Vec<InstalledIde>, _seen: &mut HashSet<String>) {}

fn add_jetbrains_candidates(ides: &mut Vec<InstalledIde>, seen: &mut HashSet<String>, root: &Path) {
    let Ok(entries) = fs::read_dir(root) else {
        return;
    };

    for entry in entries.flatten() {
        let app_dir = entry.path();
        if !app_dir.is_dir() {
            continue;
        }

        let dir_name = app_dir
            .file_name()
            .and_then(|value| value.to_str())
            .unwrap_or("JetBrains IDE")
            .to_string();
        let candidates = [
            ("IntelliJ IDEA", "idea64.exe"),
            ("WebStorm", "webstorm64.exe"),
            ("PhpStorm", "phpstorm64.exe"),
            ("PyCharm", "pycharm64.exe"),
            ("Rider", "rider64.exe"),
            ("RustRover", "rustrover64.exe"),
            ("GoLand", "goland64.exe"),
        ];

        for (label, exe) in candidates {
            let label_lower = label.to_lowercase();
            let name = if dir_name.to_lowercase().contains(label_lower.as_str()) {
                dir_name.clone()
            } else {
                label.to_string()
            };
            add_installed_ide(ides, seen, &name, app_dir.join("bin").join(exe));
        }
    }
}

fn status_kind(x: &str, y: &str) -> String {
    match (x, y) {
        ("?", "?") => "untracked",
        ("U", _) | (_, "U") | ("A", "A") | ("D", "D") => "conflicted",
        ("R", _) | (_, "R") => "renamed",
        ("A", _) | (_, "A") => "added",
        ("D", _) | (_, "D") => "deleted",
        _ => "modified",
    }
    .to_string()
}

fn parse_porcelain_file(line: &str) -> Option<GitChangedFile> {
    if line.len() < 4 {
        return None;
    }

    let x = line.get(0..1)?.to_string();
    let y = line.get(1..2)?.to_string();
    let path_part = line.get(3..)?.trim();
    if path_part.is_empty() {
        return None;
    }

    let (path, original_path) =
        if matches!(x.as_str(), "R" | "C") || matches!(y.as_str(), "R" | "C") {
            if let Some((old_path, new_path)) = path_part.split_once(" -> ") {
                (
                    new_path.trim().to_string(),
                    Some(old_path.trim().to_string()),
                )
            } else {
                (path_part.to_string(), None)
            }
        } else {
            (path_part.to_string(), None)
        };

    Some(GitChangedFile {
        status: status_kind(&x, &y),
        path,
        original_path,
        x,
        y,
    })
}

fn parse_branch_status(line: &str) -> (Option<String>, Option<String>, u32, u32) {
    let value = line.trim_start_matches("## ").trim();
    if value == "No commits yet on" {
        return (None, None, 0, 0);
    }

    let (head, meta) = value
        .split_once(" [")
        .map(|(left, right)| (left, Some(right.trim_end_matches(']'))))
        .unwrap_or((value, None));
    let (branch, upstream) = head
        .split_once("...")
        .map(|(left, right)| (left.to_string(), Some(right.to_string())))
        .unwrap_or((head.to_string(), None));

    let mut ahead = 0;
    let mut behind = 0;
    if let Some(meta) = meta {
        for part in meta.split(',').map(str::trim) {
            if let Some(value) = part.strip_prefix("ahead ") {
                ahead = value.parse().unwrap_or(0);
            } else if let Some(value) = part.strip_prefix("behind ") {
                behind = value.parse().unwrap_or(0);
            }
        }
    }

    (Some(branch), upstream, ahead, behind)
}

fn parse_porcelain_status(stdout: &str) -> GitStatusSummary {
    let mut branch = None;
    let mut upstream = None;
    let mut ahead = 0;
    let mut behind = 0;
    let mut files = Vec::new();

    for line in stdout.lines() {
        if line.starts_with("## ") {
            (branch, upstream, ahead, behind) = parse_branch_status(line);
        } else if let Some(file) = parse_porcelain_file(line) {
            files.push(file);
        }
    }

    GitStatusSummary {
        branch,
        upstream,
        ahead,
        behind,
        files,
    }
}

fn git_output_text(
    repo_path: &str,
    git_path: Option<String>,
    args: Vec<&str>,
) -> Result<String, String> {
    let result = ensure_git_success(run_git_command(
        Some(repo_path),
        git_path,
        args.into_iter().map(str::to_string).collect(),
    )?)?;
    Ok(result.stdout.trim().to_string())
}

fn git_output_text_optional(
    repo_path: &str,
    git_path: Option<String>,
    args: Vec<&str>,
) -> Option<String> {
    git_output_text(repo_path, git_path, args)
        .ok()
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())
}

fn parse_remote_names(stdout: &str) -> Vec<String> {
    stdout
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(ToString::to_string)
        .collect()
}

fn split_upstream_remote(upstream: &str) -> Option<String> {
    upstream
        .split_once('/')
        .map(|(remote, _)| remote.to_string())
        .filter(|remote| !remote.is_empty())
}

fn ahead_behind_counts(repo_path: &str, git_path: Option<String>) -> (u32, u32) {
    let Some(stdout) = git_output_text_optional(
        repo_path,
        git_path,
        vec!["rev-list", "--left-right", "--count", "HEAD...@{u}"],
    ) else {
        return (0, 0);
    };

    let mut parts = stdout.split_whitespace();
    let ahead = parts
        .next()
        .and_then(|value| value.parse().ok())
        .unwrap_or(0);
    let behind = parts
        .next()
        .and_then(|value| value.parse().ok())
        .unwrap_or(0);
    (ahead, behind)
}

fn command_with_paths(base_args: &[&str], paths: Vec<String>) -> Result<Vec<String>, String> {
    if paths.is_empty() {
        return Err("At least one file must be selected.".to_string());
    }

    let mut args = base_args
        .iter()
        .map(|value| value.to_string())
        .collect::<Vec<_>>();
    args.push("--".to_string());
    for path in paths {
        args.push(validate_git_path(&path)?);
    }
    Ok(args)
}

fn is_binary_file(path: &Path) -> bool {
    fs::read(path)
        .map(|content| content.contains(&0))
        .unwrap_or(false)
}

/// Max decoded image size embedded into the UI as a data URL (base64 expands payload).
const MAX_IMAGE_PREVIEW_BYTES: u64 = 5 * 1024 * 1024;

fn repository_relative_image_mime(path: &str) -> Option<&'static str> {
    let lower = path.to_lowercase();
    let ext = Path::new(&lower).extension()?.to_str()?;
    match ext {
        "png" => Some("image/png"),
        "jpg" | "jpeg" => Some("image/jpeg"),
        "gif" => Some("image/gif"),
        "webp" => Some("image/webp"),
        "bmp" | "dib" => Some("image/bmp"),
        "ico" => Some("image/x-icon"),
        "avif" => Some("image/avif"),
        "tif" | "tiff" => Some("image/tiff"),
        "heic" | "heif" => Some("image/heif"),
        "svg" => Some("image/svg+xml"),
        _ => None,
    }
}

fn ico_preview_as_png(bytes: &[u8]) -> Option<(String, String)> {
    let icon_dir = ico::IconDir::read(Cursor::new(bytes)).ok()?;
    let best_entry = icon_dir
        .entries()
        .iter()
        .max_by_key(|entry| (entry.width() * entry.height(), entry.bits_per_pixel()))?;
    let image = best_entry.decode().ok()?;
    let mut png = Vec::new();
    image.write_png(&mut png).ok()?;
    Some(("image/png".to_string(), STANDARD.encode(png)))
}

fn image_preview_pair(path: &str, bytes: &[u8]) -> (Option<String>, Option<String>) {
    let Some(mime) = repository_relative_image_mime(path) else {
        return (None, None);
    };
    if bytes.len() as u64 > MAX_IMAGE_PREVIEW_BYTES {
        return (None, None);
    }
    if Path::new(path)
        .extension()
        .and_then(|ext| ext.to_str())
        .is_some_and(|ext| ext.eq_ignore_ascii_case("ico"))
    {
        if let Some((mime, encoded_png)) = ico_preview_as_png(bytes) {
            return (Some(mime), Some(encoded_png));
        }
    }
    (Some(mime.to_string()), Some(STANDARD.encode(bytes)))
}

fn diff_image_preview(
    repo_path: &str,
    git_path: Option<String>,
    file_path: &str,
    is_deleted: bool,
) -> (Option<String>, Option<String>) {
    if repository_relative_image_mime(file_path).is_none() {
        return (None, None);
    }

    if is_deleted {
        let spec = format!("HEAD:{file_path}");
        let size_bytes = match run_git_output(
            repo_path,
            git_path.clone(),
            vec!["cat-file".to_string(), "-s".to_string(), spec.clone()],
        ) {
            Ok(buf) => buf,
            Err(_) => return (None, None),
        };
        let size_str = String::from_utf8_lossy(&size_bytes);
        let Ok(size) = size_str.trim().parse::<u64>() else {
            return (None, None);
        };
        if size > MAX_IMAGE_PREVIEW_BYTES {
            return (None, None);
        }
        let bytes = match run_git_output(
            repo_path,
            git_path,
            vec!["show".to_string(), "--no-ext-diff".to_string(), spec],
        ) {
            Ok(b) => b,
            Err(_) => return (None, None),
        };
        return image_preview_pair(file_path, &bytes);
    }

    let absolute = PathBuf::from(repo_path).join(file_path);
    let Ok(meta) = fs::metadata(&absolute) else {
        return (None, None);
    };
    if meta.len() > MAX_IMAGE_PREVIEW_BYTES {
        return (None, None);
    }
    let Ok(bytes) = fs::read(&absolute) else {
        return (None, None);
    };
    image_preview_pair(file_path, &bytes)
}

fn normalize_repo_path(path: &str) -> Result<String, String> {
    let trimmed = path.trim().trim_matches('/');
    if trimmed.contains('\\') || trimmed.contains('\0') {
        return Err("Repository paths must use forward slashes.".to_string());
    }

    let mut parts = Vec::new();
    for part in trimmed.split('/') {
        if part.is_empty() || part == "." {
            continue;
        }
        if part == ".." {
            return Err("Repository paths cannot contain parent directory segments.".to_string());
        }
        parts.push(part);
    }

    Ok(parts.join("/"))
}

fn normalize_reference(reference: &str) -> Result<String, String> {
    let trimmed = reference.trim();
    if trimmed.is_empty() || trimmed.contains('\0') {
        return Err("Branch or reference is required.".to_string());
    }
    Ok(trimmed.to_string())
}

fn git_ref_exists(repo_path: &str, git_path: Option<String>, ref_name: &str) -> bool {
    run_git_command(
        Some(repo_path),
        git_path,
        vec![
            "show-ref".to_string(),
            "--verify".to_string(),
            "--quiet".to_string(),
            ref_name.to_string(),
        ],
    )
    .map(|result| result.success)
    .unwrap_or(false)
}

fn treeish(reference: &str, path: &str) -> String {
    if path.is_empty() {
        reference.to_string()
    } else {
        format!("{reference}:{path}")
    }
}

fn dedupe_key(path: &Path) -> String {
    let path = path
        .canonicalize()
        .unwrap_or_else(|_| path.to_path_buf())
        .to_string_lossy()
        .replace('\\', "/")
        .trim_end_matches('/')
        .to_string();

    #[cfg(windows)]
    {
        path.to_lowercase()
    }

    #[cfg(not(windows))]
    {
        path
    }
}

fn add_if_exists(paths: &mut Vec<PathBuf>, path: PathBuf) {
    if !path.exists() {
        return;
    }

    let key = dedupe_key(&path);
    if !paths.iter().any(|existing| dedupe_key(existing) == key) {
        paths.push(path);
    }
}

fn default_repo_scan_roots() -> Vec<PathBuf> {
    let mut roots = Vec::new();

    if let Some(home) = env::var_os("USERPROFILE").or_else(|| env::var_os("HOME")) {
        let home = PathBuf::from(home);
        add_if_exists(&mut roots, home.join("Repos"));
        add_if_exists(&mut roots, home.join("repos"));
        add_if_exists(&mut roots, home.join("Code"));
        add_if_exists(&mut roots, home.join("code"));
        add_if_exists(&mut roots, home.join("Source"));
        add_if_exists(&mut roots, home.join("source"));
        add_if_exists(&mut roots, home.join("Projects"));
        add_if_exists(&mut roots, home.join("GitHub"));
        add_if_exists(&mut roots, home.join("Documents").join("GitHub"));
        add_if_exists(&mut roots, home.join("Documents").join("Repos"));
        add_if_exists(&mut roots, home.join("Desktop"));
    }

    #[cfg(windows)]
    {
        for drive in b'A'..=b'Z' {
            let root = format!("{}:\\", drive as char);
            let root_path = PathBuf::from(&root);
            if !root_path.exists() {
                continue;
            }

            for folder in [
                "Repos", "repos", "Code", "code", "Source", "source", "Projects", "GitHub", "Dev",
            ] {
                add_if_exists(&mut roots, root_path.join(folder));
            }
        }
    }

    roots
}

fn is_skippable_dir(path: &Path) -> bool {
    let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
        return false;
    };

    matches!(
        name,
        ".cache"
            | ".cargo"
            | ".gradle"
            | ".npm"
            | ".rustup"
            | ".svn"
            | ".tauri"
            | "AppData"
            | "Library"
            | "node_modules"
            | "target"
            | "vendor"
    )
}

fn is_git_repo(path: &Path) -> bool {
    path.join(".git").exists()
}

fn normalize_git_remote_url(value: &str) -> String {
    let mut remote = value.trim().trim_end_matches('/').to_lowercase();
    if let Some(stripped) = remote.strip_suffix(".git") {
        remote = stripped.to_string();
    }
    remote
}

fn local_remote_urls(repo_path: &Path, git_path: Option<String>) -> Vec<String> {
    let git_binary = resolve_git_binary(git_path);
    let Ok(output) = new_background_command(&git_binary)
        .current_dir(repo_path)
        .args(["config", "--get-regexp", r"^remote\..*\.url$"])
        .output()
    else {
        return Vec::new();
    };

    if !output.status.success() {
        return Vec::new();
    }

    String::from_utf8_lossy(&output.stdout)
        .lines()
        .filter_map(|line| line.split_once(' ').map(|(_, url)| url.trim().to_string()))
        .filter(|url| !url.is_empty())
        .collect()
}

fn matched_server_remote(
    repo_path: &Path,
    git_path: Option<String>,
    allowed_remote_urls: &HashSet<String>,
) -> Option<String> {
    local_remote_urls(repo_path, git_path)
        .into_iter()
        .find(|remote| allowed_remote_urls.contains(&normalize_git_remote_url(remote)))
}

fn collect_repo_candidates(
    root: &Path,
    depth: u8,
    max_depth: u8,
    results: &mut Vec<LocalRepoCandidate>,
    seen: &mut HashSet<String>,
    allowed_remote_urls: &HashSet<String>,
    git_path: Option<String>,
    max_results: usize,
) {
    if results.len() >= max_results || is_skippable_dir(root) {
        return;
    }

    if is_git_repo(root) {
        if seen.insert(dedupe_key(root)) {
            let path = repo_display_path(root);
            let Some(matched_remote_url) =
                matched_server_remote(root, git_path.clone(), allowed_remote_urls)
            else {
                return;
            };
            let name = root
                .file_name()
                .and_then(|value| value.to_str())
                .unwrap_or(&path)
                .to_string();
            results.push(LocalRepoCandidate {
                name,
                path,
                matched_remote_url,
            });
        }
        return;
    }

    if depth >= max_depth {
        return;
    }

    let Ok(entries) = fs::read_dir(root) else {
        return;
    };

    for entry in entries.flatten() {
        if results.len() >= max_results {
            return;
        }

        let path = entry.path();
        if path.is_dir() {
            collect_repo_candidates(
                &path,
                depth + 1,
                max_depth,
                results,
                seen,
                allowed_remote_urls,
                git_path.clone(),
                max_results,
            );
        }
    }
}

#[tauri::command]
fn git_clone(
    repo_url: String,
    destination_path: String,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    if repo_url.trim().is_empty() {
        return Err("Repository URL is required".to_string());
    }
    if destination_path.trim().is_empty() {
        return Err("Destination path is required".to_string());
    }

    run_git_command(
        None,
        git_path,
        vec![
            "clone".to_string(),
            repo_url.trim().to_string(),
            destination_path.trim().to_string(),
        ],
    )
}

#[tauri::command]
fn git_pull(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec!["pull".to_string()],
    )?)
}

#[tauri::command]
fn git_push(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec!["push".to_string()],
    )?)
}

#[tauri::command]
fn git_publish_branch(
    repo_path: String,
    remote: Option<String>,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let status = repository_sync_status(repo_path.clone(), git_path.clone())?;
    if status.is_detached {
        return Err("Cannot publish a detached HEAD.".to_string());
    }

    let branch = status
        .branch
        .ok_or_else(|| "Cannot determine the current branch.".to_string())?;
    let remote = remote
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .or(status.default_remote.as_deref())
        .ok_or_else(|| "No Git remote is configured for this repository.".to_string())?
        .to_string();

    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec!["push".to_string(), "-u".to_string(), remote, branch],
    )?)
}

#[tauri::command]
fn git_status(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
    run_git_command(
        Some(repo_path.trim()),
        git_path,
        vec![
            "status".to_string(),
            "--short".to_string(),
            "--branch".to_string(),
        ],
    )
}

#[tauri::command]
fn git_branch(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
    run_git_command(
        Some(repo_path.trim()),
        git_path,
        vec!["branch".to_string(), "--all".to_string()],
    )
}

#[tauri::command]
fn git_fetch(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec!["fetch".to_string(), "--prune".to_string()],
    )?)
}

#[tauri::command]
fn git_sync(repo_path: String, git_path: Option<String>) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let fetch = ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path.clone(),
        vec!["fetch".to_string(), "--prune".to_string()],
    )?)?;
    let pull = ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path.clone(),
        vec!["pull".to_string()],
    )?)?;
    let push = ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec!["push".to_string()],
    )?)?;

    Ok(GitCommandResult {
        command: "git fetch --prune && git pull && git push".to_string(),
        stdout: [fetch.stdout, pull.stdout, push.stdout]
            .into_iter()
            .filter(|value| !value.is_empty())
            .collect::<Vec<_>>()
            .join("\n\n"),
        stderr: [fetch.stderr, pull.stderr, push.stderr]
            .into_iter()
            .filter(|value| !value.is_empty())
            .collect::<Vec<_>>()
            .join("\n\n"),
        success: true,
    })
}

#[tauri::command]
fn repository_sync_status(
    repo_path: String,
    git_path: Option<String>,
) -> Result<GitRepositorySyncStatus, String> {
    let repo_path = validate_repo_dir(&repo_path)?;

    let branch_output = git_output_text(
        &repo_path,
        git_path.clone(),
        vec!["rev-parse", "--abbrev-ref", "HEAD"],
    )?;
    let is_detached = branch_output == "HEAD";
    let branch = if is_detached {
        None
    } else {
        Some(branch_output)
    };

    let upstream = git_output_text_optional(
        &repo_path,
        git_path.clone(),
        vec!["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
    );
    let upstream_remote = upstream.as_deref().and_then(split_upstream_remote);
    let (ahead, behind) = if upstream.is_some() {
        ahead_behind_counts(&repo_path, git_path.clone())
    } else {
        (0, 0)
    };

    let remotes = git_output_text_optional(&repo_path, git_path, vec!["remote"])
        .map(|stdout| parse_remote_names(&stdout))
        .unwrap_or_default();
    let default_remote = remotes
        .iter()
        .find(|remote| remote.as_str() == "origin")
        .or_else(|| remotes.first())
        .cloned();
    let has_remote = default_remote.is_some();
    let is_unpublished = branch.is_some() && upstream.is_none() && has_remote && !is_detached;

    Ok(GitRepositorySyncStatus {
        branch,
        upstream,
        upstream_remote,
        default_remote,
        ahead,
        behind,
        has_remote,
        is_detached,
        is_unpublished,
    })
}

#[tauri::command]
fn working_tree_status(
    repo_path: String,
    git_path: Option<String>,
) -> Result<GitStatusSummary, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let result = ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec![
            "status".to_string(),
            "--porcelain=v1".to_string(),
            "--branch".to_string(),
            "--renames".to_string(),
        ],
    )?)?;
    Ok(parse_porcelain_status(&result.stdout))
}

#[tauri::command]
fn get_file_diff(
    repo_path: String,
    path: String,
    status: Option<String>,
    git_path: Option<String>,
) -> Result<GitFileDiff, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let file_path = validate_git_path(&path)?;
    let status = status.unwrap_or_default();
    let is_untracked = status == "untracked";
    let is_deleted = status == "deleted";

    if is_untracked {
        let absolute_path = PathBuf::from(&repo_path).join(&file_path);
        if is_binary_file(&absolute_path) {
            let bytes = fs::read(&absolute_path).unwrap_or_default();
            let (preview_mime, preview_base64) = image_preview_pair(&file_path, &bytes);
            return Ok(GitFileDiff {
                path: file_path,
                diff: "Binary file. Diff preview is not available for untracked binary files."
                    .to_string(),
                is_binary: true,
                is_deleted: false,
                is_untracked: true,
                preview_mime,
                preview_base64,
            });
        }
        let content = fs::read_to_string(&absolute_path)
            .map_err(|err| format!("Unable to read untracked file: {err}"))?;
        let diff = format!(
            "diff --git a/{0} b/{0}\nnew file mode 100644\n--- /dev/null\n+++ b/{0}\n{1}",
            file_path,
            content
                .lines()
                .map(|line| format!("+{line}"))
                .collect::<Vec<_>>()
                .join("\n")
        );
        return Ok(GitFileDiff {
            path: file_path,
            diff,
            is_binary: false,
            is_deleted: false,
            is_untracked: true,
            preview_mime: None,
            preview_base64: None,
        });
    }

    let output = run_git_command(
        Some(&repo_path),
        git_path.clone(),
        vec![
            "diff".to_string(),
            "--no-ext-diff".to_string(),
            "--".to_string(),
            file_path.clone(),
        ],
    )?;
    let mut diff = output.stdout;
    if diff.is_empty() {
        diff = output.stderr;
    }
    let is_binary = diff.contains("Binary files") || diff.contains("GIT binary patch");
    let (preview_mime, preview_base64) =
        diff_image_preview(repo_path.trim(), git_path, &file_path, is_deleted);
    Ok(GitFileDiff {
        path: file_path,
        diff: if diff.is_empty() {
            "No unstaged diff is available for this file.".to_string()
        } else {
            diff
        },
        is_binary,
        is_deleted,
        is_untracked: false,
        preview_mime,
        preview_base64,
    })
}

#[tauri::command]
fn stage_files(
    repo_path: String,
    paths: Vec<String>,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let args = command_with_paths(&["add"], paths)?;
    ensure_git_success(run_git_command(Some(&repo_path), git_path, args)?)
}

#[tauri::command]
fn unstage_files(
    repo_path: String,
    paths: Vec<String>,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let args = command_with_paths(&["restore", "--staged"], paths)?;
    ensure_git_success(run_git_command(Some(&repo_path), git_path, args)?)
}

#[tauri::command]
fn commit_changes(
    repo_path: String,
    paths: Vec<String>,
    summary: String,
    description: Option<String>,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let summary = summary.trim();
    if summary.is_empty() {
        return Err("Commit summary is required.".to_string());
    }

    let paths = paths
        .into_iter()
        .map(|path| validate_git_path(&path))
        .collect::<Result<Vec<_>, _>>()?;
    if paths.is_empty() {
        return Err("Select at least one file to commit.".to_string());
    }
    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path.clone(),
        command_with_paths(&["add"], paths.clone())?,
    )?)?;

    let mut args = vec!["commit".to_string(), "--only".to_string()];
    args.push("-m".to_string());
    args.push(summary.to_string());
    if let Some(description) = description
        .as_deref()
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        args.push("-m".to_string());
        args.push(description.to_string());
    }
    args.push("--".to_string());
    for path in paths {
        args.push(path);
    }

    ensure_git_success(run_git_command(Some(&repo_path), git_path, args)?)
}

#[tauri::command]
fn checkout_branch(
    repo_path: String,
    branch: String,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let branch = normalize_reference(&branch)?;
    let local_ref = format!("refs/heads/{branch}");
    if git_ref_exists(&repo_path, git_path.clone(), &local_ref) {
        return ensure_git_success(run_git_command(
            Some(&repo_path),
            git_path,
            vec!["checkout".to_string(), branch],
        )?);
    }

    let remote_ref = format!("refs/remotes/{branch}");
    if git_ref_exists(&repo_path, git_path.clone(), &remote_ref) {
        if let Some((_, local_branch)) = branch.split_once('/') {
            let tracking_local_ref = format!("refs/heads/{local_branch}");
            if git_ref_exists(&repo_path, git_path.clone(), &tracking_local_ref) {
                return ensure_git_success(run_git_command(
                    Some(&repo_path),
                    git_path,
                    vec!["checkout".to_string(), local_branch.to_string()],
                )?);
            }
        }

        return ensure_git_success(run_git_command(
            Some(&repo_path),
            git_path,
            vec!["checkout".to_string(), "--track".to_string(), branch],
        )?);
    }

    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec!["checkout".to_string(), branch],
    )?)
}

#[tauri::command]
fn create_branch(
    repo_path: String,
    branch: String,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let branch = normalize_reference(&branch)?;
    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec!["checkout".to_string(), "-b".to_string(), branch],
    )?)
}

#[tauri::command]
fn delete_branch(
    repo_path: String,
    branch: String,
    force: Option<bool>,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let branch = normalize_reference(&branch)?;
    let current = run_git_output(
        &repo_path,
        git_path.clone(),
        vec![
            "rev-parse".to_string(),
            "--abbrev-ref".to_string(),
            "HEAD".to_string(),
        ],
    )?;
    if String::from_utf8_lossy(&current).trim() == branch {
        return Err("Cannot delete the current branch.".to_string());
    }
    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec![
            "branch".to_string(),
            if force.unwrap_or(false) { "-D" } else { "-d" }.to_string(),
            branch,
        ],
    )?)
}

#[tauri::command]
fn rename_branch(
    repo_path: String,
    old_branch: String,
    new_branch: String,
    git_path: Option<String>,
) -> Result<GitCommandResult, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let old_branch = normalize_reference(&old_branch)?;
    let new_branch = normalize_reference(&new_branch)?;
    if old_branch == new_branch {
        return Err("Choose a different branch name.".to_string());
    }
    ensure_git_success(run_git_command(
        Some(&repo_path),
        git_path,
        vec![
            "branch".to_string(),
            "-m".to_string(),
            old_branch,
            new_branch,
        ],
    )?)
}

#[tauri::command]
fn commit_history(
    repo_path: String,
    limit: Option<u32>,
    git_path: Option<String>,
) -> Result<Vec<GitCommitSummary>, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let max_count = limit.unwrap_or(100).clamp(1, 500).to_string();
    let output = run_git_output(
        &repo_path,
        git_path,
        vec![
            "log".to_string(),
            format!("--max-count={max_count}"),
            "--date=short".to_string(),
            "--pretty=format:%H%x1f%h%x1f%an%x1f%ad%x1f%s%x1e".to_string(),
        ],
    )?;
    let commits = String::from_utf8_lossy(&output)
        .split('\x1e')
        .filter_map(|record| {
            let fields = record.trim_matches('\n').split('\x1f').collect::<Vec<_>>();
            if fields.len() < 5 {
                return None;
            }
            Some(GitCommitSummary {
                hash: fields[0].to_string(),
                short_hash: fields[1].to_string(),
                author: fields[2].to_string(),
                date: fields[3].to_string(),
                title: fields[4].to_string(),
            })
        })
        .collect();
    Ok(commits)
}

#[tauri::command]
fn commit_detail(
    repo_path: String,
    hash: String,
    git_path: Option<String>,
) -> Result<GitCommitDetail, String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let hash = normalize_reference(&hash)?;
    let meta = run_git_output(
        &repo_path,
        git_path.clone(),
        vec![
            "show".to_string(),
            "--quiet".to_string(),
            "--date=short".to_string(),
            "--pretty=format:%H%x1f%h%x1f%an%x1f%ad%x1f%s%x1f%B".to_string(),
            hash.clone(),
        ],
    )?;
    let meta_text = String::from_utf8_lossy(&meta);
    let fields = meta_text.splitn(6, '\x1f').collect::<Vec<_>>();
    if fields.len() < 6 {
        return Err("Unable to parse commit metadata.".to_string());
    }
    let files_output = run_git_output(
        &repo_path,
        git_path.clone(),
        vec![
            "show".to_string(),
            "--name-status".to_string(),
            "--pretty=format:".to_string(),
            hash.clone(),
        ],
    )?;
    let files = String::from_utf8_lossy(&files_output)
        .lines()
        .filter_map(|line| {
            let mut parts = line.split('\t');
            let status_code = parts.next()?.chars().next()?.to_string();
            let first_path = parts.next()?.to_string();
            let second_path = parts.next().map(ToString::to_string);
            let path = second_path.clone().unwrap_or_else(|| first_path.clone());
            Some(GitChangedFile {
                status: status_kind(&status_code, " "),
                path,
                original_path: second_path.map(|_| first_path),
                x: status_code,
                y: " ".to_string(),
            })
        })
        .collect();
    let diff = run_git_output(
        &repo_path,
        git_path,
        vec![
            "show".to_string(),
            "--format=".to_string(),
            "--no-ext-diff".to_string(),
            hash,
        ],
    )
    .map(|output| String::from_utf8_lossy(&output).trim().to_string())
    .unwrap_or_default();

    Ok(GitCommitDetail {
        hash: fields[0].to_string(),
        short_hash: fields[1].to_string(),
        author: fields[2].to_string(),
        date: fields[3].to_string(),
        title: fields[4].to_string(),
        message: fields[5].trim().to_string(),
        files,
        diff,
    })
}

#[tauri::command]
fn open_in_file_explorer(repo_path: String) -> Result<(), String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let repo_path = normalize_external_repo_path(&repo_path);
    #[cfg(target_os = "windows")]
    let mut command = {
        let mut cmd = Command::new("explorer");
        cmd.arg(&repo_path);
        cmd
    };
    #[cfg(target_os = "macos")]
    let mut command = {
        let mut cmd = Command::new("open");
        cmd.arg(&repo_path);
        cmd
    };
    #[cfg(all(unix, not(target_os = "macos")))]
    let mut command = {
        let mut cmd = Command::new("xdg-open");
        cmd.arg(&repo_path);
        cmd
    };

    command
        .spawn()
        .map_err(|err| format!("Failed to open file explorer: {err}"))?;
    Ok(())
}

#[tauri::command]
fn open_in_external_editor(repo_path: String, editor_path: String) -> Result<(), String> {
    let repo_path = validate_repo_dir(&repo_path)?;
    let repo_path = normalize_external_repo_path(&repo_path);
    let editor_path = editor_path.trim();
    if editor_path.is_empty() {
        return Err("Configure an external editor command in Settings first.".to_string());
    }

    Command::new(editor_path)
        .arg(repo_path)
        .spawn()
        .map_err(|err| format!("Failed to open external editor: {err}"))?;
    Ok(())
}

#[tauri::command]
fn scan_installed_ides() -> Result<Vec<InstalledIde>, String> {
    let mut ides = Vec::new();
    let mut seen = HashSet::new();

    add_windows_ide_candidates(&mut ides, &mut seen);

    #[cfg(target_os = "macos")]
    {
        let candidates = [
            (
                "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
                "Visual Studio Code",
            ),
            (
                "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
                "Visual Studio Code Insiders",
            ),
            ("/Applications/Cursor.app/Contents/Resources/app/bin/cursor", "Cursor"),
            ("/Applications/Windsurf.app/Contents/Resources/app/bin/windsurf", "Windsurf"),
            ("/Applications/VSCodium.app/Contents/Resources/app/bin/codium", "VSCodium"),
            ("/Applications/Zed.app/Contents/MacOS/zed", "Zed"),
            ("/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "Sublime Text"),
        ];

        for (path, name) in candidates {
            add_installed_ide(&mut ides, &mut seen, name, PathBuf::from(path));
        }
    }

    add_path_ide_candidate(
        &mut ides,
        &mut seen,
        &["code", "Code.exe", "code.cmd"],
        "Visual Studio Code",
    );
    add_path_ide_candidate(
        &mut ides,
        &mut seen,
        &["code-insiders", "Code - Insiders.exe", "code-insiders.cmd"],
        "Visual Studio Code Insiders",
    );
    add_path_ide_candidate(
        &mut ides,
        &mut seen,
        &["cursor", "Cursor.exe", "cursor.cmd"],
        "Cursor",
    );
    add_path_ide_candidate(
        &mut ides,
        &mut seen,
        &["windsurf", "Windsurf.exe", "windsurf.cmd"],
        "Windsurf",
    );
    add_path_ide_candidate(
        &mut ides,
        &mut seen,
        &["codium", "VSCodium.exe", "codium.cmd"],
        "VSCodium",
    );
    add_path_ide_candidate(&mut ides, &mut seen, &["zed", "Zed.exe"], "Zed");
    add_path_ide_candidate(
        &mut ides,
        &mut seen,
        &["subl", "sublime_text.exe"],
        "Sublime Text",
    );

    ides.sort_by(|a, b| {
        a.name
            .cmp(&b.name)
            .then(a.executable_path.cmp(&b.executable_path))
    });
    Ok(ides)
}

#[tauri::command]
fn scan_local_repos(
    roots: Option<Vec<String>>,
    allowed_remote_urls: Vec<String>,
    git_path: Option<String>,
    max_depth: Option<u8>,
    max_results: Option<usize>,
) -> Result<Vec<LocalRepoCandidate>, String> {
    let max_depth = max_depth.unwrap_or(4).clamp(1, 8);
    let max_results = max_results.unwrap_or(200).clamp(1, 500);
    let allowed_remote_urls = allowed_remote_urls
        .into_iter()
        .map(|url| normalize_git_remote_url(&url))
        .filter(|url| !url.is_empty())
        .collect::<HashSet<_>>();

    if allowed_remote_urls.is_empty() {
        return Err(
            "No selected server repository URLs are available to match against.".to_string(),
        );
    }

    let root_paths = roots
        .unwrap_or_default()
        .into_iter()
        .map(|path| path.trim().to_string())
        .filter(|path| !path.is_empty())
        .map(PathBuf::from)
        .collect::<Vec<_>>();
    let root_paths = if root_paths.is_empty() {
        default_repo_scan_roots()
    } else {
        root_paths
    };

    if root_paths.is_empty() {
        return Err(
            "No scan roots found. Set a default clone directory or enter a scan root.".to_string(),
        );
    }

    let mut results = Vec::new();
    let mut seen = HashSet::new();
    for root in root_paths {
        if root.exists() && root.is_dir() {
            collect_repo_candidates(
                &root,
                0,
                max_depth,
                &mut results,
                &mut seen,
                &allowed_remote_urls,
                git_path.clone(),
                max_results,
            );
        }
        if results.len() >= max_results {
            break;
        }
    }

    results.sort_by(|a, b| {
        a.name
            .to_lowercase()
            .cmp(&b.name.to_lowercase())
            .then_with(|| a.path.to_lowercase().cmp(&b.path.to_lowercase()))
    });

    Ok(results)
}

#[tauri::command]
fn local_repo_branches(
    repo_path: String,
    git_path: Option<String>,
) -> Result<Vec<LocalRepoBranch>, String> {
    let current = run_git_output(
        repo_path.trim(),
        git_path.clone(),
        vec![
            "rev-parse".to_string(),
            "--abbrev-ref".to_string(),
            "HEAD".to_string(),
        ],
    )
    .ok()
    .map(|output| String::from_utf8_lossy(&output).trim().to_string())
    .filter(|value| !value.is_empty() && value != "HEAD");

    let output = run_git_output(
        repo_path.trim(),
        git_path,
        vec![
            "for-each-ref".to_string(),
            "--format=%(refname)".to_string(),
            "refs/heads".to_string(),
            "refs/remotes".to_string(),
        ],
    )?;

    let ref_names = String::from_utf8_lossy(&output)
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty() && !line.ends_with("/HEAD"))
        .map(str::to_string)
        .collect::<Vec<_>>();

    let local_names = ref_names
        .iter()
        .filter_map(|ref_name| ref_name.strip_prefix("refs/heads/"))
        .map(str::to_string)
        .collect::<HashSet<_>>();

    let mut branches = ref_names
        .iter()
        .filter_map(|ref_name| {
            if let Some(name) = ref_name.strip_prefix("refs/heads/") {
                return Some(LocalRepoBranch {
                    name: name.to_string(),
                    current: current.as_deref() == Some(name),
                    is_remote: false,
                    remote_name: None,
                    local_name: name.to_string(),
                });
            }

            let name = ref_name.strip_prefix("refs/remotes/")?;
            let (remote_name, local_name) = name.split_once('/')?;
            if local_names.contains(local_name) {
                return None;
            }

            Some(LocalRepoBranch {
                name: name.to_string(),
                current: false,
                is_remote: true,
                remote_name: Some(remote_name.to_string()),
                local_name: local_name.to_string(),
            })
        })
        .collect::<Vec<_>>();

    branches.sort_by(|a, b| {
        b.current
            .cmp(&a.current)
            .then_with(|| a.is_remote.cmp(&b.is_remote))
            .then_with(|| {
                a.local_name
                    .to_lowercase()
                    .cmp(&b.local_name.to_lowercase())
            })
            .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
    });

    Ok(branches)
}

#[tauri::command]
fn local_repo_tree(
    repo_path: String,
    reference: String,
    path: String,
    git_path: Option<String>,
) -> Result<Vec<LocalRepoEntry>, String> {
    let reference = normalize_reference(&reference)?;
    let path = normalize_repo_path(&path)?;
    let output = run_git_output(
        repo_path.trim(),
        git_path,
        vec![
            "ls-tree".to_string(),
            "-l".to_string(),
            treeish(&reference, &path),
        ],
    )?;

    let mut entries = Vec::new();
    for line in String::from_utf8_lossy(&output).lines() {
        let Some((meta, name)) = line.split_once('\t') else {
            continue;
        };
        let meta_parts: Vec<&str> = meta.split_whitespace().collect();
        if meta_parts.len() < 4 {
            continue;
        }

        let entry_type = meta_parts[1].to_string();
        let size = meta_parts[3].parse::<u64>().ok();
        let entry_path = if path.is_empty() {
            name.to_string()
        } else {
            format!("{path}/{name}")
        };

        entries.push(LocalRepoEntry {
            name: name.to_string(),
            path: entry_path,
            entry_type,
            size,
        });
    }

    entries.sort_by(|a, b| {
        let a_is_tree = a.entry_type == "tree";
        let b_is_tree = b.entry_type == "tree";
        b_is_tree
            .cmp(&a_is_tree)
            .then_with(|| a.name.to_lowercase().cmp(&b.name.to_lowercase()))
    });

    Ok(entries)
}

#[tauri::command]
fn local_repo_file(
    repo_path: String,
    reference: String,
    path: String,
    git_path: Option<String>,
) -> Result<LocalRepoFile, String> {
    const MAX_TEXT_PREVIEW_BYTES: u64 = 256 * 1024;

    let reference = normalize_reference(&reference)?;
    let path = normalize_repo_path(&path)?;
    if path.is_empty() {
        return Err("File path is required.".to_string());
    }

    let spec = treeish(&reference, &path);
    let object_type = run_git_output(
        repo_path.trim(),
        git_path.clone(),
        vec!["cat-file".to_string(), "-t".to_string(), spec.clone()],
    )?;
    if String::from_utf8_lossy(&object_type).trim() != "blob" {
        return Err("Selected path is not a file.".to_string());
    }

    let size_output = run_git_output(
        repo_path.trim(),
        git_path.clone(),
        vec!["cat-file".to_string(), "-s".to_string(), spec.clone()],
    )?;
    let size = String::from_utf8_lossy(&size_output)
        .trim()
        .parse::<u64>()
        .map_err(|_| "Unable to determine file size.".to_string())?;

    if size > MAX_TEXT_PREVIEW_BYTES {
        if size <= MAX_IMAGE_PREVIEW_BYTES && repository_relative_image_mime(&path).is_some() {
            let raw = run_git_output(
                repo_path.trim(),
                git_path,
                vec![
                    "show".to_string(),
                    "--no-ext-diff".to_string(),
                    spec.clone(),
                ],
            )?;
            let (preview_mime, preview_base64) = image_preview_pair(&path, &raw);
            return Ok(LocalRepoFile {
                path,
                size,
                content: None,
                is_binary: preview_base64.is_none(),
                too_large: preview_base64.is_none(),
                preview_mime,
                preview_base64,
            });
        }
        return Ok(LocalRepoFile {
            path,
            size,
            content: None,
            is_binary: false,
            too_large: true,
            preview_mime: None,
            preview_base64: None,
        });
    }

    let raw = run_git_output(
        repo_path.trim(),
        git_path,
        vec!["show".to_string(), "--no-ext-diff".to_string(), spec],
    )?;
    let (preview_mime, preview_base64) = image_preview_pair(&path, &raw);
    let has_null = raw.contains(&0);
    let content = if has_null {
        None
    } else {
        String::from_utf8(raw).ok()
    };
    let is_binary = content.is_none();

    Ok(LocalRepoFile {
        path,
        size,
        is_binary,
        too_large: false,
        content,
        preview_mime,
        preview_base64,
    })
}

#[tauri::command]
fn test_gitea_connection(
    server_url: String,
    auth_method: String,
    token: Option<String>,
    username: Option<String>,
    password: Option<String>,
) -> Result<ServerConnectionResult, String> {
    // A lightweight compatibility check against the canonical Gitea version endpoint.
    let api_base_url = normalize_api_base_url(&server_url)?;
    let version_url = format!("{api_base_url}/version");

    let mut request = Client::new()
        .get(&version_url)
        .header(ACCEPT, "application/json");

    if auth_method == "token" {
        if let Some(value) = token.as_deref() {
            if !value.trim().is_empty() {
                request = request.header(AUTHORIZATION, format!("token {}", value.trim()));
            }
        }
    } else if auth_method == "password" {
        request = request.basic_auth(username.unwrap_or_default(), password);
    }

    let response = request
        .send()
        .map_err(|err| format!("Failed to connect to Gitea server: {err}"))?;

    if !response.status().is_success() {
        return Ok(ServerConnectionResult {
            ok: false,
            message: format!("Server responded with status {}", response.status()),
            api_base_url,
            version: None,
        });
    }

    let response_json: serde_json::Value = response
        .json()
        .map_err(|err| format!("Failed to parse Gitea response: {err}"))?;

    let version = response_json
        .get("version")
        .and_then(|value| value.as_str())
        .map(ToString::to_string);

    Ok(ServerConnectionResult {
        ok: true,
        message: "Gitea API is reachable and compatible.".to_string(),
        api_base_url,
        version,
    })
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_opener::init())
        .setup(|app| {
            use tauri::Manager;
            if let Some(window) = app.get_webview_window("main") {
                let _ = window.set_background_color(Some(tauri::utils::config::Color(0, 0, 0, 0)));
            }
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            git_clone,
            git_pull,
            git_push,
            git_publish_branch,
            git_status,
            git_branch,
            git_fetch,
            git_sync,
            repository_sync_status,
            working_tree_status,
            get_file_diff,
            stage_files,
            unstage_files,
            commit_changes,
            checkout_branch,
            create_branch,
            delete_branch,
            rename_branch,
            commit_history,
            commit_detail,
            open_in_file_explorer,
            open_in_external_editor,
            scan_installed_ides,
            scan_local_repos,
            local_repo_branches,
            local_repo_tree,
            local_repo_file,
            test_gitea_connection
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
