//! `mineral serve` — 起 daemon,bind unix socket,跑 `Server::serve` accept loop。
//!
//! 调用方(main.rs)负责 build channels 后传进来。本模块负责:
//! 1. 解析 socket 路径
//! 2. stale socket 检测(已活 daemon → bail;残留 socket 文件 → 删)
//! 3. bind + Server::spawn + serve

use std::sync::Arc;

use color_eyre::eyre::{WrapErr, bail};
use mineral_channel_core::MusicChannel;
use mineral_persist::ServerStore;
use mineral_server::{Server, ServerConfig, resolve_audio_mode};
use tokio::net::{UnixListener, UnixStream};
use tokio::signal::unix::{Signal, SignalKind, signal};

/// 停机时 await 埋点 actor join 的超时上限:actor 结算末尾一行 + 退出远快于此,超时纯是
/// 兜底(埋点故障绝不无限拖住 daemon 退出)。
const STATS_JOIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);

/// daemon 入口。需要 caller 已 `tokio::Runtime::block_on` 或在 async ctx 中调。
///
/// accept loop 与关闭信号(SIGINT / SIGTERM)竞争:收到信号时主动停掉 server
/// (走 [`Server::shutdown`] 的 Drop 链)并 unlink socket 文件,避免残留 stale
/// socket 留给下次启动清理。
///
/// # Params:
///   - `channels`: 已构造好的全部音乐源 handle。空 vec 也合法。
///   - `persist`: 持久化句柄,透传给 [`Server::spawn`] 供 PlayerCore 持有。
///   - `config`: 已加载的全局配置(audio 后端 / daemon 切片在此派生)。
///   - `script`: 脚本部件包(daemon 入口经 `load_with_vm` 装配;无脚本时 VM 槽为空)。
///   - `config_tree`: 有效配置底树(与 `config` 同一次加载的合成树,交配置宿主)。
///   - `config_path`: 用户 config.lua 路径(热重载 mtime 轮询的目标)。
pub async fn run(
    channels: Vec<Arc<dyn MusicChannel>>,
    persist: ServerStore,
    config: mineral_config::Config,
    script: mineral_server::ScriptParts,
    config_tree: serde_json::Value,
    config_path: std::path::PathBuf,
) -> color_eyre::Result<()> {
    mineral_log::info!(target: "daemon", "starting mineral daemon");
    // 信号 handler 必须在 bind 之前装好:unix socket 一 bind,client 就能连上(连接进
    // backlog,无需 accept),并可能立刻请求退出。若此刻 handler 还没装(Server::spawn
    // 的 audio 初始化耗时不短),SIGTERM 会走默认处置直接杀进程 —— stale socket 残留、
    // audio / MPRIS 收尾全跳过。提前装好即可关掉这段竞态窗口。
    let mut term = signal(SignalKind::terminate()).wrap_err("install SIGTERM handler")?;
    let mut interrupt = signal(SignalKind::interrupt()).wrap_err("install SIGINT handler")?;

    let socket_path = mineral_paths::socket_path()?;
    prepare_socket(&socket_path).await?;
    let listener = UnixListener::bind(&socket_path)
        .wrap_err_with(|| format!("bind unix socket {}", socket_path.display()))?;
    mineral_log::info!(target: "daemon", socket_path = %socket_path.display(), "unix socket bound");
    println!("mineral daemon listening on {}", socket_path.display());

    // `MINERAL_AUDIO_NULL` 强制 null 后端(无设备 e2e / headless 确定性复现降级);
    // 否则按 config 的 `audio.backend` 落(env 命中短路 config)。env 只在 binary 边缘读。
    let audio_mode = resolve_audio_mode(
        std::env::var_os("MINERAL_AUDIO_NULL").is_some(),
        *config.audio().backend(),
    );
    // 脚本线程先于 Server 起(只需 VM + host);其投递句柄喂给 Server 的事件
    // 出口。runtime 句柄持有到本函数结束 —— Drop = 停机 + join。
    let watchdog = mineral_script::WatchdogConfig::builder()
        .instruction_interval(*config.script().watchdog_instruction_interval())
        .soft_wall(std::time::Duration::from_millis(
            *config.script().watchdog_soft_wall_ms(),
        ))
        .hard_wall(std::time::Duration::from_millis(
            *config.script().watchdog_hard_wall_ms(),
        ))
        .build();
    // 投递句柄是热重载间接层:daemon 恒持有(初始无脚本也可经重载升级为有)。
    let script_sender = mineral_script::ScriptSender::detached();
    let (script_runtime, pumps) = script.spawn_runtime(watchdog, &script_sender, &channels);
    let (stats, stats_actor) = spawn_recorder(&config).await;
    // 留一份句柄给停机路径记 app_lifecycle stop + 发 Shutdown(server 会 move 走原句柄)。
    let stats_stop = stats.clone();
    let server = Server::spawn(
        channels,
        audio_mode,
        persist,
        ServerConfig::from_config(&config),
        config_tree,
        Some(script_sender.clone()),
        stats,
    )
    .await?;
    let reload_parts = server.attach_script_pumps(pumps);
    // config.lua 热重载:mtime 轮询,变更即重 eval 换脚本线程(失败保留旧)。
    mineral_server::spawn_script_reloader(config_path, script_runtime, script_sender, reload_parts);
    mineral_log::info!(target: "daemon", "server core initialized");
    // 接入系统媒体服务(MPRIS)。无 D-Bus session 等失败时降级:daemon 照常跑。
    if let Err(e) = server.start_media_service() {
        mineral_log::warn!(target: "media", error = mineral_log::chain(&e), "system media service unavailable");
    }
    let outcome = tokio::select! {
        result = server.serve(listener) => result,
        () = wait_for_signal(&mut term, &mut interrupt) => {
            mineral_log::info!(target: "daemon", "shutdown signal received, stopping daemon");
            Ok(())
        }
        // client 经 IPC 请求关停(`mineral stop` / TUI 的「退出并停止 daemon」),
        // 与信号路径走完全相同的下方收尾。
        () = server.shutdown_requested() => {
            mineral_log::info!(target: "daemon", "shutdown requested by client, stopping daemon");
            Ok(())
        }
    };

    // graceful 收尾:先记 daemon stop 并 flush(保证落库不被进程退出截断),
    // 再停 server(Drop 链停 audio engine / scheduler)+ 清 socket。
    mineral_log::info!(target: "daemon", "shutting down");
    stats_stop.daemon_stopped();
    stats_stop.flush().await;
    server.shutdown();
    // 埋点优雅停机:发 Shutdown 让 actor 把在播 pending 结算成 stop 行,带超时 await join
    // (在播行丢失由此收窄到仅硬 kill;超时兜底不无限阻塞退出)。
    stats_stop.shutdown().await;
    drop(stats_stop);
    if let Err(e) = tokio::time::timeout(STATS_JOIN_TIMEOUT, stats_actor).await {
        mineral_log::warn!(target: "daemon", error = mineral_log::chain(&e), "埋点 actor join 超时,末尾在播行可能未落库");
    }
    if let Err(e) = std::fs::remove_file(&socket_path) {
        mineral_log::warn!(
            target: "daemon",
            socket_path = %socket_path.display(),
            error = mineral_log::chain(&e),
            "remove socket on shutdown failed"
        );
    } else {
        mineral_log::debug!(target: "daemon", socket_path = %socket_path.display(), "socket cleaned up");
    }
    outcome
}

/// 等待第一个到达的关闭信号(SIGINT 或 SIGTERM)。
///
/// handler 由 caller 在 bind socket **之前**就装好(`signal(...)` 调用时即安装),
/// 这里只 `recv` 等触发,避免「socket 已可连但 handler 未就绪」的竞态。
///
/// # Params:
///   - `term`: 已安装的 SIGTERM 信号流。
///   - `interrupt`: 已安装的 SIGINT 信号流。
async fn wait_for_signal(term: &mut Signal, interrupt: &mut Signal) {
    tokio::select! {
        _ = term.recv() => {}
        _ = interrupt.recv() => {}
    }
}

/// 检测 stale socket。
///
/// - 不存在 → OK,直接返回
/// - 存在 → 试 connect:
///   - 连得上 → daemon 已活,bail
///   - 连不上(ConnectionRefused / NotFound)→ 残留 socket 文件,删
async fn prepare_socket(path: &std::path::Path) -> color_eyre::Result<()> {
    if !path.exists() {
        mineral_log::debug!(target: "daemon", "socket path fresh, no cleanup needed");
        return Ok(());
    }
    match UnixStream::connect(path).await {
        Ok(_) => {
            mineral_log::error!(target: "daemon", socket_path = %path.display(), "another daemon already running");
            bail!(
                "another mineral daemon is already running at {}",
                path.display()
            )
        }
        Err(_) => {
            mineral_log::warn!(target: "daemon", socket_path = %path.display(), "removing stale socket");
            std::fs::remove_file(path)
                .wrap_err_with(|| format!("remove stale socket {}", path.display()))?;
            Ok(())
        }
    }
}

/// 起埋点 recorder:开 stats.db(失败降级 disabled no-op)+ 折算采集参数 + spawn actor。
/// 返回 actor 的 `JoinHandle` 供停机路径带超时 await(在播 pending 结算 stop 后退出)。
///
/// # Params:
///   - `config`: 全局配置(取 `stats` 段折算;`report` 口径留给报告层现读)
///
/// # Return:
///   (recorder 句柄, actor 的 `JoinHandle`);降级路径亦返回可用句柄(打点静默丢弃)
async fn spawn_recorder(
    config: &mineral_config::Config,
) -> (mineral_server::StatsRecorder, tokio::task::JoinHandle<()>) {
    let store = match mineral_paths::data_dir() {
        Ok(dir) => match std::fs::create_dir_all(&dir) {
            Ok(()) => match mineral_stats::StatsStore::open(&dir.join("stats.db")).await {
                Ok(store) => store,
                Err(e) => {
                    mineral_log::warn!(target: "daemon", error = mineral_log::chain(&e), "打开 stats.db 失败,埋点降级");
                    mineral_stats::StatsStore::disabled()
                }
            },
            Err(e) => {
                mineral_log::warn!(target: "daemon", error = mineral_log::chain(&e), "建数据目录失败,埋点降级");
                mineral_stats::StatsStore::disabled()
            }
        },
        Err(e) => {
            mineral_log::warn!(target: "daemon", error = mineral_log::chain(&e), "定位数据目录失败,埋点降级");
            mineral_stats::StatsStore::disabled()
        }
    };
    let params = mineral_server::params_from_config(config.stats());
    mineral_server::StatsRecorder::spawn(store, params)
}
