use actix_web::{
    dev::{
        always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
        ServiceRequest, ServiceResponse,
    },
    error::Error,
    guard::{Guard, GuardContext},
    http::{
        header::{self, ContentType},
        Method, StatusCode,
    },
    HttpMessage, HttpRequest, HttpResponse, ResponseError,
};
use derive_more::{Deref, Display, Error};
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
use static_files::Resource;
use std::{collections::HashMap, ops::Deref, rc::Rc, sync::Arc};

pub type DefaultResourceFiles = HashMap<&'static str, Resource>;

/// Resource file with static data and metadata.
pub trait ResourceFile {
    fn data(&self) -> &'static [u8];
    fn modified(&self) -> u64;
    fn mime_type(&self) -> &str;
}

/// Basic abstraction for a dictionary of resources.
pub trait ResourceFilesCollection {
    type Resource: ResourceFile;
    /// Get a resource by path
    fn get_resource(&self, path: &str) -> Option<&Self::Resource>;
    /// Check if a resource exists by path
    fn contains_key(&self, path: &str) -> bool;
}

impl<R> ResourceFilesCollection for Rc<R>
where
    R: ResourceFilesCollection,
{
    type Resource = R::Resource;
    fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
        let r: &R = self;
        r.get_resource(path)
    }

    fn contains_key(&self, path: &str) -> bool {
        let r: &R = self;
        r.contains_key(path)
    }
}

impl<R> ResourceFilesCollection for Arc<R>
where
    R: ResourceFilesCollection,
{
    type Resource = R::Resource;
    fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
        let r: &R = self;
        r.get_resource(path)
    }

    fn contains_key(&self, path: &str) -> bool {
        let r: &R = self;
        r.contains_key(path)
    }
}

mod legacy_static_files {
    use super::*;

    impl ResourceFile for Resource {
        fn data(&self) -> &'static [u8] {
            self.data
        }

        fn modified(&self) -> u64 {
            self.modified
        }

        fn mime_type(&self) -> &str {
            self.mime_type
        }
    }

    impl ResourceFilesCollection for DefaultResourceFiles {
        type Resource = Resource;
        fn get_resource(&self, path: &str) -> Option<&Self::Resource> {
            self.get(path)
        }

        fn contains_key(&self, path: &str) -> bool {
            self.contains_key(path)
        }
    }
}

/// Static resource files handling
///
/// `ResourceFiles` service must be registered with `App::service` method.
///
/// ```rust
/// use std::collections::HashMap;
///
/// use actix_web::App;
///
/// fn main() {
///     // serve root directory with default options:
///     // - resolve index.html
///     let files: HashMap<&'static str, static_files::Resource> = HashMap::new();
///     let app = App::new()
///         .service(actix_web_static_files::ResourceFiles::new("/", files));
///     // or subpath with additional option to not resolve index.html
///     let files: HashMap<&'static str, static_files::Resource> = HashMap::new();
///     let app = App::new()
///         .service(actix_web_static_files::ResourceFiles::new("/imgs", files)
///             .do_not_resolve_defaults());
/// }
/// ```
#[allow(clippy::needless_doctest_main)]
pub struct ResourceFiles<C = DefaultResourceFiles> {
    not_resolve_defaults: bool,
    use_guard: bool,
    not_found_resolves_to: Option<String>,
    inner: Rc<ResourceFilesInner<C>>,
}

pub struct ResourceFilesInner<C> {
    path: String,
    files: C,
}

const INDEX_HTML: &str = "index.html";

impl<F> ResourceFiles<F>
where
    F: ResourceFilesCollection + 'static,
{
    #[must_use]
    pub fn new(path: &str, files: F) -> Self {
        let inner = ResourceFilesInner {
            path: path.into(),
            files,
        };
        Self {
            inner: Rc::new(inner),
            not_resolve_defaults: false,
            not_found_resolves_to: None,
            use_guard: false,
        }
    }

    /// By default trying to resolve '.../' to '.../index.html' if it exists.
    /// Turn off this resolution by calling this function.
    #[must_use]
    pub fn do_not_resolve_defaults(mut self) -> Self {
        self.not_resolve_defaults = true;
        self
    }

    /// Resolves not found references to this path.
    ///
    /// This can be useful for angular-like applications.
    #[must_use]
    pub fn resolve_not_found_to<S: ToString>(mut self, path: S) -> Self {
        self.not_found_resolves_to = Some(path.to_string());
        self
    }

    /// Resolves not found references to root path.
    ///
    /// This can be useful for angular-like applications.
    #[must_use]
    pub fn resolve_not_found_to_root(self) -> Self {
        self.resolve_not_found_to(INDEX_HTML)
    }

    /// If this is called, we will use a [`Guard`] to check if this request should be handled.
    /// If set to true, we skip using the handler for files that haven't been found, instead of sending 404s.
    /// Would be ignored, if `resolve_not_found_to` or `resolve_not_found_to_root` is used.
    ///
    /// Can be useful if you want to share files on a (sub)path that's also used by a different route handler.
    #[must_use]
    pub fn skip_handler_when_not_found(mut self) -> Self {
        self.use_guard = true;
        self
    }

    fn select_guard(&self) -> Box<dyn Guard> {
        if self.not_resolve_defaults {
            Box::new(NotResolveDefaultsGuard::from(self))
        } else {
            Box::new(ResolveDefaultsGuard::from(self))
        }
    }
}

impl<C> Deref for ResourceFiles<C> {
    type Target = ResourceFilesInner<C>;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

struct NotResolveDefaultsGuard<C> {
    inner: Rc<ResourceFilesInner<C>>,
}

impl<C> Guard for NotResolveDefaultsGuard<C>
where
    C: ResourceFilesCollection,
{
    fn check(&self, ctx: &GuardContext<'_>) -> bool {
        self.inner
            .files
            .contains_key(ctx.head().uri.path().trim_start_matches('/'))
    }
}

impl<C> From<&ResourceFiles<C>> for NotResolveDefaultsGuard<C> {
    fn from(files: &ResourceFiles<C>) -> Self {
        Self {
            inner: files.inner.clone(),
        }
    }
}

struct ResolveDefaultsGuard<C> {
    inner: Rc<ResourceFilesInner<C>>,
}

impl<C> Guard for ResolveDefaultsGuard<C>
where
    C: ResourceFilesCollection,
{
    fn check(&self, ctx: &GuardContext<'_>) -> bool {
        let path = ctx.head().uri.path().trim_start_matches('/');
        self.inner.files.contains_key(path)
            || ((path.is_empty() || path.ends_with('/'))
                && self
                    .inner
                    .files
                    .contains_key((path.to_string() + INDEX_HTML).as_str()))
    }
}

impl<C> From<&ResourceFiles<C>> for ResolveDefaultsGuard<C> {
    fn from(files: &ResourceFiles<C>) -> Self {
        Self {
            inner: files.inner.clone(),
        }
    }
}

impl<C> HttpServiceFactory for ResourceFiles<C>
where
    C: ResourceFilesCollection + 'static,
{
    fn register(self, config: &mut AppService) {
        let prefix = self.path.trim_start_matches('/');
        let rdef = if config.is_root() {
            ResourceDef::root_prefix(prefix)
        } else {
            ResourceDef::prefix(prefix)
        };
        let guards = if self.use_guard && self.not_found_resolves_to.is_none() {
            Some(vec![self.select_guard()])
        } else {
            None
        };
        config.register_service(rdef, guards, self, None);
    }
}

impl<C> ServiceFactory<ServiceRequest> for ResourceFiles<C>
where
    C: ResourceFilesCollection + 'static,
{
    type Response = ServiceResponse;
    type Error = Error;
    type Config = ();
    type Service = ResourceFilesService<C>;
    type InitError = ();
    type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;

    fn new_service(&self, _: ()) -> Self::Future {
        ok(ResourceFilesService {
            resolve_defaults: !self.not_resolve_defaults,
            not_found_resolves_to: self.not_found_resolves_to.clone(),
            inner: self.inner.clone(),
        })
        .boxed_local()
    }
}

#[derive(Deref)]
pub struct ResourceFilesService<C> {
    resolve_defaults: bool,
    not_found_resolves_to: Option<String>,
    #[deref]
    inner: Rc<ResourceFilesInner<C>>,
}

impl<C> Service<ServiceRequest> for ResourceFilesService<C>
where
    C: ResourceFilesCollection,
{
    type Response = ServiceResponse;
    type Error = Error;
    type Future = Ready<Result<Self::Response, Self::Error>>;

    always_ready!();

    fn call(&self, req: ServiceRequest) -> Self::Future {
        match *req.method() {
            Method::HEAD | Method::GET => (),
            _ => {
                return ok(ServiceResponse::new(
                    req.into_parts().0,
                    HttpResponse::MethodNotAllowed()
                        .insert_header(ContentType::plaintext())
                        .insert_header((header::ALLOW, "GET, HEAD"))
                        .body("This resource only supports GET and HEAD."),
                ));
            }
        }

        let req_path = req.match_info().unprocessed();
        let mut item = self.files.get_resource(req_path);

        if item.is_none()
            && self.resolve_defaults
            && (req_path.is_empty() || req_path.ends_with('/'))
        {
            let index_req_path = req_path.to_string() + INDEX_HTML;
            item = self
                .files
                .get_resource(index_req_path.trim_start_matches('/'));
        }

        let (req, response) = if item.is_some() {
            let (req, _) = req.into_parts();
            let response = respond_to(&req, item);
            (req, response)
        } else {
            let real_path = match get_pathbuf(req_path) {
                Ok(item) => item,
                Err(e) => return ok(req.error_response(e)),
            };

            let (req, _) = req.into_parts();

            let mut item = self.files.get_resource(real_path.as_str());

            if item.is_none() && self.not_found_resolves_to.is_some() {
                let not_found_path = self.not_found_resolves_to.as_ref().unwrap();
                item = self.files.get_resource(not_found_path.as_str());
            }

            let response = respond_to(&req, item);
            (req, response)
        };

        ok(ServiceResponse::new(req, response))
    }
}

fn respond_to<Resource: ResourceFile>(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse {
    if let Some(file) = item {
        let etag = Some(header::EntityTag::new_strong(format!(
            "{:x}:{:x}",
            file.data().len(),
            file.modified()
        )));

        let precondition_failed = !any_match(etag.as_ref(), req);

        let not_modified = !none_match(etag.as_ref(), req);

        let mut resp = HttpResponse::build(StatusCode::OK);
        resp.insert_header((header::CONTENT_TYPE, file.mime_type()));

        if let Some(etag) = etag {
            resp.insert_header(header::ETag(etag));
        }

        if precondition_failed {
            return resp.status(StatusCode::PRECONDITION_FAILED).finish();
        } else if not_modified {
            return resp.status(StatusCode::NOT_MODIFIED).finish();
        }

        resp.body(file.data())
    } else {
        HttpResponse::NotFound().body("Not found")
    }
}

/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
    match req.get_header::<header::IfMatch>() {
        None | Some(header::IfMatch::Any) => true,
        Some(header::IfMatch::Items(ref items)) => {
            if let Some(some_etag) = etag {
                for item in items {
                    if item.strong_eq(some_etag) {
                        return true;
                    }
                }
            }
            false
        }
    }
}

/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
    match req.get_header::<header::IfNoneMatch>() {
        Some(header::IfNoneMatch::Any) => false,
        Some(header::IfNoneMatch::Items(ref items)) => {
            if let Some(some_etag) = etag {
                for item in items {
                    if item.weak_eq(some_etag) {
                        return false;
                    }
                }
            }
            true
        }
        None => true,
    }
}

#[derive(Debug, PartialEq, Display, Error)]
pub enum UriSegmentError {
    /// The segment started with the wrapped invalid character.
    #[display(fmt = "The segment started with the wrapped invalid character")]
    BadStart(#[error(not(source))] char),

    /// The segment contained the wrapped invalid character.
    #[display(fmt = "The segment contained the wrapped invalid character")]
    BadChar(#[error(not(source))] char),

    /// The segment ended with the wrapped invalid character.
    #[display(fmt = "The segment ended with the wrapped invalid character")]
    BadEnd(#[error(not(source))] char),
}

/// Return `BadRequest` for `UriSegmentError`
impl ResponseError for UriSegmentError {
    fn error_response(&self) -> HttpResponse {
        HttpResponse::new(StatusCode::BAD_REQUEST)
    }
}

fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
    let mut buf = Vec::new();
    for segment in path.split('/') {
        if segment == ".." {
            buf.pop();
        } else if segment.starts_with('.') {
            return Err(UriSegmentError::BadStart('.'));
        } else if segment.starts_with('*') {
            return Err(UriSegmentError::BadStart('*'));
        } else if segment.ends_with(':') {
            return Err(UriSegmentError::BadEnd(':'));
        } else if segment.ends_with('>') {
            return Err(UriSegmentError::BadEnd('>'));
        } else if segment.ends_with('<') {
            return Err(UriSegmentError::BadEnd('<'));
        } else if segment.is_empty() {
            continue;
        } else if cfg!(windows) && segment.contains('\\') {
            return Err(UriSegmentError::BadChar('\\'));
        } else {
            buf.push(segment);
        }
    }

    Ok(buf.join("/"))
}

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

    fn assert_send_and_sync<T: Send + Sync + 'static>() {}

    #[test]
    fn test_error_impl() {
        // ensure backwards compatibility when migrating away from failure
        assert_send_and_sync::<UriSegmentError>();
    }
}
