use crate::Api;
use futures::Future;
use hyper;
use hyper::header::HeaderName;
use hyper::{body::Payload, service::Service, Error, Request, Response, StatusCode};
use std::default::Default;
use std::io;
use std::marker::PhantomData;
use swagger::auth::{AuthData, Authorization, Bearer, Scopes};
use swagger::context::ContextualPayload;
use swagger::{EmptyContext, Has, Pop, Push, XSpanIdString};
use url::form_urlencoded;

pub struct MakeAddContext<T, A> {
    inner: T,
    marker: PhantomData<A>,
}

impl<T, A, B, C, D> MakeAddContext<T, A>
where
    A: Default + Push<XSpanIdString, Result = B>,
    B: Push<Option<AuthData>, Result = C>,
    C: Push<Option<Authorization>, Result = D>,
{
    pub fn new(inner: T) -> MakeAddContext<T, A> {
        MakeAddContext {
            inner,
            marker: PhantomData,
        }
    }
}

// Make a service that adds context.
impl<'a, T, SC, A, B, C, D, E, ME, S, OB, F> hyper::service::MakeService<&'a SC>
    for MakeAddContext<T, A>
where
    A: Default + Push<XSpanIdString, Result = B>,
    B: Push<Option<AuthData>, Result = C>,
    C: Push<Option<Authorization>, Result = D>,
    D: Send + 'static,
    T: hyper::service::MakeService<
        &'a SC,
        Error = E,
        MakeError = ME,
        Service = S,
        ReqBody = ContextualPayload<hyper::Body, D>,
        ResBody = OB,
        Future = F,
    >,
    S: Service<Error = E, ReqBody = ContextualPayload<hyper::Body, D>, ResBody = OB> + 'static,
    ME: swagger::ErrorBound,
    E: swagger::ErrorBound,
    F: Future<Item = S, Error = ME> + Send + 'static,
    S::Future: Send,
    OB: Payload,
{
    type ReqBody = hyper::Body;
    type ResBody = OB;
    type Error = E;
    type MakeError = ME;
    type Service = AddContext<S, A>;
    type Future = Box<dyn Future<Item = Self::Service, Error = ME> + Send + 'static>;

    fn make_service(&mut self, ctx: &'a SC) -> Self::Future {
        Box::new(self.inner.make_service(ctx).map(|s| AddContext::new(s)))
    }
}

/// Middleware to extract authentication data from request
pub struct AddContext<T, A> {
    inner: T,
    marker: PhantomData<A>,
}

impl<T, A, B, C, D> AddContext<T, A>
where
    A: Default + Push<XSpanIdString, Result = B>,
    B: Push<Option<AuthData>, Result = C>,
    C: Push<Option<Authorization>, Result = D>,
    T: Service,
{
    pub fn new(inner: T) -> AddContext<T, A> {
        AddContext {
            inner,
            marker: PhantomData,
        }
    }
}

impl<T, A, B, C, D> Service for AddContext<T, A>
where
    A: Default + Push<XSpanIdString, Result = B>,
    B: Push<Option<AuthData>, Result = C>,
    C: Push<Option<Authorization>, Result = D>,
    D: Send + 'static,
    T: Service<ReqBody = ContextualPayload<hyper::Body, D>>,
    T::Future: Future<Item = Response<T::ResBody>, Error = T::Error> + Send + 'static,
{
    type ReqBody = hyper::Body;
    type ResBody = T::ResBody;
    type Error = T::Error;
    type Future = Box<dyn Future<Item = Response<T::ResBody>, Error = T::Error> + Send + 'static>;

    fn call(&mut self, req: Request<Self::ReqBody>) -> Self::Future {
        let context = A::default().push(XSpanIdString::get_or_generate(&req));
        let (head, body) = req.into_parts();
        let headers = head.headers.clone();

        {
            use std::ops::Deref;
            use swagger::auth::Basic;
            if let Some(basic) = swagger::auth::from_headers::<Basic>(&headers) {
                let auth_data = AuthData::Basic(basic);
                let context = context.push(Some(auth_data));
                let context = context.push(None::<Authorization>);

                let body = ContextualPayload {
                    inner: body,
                    context: context,
                };

                return Box::new(self.inner.call(hyper::Request::from_parts(head, body)));
            }
        }

        let context = context.push(None::<AuthData>);
        let context = context.push(None::<Authorization>);
        let body = ContextualPayload {
            inner: body,
            context: context,
        };

        Box::new(self.inner.call(hyper::Request::from_parts(head, body)))
    }
}