//! Utility functions for handlers

use std::collections::HashSet;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

use aphoria::AphoriaError;

/// Safe print that handles broken pipe gracefully (no panic)
macro_rules! safe_println {
    () => {
        let _ = writeln!(std::io::stdout());
    };
    ($($arg:tt)*) => {{
        let _ = writeln!(std::io::stdout(), $($arg)*);
    }};
}

/// Truncate a string for display, replacing newlines and tabs with spaces
pub fn truncate_for_display(s: &str, max: usize) -> String {
    let s = s.replace(['\n', '\t'], " ");
    if s.len() <= max {
        s
    } else {
        format!("{}...", &s[..max.saturating_sub(3)])
    }
}

/// Information about a skill source directory
#[derive(Debug)]
struct SkillSource {
    /// Skill name (e.g., "aphoria-dev")
    name: String,
    /// Full path to source directory
    source_path: PathBuf,
    /// Whether this skill has subdirectories (like checklists/)
    has_subdirs: bool,
}

/// Statistics from copying a skill
#[derive(Debug, Default)]
struct CopyStats {
    files_copied: usize,
    dirs_created: usize,
}

/// Main handler for the install-claude command
pub async fn handle_install_claude(dry_run: bool, force: bool) -> ExitCode {
    // Find the project root containing .claude/skills/
    let project_root = match find_project_root() {
        Ok(root) => root,
        Err(e) => {
            eprintln!("{e}");
            return ExitCode::from(1);
        }
    };

    // Find all aphoria-* skill directories
    let skill_sources = match find_skill_sources(&project_root) {
        Ok(sources) => sources,
        Err(e) => {
            eprintln!("{e}");
            return ExitCode::from(1);
        }
    };

    if skill_sources.is_empty() {
        eprintln!("Error: No Aphoria skills found in project");
        return ExitCode::from(1);
    }

    // Resolve target directory (~/.claude/skills/)
    let target_dir = match std::env::var("HOME")
        .map(PathBuf::from)
        .or_else(|_| dirs::home_dir().ok_or_else(|| "Cannot determine home directory".to_string()))
    {
        Ok(home) => home.join(".claude").join("skills"),
        Err(e) => {
            eprintln!("Error: {e}");
            return ExitCode::from(1);
        }
    };

    if dry_run {
        return handle_dry_run(&skill_sources, &target_dir, force);
    }

    // Create target directory if it doesn't exist
    if let Err(e) = std::fs::create_dir_all(&target_dir) {
        eprintln!("Error: Cannot create {}: {}", target_dir.display(), e);
        return ExitCode::from(1);
    }

    safe_println!("Installing Aphoria Claude Code skills...\n");

    let mut installed = 0;
    let mut updated = 0;
    let mut failed = 0;
    let mut failures = Vec::new();

    for skill in &skill_sources {
        let target_path = target_dir.join(&skill.name);
        let exists = target_path.exists();

        match copy_skill_recursively(&skill.source_path, &target_path, force) {
            Ok(stats) => {
                if exists {
                    updated += 1;
                } else {
                    installed += 1;
                }

                let file_desc = if skill.has_subdirs {
                    format!("{} files: SKILL.md + subdirectories", stats.files_copied)
                } else {
                    format!("{} file", stats.files_copied)
                };
                safe_println!("✓ {} ({})", skill.name, file_desc);
            }
            Err(e) => {
                failed += 1;
                failures.push(format!("✗ {}: {}", skill.name, e));
                eprintln!("✗ {}: {}", skill.name, e);
            }
        }
    }

    safe_println!();

    if failed > 0 {
        eprintln!("Failed to install {} skill(s):", failed);
        for failure in failures {
            eprintln!("  {failure}");
        }
        return ExitCode::from(1);
    }

    let total = installed + updated;
    safe_println!("Installed {} skill(s) to {}\n", total, target_dir.display());

    safe_println!("Available skills:");
    safe_println!("  Core Development:");
    safe_println!("    /aphoria-dev                      - Development guidelines");
    safe_println!("    /aphoria-docs                     - Curate and maintain documentation");
    safe_println!("    /aphoria-doc-evaluator            - Evaluate documentation quality");
    safe_println!();
    safe_println!("  Workflow Automation:");
    safe_println!("    /aphoria-post-commit-hook         - Install post-commit automation");
    safe_println!("    /aphoria-ci-setup                 - Set up CI/CD automation");
    safe_println!();
    safe_println!("  Claim & Extractor Creation:");
    safe_println!("    /aphoria-claims                   - Author and review claims from diffs");
    safe_println!("    /aphoria-suggest                  - Suggest new claims from patterns");
    safe_println!(
        "    /aphoria-custom-extractor-creator - Create declarative/programmatic extractors"
    );
    safe_println!();
    safe_println!("  Quality & Optimization:");
    safe_println!("    /aphoria-self-review              - Run self-review SOP on scan results");
    safe_println!("    /aphoria-llm-optimization         - Optimize LLM extraction quality");
    safe_println!();
    safe_println!("  Content Import:");
    safe_println!("    /aphoria-corpus-import            - Import external docs (RFCs, wikis)");
    safe_println!();
    safe_println!("  Setup:");
    safe_println!("    /aphoria-install                  - Install Aphoria and StemeDB");
    safe_println!("    /aphoria-dogfood                  - Set up dogfooding exercises");

    ExitCode::SUCCESS
}

/// Handle dry-run mode (preview without installing)
fn handle_dry_run(skill_sources: &[SkillSource], target_dir: &Path, force: bool) -> ExitCode {
    safe_println!("[DRY RUN] Would install Aphoria Claude Code skills:\n");

    for skill in skill_sources {
        let target_path = target_dir.join(&skill.name);
        let exists = target_path.exists();

        safe_println!("  {}", skill.name);
        safe_println!("    Source: {}", skill.source_path.display());
        safe_println!("    Target: {}", target_path.display());

        if !exists {
            safe_println!("    Action: CREATE (does not exist)");
        } else if force {
            safe_println!("    Action: OVERWRITE (--force specified)");
        } else if needs_update(&skill.source_path, &target_path) {
            safe_println!("    Action: UPDATE (newer version available)");
        } else {
            safe_println!("    Action: SKIP (up to date)");
        }

        // Count files
        if let Ok(file_count) = count_files(&skill.source_path) {
            if skill.has_subdirs {
                safe_println!("    Files: {} (SKILL.md + subdirectories)", file_count);
            } else {
                safe_println!("    Files: {}", file_count);
            }
        }
        safe_println!();
    }

    safe_println!("Run without --dry-run to install.");
    ExitCode::SUCCESS
}

/// Find the project root containing .claude/skills/
fn find_project_root() -> Result<PathBuf, AphoriaError> {
    let mut current = std::env::current_dir()
        .map_err(|e| AphoriaError::SkillInstall(format!("Cannot get current directory: {e}")))?;

    loop {
        let skills_dir = current.join(".claude").join("skills");
        if skills_dir.is_dir() {
            return Ok(current);
        }

        if !current.pop() {
            return Err(AphoriaError::SkillInstall(
                "Error: Not in Aphoria project. Run from stemedb directory.".to_string(),
            ));
        }
    }
}

/// Find all aphoria-* skill directories (handles both nested and flat paths)
fn find_skill_sources(project_root: &Path) -> Result<Vec<SkillSource>, AphoriaError> {
    let skills_dir = project_root.join(".claude").join("skills");

    let mut sources = Vec::new();
    let mut seen_names = HashSet::new();

    // Check nested path first: .claude/skills/.claude/skills/
    let nested_dir = skills_dir.join(".claude").join("skills");
    if nested_dir.is_dir() {
        scan_for_aphoria_skills(&nested_dir, &mut sources, &mut seen_names)?;
    }

    // Also check flat path: .claude/skills/
    scan_for_aphoria_skills(&skills_dir, &mut sources, &mut seen_names)?;

    // Sort by name for consistent output
    sources.sort_by(|a, b| a.name.cmp(&b.name));

    Ok(sources)
}

/// Scan a directory for aphoria-* skills
fn scan_for_aphoria_skills(
    path: &Path,
    sources: &mut Vec<SkillSource>,
    seen_names: &mut HashSet<String>,
) -> Result<(), AphoriaError> {
    let entries = std::fs::read_dir(path).map_err(|e| {
        AphoriaError::SkillInstall(format!("Cannot read directory {}: {}", path.display(), e))
    })?;

    for entry in entries {
        let entry = entry.map_err(|e| {
            AphoriaError::SkillInstall(format!("Cannot read entry in {}: {}", path.display(), e))
        })?;

        let entry_path = entry.path();
        if !entry_path.is_dir() {
            continue;
        }

        let name = match entry_path.file_name().and_then(|n| n.to_str()) {
            Some(n) => n,
            None => continue,
        };

        // Only include directories starting with "aphoria-"
        if !name.starts_with("aphoria-") {
            continue;
        }

        // Skip if we've already seen this skill name (deduplication)
        if seen_names.contains(name) {
            continue;
        }

        // Only include if it has a SKILL.md file
        let skill_md = entry_path.join("SKILL.md");
        if !skill_md.is_file() {
            continue;
        }

        // Check if it has subdirectories
        let has_subdirs = std::fs::read_dir(&entry_path)
            .ok()
            .and_then(|entries| {
                entries.filter_map(Result::ok).any(|e| e.path().is_dir()).then_some(true)
            })
            .unwrap_or(false);

        sources.push(SkillSource {
            name: name.to_string(),
            source_path: entry_path.clone(),
            has_subdirs,
        });
        seen_names.insert(name.to_string());
    }

    Ok(())
}

/// Copy a skill directory recursively, preserving structure
fn copy_skill_recursively(
    source: &Path,
    target: &Path,
    force: bool,
) -> Result<CopyStats, AphoriaError> {
    let mut stats = CopyStats::default();

    // Create target directory
    std::fs::create_dir_all(target).map_err(|e| {
        AphoriaError::SkillInstall(format!("Cannot create {}: {}", target.display(), e))
    })?;
    stats.dirs_created += 1;

    // Walk source directory
    copy_dir_contents(source, target, force, &mut stats)?;

    Ok(stats)
}

/// Recursively copy directory contents
fn copy_dir_contents(
    source: &Path,
    target: &Path,
    force: bool,
    stats: &mut CopyStats,
) -> Result<(), AphoriaError> {
    let entries = std::fs::read_dir(source).map_err(|e| {
        AphoriaError::SkillInstall(format!("Cannot read directory {}: {}", source.display(), e))
    })?;

    for entry in entries {
        let entry = entry.map_err(|e| {
            AphoriaError::SkillInstall(format!("Cannot read entry in {}: {}", source.display(), e))
        })?;

        let source_path = entry.path();
        let file_name = match source_path.file_name() {
            Some(name) => name,
            None => continue,
        };

        let target_path = target.join(file_name);

        if source_path.is_dir() {
            // Create subdirectory
            std::fs::create_dir_all(&target_path).map_err(|e| {
                AphoriaError::SkillInstall(format!(
                    "Cannot create directory {}: {}",
                    target_path.display(),
                    e
                ))
            })?;
            stats.dirs_created += 1;

            // Recurse into subdirectory
            copy_dir_contents(&source_path, &target_path, force, stats)?;
        } else {
            // Skip existing files unless force is specified
            if !force && target_path.exists() {
                continue;
            }

            // Copy file
            std::fs::copy(&source_path, &target_path).map_err(|e| {
                AphoriaError::SkillInstall(format!(
                    "Cannot copy {} to {}: {}",
                    source_path.display(),
                    target_path.display(),
                    e
                ))
            })?;
            stats.files_copied += 1;
        }
    }

    Ok(())
}

/// Check if a skill needs updating (source is newer than target)
fn needs_update(source: &Path, target: &Path) -> bool {
    // Simple heuristic: compare modification times of SKILL.md files
    let source_md = source.join("SKILL.md");
    let target_md = target.join("SKILL.md");

    if !target_md.exists() {
        return true;
    }

    let source_modified = std::fs::metadata(&source_md).and_then(|m| m.modified()).ok();
    let target_modified = std::fs::metadata(&target_md).and_then(|m| m.modified()).ok();

    match (source_modified, target_modified) {
        (Some(src), Some(tgt)) => src > tgt,
        _ => false,
    }
}

/// Count files in a directory (recursively)
fn count_files(path: &Path) -> Result<usize, std::io::Error> {
    let mut count = 0;

    for entry in std::fs::read_dir(path)? {
        let entry = entry?;
        let entry_path = entry.path();

        if entry_path.is_dir() {
            count += count_files(&entry_path)?;
        } else {
            count += 1;
        }
    }

    Ok(count)
}
