// CLI crate needs stdout/stderr for user interaction
#![allow(clippy::print_stdout, clippy::print_stderr)]

mod args;
pub mod commands;
mod error;
pub mod process;

use anyhow::{Context, Result};
use fyi_core::{Storage, StorageError};

pub use args::{Cli, Commands, OtelCommands};

/// Result of running a CLI command.
///
/// Commands can either complete normally or request the process to exit with a specific code.
/// This allows the library to signal exit behavior without calling `std::process::exit()` directly,
/// which enables proper testing and cleanup.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunResult {
    /// Command completed successfully
    Success,
    /// Command requests the process to exit with the given code.
    /// Used when a command must always exit with a specific code regardless of internal errors.
    ExitWithCode(i32),
}

/// Run the CLI with a storage factory.
/// The factory is only called when a command needs storage.
///
/// Returns `RunResult::Success` for normal completion, or `RunResult::ExitWithCode(code)`
/// when the command wants the process to exit with a specific code.
pub fn run<S, F>(cli: Cli, storage_factory: F) -> Result<RunResult>
where
    S: Storage,
    F: FnOnce() -> Result<S, StorageError>,
{
    match cli.command {
        Commands::Activate {
            frameworks,
            local,
            settings_local,
            print,
            db_only,
            hooks_only,
            otel,
            otel_port,
            no_otel,
        } => {
            let result = commands::run_activate(
                commands::ActivateOptions::for_frameworks(frameworks)
                    .local(local)
                    .settings_local(settings_local)
                    .print(print)
                    .db_only(db_only)
                    .hooks_only(hooks_only)
                    .otel(otel)
                    .otel_port(otel_port)
                    .no_otel(no_otel),
            )?;

            if result.should_init_db
                && let Some(db_path) = result.db_path
            {
                let _ = storage_factory().context("failed to initialize storage")?;
                eprintln!("Database initialized at: {}", db_path.display());
            }

            if !print {
                eprintln!(
                    "\nRemember to reload hooks in your AI assistant to activate the new configuration."
                );
            }

            Ok(RunResult::Success)
        }

        Commands::Deactivate {
            frameworks,
            local,
            settings_local,
            print,
        } => {
            commands::run_deactivate(commands::DeactivateOptions {
                frameworks,
                local,
                settings_local,
                print,
            })?;
            Ok(RunResult::Success)
        }

        Commands::Log {
            event_type,
            json_payload,
            framework,
        } => {
            // The log command is called by AI assistant hooks during tool execution.
            // It must always exit 0 to avoid blocking the workflow, even if logging fails.
            // Errors are printed to stderr for debugging but don't affect the exit code.
            //
            // For Codex CLI: event_type may be None, and we extract it from JSON payload's "type" field.
            // Codex passes a single JSON argument containing {"type": "agent-turn-complete", ...}
            let result = storage_factory()
                .context("failed to open storage")
                .and_then(|s| commands::run_log(&s, event_type, json_payload, framework));

            if let Err(e) = result {
                eprintln!("fyi: {:#}", e);
            }
            Ok(RunResult::ExitWithCode(0))
        }

        Commands::Show {
            limit,
            session,
            event_type,
            mode,
            framework,
            json,
        } => {
            let storage = storage_factory().context("failed to open storage")?;
            commands::run_show(
                &storage,
                commands::ShowOptions {
                    limit,
                    session,
                    event_type,
                    permission_mode: mode,
                    framework,
                    json,
                },
            )?;
            Ok(RunResult::Success)
        }

        Commands::Gc { dry_run, retention } => {
            let storage = storage_factory().context("failed to open storage")?;
            commands::run_gc(&storage, dry_run, retention)?;
            Ok(RunResult::Success)
        }

        Commands::Watch {
            session,
            event_type,
            mode,
            framework,
            poll_ms,
        } => {
            let storage = storage_factory().context("failed to open storage")?;
            commands::run_watch(
                &storage,
                commands::WatchOptions {
                    session,
                    event_type,
                    permission_mode: mode,
                    framework,
                    poll_ms,
                },
            )?;
            Ok(RunResult::Success)
        }

        Commands::Otel(otel_cmd) => match otel_cmd {
            OtelCommands::Start { port } => {
                if commands::ensure_running(port, false)? {
                    eprintln!("OTel server is running on port {}", port);
                }
                Ok(RunResult::Success)
            }
            OtelCommands::Stop { port } => {
                commands::stop_server(port)?;
                Ok(RunResult::Success)
            }
            OtelCommands::Restart { port } => {
                if commands::ensure_running(port, true)? {
                    eprintln!("OTel server restarted on port {}", port);
                }
                Ok(RunResult::Success)
            }
            OtelCommands::Status { port } => {
                commands::get_status(port)?;
                Ok(RunResult::Success)
            }
            OtelCommands::Run { port } => {
                let storage = storage_factory().context("failed to open storage")?;
                commands::run_server(&storage, port)?;
                Ok(RunResult::Success)
            }
        },

        Commands::Session {
            session_or_pid,
            json,
        } => {
            let storage = storage_factory().context("failed to open storage")?;
            commands::run_session(&storage, session_or_pid, json)?;
            Ok(RunResult::Success)
        }
    }
}
