padz 1.2.0

An ergonomic, context-aware scratch pad for the terminal — a good Unix citizen
//! # CLI Layer
//!
//! This module is **one possible UI client** for padz—it is not the application itself.
//!
//! The CLI layer is the **only** place in the codebase that:
//! - Knows about terminal I/O (stdout, stderr)
//! - Uses `std::process::exit`
//! - Handles argument parsing
//! - Formats output for human consumption
//!
//! ## Responsibilities
//!
//! 1. **Argument Parsing**: Convert shell arguments into typed commands via clap
//! 2. **Context Setup**: Initialize app state with API, scope, and configuration
//! 3. **Dispatch**: Route commands to handlers via standout's App
//! 4. **Output Formatting**: Use standout templates for rendering
//! 5. **Error Handling**: Convert errors to user-friendly messages and exit codes

use super::handlers::AppState;
use super::setup::{
    build_command, parse_cli, Cli, Commands, CompletionAction, CompletionShell, ConfigSubcommand,
};
use clapfig::{Clapfig, ConfigAction, SearchMode, SearchPath};
use padzapp::config::PadzConfig;
use padzapp::error::Result;
use padzapp::init::initialize;
use standout::cli::{App, RunResult};
use standout::{embed_styles, embed_templates, OutputMode};
use std::io::IsTerminal;

pub fn run() -> Result<()> {
    // parse_cli() uses standout's App which handles
    // help display (including topics) and errors automatically.
    // It also extracts the output mode from the --output flag.
    let (cli, output_mode) = parse_cli();

    // Handle completion before context init (it doesn't need API)
    if let Some(Commands::Completion { shell, action }) = &cli.command {
        return match action {
            CompletionAction::Install => handle_install(*shell),
            CompletionAction::Print => handle_print(*shell),
        };
    }

    // Handle config via clapfig (needs paths but not full API)
    if let Some(Commands::Config { action }) = &cli.command {
        return handle_config(&cli, action);
    }

    // Initialize app state for handlers
    let app_state = create_app_state(&cli, output_mode)?;

    // Determine effective args: handle naked invocation by injecting synthetic command
    let args: Vec<String> = if cli.command.is_none() {
        // Naked padz: list if interactive, create if piped
        let synthetic_cmd = if !std::io::stdin().is_terminal() {
            "create"
        } else {
            "list"
        };
        vec!["padz".to_string(), synthetic_cmd.to_string()]
    } else {
        std::env::args().collect()
    };

    // Build app with injected state, parse, and dispatch through unified path
    let app = build_dispatch_app(app_state);
    let cmd = build_command();
    let matches = app.parse_from(cmd, args);
    handle_dispatch_result(app.dispatch(matches, output_mode))
}

/// Build the dispatch-ready App with templates, styles, command configuration, and app state
fn build_dispatch_app(app_state: AppState) -> App {
    App::builder()
        .app_state(app_state)
        .templates(embed_templates!("src/cli/templates"))
        .template_ext(".jinja")
        .styles(embed_styles!("src/styles"))
        .default_theme("default")
        .commands(Commands::dispatch_config())
        .expect("Failed to configure commands")
        .build()
        .expect("Failed to build app")
}

/// Handle the result of a dispatch operation
fn handle_dispatch_result(result: RunResult) -> Result<()> {
    match result {
        RunResult::Handled(output) => {
            if output.starts_with("Error:") {
                return Err(padzapp::error::PadzError::Api(
                    output.trim_start_matches("Error: ").to_string(),
                ));
            }
            print!("{}", output);
        }
        RunResult::Binary(data, filename) => {
            std::fs::write(&filename, &data)
                .map_err(|e| padzapp::error::PadzError::Api(e.to_string()))?;
            println!("Exported to {}", filename);
        }
        RunResult::Silent => {}
        RunResult::NoMatch(_) => {
            eprintln!("Error: Unknown command");
        }
    }
    Ok(())
}

/// Create app state containing API, scope, and configuration for handlers
fn create_app_state(cli: &Cli, output_mode: OutputMode) -> Result<AppState> {
    let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
    let data_override = cli.data.as_ref().map(std::path::PathBuf::from);

    // `padz init` (plain, non-global) is a creation operation: "create a store HERE".
    // It should use cwd directly, not walk up to find an existing store.
    // All other commands use find_project_root() for discovery, which is correct.
    let is_plain_init = matches!(
        cli.command,
        Some(Commands::Init {
            link: None,
            unlink: false
        })
    );
    let data_override = if is_plain_init && data_override.is_none() && !cli.global {
        Some(cwd.clone())
    } else {
        data_override
    };

    // Compute the local .padz dir BEFORE link resolution (used by link/unlink commands)
    let local_padz_dir = match &data_override {
        Some(path) => {
            if path.file_name().is_some_and(|name| name == ".padz") {
                path.clone()
            } else {
                path.join(".padz")
            }
        }
        None => padzapp::init::find_project_root(&cwd)
            .map(|root| root.join(".padz"))
            .unwrap_or_else(|| cwd.join(".padz")),
    };

    let padz_ctx = initialize(&cwd, cli.global, data_override);

    Ok(AppState::new(
        padz_ctx.api,
        padz_ctx.scope,
        padz_ctx.config.import_extensions(),
        output_mode,
        padz_ctx.config.mode,
        local_padz_dir,
    ))
}

fn handle_config(cli: &Cli, subcommand: &Option<ConfigSubcommand>) -> Result<()> {
    let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
    let data_override = cli.data.as_ref().map(std::path::PathBuf::from);

    // Resolve paths (lightweight version of initialize — just need dirs, not full API)
    let project_padz_dir = match data_override {
        Some(path) => {
            if path.file_name().is_some_and(|name| name == ".padz") {
                path
            } else {
                path.join(".padz")
            }
        }
        None => padzapp::init::find_project_root(&cwd)
            .map(|root| root.join(".padz"))
            .unwrap_or_else(|| cwd.join(".padz")),
    };

    let global_data_dir = std::env::var("PADZ_GLOBAL_DATA")
        .ok()
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|| {
            let proj_dirs = directories::ProjectDirs::from("com", "padz", "padz")
                .expect("Could not determine config dir");
            proj_dirs.data_dir().to_path_buf()
        });

    // Map -g flag to clapfig scope: None defaults to "local" (first registered)
    let scope: Option<String> = if cli.global {
        Some("global".into())
    } else {
        None
    };

    // Convert our ConfigSubcommand to clapfig's ConfigAction
    let action = match subcommand {
        None => ConfigAction::List {
            scope: scope.clone(),
        },
        Some(ConfigSubcommand::List) => ConfigAction::List {
            scope: scope.clone(),
        },
        Some(ConfigSubcommand::Gen { file }) => ConfigAction::Gen {
            output: file.clone(),
        },
        Some(ConfigSubcommand::Get { key }) => ConfigAction::Get {
            key: key.clone(),
            scope: scope.clone(),
        },
        Some(ConfigSubcommand::Set { key, value }) => ConfigAction::Set {
            key: key.clone(),
            value: value.clone(),
            scope,
        },
    };

    Clapfig::builder::<PadzConfig>()
        .app_name("padz")
        .file_name("padz.toml")
        .search_paths(vec![
            SearchPath::Path(global_data_dir.clone()),
            SearchPath::Path(project_padz_dir.clone()),
        ])
        .search_mode(SearchMode::Merge)
        .strict(false)
        .persist_scope("local", SearchPath::Path(project_padz_dir))
        .persist_scope("global", SearchPath::Path(global_data_dir))
        .handle_and_print(&action)
        .map_err(|e| padzapp::error::PadzError::Api(e.to_string()))?;

    Ok(())
}

fn handle_print(shell_override: Option<CompletionShell>) -> Result<()> {
    let shell = resolve_shell(shell_override)?;
    print!("{}", completion_script(shell));
    Ok(())
}

fn handle_install(shell_override: Option<CompletionShell>) -> Result<()> {
    let shell = resolve_shell(shell_override)?;
    let script = completion_script(shell);

    let path = install_path(shell)?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(&path, script)?;

    println!("Completions installed to {}", path.display());
    match shell {
        CompletionShell::Bash => {
            println!("Restart your shell or run: source {}", path.display());
        }
        CompletionShell::Zsh => {
            if !zshrc_has_zfunc() {
                println!("Add the following to your ~/.zshrc:");
                println!("  fpath=(~/.zfunc $fpath)");
                println!("  autoload -Uz compinit && compinit");
            } else {
                println!("Restart your shell to activate.");
            }
        }
    }

    Ok(())
}

fn resolve_shell(shell_override: Option<CompletionShell>) -> Result<CompletionShell> {
    shell_override.or_else(detect_shell).ok_or_else(|| {
        padzapp::error::PadzError::Api(
            "Could not detect shell from $SHELL. Use --shell bash or --shell zsh".into(),
        )
    })
}

fn detect_shell() -> Option<CompletionShell> {
    let shell = std::env::var("SHELL").ok()?;
    let name = std::path::Path::new(&shell).file_name()?.to_str()?;
    match name {
        "bash" => Some(CompletionShell::Bash),
        "zsh" => Some(CompletionShell::Zsh),
        _ => None,
    }
}

fn completion_script(shell: CompletionShell) -> &'static str {
    match shell {
        CompletionShell::Bash => {
            r#"# padz bash completions
_padz() {
    local IFS=$'\n'
    local cur="${COMP_WORDS[COMP_CWORD]}"
    local candidates
    candidates=$(COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD="$COMP_CWORD" _CLAP_COMPLETE=bash padz 2>/dev/null)
    if [[ $? -eq 0 ]]; then
        COMPREPLY=($(compgen -W "$candidates" -- "$cur"))
    fi
}
complete -F _padz padz
"#
        }
        CompletionShell::Zsh => {
            r#"#compdef padz

_padz() {
    local IFS=$'\n'
    local candidates
    candidates=("${(@f)$(COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT - 1)) _CLAP_COMPLETE=zsh padz 2>/dev/null)}")
    if [[ $? -eq 0 ]]; then
        _describe 'command' candidates
    fi
}

compdef _padz padz
"#
        }
    }
}

fn install_path(shell: CompletionShell) -> Result<std::path::PathBuf> {
    let home = std::env::var("HOME").map_err(|_| {
        padzapp::error::PadzError::Api("$HOME not set; cannot determine install path".into())
    })?;

    Ok(match shell {
        CompletionShell::Bash => {
            let data_dir =
                std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| format!("{}/.local/share", home));
            std::path::PathBuf::from(data_dir).join("bash-completion/completions/padz")
        }
        CompletionShell::Zsh => std::path::PathBuf::from(&home).join(".zfunc/_padz"),
    })
}

/// Checks if ~/.zshrc contains a reference to .zfunc in fpath.
fn zshrc_has_zfunc() -> bool {
    let Ok(home) = std::env::var("HOME") else {
        return false;
    };
    let zshrc = std::path::PathBuf::from(home).join(".zshrc");
    let Ok(content) = std::fs::read_to_string(zshrc) else {
        return false;
    };
    content.lines().any(|line| {
        let trimmed = line.trim();
        !trimmed.starts_with('#') && trimmed.contains("fpath") && trimmed.contains(".zfunc")
    })
}