// Feedback Hub client — forwards user-submitted feedback to the central
// feedback-api service. Routed through Rust (not direct fetch) so that:
// - CORS is bypassed (Tauri origin is not whitelisted server-side by design)
// - The exact payload leaving the machine is auditable in a single place
// - The pattern matches the other outbound calls (OAuth, license, updater)
//
// The feedback-api contract is documented in
// `la-compagnie-maximus/docs/feedback-hub-ops.md`. The server silently drops
// any context key outside its whitelist, so this module only sends the
// fields declared in `Context` below.

use serde::{Deserialize, Serialize};
use std::time::Duration;

fn feedback_endpoint() -> String {
    std::env::var("FEEDBACK_HUB_URL")
        .unwrap_or_else(|_| "https://feedback.lacompagniemaximus.com".to_string())
}

/// Context payload sent with a feedback submission. Keys MUST match the
/// server whitelist in `feedback-api/index.js` — unknown keys are dropped
/// silently. Each field is capped at 500 chars server-side.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FeedbackContext {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub page: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub locale: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub theme: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub viewport: Option<String>,
    #[serde(rename = "userAgent", skip_serializing_if = "Option::is_none")]
    pub user_agent: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub timestamp: Option<String>,
}

#[derive(Debug, Serialize)]
struct FeedbackPayload<'a> {
    app_id: &'a str,
    content: &'a str,
    #[serde(skip_serializing_if = "Option::is_none")]
    user_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    context: Option<FeedbackContext>,
}

#[derive(Debug, Serialize)]
pub struct FeedbackSuccess {
    pub id: String,
    pub created_at: String,
}

#[derive(Debug, Deserialize)]
struct FeedbackResponse {
    id: String,
    created_at: String,
}

/// Return a composed User-Agent string for the context payload, e.g.
/// `"Simpl'Résultat/0.8.1 (linux)"`. Uses std::env::consts::OS so we don't
/// pull in an extra Tauri plugin just for this.
#[tauri::command]
pub fn get_feedback_user_agent(app: tauri::AppHandle) -> String {
    let version = app.package_info().version.to_string();
    let os = std::env::consts::OS;
    format!("Simpl'Résultat/{} ({})", version, os)
}

/// Submit a feedback to the Feedback Hub. Error strings are stable codes
/// ("invalid", "rate_limit", "server_error", "network_error") that the
/// frontend maps to i18n messages.
#[tauri::command]
pub async fn send_feedback(
    content: String,
    user_id: Option<String>,
    context: Option<FeedbackContext>,
) -> Result<FeedbackSuccess, String> {
    let trimmed = content.trim();
    if trimmed.is_empty() {
        return Err("invalid".to_string());
    }

    let payload = FeedbackPayload {
        app_id: "simpl-resultat",
        content: trimmed,
        user_id,
        context,
    };

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(15))
        .build()
        .map_err(|_| "network_error".to_string())?;

    let url = format!("{}/api/feedback", feedback_endpoint());
    let res = client
        .post(&url)
        .json(&payload)
        .send()
        .await
        .map_err(|_| "network_error".to_string())?;

    match res.status().as_u16() {
        201 => {
            let body: FeedbackResponse = res
                .json()
                .await
                .map_err(|_| "server_error".to_string())?;
            Ok(FeedbackSuccess {
                id: body.id,
                created_at: body.created_at,
            })
        }
        400 => Err("invalid".to_string()),
        429 => Err("rate_limit".to_string()),
        _ => Err("server_error".to_string()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn context_skips_none_fields() {
        let ctx = FeedbackContext {
            page: Some("/settings".to_string()),
            locale: Some("fr".to_string()),
            theme: None,
            viewport: None,
            user_agent: None,
            timestamp: None,
        };
        let json = serde_json::to_value(&ctx).unwrap();
        let obj = json.as_object().unwrap();
        assert_eq!(obj.len(), 2);
        assert!(obj.contains_key("page"));
        assert!(obj.contains_key("locale"));
    }

    #[test]
    fn context_serializes_user_agent_camelcase() {
        let ctx = FeedbackContext {
            user_agent: Some("Simpl'Résultat/0.8.1 (linux)".to_string()),
            ..Default::default()
        };
        let json = serde_json::to_string(&ctx).unwrap();
        assert!(json.contains("\"userAgent\""));
        assert!(!json.contains("\"user_agent\""));
    }

    #[tokio::test]
    async fn empty_content_is_rejected_locally() {
        let res = send_feedback("   \n\t".to_string(), None, None).await;
        assert_eq!(res.unwrap_err(), "invalid");
    }
}
