//! Download endpoints for generating configured agent binaries
//!
//! Provides endpoints for:
//! - Viewer-only downloads
//! - Temp support session downloads (with embedded code)
//! - Permanent agent downloads (with embedded config)

use axum::{
    body::Body,
    extract::Query,
    http::{header, StatusCode},
    response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tracing::{error, info};

/// Magic marker for embedded configuration (must match agent)
const MAGIC_MARKER: &[u8] = b"GURUCONFIG";

/// Embedded configuration data structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddedConfig {
    /// Server WebSocket URL
    pub server_url: String,
    /// API key for authentication
    pub api_key: String,
    /// Company/organization name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub company: Option<String>,
    /// Site/location name
    #[serde(skip_serializing_if = "Option::is_none")]
    pub site: Option<String>,
    /// Tags for categorization
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tags: Vec<String>,
}

/// Query parameters for agent download
#[derive(Debug, Deserialize)]
pub struct AgentDownloadParams {
    /// Company/organization name
    pub company: Option<String>,
    /// Site/location name
    pub site: Option<String>,
    /// Comma-separated tags
    pub tags: Option<String>,
    /// API key (optional, will use default if not provided)
    pub api_key: Option<String>,
}

/// Query parameters for support session download
#[derive(Debug, Deserialize)]
pub struct SupportDownloadParams {
    /// 6-digit support code
    pub code: String,
}

/// Get path to base agent binary
fn get_base_binary_path() -> PathBuf {
    // Check for static/downloads/guruconnect.exe relative to working dir
    let static_path = PathBuf::from("static/downloads/guruconnect.exe");
    if static_path.exists() {
        return static_path;
    }

    // Also check without static prefix (in case running from server dir)
    let downloads_path = PathBuf::from("downloads/guruconnect.exe");
    if downloads_path.exists() {
        return downloads_path;
    }

    // Fallback to static path
    static_path
}

/// Download viewer-only binary (no embedded config, "Viewer" in filename)
pub async fn download_viewer() -> impl IntoResponse {
    let binary_path = get_base_binary_path();

    match std::fs::read(&binary_path) {
        Ok(binary_data) => {
            info!("Serving viewer download ({} bytes)", binary_data.len());

            Response::builder()
                .status(StatusCode::OK)
                .header(header::CONTENT_TYPE, "application/octet-stream")
                .header(
                    header::CONTENT_DISPOSITION,
                    "attachment; filename=\"GuruConnect-Viewer.exe\"",
                )
                .header(header::CONTENT_LENGTH, binary_data.len())
                .body(Body::from(binary_data))
                .unwrap()
        }
        Err(e) => {
            error!("Failed to read base binary from {:?}: {}", binary_path, e);
            Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Body::from("Agent binary not found"))
                .unwrap()
        }
    }
}

/// Download support session binary (code embedded in filename)
pub async fn download_support(Query(params): Query<SupportDownloadParams>) -> impl IntoResponse {
    // Validate support code (must be 6 digits)
    let code = params.code.trim();
    if code.len() != 6 || !code.chars().all(|c| c.is_ascii_digit()) {
        return Response::builder()
            .status(StatusCode::BAD_REQUEST)
            .body(Body::from("Invalid support code: must be 6 digits"))
            .unwrap();
    }

    let binary_path = get_base_binary_path();

    match std::fs::read(&binary_path) {
        Ok(binary_data) => {
            info!(
                "Serving support session download for code {} ({} bytes)",
                code,
                binary_data.len()
            );

            // Filename includes the support code
            let filename = format!("GuruConnect-{}.exe", code);

            Response::builder()
                .status(StatusCode::OK)
                .header(header::CONTENT_TYPE, "application/octet-stream")
                .header(
                    header::CONTENT_DISPOSITION,
                    format!("attachment; filename=\"{}\"", filename),
                )
                .header(header::CONTENT_LENGTH, binary_data.len())
                .body(Body::from(binary_data))
                .unwrap()
        }
        Err(e) => {
            error!("Failed to read base binary: {}", e);
            Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Body::from("Agent binary not found"))
                .unwrap()
        }
    }
}

/// Download permanent agent binary with embedded configuration
pub async fn download_agent(Query(params): Query<AgentDownloadParams>) -> impl IntoResponse {
    let binary_path = get_base_binary_path();

    // Read base binary
    let mut binary_data = match std::fs::read(&binary_path) {
        Ok(data) => data,
        Err(e) => {
            error!("Failed to read base binary: {}", e);
            return Response::builder()
                .status(StatusCode::NOT_FOUND)
                .body(Body::from("Agent binary not found"))
                .unwrap();
        }
    };

    // Build embedded config
    let config = EmbeddedConfig {
        server_url: "wss://connect.azcomputerguru.com/ws/agent".to_string(),
        api_key: params
            .api_key
            .unwrap_or_else(|| "managed-agent".to_string()),
        company: params.company.clone(),
        site: params.site.clone(),
        tags: params
            .tags
            .as_ref()
            .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
            .unwrap_or_default(),
    };

    // Serialize config to JSON
    let config_json = match serde_json::to_vec(&config) {
        Ok(json) => json,
        Err(e) => {
            error!("Failed to serialize config: {}", e);
            return Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(Body::from("Failed to generate config"))
                .unwrap();
        }
    };

    // Append magic marker + length + config to binary
    // Structure: [PE binary][GURUCONFIG][length:u32 LE][json config]
    binary_data.extend_from_slice(MAGIC_MARKER);
    binary_data.extend_from_slice(&(config_json.len() as u32).to_le_bytes());
    binary_data.extend_from_slice(&config_json);

    info!(
        "Serving permanent agent download: company={:?}, site={:?}, tags={:?} ({} bytes)",
        config.company,
        config.site,
        config.tags,
        binary_data.len()
    );

    // Generate filename based on company/site
    let filename = match (&params.company, &params.site) {
        (Some(company), Some(site)) => {
            format!(
                "GuruConnect-{}-{}-Setup.exe",
                sanitize_filename(company),
                sanitize_filename(site)
            )
        }
        (Some(company), None) => {
            format!("GuruConnect-{}-Setup.exe", sanitize_filename(company))
        }
        _ => "GuruConnect-Setup.exe".to_string(),
    };

    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "application/octet-stream")
        .header(
            header::CONTENT_DISPOSITION,
            format!("attachment; filename=\"{}\"", filename),
        )
        .header(header::CONTENT_LENGTH, binary_data.len())
        .body(Body::from(binary_data))
        .unwrap()
}

/// Sanitize a string for use in a filename
fn sanitize_filename(s: &str) -> String {
    s.chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '-' || c == '_' {
                c
            } else if c == ' ' {
                '-'
            } else {
                '_'
            }
        })
        .collect::<String>()
        .chars()
        .take(32) // Limit length
        .collect()
}

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

    #[test]
    fn test_sanitize_filename() {
        assert_eq!(sanitize_filename("Acme Corp"), "Acme-Corp");
        assert_eq!(sanitize_filename("My Company!"), "My-Company_");
        assert_eq!(sanitize_filename("Test/Site"), "Test_Site");
    }

    #[test]
    fn test_embedded_config_serialization() {
        let config = EmbeddedConfig {
            server_url: "wss://example.com/ws".to_string(),
            api_key: "test-key".to_string(),
            company: Some("Test Corp".to_string()),
            site: None,
            tags: vec!["windows".to_string()],
        };

        let json = serde_json::to_string(&config).unwrap();
        assert!(json.contains("Test Corp"));
        assert!(json.contains("windows"));
    }
}
