//! File system helpers for initialization.

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

use crate::config::default_project_configuration;
use crate::config_loader::load_project_configuration;
use crate::error::KanbusError;
use crate::models::ProjectConfiguration;
use crate::project_management_template::{
    DEFAULT_PROJECT_MANAGEMENT_TEMPLATE, DEFAULT_PROJECT_MANAGEMENT_TEMPLATE_FILENAME,
};
use serde_json;
use serde_yaml;

fn should_force_canonicalize_failure() -> bool {
    std::env::var_os("KANBUS_TEST_CANONICALIZE_FAILURE").is_some()
}

pub(crate) fn canonicalize_path(path: &Path) -> Result<PathBuf, std::io::Error> {
    if should_force_canonicalize_failure() {
        return Err(std::io::Error::other("forced canonicalize failure"));
    }
    path.canonicalize()
}

/// Ensure the current directory is inside a git repository.
///
/// # Arguments
///
/// * `root` - Path to validate.
///
/// # Errors
///
/// Returns `KanbusError::Initialization` if the directory is not a git repository.
pub fn ensure_git_repository(root: &Path) -> Result<(), KanbusError> {
    let output = Command::new("git")
        .args(["rev-parse", "--is-inside-work-tree"])
        .current_dir(root)
        .output()
        .map_err(|error| KanbusError::Io(error.to_string()))?;

    if !output.status.success() {
        return Err(KanbusError::Initialization(
            "not a git repository".to_string(),
        ));
    }

    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if stdout != "true" {
        return Err(KanbusError::Initialization(
            "not a git repository".to_string(),
        ));
    }

    Ok(())
}

/// Initialize the Kanbus project structure.
///
/// # Arguments
///
/// * `root` - Repository root.
/// * `create_local` - Whether to create project-local.
///
/// # Errors
///
/// Returns `KanbusError::Initialization` if already initialized.
pub fn initialize_project(root: &Path, create_local: bool) -> Result<(), KanbusError> {
    let project_dir = root.join("project");
    if project_dir.exists() {
        return Err(KanbusError::Initialization(
            "already initialized".to_string(),
        ));
    }

    let issues_dir = project_dir.join("issues");

    std::fs::create_dir(&project_dir).map_err(|error| KanbusError::Io(error.to_string()))?;
    std::fs::create_dir(&issues_dir).map_err(|error| KanbusError::Io(error.to_string()))?;
    let config_path = root.join(".kanbus.yml");
    if !config_path.exists() {
        let default_configuration = default_project_configuration();
        let contents = serde_yaml::to_string(&default_configuration)
            .map_err(|error| KanbusError::Io(error.to_string()))?;
        std::fs::write(&config_path, contents)
            .map_err(|error| KanbusError::Io(error.to_string()))?;
    }
    let template_path = root.join(DEFAULT_PROJECT_MANAGEMENT_TEMPLATE_FILENAME);
    if !template_path.exists() {
        std::fs::write(&template_path, DEFAULT_PROJECT_MANAGEMENT_TEMPLATE)
            .map_err(|error| KanbusError::Io(error.to_string()))?;
    }
    write_project_guard_files(&project_dir)?;
    write_tool_block_files(root)?;
    if create_local {
        ensure_project_local_directory(&project_dir)?;
    }

    Ok(())
}

/// Resolve the repository root for initialization.
///
/// # Arguments
///
/// * `cwd` - Current working directory.
///
/// # Returns
///
/// The root path used for initialization. Walks up from cwd to find .kanbus.yml.
pub fn resolve_root(cwd: &Path) -> PathBuf {
    let mut current = cwd;
    loop {
        let config_path = current.join(".kanbus.yml");
        if config_path.exists() {
            return current.to_path_buf();
        }
        match current.parent() {
            Some(parent) => current = parent,
            None => return cwd.to_path_buf(), // Fallback to cwd if not found
        }
    }
}

fn write_project_guard_files(project_dir: &Path) -> Result<(), KanbusError> {
    let agents_path = project_dir.join("AGENTS.md");
    let agents_content = [
        "# DO NOT EDIT HERE",
        "",
        "Editing anything under project/ directly is hacking the data and is a sin against The Way.",
        "Do not read or write in this folder. Do not inspect issue JSON with tools like cat or jq. Use Kanbus commands instead.",
        "",
        "See ../AGENTS.md and ../CONTRIBUTING_AGENT.md for required process.",
    ]
    .join("\n")
        + "\n";
    std::fs::write(&agents_path, agents_content)
        .map_err(|error| KanbusError::Io(error.to_string()))?;

    let do_not_edit = project_dir.join("DO_NOT_EDIT");
    let do_not_edit_content = [
        "DO NOT EDIT ANYTHING IN project/",
        "This folder is guarded by The Way.",
        "Do not inspect issue JSON with tools like cat or jq.",
        "All changes must go through Kanbus (see ../AGENTS.md and ../CONTRIBUTING_AGENT.md).",
    ]
    .join("\n")
        + "\n";
    std::fs::write(&do_not_edit, do_not_edit_content)
        .map_err(|error| KanbusError::Io(error.to_string()))?;
    Ok(())
}

fn write_tool_block_files(root: &Path) -> Result<(), KanbusError> {
    let cursorignore = root.join(".cursorignore");
    if !cursorignore.exists() {
        std::fs::write(&cursorignore, "project/\n")
            .map_err(|error| KanbusError::Io(error.to_string()))?;
    }

    let claude_dir = root.join(".claude");
    std::fs::create_dir_all(&claude_dir).map_err(|error| KanbusError::Io(error.to_string()))?;
    let claude_settings = claude_dir.join("settings.json");
    if !claude_settings.exists() {
        let payload = serde_json::json!({
            "permissions": {
                "deny": [
                    "Read(./project/**)",
                    "Edit(./project/**)"
                ]
            }
        });
        let content = serde_json::to_string_pretty(&payload)
            .map_err(|error| KanbusError::Io(error.to_string()))?;
        std::fs::write(&claude_settings, format!("{}\n", content))
            .map_err(|error| KanbusError::Io(error.to_string()))?;
    }

    let vscode_dir = root.join(".vscode");
    std::fs::create_dir_all(&vscode_dir).map_err(|error| KanbusError::Io(error.to_string()))?;
    let vscode_settings = vscode_dir.join("settings.json");
    if !vscode_settings.exists() {
        let payload = serde_json::json!({
            "files.exclude": {"**/project/**": true},
            "files.watcherExclude": {"**/project/**": true},
            "search.exclude": {"**/project/**": true},
        });
        let content = serde_json::to_string_pretty(&payload)
            .map_err(|error| KanbusError::Io(error.to_string()))?;
        std::fs::write(&vscode_settings, format!("{}\n", content))
            .map_err(|error| KanbusError::Io(error.to_string()))?;
    }
    Ok(())
}

/// Load a single Kanbus project directory by downward discovery.
///
/// # Arguments
///
/// * `root` - Repository root.
///
/// # Errors
///
/// Returns `KanbusError::IssueOperation` if no project or multiple projects are found.
pub fn load_project_directory(root: &Path) -> Result<PathBuf, KanbusError> {
    let mut projects = Vec::new();
    discover_project_directories(root, &mut projects)?;
    let mut dotfile_projects = discover_kanbus_projects(root)?;
    projects.append(&mut dotfile_projects);

    let mut normalized = Vec::new();
    for path in projects {
        match canonicalize_path(&path) {
            Ok(canonical) => normalized.push(canonical),
            Err(_) => normalized.push(path),
        }
    }
    normalized.sort();
    normalized.dedup();

    let config_path = match get_configuration_path(root) {
        Ok(path) => path,
        Err(_) => return filter_and_validate_projects(normalized),
    };

    if let Ok(configuration) = load_project_configuration(&config_path) {
        let base = config_path.parent().unwrap_or_else(|| Path::new(""));
        normalized.retain(|project_path| {
            !is_path_ignored(project_path, base, &configuration.ignore_paths)
        });
    }

    filter_and_validate_projects(normalized)
}

fn filter_and_validate_projects(normalized: Vec<PathBuf>) -> Result<PathBuf, KanbusError> {
    if normalized.is_empty() {
        return Err(KanbusError::IssueOperation(
            "project not initialized".to_string(),
        ));
    }
    if normalized.len() > 1 {
        let joined = normalized
            .iter()
            .map(|path| path.display().to_string())
            .collect::<Vec<String>>()
            .join(", ");
        return Err(KanbusError::IssueOperation(format!(
            "multiple projects found: {joined}. \
             Run this command from a directory with a single project/, \
             or remove extra entries from external_projects in .kanbus.yml."
        )));
    }
    Ok(normalized[0].clone())
}

/// Find a sibling project-local directory for a project.
///
/// # Arguments
///
/// * `project_dir` - Shared project directory.
pub fn find_project_local_directory(project_dir: &Path) -> Option<PathBuf> {
    let local_dir = project_dir
        .parent()
        .map(|parent| parent.join("project-local"))?;
    if local_dir.is_dir() {
        Some(local_dir)
    } else {
        None
    }
}

/// Ensure the project-local directory exists and is gitignored.
///
/// # Arguments
///
/// * `project_dir` - Shared project directory.
///
/// # Errors
///
/// Returns `KanbusError::Io` if filesystem operations fail.
pub fn ensure_project_local_directory(project_dir: &Path) -> Result<PathBuf, KanbusError> {
    let local_dir = project_dir
        .parent()
        .map(|parent| parent.join("project-local"))
        .ok_or_else(|| KanbusError::Io("project-local path unavailable".to_string()))?;
    let issues_dir = local_dir.join("issues");
    std::fs::create_dir_all(&issues_dir).map_err(|error| KanbusError::Io(error.to_string()))?;
    ensure_gitignore_entry(
        project_dir
            .parent()
            .ok_or_else(|| KanbusError::Io("project-local path unavailable".to_string()))?,
        "project-local/",
    )?;
    Ok(local_dir)
}

/// Locate the configuration file path.
///
/// # Arguments
///
/// * `root` - Path used for upward search.
///
/// # Errors
///
/// Returns `KanbusError::IssueOperation` if the configuration file is missing.
pub fn get_configuration_path(root: &Path) -> Result<PathBuf, KanbusError> {
    if std::env::var_os("KANBUS_TEST_CONFIGURATION_PATH_FAILURE").is_some() {
        return Err(KanbusError::Io(
            "configuration path lookup failed".to_string(),
        ));
    }
    let Some(path) = find_configuration_file(root)? else {
        return Err(KanbusError::IssueOperation(
            "project not initialized".to_string(),
        ));
    };
    Ok(path)
}

fn ensure_gitignore_entry(root: &Path, entry: &str) -> Result<(), KanbusError> {
    let gitignore_path = root.join(".gitignore");
    let existing = if gitignore_path.exists() {
        std::fs::read_to_string(&gitignore_path)
            .map_err(|error| KanbusError::Io(error.to_string()))?
    } else {
        String::new()
    };
    let lines: Vec<&str> = existing.lines().map(str::trim).collect();
    if lines.contains(&entry) {
        return Ok(());
    }
    let mut updated = existing;
    if !updated.is_empty() && !updated.ends_with('\n') {
        updated.push('\n');
    }
    updated.push_str(entry);
    updated.push('\n');
    std::fs::write(&gitignore_path, updated).map_err(|error| KanbusError::Io(error.to_string()))?;
    Ok(())
}

/// Discover configured project paths from .kanbus.yml.
///
/// # Arguments
/// * `root` - Repository root path.
///
/// # Errors
/// Returns `KanbusError` if configuration or dotfile paths are invalid.
pub fn discover_kanbus_projects(root: &Path) -> Result<Vec<PathBuf>, KanbusError> {
    let mut projects = Vec::new();
    if let Some(config_path) = find_configuration_file(root)? {
        let configuration = load_project_configuration(&config_path)?;
        projects.extend(resolve_project_directories(
            config_path.parent().unwrap_or_else(|| Path::new("")),
            &configuration,
        )?);
    }
    Ok(projects)
}

fn find_configuration_file(root: &Path) -> Result<Option<PathBuf>, KanbusError> {
    let git_root = find_git_root(root);
    let mut current = root
        .canonicalize()
        .map_err(|error| KanbusError::Io(error.to_string()))?;
    loop {
        let candidate = current.join(".kanbus.yml");
        if candidate.is_file() {
            return Ok(Some(candidate));
        }
        if let Some(root) = &git_root {
            if &current == root {
                break;
            }
        }
        let parent = match current.parent() {
            Some(parent) => parent.to_path_buf(),
            None => break,
        };
        #[cfg(windows)]
        if parent == current {
            break;
        }
        current = parent;
    }
    Ok(None)
}

fn resolve_project_directories(
    base: &Path,
    configuration: &ProjectConfiguration,
) -> Result<Vec<PathBuf>, KanbusError> {
    let mut projects = Vec::new();
    let primary = base.join(&configuration.project_directory);
    if !is_path_ignored(&primary, base, &configuration.ignore_paths) {
        projects.push(primary);
    }
    for extra in &configuration.external_projects {
        let candidate = Path::new(extra);
        let resolved = if candidate.is_absolute() {
            candidate.to_path_buf()
        } else {
            base.join(candidate)
        };
        if !resolved.is_dir() {
            return Err(KanbusError::IssueOperation(format!(
                "kanbus path not found: {}",
                resolved.display()
            )));
        }
        if !is_path_ignored(&resolved, base, &configuration.ignore_paths) {
            projects.push(resolved);
        }
    }
    Ok(projects)
}

fn is_path_ignored(path: &Path, base: &Path, ignore_paths: &[String]) -> bool {
    for ignore_pattern in ignore_paths {
        let ignore_path = base.join(ignore_pattern);
        if let Ok(ignore_canonical) = ignore_path.canonicalize() {
            if let Ok(path_canonical) = path.canonicalize() {
                if path_canonical == ignore_canonical {
                    return true;
                }
            }
        }
    }
    false
}

fn find_git_root(root: &Path) -> Option<PathBuf> {
    let output = Command::new("git")
        .args(["rev-parse", "--show-toplevel"])
        .current_dir(root)
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
    let path = PathBuf::from(stdout);
    path.is_dir().then_some(path)
}

pub(crate) fn discover_project_directories(
    root: &Path,
    projects: &mut Vec<PathBuf>,
) -> Result<(), KanbusError> {
    for entry in std::fs::read_dir(root).map_err(|error| KanbusError::Io(error.to_string()))? {
        let entry = entry.map_err(|error| KanbusError::Io(error.to_string()))?;
        let path = entry.path();
        if !path.is_dir() {
            continue;
        }
        let name = path
            .file_name()
            .and_then(|value| value.to_str())
            .unwrap_or("");
        if name == "project" {
            projects.push(path);
            continue;
        }
        if name == "project-local" {
            continue;
        }
        let nested_project = path.join("project");
        if nested_project.is_dir() {
            projects.push(nested_project);
        }
        // Avoid recursing into every subdirectory; doing so pulls in fixture
        // projects (e.g., apps/console/tests/fixtures/project) that are not real
        // Kanbus workspaces and can break commands with incomplete data.
        // Additional projects must be declared explicitly via configuration.
    }
    Ok(())
}
