Skip to content
use actix_web::dev::Payload;
use actix_web::http::header::AUTHORIZATION;
use actix_web::{Error, FromRequest, HttpRequest, HttpResponse, error::InternalError, web};
use std::collections::HashMap;
use std::future::{Ready, ready};

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

/// Extracts a PIN from an HTTP request.
/// Checks the following locations in order:
/// 1. `X-PIN` header
/// 2. `X-API-Key` header (fallback compatibility)
/// 3. `Authorization` header (`Bearer <pin>`, `Pin <pin>`, `ApiKey <pin>`, or raw `<pin>`)
/// 4. Query string `pin` or `PIN` (fallback: `api_key` or `apiKey`)
pub fn extract_pin(req: &HttpRequest) -> Option<String> {
    // 1. Try X-PIN header (case-insensitive in Actix HeaderMap)
    if let Some(val) = req.headers().get("X-PIN")
        && let Ok(s) = val.to_str()
    {
        let trimmed = s.trim();
        if !trimmed.is_empty() {
            return Some(trimmed.to_string());
        }
    }

    // 2. Try X-API-Key header (for backwards compatibility if client still sends it)
    if let Some(val) = req.headers().get("X-API-Key")
        && let Ok(s) = val.to_str()
    {
        let trimmed = s.trim();
        if !trimmed.is_empty() {
            return Some(trimmed.to_string());
        }
    }

    // 3. Try Authorization header: Bearer <pin>, Pin <pin>, ApiKey <pin>, or raw <pin>
    if let Some(val) = req.headers().get(AUTHORIZATION)
        && let Ok(s) = val.to_str()
    {
        let trimmed = s.trim();
        if let Some(token) = trimmed
            .strip_prefix("Bearer ")
            .or_else(|| trimmed.strip_prefix("bearer "))
        {
            let t = token.trim();
            if !t.is_empty() {
                return Some(t.to_string());
            }
        } else if let Some(token) = trimmed
            .strip_prefix("Pin ")
            .or_else(|| trimmed.strip_prefix("pin "))
        {
            let t = token.trim();
            if !t.is_empty() {
                return Some(t.to_string());
            }
        } else if let Some(token) = trimmed
            .strip_prefix("ApiKey ")
            .or_else(|| trimmed.strip_prefix("apikey "))
        {
            let t = token.trim();
            if !t.is_empty() {
                return Some(t.to_string());
            }
        } else if !trimmed.is_empty() && !trimmed.contains(' ') {
            return Some(trimmed.to_string());
        }
    }

    // 4. Try query string: ?pin=... or ?PIN=... (or fallback ?api_key=... / ?apiKey=...)
    if let Ok(query) = web::Query::<HashMap<String, String>>::from_query(req.query_string())
        && let Some(pin) = query
            .get("pin")
            .or_else(|| query.get("PIN"))
            .or_else(|| query.get("api_key"))
            .or_else(|| query.get("apiKey"))
    {
        let trimmed = pin.trim();
        if !trimmed.is_empty() {
            return Some(trimmed.to_string());
        }
    }

    None
}

/// Extractor that requires a valid PIN (or active authenticated session).
/// If valid, the handler continues.
/// If missing or invalid, immediately responds with HTTP 401 Unauthorized.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PinAuth;

impl FromRequest for PinAuth {
    type Error = Error;
    type Future = Ready<Result<Self, Self::Error>>;

    fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
        let is_valid_pin = if let Some(config) = req.app_data::<web::Data<Config>>() {
            if let Some(pin) = extract_pin(req) {
                config.is_valid_pin(&pin)
            } else {
                false
            }
        } else {
            false
        };

        let is_valid_session =
            if let Some(session_store) = req.app_data::<web::Data<SessionStore>>() {
                session_store.is_authenticated(req)
            } else {
                false
            };

        if is_valid_pin || is_valid_session {
            ready(Ok(PinAuth))
        } else {
            let resp = HttpResponse::Unauthorized()
                .content_type("application/json")
                .json(serde_json::json!({
                    "error": "Unauthorized",
                    "message": "Invalid or missing PIN"
                }));
            ready(Err(InternalError::from_response("", resp).into()))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::http::header::HeaderValue;
    use actix_web::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_extract_pin_from_header() {
        let req = aw_test::TestRequest::default()
            .insert_header(("X-PIN", "123456"))
            .to_http_request();
        assert_eq!(extract_pin(&req), Some("123456".to_string()));
    }

    #[test]
    fn test_extract_pin_from_legacy_header() {
        let req = aw_test::TestRequest::default()
            .insert_header(("X-API-Key", "123456"))
            .to_http_request();
        assert_eq!(extract_pin(&req), Some("123456".to_string()));
    }

    #[test]
    fn test_extract_pin_from_authorization_bearer() {
        let req = aw_test::TestRequest::default()
            .insert_header(("Authorization", "Bearer 123456"))
            .to_http_request();
        assert_eq!(extract_pin(&req), Some("123456".to_string()));
    }

    #[test]
    fn test_extract_pin_from_authorization_pin() {
        let req = aw_test::TestRequest::default()
            .insert_header(("Authorization", "Pin 123456"))
            .to_http_request();
        assert_eq!(extract_pin(&req), Some("123456".to_string()));
    }

    #[test]
    fn test_extract_pin_from_authorization_apikey() {
        let req = aw_test::TestRequest::default()
            .insert_header(("Authorization", "ApiKey 123456"))
            .to_http_request();
        assert_eq!(extract_pin(&req), Some("123456".to_string()));
    }

    #[test]
    fn test_extract_pin_from_authorization_raw() {
        let req = aw_test::TestRequest::default()
            .insert_header(("Authorization", "123456"))
            .to_http_request();
        assert_eq!(extract_pin(&req), Some("123456".to_string()));
    }

    #[test]
    fn test_extract_pin_from_query() {
        let req = aw_test::TestRequest::default()
            .uri("/some/path?pin=123456")
            .to_http_request();
        assert_eq!(extract_pin(&req), Some("123456".to_string()));

        let req2 = aw_test::TestRequest::default()
            .uri("/some/path?PIN=123456")
            .to_http_request();
        assert_eq!(extract_pin(&req2), Some("123456".to_string()));

        let req3 = aw_test::TestRequest::default()
            .uri("/some/path?api_key=123456")
            .to_http_request();
        assert_eq!(extract_pin(&req3), Some("123456".to_string()));
    }

    #[test]
    fn test_extract_pin_missing() {
        let req = aw_test::TestRequest::default().to_http_request();
        assert_eq!(extract_pin(&req), None);
    }

    #[actix_web::test]
    async fn test_pin_auth_extractor_valid() {
        let config = web::Data::new(test_config());
        let req = aw_test::TestRequest::default()
            .app_data(config)
            .insert_header(("X-PIN", "123456"))
            .to_http_request();

        let mut payload = Payload::None;
        let res = PinAuth::from_request(&req, &mut payload).await;
        assert!(res.is_ok());
        assert_eq!(res.unwrap(), PinAuth);
    }

    #[actix_web::test]
    async fn test_pin_auth_extractor_invalid() {
        let config = web::Data::new(test_config());
        let req = aw_test::TestRequest::default()
            .app_data(config)
            .insert_header(("X-PIN", "9999"))
            .to_http_request();

        let mut payload = Payload::None;
        let res = PinAuth::from_request(&req, &mut payload).await;
        assert!(res.is_err());
    }

    #[actix_web::test]
    async fn test_pin_auth_extractor_missing() {
        let config = web::Data::new(test_config());
        let req = aw_test::TestRequest::default()
            .app_data(config)
            .to_http_request();

        let mut payload = Payload::None;
        let res = PinAuth::from_request(&req, &mut payload).await;
        assert!(res.is_err());
    }

    #[actix_web::test]
    async fn test_pin_auth_extractor_session_valid() {
        let config = web::Data::new(test_config());
        let session_store = web::Data::new(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 req = aw_test::TestRequest::default()
            .app_data(config)
            .app_data(session_store)
            .cookie(cookie)
            .to_http_request();

        let mut payload = Payload::None;
        let res = PinAuth::from_request(&req, &mut payload).await;
        assert!(res.is_ok());
        assert_eq!(res.unwrap(), PinAuth);
    }
}