use axum::{
    body::Body,
    http::StatusCode,
    response::{IntoResponse, Response},
};

use super::file_config::PolicyEffect;

/// Request context used for template interpolation in effect messages.
pub struct EffectContext<'a> {
    pub path: &'a str,
    pub method: &'a str,
    pub user_id: Option<&'a str>,
    pub user_email: Option<&'a str>,
}

/// Execute a single policy effect.
///
/// Returns `Some(Response)` for **terminant** effects that short-circuit the
/// request chain (Redirect, Response). Returns `None` for **side-effect-only**
/// effects that allow processing to continue (Log, AuditLog, Notify).
pub async fn execute_effect(effect: &PolicyEffect, ctx: &EffectContext<'_>) -> Option<Response> {
    match effect {
        PolicyEffect::Redirect { to } => {
            let location = interpolate(to, ctx);
            let response = Response::builder()
                .status(StatusCode::FOUND)
                .header("Location", location)
                .body(Body::empty())
                .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response());
            Some(response)
        }

        PolicyEffect::Log { level, message } => {
            let msg = message
                .as_deref()
                .map(|m| interpolate(m, ctx))
                .unwrap_or_else(|| format!("RBAC: {} {}", ctx.method, ctx.path));
            match level.as_str() {
                "debug" => tracing::debug!("{}", msg),
                "warn" => tracing::warn!("{}", msg),
                "error" => tracing::error!("{}", msg),
                _ => tracing::info!("{}", msg),
            }
            None
        }

        PolicyEffect::AuditLog { level, event } => {
            let event_name = event.as_deref().unwrap_or("rbac_event");
            let user = ctx.user_id.unwrap_or("anonymous");
            let msg = format!(
                "RBAC audit: event={} path={} method={} user={}",
                event_name, ctx.path, ctx.method, user
            );
            match level.as_str() {
                "debug" => {
                    tracing::debug!(audit = true, event = event_name, user = user, "{}", msg)
                }
                "warn" => {
                    tracing::warn!(audit = true, event = event_name, user = user, "{}", msg)
                }
                "error" => {
                    tracing::error!(audit = true, event = event_name, user = user, "{}", msg)
                }
                _ => tracing::info!(audit = true, event = event_name, user = user, "{}", msg),
            }
            None
        }

        PolicyEffect::Notify { channel, message } => {
            let msg = message
                .as_deref()
                .map(|m| interpolate(m, ctx))
                .unwrap_or_else(|| ctx.path.to_string());
            let channel = channel.clone();
            // Non-blocking: fire-and-forget notification.
            // Replace with actual channel dispatch when integrations are added.
            tokio::spawn(async move {
                tracing::info!(channel = %channel, "RBAC notify: {}", msg);
            });
            None
        }

        PolicyEffect::Response { status, body } => {
            let status_code = StatusCode::from_u16(*status).unwrap_or(StatusCode::FORBIDDEN);
            let response = if let Some(body) = body {
                (status_code, axum::Json(body.clone())).into_response()
            } else {
                status_code.into_response()
            };
            Some(response)
        }
    }
}

/// Replace `{request.path}`, `{request.method}`, `{user.id}`, `{user.email}` in
/// `template`.
fn interpolate(template: &str, ctx: &EffectContext<'_>) -> String {
    template
        .replace("{request.path}", ctx.path)
        .replace("{request.method}", ctx.method)
        .replace("{user.id}", ctx.user_id.unwrap_or(""))
        .replace("{user.email}", ctx.user_email.unwrap_or(""))
}

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

    fn ctx<'a>(path: &'a str) -> EffectContext<'a> {
        EffectContext {
            path,
            method: "GET",
            user_id: Some("u1"),
            user_email: Some("u@test.com"),
        }
    }

    #[test]
    fn interpolate_replaces_all_tokens() {
        let result = interpolate(
            "{user.email} accessed {request.path} via {request.method}",
            &ctx("/admin"),
        );
        assert_eq!(result, "u@test.com accessed /admin via GET");
    }

    #[test]
    fn interpolate_handles_missing_user() {
        let ctx = EffectContext {
            path: "/test",
            method: "POST",
            user_id: None,
            user_email: None,
        };
        let result = interpolate("{user.id}:{user.email}", &ctx);
        assert_eq!(result, ":");
    }

    #[tokio::test]
    async fn log_effect_returns_none() {
        let effect = PolicyEffect::Log {
            level: "info".to_string(),
            message: Some("test".to_string()),
        };
        let result = execute_effect(&effect, &ctx("/")).await;
        assert!(result.is_none());
    }

    #[tokio::test]
    async fn audit_log_effect_returns_none() {
        let effect = PolicyEffect::AuditLog {
            level: "warn".to_string(),
            event: Some("access_denied".to_string()),
        };
        let result = execute_effect(&effect, &ctx("/protected")).await;
        assert!(result.is_none());
    }

    #[tokio::test]
    async fn redirect_effect_returns_response() {
        let effect = PolicyEffect::Redirect {
            to: "/login?return={request.path}".to_string(),
        };
        let result = execute_effect(&effect, &ctx("/admin")).await;
        assert!(result.is_some());
        let response = result.unwrap();
        assert_eq!(response.status(), StatusCode::FOUND);
        let location = response
            .headers()
            .get("Location")
            .unwrap()
            .to_str()
            .unwrap();
        assert_eq!(location, "/login?return=/admin");
    }

    #[tokio::test]
    async fn response_effect_returns_403() {
        let effect = PolicyEffect::Response {
            status: 403,
            body: Some(serde_json::json!({"error": "Forbidden"})),
        };
        let result = execute_effect(&effect, &ctx("/secret")).await;
        assert!(result.is_some());
        assert_eq!(result.unwrap().status(), StatusCode::FORBIDDEN);
    }
}
