//! `POST /api/heartbeat` — the agent management loop. The client sends every
//! ~15 s (3 s when active connections exist). The reply may carry, in any
//! combination:
//!   - `sysinfo: true` — force the client to re-upload sysinfo immediately,
//!   - `disconnect: [conn_id, ...]` — tell the client to drop those sessions,
//!   - `modified_at` + `strategy` — push a config-options merge.
//!
//! Auth: none (the client identifies the device by `(id, uuid)` body fields).

use crate::api::error::ApiError;
use crate::api::state::AppState;
use crate::api::strategy;
use axum::extract::Extension;
use axum::Json;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;

#[derive(Debug, Deserialize)]
pub struct HeartbeatBody {
    #[serde(default)]
    pub id: String,
    #[serde(default)]
    pub uuid: String,
    #[serde(default)]
    pub ver: i64,
    #[serde(default)]
    pub conns: Option<Vec<i64>>,
    #[serde(default)]
    pub modified_at: i64,
}

#[derive(Debug, Serialize)]
pub struct HeartbeatResp {
    /// Present-and-truthy → client re-uploads sysinfo immediately.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sysinfo: Option<bool>,
    /// Conn IDs the client should drop. Always present (possibly empty).
    pub disconnect: Vec<i64>,
    /// Strategy version. Echoed back by the client; when it changes, the
    /// client re-merges `strategy.config_options` into local config.
    pub modified_at: i64,
    pub strategy: Value,
}

pub async fn heartbeat(
    Extension(state): Extension<Arc<AppState>>,
    Json(body): Json<HeartbeatBody>,
) -> Result<Json<HeartbeatResp>, ApiError> {
    if body.id.is_empty() || body.uuid.is_empty() {
        return Err(ApiError::BadRequest("id and uuid required".into()));
    }
    let conns_json = serde_json::to_string(&body.conns.unwrap_or_default())
        .unwrap_or_else(|_| "[]".into());

    let needs_sysinfo = state
        .db
        .sysinfo_heartbeat(
            &body.id,
            &body.uuid,
            body.ver,
            &conns_json,
            &state.cfg.sysinfo_ver,
        )
        .await?;

    // One-shot operator commands queued for this peer (force-disconnect,
    // force-sysinfo). Read-and-delete in one transaction.
    let mut disconnect: Vec<i64> = vec![];
    let mut force_sysinfo = needs_sysinfo;
    for cmd in state
        .db
        .heartbeat_pop_commands(&body.id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?
    {
        match cmd.kind.as_str() {
            "disconnect" => {
                if let Some(payload) = cmd.payload {
                    if let Ok(arr) = serde_json::from_str::<Vec<i64>>(&payload) {
                        disconnect.extend(arr);
                    }
                }
            }
            "sysinfo" => force_sysinfo = true,
            other => hbb_common::log::warn!("unknown heartbeat_command kind {:?}", other),
        }
    }

    // Strategy resolution (peer > device-group > user, highest priority wins).
    let (modified_at, strategy) = strategy::resolve_for(&state, &body.id).await;

    Ok(Json(HeartbeatResp {
        sysinfo: if force_sysinfo { Some(true) } else { None },
        disconnect,
        modified_at,
        strategy,
    }))
}
