//! Leptos server functions for the activities feature.
//!
//! All functions use the `OtpServiceExtension` pattern so they remain
//! decoupled from `rustelo_server` — the same bridge used by auth, bookmarks,
//! and user-dashboard features.

use leptos::prelude::*;

/// Access status for a gated activity download.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ActivityAccessStatus {
    /// Is the request coming from an authenticated session?
    pub authenticated: bool,
    /// Did the user already complete the required questionnaire?
    pub questionnaire_completed: bool,
    /// Combined: `authenticated && questionnaire_completed` (or download is
    /// not gated at all — callers should check `require_auth` from content
    /// metadata).
    pub can_download: bool,
}

/// Check whether the current session can download the PDF for `activity_id`.
///
/// Returns `ActivityAccessStatus` instead of a hard error so WASM can render
/// the right UI state without a round-trip failure.
#[server(CheckActivityAccess, "/api")]
pub async fn server_check_activity_access(
    activity_id: String,
) -> Result<ActivityAccessStatus, ServerFnError> {
    #[cfg(feature = "ssr")]
    {
        use axum::{http::header, Extension};
        use leptos_axum::extract;

        use crate::auth::otp_service::server::OtpServiceExtension;

        let Extension(OtpServiceExtension(svc)) = extract::<Extension<OtpServiceExtension>>()
            .await
            .map_err(|_| ServerFnError::new("OTP service not configured"))?;

        let headers: axum::http::HeaderMap = extract()
            .await
            .map_err(|_| ServerFnError::new("Could not extract headers"))?;

        let session_token = headers
            .get(header::COOKIE)
            .and_then(|v| v.to_str().ok())
            .and_then(|cookies| {
                cookies
                    .split(';')
                    .find_map(|c| c.trim().strip_prefix("session_token=").map(str::to_string))
            });

        let Some(token) = session_token else {
            return Ok(ActivityAccessStatus {
                authenticated: false,
                questionnaire_completed: false,
                can_download: false,
            });
        };

        let completed = svc
            .check_activity_completion(&token, activity_id)
            .await
            .unwrap_or(false);

        return Ok(ActivityAccessStatus {
            authenticated: true,
            questionnaire_completed: completed,
            can_download: completed,
        });
    }

    #[allow(unreachable_code)]
    Ok(ActivityAccessStatus {
        authenticated: false,
        questionnaire_completed: false,
        can_download: false,
    })
}

/// Record completion of an external questionnaire callback.
///
/// Called after the HMAC callback has been verified server-side and the
/// completion recorded in the DB.  The WASM side calls this to keep the
/// reactive state in sync.
#[server(RecordActivityCompletion, "/api")]
pub async fn server_record_activity_completion(activity_id: String) -> Result<(), ServerFnError> {
    #[cfg(feature = "ssr")]
    {
        use axum::{http::header, Extension};
        use leptos_axum::extract;

        use crate::auth::otp_service::server::OtpServiceExtension;

        let Extension(OtpServiceExtension(svc)) = extract::<Extension<OtpServiceExtension>>()
            .await
            .map_err(|_| ServerFnError::new("OTP service not configured"))?;

        let headers: axum::http::HeaderMap = extract()
            .await
            .map_err(|_| ServerFnError::new("Could not extract headers"))?;

        let token = headers
            .get(header::COOKIE)
            .and_then(|v| v.to_str().ok())
            .and_then(|cookies| {
                cookies
                    .split(';')
                    .find_map(|c| c.trim().strip_prefix("session_token=").map(str::to_string))
            })
            .ok_or_else(|| ServerFnError::new("Not authenticated"))?;

        svc.record_activity_completion(&token, activity_id, "external".to_string())
            .await
            .map_err(ServerFnError::new)?;
    }
    Ok(())
}

/// Load the questionnaire definition for an internal questionnaire.
///
/// Reads `{SITE_CONTENT_PATH}/config/questionnaires/{config_name}.json` from
/// disk and deserialises it into a `QuestionnaireDefinition`.  The config name
/// is validated (alphanumeric + `-` + `_` only) to prevent path traversal.
#[server(GetQuestionnaireDefinition, "/api")]
pub async fn server_get_questionnaire_definition(
    config_name: String,
) -> Result<super::questionnaire::QuestionnaireDefinition, ServerFnError> {
    #[cfg(feature = "ssr")]
    {
        if config_name.is_empty()
            || !config_name
                .chars()
                .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
        {
            return Err(ServerFnError::new(
                "Invalid questionnaire config name: only alphanumeric, '-', '_' allowed",
            ));
        }

        let content_path =
            std::env::var("SITE_CONTENT_PATH").unwrap_or_else(|_| "site".to_string());

        let path = std::path::PathBuf::from(&content_path)
            .join("config")
            .join("questionnaires")
            .join(format!("{config_name}.json"));

        // Canonical-path check: prevent any `..` escape after resolution.
        let canonical_base = std::path::PathBuf::from(&content_path)
            .canonicalize()
            .map_err(|_| ServerFnError::new("Invalid SITE_CONTENT_PATH"))?;

        let canonical_path = path
            .canonicalize()
            .map_err(|_| ServerFnError::new("Questionnaire config not found"))?;

        if !canonical_path.starts_with(&canonical_base) {
            return Err(ServerFnError::new("Path traversal detected"));
        }

        let json = std::fs::read_to_string(&canonical_path)
            .map_err(|_| ServerFnError::new("Questionnaire config not found"))?;

        return serde_json::from_str(&json)
            .map_err(|e| ServerFnError::new(format!("Invalid questionnaire JSON: {e}")));
    }

    #[allow(unreachable_code)]
    Err(ServerFnError::new("SSR only"))
}

/// Submit an internal questionnaire.
///
/// Validates that responses is non-empty and that all keys are alphanumeric,
/// then delegates to the OTP service which records the completion + responses.
#[server(SubmitInternalQuestionnaire, "/api")]
pub async fn server_submit_internal_questionnaire(
    activity_id: String,
    responses: Vec<(String, String)>,
) -> Result<(), ServerFnError> {
    #[cfg(feature = "ssr")]
    {
        use axum::{http::header, Extension};
        use leptos_axum::extract;

        use crate::auth::otp_service::server::OtpServiceExtension;

        if responses.is_empty() {
            return Err(ServerFnError::new(
                "Questionnaire must have at least one response",
            ));
        }

        // Validate question keys to prevent injection.
        for (key, _) in &responses {
            if !key
                .chars()
                .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
            {
                return Err(ServerFnError::new(format!(
                    "Invalid question key '{key}': only alphanumeric, '_', '-' allowed"
                )));
            }
        }

        let Extension(OtpServiceExtension(svc)) = extract::<Extension<OtpServiceExtension>>()
            .await
            .map_err(|_| ServerFnError::new("OTP service not configured"))?;

        let headers: axum::http::HeaderMap = extract()
            .await
            .map_err(|_| ServerFnError::new("Could not extract headers"))?;

        let token = headers
            .get(header::COOKIE)
            .and_then(|v| v.to_str().ok())
            .and_then(|cookies| {
                cookies
                    .split(';')
                    .find_map(|c| c.trim().strip_prefix("session_token=").map(str::to_string))
            })
            .ok_or_else(|| ServerFnError::new("Not authenticated"))?;

        svc.submit_internal_questionnaire(&token, activity_id, responses)
            .await
            .map_err(ServerFnError::new)?;
    }
    Ok(())
}
