//! `POST /api/agent/perf-events` — performance-related Windows event log
//! entries the agent surfaced from `Microsoft-Windows-Diagnostics-
//! Performance/Operational`, `Microsoft-Windows-Resource-Exhaustion-
//! Detector/Operational`, and a few hand-picked IDs from `System`
//! (unexpected reboots, BSODs, dirty shutdowns). The admin UI shows
//! the recent ones in the device's Performance section.
//!
//! Auth: same per-peer signed-API gate as the other agent endpoints.
//! Server-side dedup is via the UNIQUE (peer_id, provider, record_id)
//! index — the agent persists a per-channel cursor to disk, but a
//! restart that loses the cursor can safely re-emit overlapping ranges.

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

#[derive(Debug, Deserialize)]
pub struct PerfEventIn {
    pub at: i64,
    pub provider: String,
    pub event_id: i64,
    #[serde(default = "default_level")]
    pub level: i64,
    #[serde(default)]
    pub record_id: i64,
    #[serde(default)]
    pub summary: String,
    #[serde(default)]
    pub detail_json: String,
}

fn default_level() -> i64 {
    4 // WEL "Information"
}

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

const MAX_EVENTS_PER_POST: usize = 128;
const MAX_PROVIDER_LEN: usize = 64;
const MAX_SUMMARY_LEN: usize = 512;
const MAX_DETAIL_LEN: usize = 8 * 1024;

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

    let payload: PerfEventsBody = 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})"
        )));
    }

    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() {
        return Ok("ID_NOT_FOUND".to_string());
    }

    let mut accepted = 0usize;
    for e in &payload.events {
        let provider = e.provider.trim();
        if provider.is_empty() {
            continue;
        }
        let row = PerfEventRow {
            at: e.at,
            provider: truncate(provider, MAX_PROVIDER_LEN),
            event_id: e.event_id,
            level: e.level,
            record_id: e.record_id,
            summary: truncate(&e.summary, MAX_SUMMARY_LEN),
            detail_json: truncate(&e.detail_json, MAX_DETAIL_LEN),
            received_at: 0, // server fills via DEFAULT on INSERT
        };
        if let Err(err) = state.db.perf_event_insert(&id, &payload.uuid, &row).await {
            hbb_common::log::warn!(
                "perf_event_insert for peer {} failed: {}",
                id,
                err
            );
            continue;
        }
        accepted += 1;
    }

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

fn truncate(s: &str, max_chars: usize) -> String {
    if s.chars().count() <= max_chars {
        s.to_string()
    } else {
        s.chars().take(max_chars).collect()
    }
}
