//! `POST /api/agent/exec-result` — agent posts back the result of a
//! PowerShell command queued via the heartbeat reply.
//!
//! Auth: same per-peer signed-API gate as the other agent endpoints
//! ([`crate::api::device_auth`]). Because remote exec is only ever
//! dispatched against `peer.managed = 1` peers, *this* endpoint
//! additionally refuses unsigned posts even when the peer happens to be
//! `managed=0` — there's no legacy compatibility story for exec, so we
//! fail closed.

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 ExecResultBody {
    pub id: String,
    pub uuid: String,
    pub cmd_id: String,
    pub exit_code: i64,
    #[serde(default)]
    pub stdout: String,
    #[serde(default)]
    pub stderr: String,
    #[serde(default)]
    pub timed_out: bool,
    #[serde(default)]
    pub truncated: bool,
}

pub async fn exec_result(
    Extension(state): Extension<Arc<AppState>>,
    headers: HeaderMap,
    body: Bytes,
) -> Result<String, ApiError> {
    let outcome =
        device_auth::verify(&state, "POST", "/api/agent/exec-result", &headers, &body).await?;
    let payload: ExecResultBody = serde_json::from_slice(&body)
        .map_err(|_| ApiError::BadRequest("invalid json".into()))?;
    if payload.id.is_empty() || payload.uuid.is_empty() || payload.cmd_id.is_empty() {
        return Err(ApiError::BadRequest(
            "id, uuid, and cmd_id required".into(),
        ));
    }

    // Bind identity to body. Unsigned posts are flat-out rejected here
    // even when the peer is currently managed=0 — exec is a signed-only
    // feature, no legacy path.
    let id = match outcome {
        AuthOutcome::Verified { id: signed_id } => {
            if payload.id != signed_id {
                return Err(ApiError::Unauthorized);
            }
            signed_id
        }
        AuthOutcome::LegacyUnsigned => return Err(ApiError::Unauthorized),
    };

    let updated = state
        .db
        .exec_finish(
            &payload.cmd_id,
            &id,
            payload.exit_code,
            &payload.stdout,
            &payload.stderr,
            payload.timed_out,
            payload.truncated,
        )
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if !updated {
        // Either the cmd_id doesn't exist, belongs to another peer, or
        // is already in a terminal state. The agent doesn't need to
        // distinguish — log on our side and return OK so it doesn't
        // retry forever.
        hbb_common::log::warn!(
            "exec-result: no-op update for cmd_id={} peer={} (already finalized or wrong peer)",
            payload.cmd_id,
            id
        );
    }
    Ok("OK".to_string())
}
