Skip to content
use actix_web::{HttpRequest, HttpResponse, Responder, web};
use serde::{Deserialize, Serialize};

use crate::config::Config;
use crate::session::SessionStore;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExportedConfig {
    pub name: String,
    pub url: String,
    pub pin: String,
    pub api_key: String,
    pub active: String,
}

#[derive(Debug, Clone, Deserialize, Default)]
pub struct ExportConfigQuery {
    pub url: Option<String>,
    pub name: Option<String>,
}

pub fn resolve_server_url(req: &HttpRequest, query_url: Option<&str>) -> String {
    if let Some(url) = query_url.map(|s| s.trim()).filter(|s| !s.is_empty()) {
        return url.trim_end_matches('/').to_string();
    }

    if let Ok(url) = std::env::var("NOTIFY_URL").or_else(|_| std::env::var("SERVER_URL")) {
        let trimmed = url.trim().trim_end_matches('/');
        if !trimmed.is_empty() {
            return trimmed.to_string();
        }
    }

    let conn = req.connection_info();
    let scheme = conn.scheme();
    let host = conn.host();

    let resolved_host = if host.starts_with("0.0.0.0") {
        host.replacen("0.0.0.0", "127.0.0.1", 1)
    } else {
        host.to_string()
    };

    format!("{scheme}://{resolved_host}")
        .trim_end_matches('/')
        .to_string()
}

pub async fn export_config(
    req: HttpRequest,
    session_store: web::Data<SessionStore>,
    query: web::Query<ExportConfigQuery>,
    config: web::Data<Config>,
) -> impl Responder {
    let is_session_auth = session_store.is_authenticated(&req);
    let is_pin_auth = crate::auth::extract_pin(&req)
        .map(|k| config.is_valid_pin(&k))
        .unwrap_or(false);

    if !is_session_auth && !is_pin_auth {
        return HttpResponse::Unauthorized().finish();
    }

    let raw_name = query.name.as_deref().unwrap_or("notify").trim();
    let name = if raw_name.is_empty() {
        "notify"
    } else {
        raw_name
    };

    let url = resolve_server_url(&req, query.url.as_deref());

    let exported = ExportedConfig {
        name: name.to_string(),
        url,
        pin: config.pin.clone(),
        api_key: config.pin.clone(),
        active: name.to_string(),
    };

    let json_body = match serde_json::to_string_pretty(&exported) {
        Ok(s) => s + "\n",
        Err(e) => {
            log::error!("Failed to serialize exported config: {e}");
            return HttpResponse::InternalServerError().finish();
        }
    };

    let filename = format!("{name}.json");

    HttpResponse::Ok()
        .content_type("application/json; charset=utf-8")
        .insert_header((
            actix_web::http::header::CONTENT_DISPOSITION,
            format!("attachment; filename=\"{filename}\""),
        ))
        .insert_header((
            actix_web::http::header::CACHE_CONTROL,
            "no-cache, no-store, must-revalidate",
        ))
        .body(json_body)
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::header::HeaderValue;
    use actix_web::{App, test as aw_test};

    fn test_config() -> Config {
        Config::new(
            8080,
            HeaderValue::from_static(crate::config::DEFAULT_CACHE_CONTROL),
            "notify.db".to_string(),
            false,
            "123456".to_string(),
        )
    }

    #[test]
    fn test_resolve_server_url_with_query_override() {
        let req = aw_test::TestRequest::default().to_http_request();
        let url = resolve_server_url(&req, Some("https://example.com/notify/"));
        assert_eq!(url, "https://example.com/notify");
    }

    #[test]
    fn test_resolve_server_url_replaces_0000() {
        let req = aw_test::TestRequest::default()
            .insert_header(("Host", "0.0.0.0:8080"))
            .to_http_request();
        let url = resolve_server_url(&req, None);
        assert_eq!(url, "http://127.0.0.1:8080");
    }

    #[test]
    fn test_resolve_server_url_preserves_lan_host() {
        let req = aw_test::TestRequest::default()
            .insert_header(("Host", "192.168.1.55:8080"))
            .to_http_request();
        let url = resolve_server_url(&req, None);
        assert_eq!(url, "http://192.168.1.55:8080");
    }

    #[actix_web::test]
    async fn test_export_config_endpoint_unauthorized() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(crate::session::SessionStore::in_memory());
        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .route("/config/export", web::get().to(export_config)),
        )
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/config/export")
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::UNAUTHORIZED);
    }

    #[actix_web::test]
    async fn test_export_config_endpoint_authorized_with_pin() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(crate::session::SessionStore::in_memory());
        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .route("/config/export", web::get().to(export_config)),
        )
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/config/export")
            .insert_header(("X-PIN", "123456"))
            .insert_header(("Host", "10.0.0.42:8080"))
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::OK);

        assert_eq!(
            resp.headers()
                .get(actix_web::http::header::CONTENT_TYPE)
                .and_then(|v| v.to_str().ok()),
            Some("application/json; charset=utf-8")
        );
        assert_eq!(
            resp.headers()
                .get(actix_web::http::header::CONTENT_DISPOSITION)
                .and_then(|v| v.to_str().ok()),
            Some("attachment; filename=\"notify.json\"")
        );

        let body = aw_test::read_body(resp).await;
        let exported: ExportedConfig = serde_json::from_slice(&body).unwrap();
        assert_eq!(exported.name, "notify");
        assert_eq!(exported.url, "http://10.0.0.42:8080");
        assert_eq!(exported.pin, "123456");
        assert_eq!(exported.api_key, "123456");
        assert_eq!(exported.active, "notify");
    }

    #[actix_web::test]
    async fn test_export_config_endpoint_with_custom_name_and_session() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(crate::session::SessionStore::in_memory());
        let token = session_store.create_session().await;
        let cookie =
            actix_web::cookie::Cookie::build(crate::session::SESSION_COOKIE_NAME, token).finish();

        let app = aw_test::init_service(
            App::new()
                .app_data(config)
                .app_data(session_store)
                .route("/config/export", web::get().to(export_config)),
        )
        .await;

        let req = aw_test::TestRequest::get()
            .uri("/config/export?name=home&url=https://home.example.com")
            .cookie(cookie)
            .to_request();
        let resp = aw_test::call_service(&app, req).await;
        assert_eq!(resp.status(), actix_web::http::StatusCode::OK);

        assert_eq!(
            resp.headers()
                .get(actix_web::http::header::CONTENT_DISPOSITION)
                .and_then(|v| v.to_str().ok()),
            Some("attachment; filename=\"home.json\"")
        );

        let body = aw_test::read_body(resp).await;
        let exported: ExportedConfig = serde_json::from_slice(&body).unwrap();
        assert_eq!(exported.name, "home");
        assert_eq!(exported.url, "https://home.example.com");
        assert_eq!(exported.pin, "123456");
        assert_eq!(exported.active, "home");
    }
}