use clap::Parser;
use std::process;

mod agent;
mod agent_ui;
mod cli;
mod commands;
mod config;
mod db;
mod hooks;
mod import_export;
mod integrations;
mod mcp;
mod models;
mod redact;
mod repository;
mod risk;
mod search;
mod session_ui;
mod settings_ui;
mod stats_ui;
mod suggest;
mod suggest_ui;
#[cfg(test)]
mod test_utils;
mod theme;
mod update;
mod util;

use cli::{Cli, Commands};

fn main() {
    // Install a panic handler that restores the terminal from raw mode.
    // Without this, a panic in a TUI screen leaves the terminal unusable.
    let default_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let _ = crossterm::terminal::disable_raw_mode();
        let _ = crossterm::execute!(
            std::io::stderr(),
            crossterm::event::DisableMouseCapture,
            crossterm::terminal::LeaveAlternateScreen
        );
        default_hook(info);
    }));

    let cli = Cli::parse();

    if let Err(e) = run(cli) {
        eprintln!("Error: {e}");
        process::exit(1);
    }
}

fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
    print_setup_hint(&cli.command);

    // Initialize theme only for user-facing commands.
    // Internal commands (Add, Get, hooks, etc.) don't render TUI,
    // so skip the config read + theme init on the hot path.
    if is_user_facing_command(&cli.command) {
        let cfg = config::load_config().unwrap_or_default();
        theme::init_theme(cfg.theme);
        // Apply user risk-ignore suppressions to all risk assessment.
        risk::set_ignore_patterns(&cfg.agent.risk_ignore_patterns);
    }

    run_command(cli.command)
}

#[allow(clippy::too_many_lines)]
fn run_command(command: Commands) -> Result<(), Box<dyn std::error::Error>> {
    match command {
        Commands::Enable => run_toggle(true),
        Commands::Disable => run_toggle(false),
        Commands::Pause => {
            println!("export SUVADU_PAUSED=1");
            Ok(())
        }
        cmd @ Commands::Add { .. } => run_add(cmd),
        Commands::Init { target } => run_init(target),
        Commands::HookClaudeCode => integrations::handle_hook_claude_code(),
        Commands::HookClaudeCodeFailure => integrations::handle_hook_claude_code_failure(),
        Commands::HookCursor => integrations::handle_hook_cursor(),
        Commands::HookCursorPrompt => integrations::handle_hook_cursor_prompt(),
        Commands::HookClaudePrompt => integrations::handle_hook_claude_prompt(),
        Commands::McpServe => mcp::run(),
        cmd @ Commands::Search { .. } => run_search(cmd),
        Commands::Get {
            query,
            offset,
            prefix,
            cwd,
            include_agents,
        } => commands::search::handle_get(&query, offset, prefix, cwd.as_deref(), include_agents),
        Commands::Settings => commands::settings::handle_settings(),
        Commands::Status => commands::settings::handle_status(),
        Commands::Doctor => {
            commands::doctor::handle_doctor();
            Ok(())
        }
        Commands::History {
            after,
            before,
            tag,
            exit_code,
            executor,
            here,
            cwd,
            limit,
            json,
        } => commands::history::handle_history(
            after.as_deref(),
            before.as_deref(),
            tag.as_deref(),
            exit_code,
            executor.as_deref(),
            here,
            cwd.as_deref(),
            limit,
            json,
        ),
        Commands::Tag(cmd) => commands::tag::handle_tag(cmd),
        Commands::Bookmark(cmd) => commands::entry::handle_bookmark(cmd),
        Commands::Alias(cmd) => commands::alias::handle_alias(cmd),
        Commands::Note {
            entry_id,
            content,
            delete,
        } => commands::entry::handle_note(entry_id, content, delete),
        Commands::Delete {
            pattern,
            regex,
            dry_run,
            yes,
            before,
            no_backup,
        } => commands::entry::handle_delete(
            &pattern,
            regex,
            dry_run,
            yes,
            before.as_deref(),
            no_backup,
        ),
        Commands::Backup { out } => commands::entry::handle_backup(out.as_deref()),
        Commands::Gc { dry_run, vacuum } => commands::entry::handle_gc(dry_run, vacuum),
        Commands::Uninstall => commands::settings::handle_uninstall(),
        Commands::Version => {
            let hash = env!("BUILD_HASH");
            if hash.is_empty() {
                println!(
                    "suvadu v{} ({})",
                    env!("CARGO_PKG_VERSION"),
                    env!("BUILD_DATE")
                );
            } else {
                println!(
                    "suvadu v{} ({} {})",
                    env!("CARGO_PKG_VERSION"),
                    env!("BUILD_DATE"),
                    hash
                );
            }
            Ok(())
        }
        Commands::Man => cli::generate_man_page(),
        Commands::Completions { shell } => {
            cli::generate_completions(shell);
            Ok(())
        }
        Commands::Update => update::handle_update(),
        Commands::Wrap {
            command,
            executor_type,
            executor,
        } => commands::wrap::handle_wrap(&command, &executor_type, &executor),
        Commands::Export {
            format,
            after,
            before,
        } => import_export::handle_export(format.as_str(), after.as_deref(), before.as_deref()),
        Commands::Import {
            file,
            from,
            dry_run,
            allow_duplicates,
        } => run_import(&file, from, dry_run, allow_duplicates),
        Commands::Stats {
            days,
            top,
            text,
            json,
            tag,
            human,
        } => {
            if json {
                commands::stats::handle_stats_json(days, top, tag.as_deref(), human)
            } else if text {
                commands::stats::handle_stats_text(days, top, tag.as_deref(), human)
            } else {
                commands::stats::handle_stats_tui(days, top, tag.as_deref(), human)
            }
        }
        cmd @ Commands::Replay { .. } => run_replay(cmd),
        Commands::Session {
            session_id,
            list,
            after,
            tag,
            limit,
        } => commands::session::handle_session(
            session_id.as_deref(),
            list,
            after.as_deref(),
            tag.as_deref(),
            limit,
        ),
        Commands::Agent(cmd) => agent::handle_agent(cmd),
    }
}

fn run_add(cmd: Commands) -> Result<(), Box<dyn std::error::Error>> {
    let Commands::Add {
        session_id,
        command,
        cwd,
        exit_code,
        started_at,
        ended_at,
        executor_type,
        executor,
    } = cmd
    else {
        unreachable!()
    };
    // Read cached agent prompt if available (written by opencode/claude-code plugins)
    let context = commands::entry::read_agent_prompt(&session_id);
    commands::entry::handle_add_with_context(commands::entry::AddParams {
        session_id,
        command,
        cwd,
        exit_code,
        started_at,
        ended_at,
        executor_type,
        executor,
        context,
    })
}

fn run_search(cmd: Commands) -> Result<(), Box<dyn std::error::Error>> {
    let Commands::Search {
        query,
        unique,
        after,
        before,
        tag,
        exit_code,
        executor,
        here,
        include_agents,
        failed,
        field,
    } = cmd
    else {
        unreachable!()
    };
    commands::search::handle_search(&commands::search::SearchParams {
        query: query.as_ref(),
        unique,
        after: after.as_deref(),
        before: before.as_deref(),
        tag: tag.as_deref(),
        exit_code,
        executor: executor.as_deref(),
        here,
        field,
        include_agents,
        failed_only: failed,
    })
}

fn run_replay(cmd: Commands) -> Result<(), Box<dyn std::error::Error>> {
    let Commands::Replay {
        session,
        after,
        before,
        tag,
        exit_code,
        executor,
        here,
        cwd,
    } = cmd
    else {
        unreachable!()
    };
    commands::replay::handle_replay(&commands::replay::ReplayParams {
        session: session.as_deref(),
        after: after.as_deref(),
        before: before.as_deref(),
        tag: tag.as_deref(),
        exit_code,
        executor: executor.as_deref(),
        here,
        cwd: cwd.as_deref(),
    })
}

fn run_toggle(enable: bool) -> Result<(), Box<dyn std::error::Error>> {
    let mut cfg = config::load_config()?;
    cfg.enabled = enable;
    config::save_config(&cfg)?;
    let word = if enable { "enabled" } else { "disabled" };
    println!("✓ History recording {word} globally");
    Ok(())
}

fn run_init(target: cli::InitTarget) -> Result<(), Box<dyn std::error::Error>> {
    match target {
        cli::InitTarget::Zsh => {
            let cfg = config::load_config().unwrap_or_default();
            println!("{}", hooks::get_zsh_hook(&cfg)?);
            print_first_run_tip();
            Ok(())
        }
        cli::InitTarget::Bash => {
            let cfg = config::load_config().unwrap_or_default();
            println!("{}", hooks::get_bash_hook(&cfg)?);
            print_first_run_tip();
            Ok(())
        }
        cli::InitTarget::ClaudeCode => integrations::handle_init_claude_code(),
        cli::InitTarget::Cursor => integrations::handle_init_cursor(),
        cli::InitTarget::Antigravity => integrations::handle_init_ide(
            "Antigravity",
            "Suvadu detects Antigravity via the $ANTIGRAVITY_AGENT\nenvironment variable.",
            "antigravity",
        ),
        cli::InitTarget::Opencode => integrations::handle_init_opencode(),
        cli::InitTarget::Pi => integrations::handle_init_pi(),
    }
}

fn run_import(
    file: &str,
    from: cli::ImportFormat,
    dry_run: bool,
    allow_duplicates: bool,
) -> Result<(), Box<dyn std::error::Error>> {
    match from {
        cli::ImportFormat::Jsonl => import_export::handle_import(file, dry_run, allow_duplicates),
        cli::ImportFormat::ZshHistory => import_export::handle_import_zsh_history(file, dry_run),
    }
}

/// Check if a command should show the setup hint (skip internal/setup commands).
const fn is_user_facing_command(cmd: &Commands) -> bool {
    !matches!(
        cmd,
        Commands::Init { .. }
            | Commands::Add { .. }
            | Commands::Get { .. }
            | Commands::HookClaudeCode
            | Commands::HookClaudeCodeFailure
            | Commands::HookCursor
            | Commands::HookCursorPrompt
            | Commands::McpServe
            | Commands::HookClaudePrompt
            | Commands::Completions { .. }
            | Commands::Man
            | Commands::Wrap { .. }
    )
}

/// Show setup instructions when the database doesn't exist yet (fresh install).
/// Prints to stderr so it doesn't interfere with stdout.
fn print_setup_hint(cmd: &Commands) {
    if !is_user_facing_command(cmd) {
        return;
    }
    if let Ok(db_path) = db::get_db_path() {
        if !db_path.exists() {
            eprintln!();
            eprintln!("  Welcome to Suvadu!");
            eprintln!();
            eprintln!("  Quick setup — add to your shell config:");
            eprintln!("    echo 'eval \"$(suv init zsh)\"' >> ~/.zshrc && source ~/.zshrc");
            eprintln!();
            eprintln!("  Or for Bash:");
            eprintln!("    echo 'eval \"$(suv init bash)\"' >> ~/.bashrc && source ~/.bashrc");
            eprintln!();
            eprintln!("  Track AI agent commands:");
            eprintln!("    suv init claude-code    Claude Code");
            eprintln!("    suv init cursor         Cursor");
            eprintln!("    suv init antigravity    Antigravity");
            eprintln!();
        }
    }
}

/// Show agent integration tips during `suv init zsh/bash` (first run only).
/// Prints to stderr so it doesn't interfere with shell hook eval on stdout.
fn print_first_run_tip() {
    if let Ok(db_path) = db::get_db_path() {
        if !db_path.exists() {
            eprintln!();
            eprintln!("  Suvadu installed successfully");
            eprintln!();
            eprintln!("  Track AI agent commands:");
            eprintln!("    suv init claude-code    Claude Code");
            eprintln!("    suv init cursor         Cursor");
            eprintln!("    suv init antigravity    Antigravity");
            eprintln!();
            eprintln!("  Try it: suv search");
        }
    }
}

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

    // ── is_user_facing_command ──────────────────────────────────

    #[test]
    fn internal_commands_are_not_user_facing() {
        // Commands on the hot path or that produce machine output should NOT
        // trigger theme init or setup hints.
        assert!(!is_user_facing_command(&Commands::Add {
            session_id: "s".into(),
            command: "ls".into(),
            cwd: "/".into(),
            exit_code: Some(0),
            started_at: 0,
            ended_at: 0,
            executor_type: None,
            executor: None,
        }));
        assert!(!is_user_facing_command(&Commands::Get {
            query: String::new(),
            offset: 0,
            prefix: false,
            cwd: None,
            include_agents: false,
        }));
        assert!(!is_user_facing_command(&Commands::Init {
            target: cli::InitTarget::Zsh,
        }));
        assert!(!is_user_facing_command(&Commands::HookClaudeCode));
        assert!(!is_user_facing_command(&Commands::HookClaudeCodeFailure));
        assert!(!is_user_facing_command(&Commands::HookCursor));
        assert!(!is_user_facing_command(&Commands::HookCursorPrompt));
        assert!(!is_user_facing_command(&Commands::McpServe));
        assert!(!is_user_facing_command(&Commands::HookClaudePrompt));
        assert!(!is_user_facing_command(&Commands::Man));
        assert!(!is_user_facing_command(&Commands::Completions {
            shell: clap_complete::Shell::Zsh,
        }));
        assert!(!is_user_facing_command(&Commands::Wrap {
            command: vec!["ls".into()],
            executor_type: "human".into(),
            executor: "human".into(),
        }));
    }

    #[test]
    fn user_facing_commands_are_detected() {
        assert!(is_user_facing_command(&Commands::Enable));
        assert!(is_user_facing_command(&Commands::Disable));
        assert!(is_user_facing_command(&Commands::Settings));
        assert!(is_user_facing_command(&Commands::Status));
        assert!(is_user_facing_command(&Commands::Version));
        assert!(is_user_facing_command(&Commands::Update));
        assert!(is_user_facing_command(&Commands::Uninstall));
    }

    #[test]
    fn search_is_user_facing() {
        let cmd = Commands::Search {
            query: None,
            unique: false,
            after: None,
            before: None,
            tag: None,
            exit_code: None,
            executor: None,
            here: false,
            include_agents: false,
            failed: false,
            field: crate::models::SearchField::Command,
        };
        assert!(is_user_facing_command(&cmd));
    }

    #[test]
    fn stats_is_user_facing() {
        let cmd = Commands::Stats {
            days: None,
            top: 10,
            text: false,
            json: false,
            tag: None,
            human: false,
        };
        assert!(is_user_facing_command(&cmd));
    }

    #[test]
    fn pause_is_user_facing() {
        assert!(is_user_facing_command(&Commands::Pause));
    }

    // ── run_command dispatch smoke tests ────────────────────────

    #[test]
    fn pause_outputs_export_line() {
        // Pause doesn't touch the DB or config — safe to test directly.
        let result = run_command(Commands::Pause);
        assert!(result.is_ok());
    }

    #[test]
    fn version_command_succeeds() {
        let result = run_command(Commands::Version);
        assert!(result.is_ok());
    }
}
