//! `POST /api/agent/login-event` — agent-side reporting of user logon /
//! logoff events observed on the controlled machine. Surfaces a per-device
//! login history on the admin Devices detail page.
//!
//! Auth: same per-peer signed-API gate as `/api/sysinfo` /
//! `/api/heartbeat` / `/api/unattended-password` — see
//! [`crate::api::device_auth`]. Stock RustDesk doesn't post here at all,
//! so in practice every caller is a managed agent; we still keep the
//! `LegacyUnsigned → enforce_managed_for_id` path for symmetry with the
//! other agent endpoints.
//!
//! Body shape (events batched so an agent that was offline can catch up
//! on reconnect):
//!
//! ```json
//! {
//!   "id":   "<peer id>",
//!   "uuid": "<peer uuid>",
//!   "events": [
//!     {
//!       "at":           1717920000,
//!       "kind":         "logon",       // or "logoff"
//!       "username":     "alice",
//!       "domain":       "CORP",
//!       "session_id":   2,
//!       "session_kind": "rdp"          // or "console"
//!     }
//!   ]
//! }
//! ```
//!
//! Response: `"OK"` on success, `"ID_NOT_FOUND"` for an unregistered peer
//! (same shape as `/api/unattended-password` so the agent can use a single
//! retry helper for both).

use crate::api::device_auth::{self, AuthOutcome};
use crate::api::error::ApiError;
use crate::api::state::AppState;
use axum::body::Bytes;
use axum::extract::Extension;
use axum::http::HeaderMap;
use serde::Deserialize;
use std::sync::Arc;

#[derive(Debug, Deserialize)]
pub struct LoginEventIn {
    pub at: i64,
    pub kind: String,
    #[serde(default)]
    pub username: String,
    #[serde(default)]
    pub domain: String,
    #[serde(default)]
    pub session_id: i64,
    #[serde(default)]
    pub session_kind: String,
}

#[derive(Debug, Deserialize)]
pub struct LoginEventBody {
    pub id: String,
    pub uuid: String,
    pub events: Vec<LoginEventIn>,
}

/// Cap per-request to bound DB cost from a misbehaving / catching-up agent.
const MAX_EVENTS_PER_POST: usize = 256;

pub async fn login_event(
    Extension(state): Extension<Arc<AppState>>,
    headers: HeaderMap,
    body: Bytes,
) -> Result<String, ApiError> {
    let outcome =
        device_auth::verify(&state, "POST", "/api/agent/login-event", &headers, &body).await?;

    let payload: LoginEventBody = serde_json::from_slice(&body)
        .map_err(|_| ApiError::BadRequest("invalid json".into()))?;

    if payload.id.is_empty() || payload.uuid.is_empty() {
        return Err(ApiError::BadRequest("id and uuid are required".into()));
    }
    if payload.events.is_empty() {
        return Ok("OK".to_string());
    }
    if payload.events.len() > MAX_EVENTS_PER_POST {
        return Err(ApiError::BadRequest(format!(
            "too many events in one POST (max {MAX_EVENTS_PER_POST})"
        )));
    }

    // Bind the trusted identity to the body. Same rule as the other agent
    // endpoints: signed → header id must equal body id; unsigned → peer
    // must not be `managed=1`.
    let id = match outcome {
        AuthOutcome::Verified { id: signed_id } => {
            if payload.id != signed_id {
                return Err(ApiError::Unauthorized);
            }
            signed_id
        }
        AuthOutcome::LegacyUnsigned => {
            device_auth::enforce_managed_for_id(&state, &payload.id).await?;
            payload.id.clone()
        }
    };

    let peer = state
        .db
        .get_peer(&id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if peer.is_none() {
        // Same shape as /api/unattended-password — agent treats this as
        // "retry later, rendezvous hasn't registered me yet".
        return Ok("ID_NOT_FOUND".to_string());
    }

    let mut accepted = 0usize;
    for ev in &payload.events {
        let kind = ev.kind.trim();
        if kind != "logon" && kind != "logoff" {
            // Unknown kinds are ignored rather than 400ing the whole batch;
            // a future agent build that adds e.g. "lock" should be able to
            // post a mixed batch against an older server without losing
            // the known-kind rows.
            continue;
        }
        if let Err(e) = state
            .db
            .login_event_insert(
                &id,
                &payload.uuid,
                ev.at,
                kind,
                ev.username.trim(),
                ev.domain.trim(),
                ev.session_id,
                ev.session_kind.trim(),
            )
            .await
        {
            // Don't fail the whole batch on a single insert error — the
            // agent's retry loop will resend the events that didn't land,
            // and we'd rather record what we can than reject everything.
            hbb_common::log::warn!(
                "login_event_insert for peer {} failed: {}",
                id,
                e
            );
            continue;
        }
        accepted += 1;
    }

    hbb_common::log::debug!(
        "login-event: peer={} accepted={}/{}",
        id,
        accepted,
        payload.events.len()
    );
    Ok("OK".to_string())
}
