use actix_identity::Identity;
use std::future::{Ready, ready};
use std::rc::Rc;

use crate::app_config::AppConfig;
use crate::constants;
use actix_web::body::EitherBody;
use actix_web::dev::Payload;
use actix_web::{
    Error, FromRequest, HttpResponse,
    dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready},
};
use futures_util::future::LocalBoxFuture;

// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
//    next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
#[derive(Default)]
pub struct AuthChecker;

// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for AuthChecker
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Transform = AuthMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(AuthMiddleware {
            service: Rc::new(service),
        }))
    }
}

pub struct AuthMiddleware<S> {
    service: Rc<S>,
}

impl<S, B> Service<ServiceRequest> for AuthMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let service = Rc::clone(&self.service);

        Box::pin(async move {
            // Check if no authentication is required
            if constants::ROUTES_WITHOUT_AUTH.contains(&req.path())
                || !req.path().starts_with("/web_api/")
            {
                log::trace!("No authentication is required")
            }
            // Dev only, check for auto login
            else if AppConfig::get().unsecure_disable_login {
                log::trace!("Authentication is disabled")
            }
            // Check cookie authentication
            else {
                let identity: Option<Identity> =
                    Identity::from_request(req.request(), &mut Payload::None)
                        .into_inner()
                        .ok();

                if identity.is_none() {
                    log::error!(
                        "Missing identity information in request, user is not authenticated!"
                    );
                    return Ok(req
                        .into_response(HttpResponse::PreconditionFailed().finish())
                        .map_into_right_body());
                };
            }

            service
                .call(req)
                .await
                .map(ServiceResponse::map_into_left_body)
        })
    }
}