#![expect(
    clippy::disallowed_methods,
    reason = "CLI `server start` command: sync config/record file I/O in startup command; acquire_lock uses spawn_blocking"
)]

use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};

use anyhow::{Context, Result, anyhow, bail};
use fabro_config::bind::{Bind, BindRequest};
use fabro_config::daemon::ServerDaemon;
use fabro_config::user::default_settings_path;
use fabro_config::{RuntimeDirectory, Storage};
use fabro_server::jwt_auth::auth_method_name;
use fabro_server::serve::{DEFAULT_TCP_PORT, ServeArgs, resolve_runtime_server_settings_for_start};
use fabro_server::{
    migrate_startup_vault, process_env_snapshot, validate_startup, validate_startup_configuration,
};
use fabro_static::EnvVars;
use fabro_types::settings::{LogDestination, ServerAuthMethod};
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use fabro_vault::SecretStore;
use tokio::process::Command as TokioCommand;
use tokio::task::spawn_blocking;
use tokio::time;

use crate::local_server;

const SERVER_START_HEALTH_PROBE_TIMEOUT: Duration = Duration::from_millis(250);

pub(crate) struct ForegroundServerLogBootstrap {
    #[expect(dead_code, reason = "held for its Drop to release the server lock")]
    lock_file:              std::fs::File,
    pub(crate) destination: LogDestination,
}

pub(crate) async fn execute(
    bind: BindRequest,
    foreground: bool,
    mut serve_args: ServeArgs,
    storage_dir: PathBuf,
    foreground_log_bootstrap: Option<ForegroundServerLogBootstrap>,
    styles: &'static Styles,
    printer: Printer,
) -> Result<()> {
    serve_args.bind = Some(bind.to_string());

    if foreground {
        Box::pin(execute_foreground(
            bind,
            serve_args,
            storage_dir,
            foreground_log_bootstrap
                .context("internal error: missing foreground server log bootstrap")?,
            styles,
            printer,
        ))
        .await
    } else {
        execute_daemon(
            &bind,
            &serve_args,
            &storage_dir,
            true,
            Some(styles),
            printer,
        )
        .await
    }
}

pub(crate) async fn prepare_foreground_server_log(
    runtime_directory: &RuntimeDirectory,
    destination: LogDestination,
) -> Result<ForegroundServerLogBootstrap> {
    let lock_file = acquire_lock(runtime_directory).await?;
    if let Some(existing) = ServerDaemon::load_running(runtime_directory)? {
        bail!(
            "Server already running (pid {}) on {}",
            existing.pid,
            existing.bind
        );
    }

    if destination == LogDestination::File {
        let log_path = runtime_directory.log_path();
        if let Some(parent) = log_path.parent() {
            std::fs::create_dir_all(parent)
                .with_context(|| format!("creating log directory {}", parent.display()))?;
        }
        std::fs::File::create(&log_path)
            .with_context(|| format!("creating server log file {}", log_path.display()))?;
    }

    Ok(ForegroundServerLogBootstrap {
        lock_file,
        destination,
    })
}

pub(crate) async fn ensure_server_running_for_storage(
    storage_dir: &Path,
    config_path: &Path,
) -> Result<Bind> {
    ensure_storage_server_autostart_allowed(
        std::env::var_os(EnvVars::FABRO_CONFIG).as_deref(),
        config_path,
        &default_settings_path(),
    )?;
    ensure_server_running_with_bind(None, config_path, storage_dir).await
}

pub(crate) async fn ensure_server_running_on_socket(
    socket_path: &Path,
    config_path: &Path,
    storage_dir: &Path,
) -> Result<Bind> {
    ensure_server_running_with_bind(
        Some(BindRequest::Unix(socket_path.to_path_buf())),
        config_path,
        storage_dir,
    )
    .await
}

async fn ensure_server_running_with_bind(
    bind_request: Option<BindRequest>,
    config_path: &Path,
    storage_dir: &Path,
) -> Result<Bind> {
    let runtime_directory = RuntimeDirectory::new(storage_dir);
    if let Some(existing) = ServerDaemon::load_running(&runtime_directory)? {
        if bind_request
            .as_ref()
            .is_none_or(|requested| bind_matches_request(&existing.bind, requested))
        {
            return Ok(existing.bind);
        }
        bail!(
            "Server already running (pid {}) on {}",
            existing.pid,
            existing.bind
        );
    }

    let serve_args = ServeArgs {
        bind: bind_request.as_ref().map(ToString::to_string),
        web: false,
        no_web: false,
        model: None,
        provider: None,
        environment: None,
        max_concurrent_runs: server_max_concurrent_runs_override(),
        config: Some(config_path.to_path_buf()),
        #[cfg(debug_assertions)]
        watch_web: false,
    };

    let bind_request = if let Some(bind_request) = bind_request {
        bind_request
    } else {
        local_server::LocalServerConfig::load(Some(config_path), None)?.bind_request(None)?
    };

    match execute_daemon(
        &bind_request,
        &serve_args,
        storage_dir,
        false,
        None,
        Printer::Silent,
    )
    .await
    {
        Ok(()) => ServerDaemon::load_running(&runtime_directory)?
            .map(|server| server.bind)
            .ok_or_else(|| {
                anyhow!(
                    "Server started but no active record was found for {}",
                    storage_dir.display()
                )
            }),
        Err(err) => {
            if let Some(existing) = ServerDaemon::load_running(&runtime_directory)? {
                Ok(existing.bind)
            } else {
                Err(err)
            }
        }
    }
}

fn ensure_storage_server_autostart_allowed(
    config_env: Option<&std::ffi::OsStr>,
    config_path: &Path,
    default_settings_path: &Path,
) -> Result<()> {
    if config_env.is_none() && config_path == default_settings_path && !config_path.exists() {
        bail!(
            "Cannot reach Fabro server: no settings.toml configured.\n\nRun one of:\n  fabro server start    # browser-based wizard\n  fabro install         # terminal wizard"
        );
    }
    Ok(())
}

fn bind_matches_request(existing: &Bind, requested: &BindRequest) -> bool {
    match (existing, requested) {
        (Bind::Unix(existing_path), BindRequest::Unix(requested_path)) => {
            existing_path == requested_path
        }
        (Bind::Tcp(existing_addr), BindRequest::Tcp(requested_addr)) => {
            existing_addr == requested_addr
        }
        (Bind::Tcp(existing_addr), BindRequest::TcpHost(requested_host)) => {
            existing_addr.ip() == *requested_host
        }
        _ => false,
    }
}

fn server_max_concurrent_runs_override() -> Option<usize> {
    std::env::var(EnvVars::FABRO_SERVER_MAX_CONCURRENT_RUNS)
        .ok()
        .and_then(|value| value.parse::<usize>().ok())
        .filter(|value| *value > 0)
}

fn configured_auth_methods(config_path: Option<&Path>) -> Vec<ServerAuthMethod> {
    local_server::LocalServerConfig::load(config_path, None)
        .ok()
        .map(|settings| settings.auth_methods().to_vec())
        .unwrap_or_default()
}

// ---------------------------------------------------------------------------
// Foreground mode
// ---------------------------------------------------------------------------

async fn execute_foreground(
    bind: BindRequest,
    serve_args: ServeArgs,
    storage_dir: PathBuf,
    log_bootstrap: ForegroundServerLogBootstrap,
    styles: &'static Styles,
    _printer: Printer,
) -> Result<()> {
    super::foreground::serve_with_daemon_record(
        serve_args,
        bind,
        storage_dir,
        styles,
        Some(log_bootstrap.destination),
    )
    .await
}

// ---------------------------------------------------------------------------
// Daemon mode
// ---------------------------------------------------------------------------

async fn execute_daemon(
    bind: &BindRequest,
    serve_args: &ServeArgs,
    storage_dir: &Path,
    announce: bool,
    styles: Option<&Styles>,
    printer: Printer,
) -> Result<()> {
    let runtime_directory = RuntimeDirectory::new(storage_dir);
    let lock_file = acquire_lock(&runtime_directory).await?;
    let _lock_file = lock_file;

    if let Some(existing) = ServerDaemon::load_running(&runtime_directory)? {
        if announce {
            bail!(
                "Server already running (pid {}) on {}",
                existing.pid,
                existing.bind
            );
        }
        return Ok(());
    }

    let resolved_settings = resolve_runtime_server_settings_for_start(serve_args, storage_dir)?;
    let destination = fabro_config::resolve_log_destination(resolved_settings.logging.destination)?;
    if matches!(destination, LogDestination::Stdout) {
        bail!(
            "[server.logging].destination = \"stdout\" is incompatible with daemon mode; use `fabro server start --foreground`"
        );
    }
    validate_startup_configuration(&resolved_settings)?;
    let storage = Storage::new(storage_dir);
    migrate_startup_vault(storage.secrets_path());
    let startup_vault = SecretStore::open_snapshot(storage.sqlite_path(), storage.secrets_path())
        .await
        .context("loading secrets for startup validation")?;
    validate_startup(
        runtime_directory.env_path().as_path(),
        process_env_snapshot(),
        &resolved_settings,
        &startup_vault,
    )?;

    let log_path = runtime_directory.log_path();
    if let Some(parent) = log_path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("creating log directory {}", parent.display()))?;
    }

    let log_file = std::fs::File::create(&log_path)
        .with_context(|| format!("creating server log file {}", log_path.display()))?;
    let stdout_log = log_file
        .try_clone()
        .with_context(|| format!("cloning server log file handle for {}", log_path.display()))?;
    let exe = std::env::current_exe().context("resolving current fabro executable path")?;

    let mut cmd = TokioCommand::new(&exe);
    cmd.args(["server", "__serve"])
        .arg("--bind")
        .arg(bind.to_string());

    if let Some(ref model) = serve_args.model {
        cmd.args(["--model", model]);
    }
    if let Some(ref provider) = serve_args.provider {
        cmd.args(["--provider", provider]);
    }
    if serve_args.web {
        cmd.arg("--web");
    }
    if serve_args.no_web {
        cmd.arg("--no-web");
    }
    if let Some(ref environment) = serve_args.environment {
        cmd.args(["--environment", environment]);
    }
    if let Some(max) = serve_args.max_concurrent_runs {
        cmd.args(["--max-concurrent-runs", &max.to_string()]);
    }
    if let Some(ref config) = serve_args.config {
        cmd.arg("--config").arg(config);
    }
    #[cfg(debug_assertions)]
    if serve_args.watch_web {
        cmd.arg("--watch-web");
    }

    cmd.arg("--storage-dir").arg(storage_dir);

    cmd.env_remove(EnvVars::FABRO_JSON);
    cmd.stdout(stdout_log)
        .stderr(log_file)
        .stdin(std::process::Stdio::null());

    #[cfg(unix)]
    fabro_proc::pre_exec_setsid(cmd.as_std_mut());

    let mut child = cmd
        .spawn()
        .with_context(|| format!("spawning fabro server subprocess {}", exe.display()))?;

    if let Ok(Some(status)) = child.try_wait() {
        ServerDaemon::remove(&runtime_directory);
        let tail = read_log_tail(&log_path, 20);
        if !tail.is_empty() {
            fabro_util::printerr!(printer, "{tail}");
        }
        bail!("Server exited immediately with status {status}");
    }

    let poll_interval = Duration::from_millis(50);
    let timeout = Duration::from_secs(5);
    let deadline = Instant::now() + timeout;

    while Instant::now() < deadline {
        let daemon = match ServerDaemon::read(&runtime_directory) {
            Ok(daemon) => daemon,
            Err(err) => {
                ServerDaemon::remove(&runtime_directory);
                let _ = child.kill().await;
                let _ = child.wait().await;
                return Err(err);
            }
        };
        if let Some(daemon) = daemon {
            if try_health(&daemon.bind).await {
                if announce {
                    let pid = child.id().unwrap_or_default();
                    maybe_warn_host_port_fallback(bind, &daemon.bind, printer);
                    fabro_util::printerr!(
                        printer,
                        "Server started (pid {}) on {}",
                        pid,
                        daemon.bind
                    );
                    if let Some(url) = super::bind_to_browser_url(&daemon.bind) {
                        let styled = match styles {
                            Some(s) => format!("{}", s.cyan.apply_to(&url)),
                            None => url,
                        };
                        fabro_util::printerr!(printer, "Web UI: {styled}");
                    }
                    print_auth_methods(printer, serve_args);
                }
                return Ok(());
            }
        }

        if let Ok(Some(status)) = child.try_wait() {
            ServerDaemon::remove(&runtime_directory);
            let tail = read_log_tail(&log_path, 20);
            if !tail.is_empty() {
                fabro_util::printerr!(printer, "{tail}");
            }
            bail!("Server exited during startup with status {status}");
        }

        let remaining = deadline.saturating_duration_since(Instant::now());
        if remaining.is_zero() {
            break;
        }
        time::sleep(poll_interval.min(remaining)).await;
    }

    ServerDaemon::remove(&runtime_directory);
    let _ = child.kill().await;
    let _ = child.wait().await;
    let tail = read_log_tail(&log_path, 20);
    if !tail.is_empty() {
        fabro_util::printerr!(printer, "{tail}");
    }
    bail!("Server did not become ready within {timeout:?}");
}

fn print_auth_methods(printer: Printer, serve_args: &ServeArgs) {
    let auth_methods = configured_auth_methods(serve_args.config.as_deref());
    let names: Vec<&str> = auth_methods.iter().map(|m| auth_method_name(*m)).collect();
    fabro_util::printerr!(printer, "Auth: {}", names.join(", "));
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

async fn acquire_lock(runtime_directory: &RuntimeDirectory) -> Result<std::fs::File> {
    let lock_path = runtime_directory.lock_path();
    if let Some(parent) = lock_path.parent() {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("creating server lock directory {}", parent.display()))?;
    }
    let lock_path_for_open = lock_path.clone();
    #[expect(
        clippy::disallowed_methods,
        reason = "OpenOptions::open inside spawn_blocking; file-lock semantics need a real \
                  std::fs::File handle for fabro_proc::try_flock_exclusive"
    )]
    let lock_file = spawn_blocking(move || {
        std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(false)
            .open(&lock_path_for_open)
    })
    .await
    .context("lock-file open task failed")?
    .with_context(|| format!("opening server lock file {}", lock_path.display()))?;

    let poll_interval = Duration::from_millis(50);
    let timeout = Duration::from_secs(5);
    let mut elapsed = Duration::ZERO;

    while !fabro_proc::try_flock_exclusive(&lock_file)? {
        if elapsed >= timeout {
            bail!("timed out waiting for server lock");
        }
        time::sleep(poll_interval).await;
        elapsed += poll_interval;
    }

    Ok(lock_file)
}

async fn try_health(bind: &Bind) -> bool {
    let Ok((base_url, client)) = build_health_client(bind) else {
        return false;
    };

    let response = time::timeout(
        SERVER_START_HEALTH_PROBE_TIMEOUT,
        client.get(format!("{base_url}/health")).send(),
    )
    .await;

    matches!(response, Ok(Ok(response)) if response.status().is_success())
}

fn build_health_client(bind: &Bind) -> Result<(String, fabro_http::HttpClient)> {
    match bind {
        Bind::Tcp(addr) => Ok((
            format!("http://{addr}"),
            fabro_http::HttpClientBuilder::new()
                .no_proxy()
                .timeout(SERVER_START_HEALTH_PROBE_TIMEOUT)
                .build()?,
        )),
        Bind::Unix(path) => {
            #[cfg(unix)]
            {
                Ok((
                    "http://fabro".to_string(),
                    fabro_http::HttpClientBuilder::new()
                        .unix_socket(path)
                        .no_proxy()
                        .timeout(SERVER_START_HEALTH_PROBE_TIMEOUT)
                        .build()?,
                ))
            }
            #[cfg(not(unix))]
            {
                let _ = path;
                bail!("Unix-socket HTTP client is not supported on this platform")
            }
        }
    }
}

fn maybe_warn_host_port_fallback(requested: &BindRequest, resolved: &Bind, printer: Printer) {
    let BindRequest::TcpHost(host) = requested else {
        return;
    };
    let Bind::Tcp(addr) = resolved else {
        return;
    };
    if addr.ip() == *host && addr.port() != DEFAULT_TCP_PORT {
        fabro_util::printerr!(
            printer,
            "Warning: TCP port {DEFAULT_TCP_PORT} is unavailable on {host}; falling back to a random port."
        );
    }
}

fn read_log_tail(log_path: &Path, lines: usize) -> String {
    match std::fs::read_to_string(log_path) {
        Ok(content) => {
            let tail: Vec<&str> = content.lines().rev().take(lines).collect();
            tail.into_iter().rev().collect::<Vec<_>>().join("\n")
        }
        Err(_) => String::new(),
    }
}

#[cfg(test)]
mod tests {
    use std::time::Duration;

    use fabro_config::bind::{Bind, BindRequest};
    use fabro_server::serve::ServeArgs;
    use fabro_static::EnvVars;
    use fabro_types::settings::LogDestination;
    use fabro_util::Home;
    use fabro_util::printer::Printer;
    use temp_env::with_var;
    use tokio::net::TcpListener;
    use tokio::runtime::Runtime;
    use tokio::time;

    use super::{
        ensure_storage_server_autostart_allowed, execute_daemon, prepare_foreground_server_log,
        try_health,
    };

    fn runtime() -> Runtime {
        Runtime::new().expect("runtime should build")
    }

    fn write_server_settings(path: &std::path::Path, destination: &str) {
        std::fs::write(
            path,
            format!(
                r#"
_version = 1

[server.auth]
methods = ["dev-token"]

[server.logging]
destination = "{destination}"
"#
            ),
        )
        .expect("settings fixture should write");
    }

    fn serve_args_with_config(config_path: &std::path::Path) -> ServeArgs {
        ServeArgs {
            bind: Some("127.0.0.1:0".to_string()),
            web: false,
            no_web: false,
            model: None,
            provider: None,
            environment: None,
            max_concurrent_runs: None,
            config: Some(config_path.to_path_buf()),
            #[cfg(debug_assertions)]
            watch_web: false,
        }
    }

    #[tokio::test]
    async fn try_health_returns_false_for_tcp_peer_that_accepts_without_http_response() {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        let server = tokio::spawn(async move {
            if let Ok((_stream, _addr)) = listener.accept().await {
                time::sleep(Duration::from_secs(10)).await;
            }
        });

        let ready = try_health(&Bind::Tcp(addr)).await;

        server.abort();
        assert!(!ready);
    }

    #[test]
    fn ensure_server_running_for_storage_errors_when_install_mode_is_required() {
        let temp_home = tempfile::tempdir().unwrap();
        let default_settings_path = Home::new(temp_home.path()).user_config();
        let result = ensure_storage_server_autostart_allowed(
            None,
            &default_settings_path,
            &default_settings_path,
        );

        let err =
            result.expect_err("missing default settings.toml should not auto-start install mode");
        let message = err.to_string();
        assert!(
            message.contains("Cannot reach Fabro server: no settings.toml configured."),
            "unexpected error: {message}"
        );
        assert!(
            message.contains("fabro server start"),
            "unexpected error: {message}"
        );
        assert!(
            message.contains("fabro install"),
            "unexpected error: {message}"
        );
    }

    #[test]
    fn prepare_foreground_server_log_with_stdout_does_not_create_server_log() {
        let storage_dir = tempfile::tempdir().unwrap();
        let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.path());

        let _bootstrap = runtime()
            .block_on(prepare_foreground_server_log(
                &runtime_directory,
                LogDestination::Stdout,
            ))
            .expect("stdout foreground bootstrap should succeed");

        assert!(
            !runtime_directory.log_path().exists(),
            "stdout destination should not create server.log"
        );
    }

    #[test]
    fn execute_daemon_rejects_configured_stdout_before_creating_server_log() {
        let storage_dir = tempfile::tempdir().unwrap();
        let config_dir = tempfile::tempdir().unwrap();
        let config_path = config_dir.path().join("settings.toml");
        write_server_settings(&config_path, "stdout");

        let bind = BindRequest::Tcp("127.0.0.1:0".parse().unwrap());
        let serve_args = serve_args_with_config(&config_path);
        let err = runtime()
            .block_on(execute_daemon(
                &bind,
                &serve_args,
                storage_dir.path(),
                false,
                None,
                Printer::Silent,
            ))
            .expect_err("daemon mode should reject stdout logging");

        assert!(
            err.to_string().contains("incompatible with daemon mode"),
            "unexpected error: {err}"
        );
        let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.path());
        assert!(
            !runtime_directory.log_path().exists(),
            "daemon rejection should happen before server.log is created"
        );
    }

    #[test]
    fn execute_daemon_rejects_invalid_env_destination_before_creating_server_log() {
        let storage_dir = tempfile::tempdir().unwrap();
        let config_dir = tempfile::tempdir().unwrap();
        let config_path = config_dir.path().join("settings.toml");
        write_server_settings(&config_path, "file");

        let bind = BindRequest::Tcp("127.0.0.1:0".parse().unwrap());
        let serve_args = serve_args_with_config(&config_path);

        with_var(EnvVars::FABRO_LOG_DESTINATION, Some("stdot"), || {
            let err = runtime()
                .block_on(execute_daemon(
                    &bind,
                    &serve_args,
                    storage_dir.path(),
                    false,
                    None,
                    Printer::Silent,
                ))
                .expect_err("daemon mode should reject invalid env destination");

            let message = err.to_string();
            assert!(message.contains(EnvVars::FABRO_LOG_DESTINATION));
            assert!(message.contains("stdot"));
        });

        let runtime_directory = fabro_config::RuntimeDirectory::new(storage_dir.path());
        assert!(
            !runtime_directory.log_path().exists(),
            "invalid env rejection should happen before server.log is created"
        );
    }
}
