// Copyright (c) 2020  Teddy Wing
//
// This file is part of FastCGI-Conduit.
//
// FastCGI-Conduit is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// FastCGI-Conduit is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with FastCGI-Conduit. If not, see .
use std::io;
use std::io::Read;
use std::net::SocketAddr;
use inflector::cases::traincase::to_train_case;
use snafu::{ResultExt, Snafu};
/// Errors parsing a FastCGI request.
#[derive(Debug, Snafu)]
pub enum Error {
    /// The HTTP method is invalid.
    #[snafu(context(false))]
    InvalidMethod { source: http::method::InvalidMethod },
    /// An invalid HTTP header name.
    #[snafu(context(false))]
    InvalidHeaderName { source: conduit::header::InvalidHeaderName },
    /// An invalid HTTP header value.
    #[snafu(context(false))]
    InvalidHeaderValue { source: conduit::header::InvalidHeaderValue },
    /// An invalid remote address.
    #[snafu(context(false))]
    InvalidRemoteAddr { source: RemoteAddrError },
}
/// A convenience `Result` that contains a request `Error`.
pub type RequestResult = std::result::Result;
/// Errors parsing an HTTP remote address.
#[derive(Debug, Snafu)]
pub enum RemoteAddrError {
    /// Error parsing the address part.
    #[snafu(display("Could not parse address {}: {}", address, source))]
    AddrParseError {
        address: String,
        source: std::net::AddrParseError,
    },
    /// Error parsing the port part.
    #[snafu(display("Could not parse port {}: {}", port, source))]
    PortParseError {
        port: String,
        source: std::num::ParseIntError
    },
}
/// Wraps a [`fastcgi::Request`][fastcgi::Request] to implement
/// [`conduit::RequestExt`][conduit::RequestExt].
///
/// [fastcgi::Request]: ../../fastcgi/struct.Request.html
/// [conduit::RequestExt]: ../../conduit/trait.RequestExt.html
pub struct FastCgiRequest<'a> {
    request: &'a mut fastcgi::Request,
    http_version: conduit::Version,
    host: String,
    method: conduit::Method,
    headers: conduit::HeaderMap,
    path: String,
    query: Option,
    remote_addr: SocketAddr,
    content_length: Option,
    extensions: conduit::Extensions,
}
impl<'a> FastCgiRequest<'a> {
    /// Create a new `FastCgiRequest`.
    pub fn new(request: &'a mut fastcgi::Request) -> RequestResult {
        let version = Self::version(request);
        let host = Self::host(request);
        let method = Self::method(request)?;
        let headers = Self::headers(request.params())?;
        let path = Self::path(request);
        let query = Self::query(request);
        let remote_addr = Self::remote_addr(request)?;
        let content_length = Self::content_length(request);
        Ok(Self {
            request: request,
            http_version: version,
            host: host,
            method: method,
            headers: headers,
            path: path,
            query: query,
            remote_addr: remote_addr,
            content_length: content_length,
            extensions: conduit::TypeMap::new(),
        })
    }
    /// Extract the HTTP version.
    fn version(request: &fastcgi::Request) -> conduit::Version {
        match request.param("SERVER_PROTOCOL").unwrap_or_default().as_str() {
            "HTTP/0.9" => conduit::Version::HTTP_09,
            "HTTP/1.0" => conduit::Version::HTTP_10,
            "HTTP/1.1" => conduit::Version::HTTP_11,
            "HTTP/2.0" => conduit::Version::HTTP_2,
            "HTTP/3.0" => conduit::Version::HTTP_3,
            _ => conduit::Version::default(),
        }
    }
    /// Get the request scheme (HTTP or HTTPS).
    fn scheme(&self) -> conduit::Scheme {
        let scheme = self.request.param("REQUEST_SCHEME").unwrap_or_default();
        if scheme == "https" {
            conduit::Scheme::Https
        } else {
            conduit::Scheme::Http
        }
    }
    /// Get the HTTP host.
    ///
    /// This looks like `localhost:8000`.
    fn host(request: &fastcgi::Request) -> String {
        request.param("HTTP_HOST").unwrap_or_default()
    }
    /// Get the HTTP method (GET, HEAD, POST, etc.).
    fn method(
        request: &fastcgi::Request
    ) -> Result {
        conduit::Method::from_bytes(
            request.param("REQUEST_METHOD")
                .unwrap_or_default()
                .as_bytes()
        )
    }
    /// Build a map of request headers.
    fn headers(params: fastcgi::Params) -> RequestResult {
        let mut map = conduit::HeaderMap::new();
        let headers = Self::headers_from_params(params);
        for (name, value) in headers
            .iter()
            .map(|(name, value)| (name.as_bytes(), value.as_bytes()))
        {
            map.append(
                conduit::header::HeaderName::from_bytes(name)?,
                conduit::header::HeaderValue::from_bytes(value)?,
            );
        }
        Ok(map)
    }
    /// Extract headers from request params. Transform these into pairs of
    /// canonical header names and values.
    fn headers_from_params(params: fastcgi::Params) -> Vec<(String, String)> {
        return params
            .filter(|(key, _)| key.starts_with("HTTP_"))
            .map(|(key, value)| {
                let key = key.get(5..).unwrap_or_default();
                let key = &key.replace("_", "-");
                let key = &to_train_case(&key);
                (key.to_owned(), value)
            })
            .collect()
    }
    /// Get the URI path.
    ///
    /// Returns `/path` when the URI is `http://localhost:8000/path?s=query`.
    /// When the path is empty, returns `/`.
    fn path(request: &fastcgi::Request) -> String {
        match request.param("SCRIPT_NAME") {
            Some(p) => p,
            None => "/".to_owned(),
        }
    }
    /// Get the URI query string.
    ///
    /// Returns `s=query&lang=en` when the URI is
    /// `http://localhost:8000/path?s=query&lang=en`.
    fn query(request: &fastcgi::Request) -> Option {
        request.param("QUERY_STRING")
    }
    /// Get the remote address of the request.
    fn remote_addr(request: &fastcgi::Request) -> Result {
        let addr = request.param("REMOTE_ADDR").unwrap_or_default();
        let port = request.param("REMOTE_PORT").unwrap_or_default();
        Ok(
            SocketAddr::new(
                addr.parse().context(AddrParseError { address: addr })?,
                port.parse().context(PortParseError { port })?,
            )
        )
    }
    /// Get the request's content length.
    fn content_length(request: &fastcgi::Request) -> Option {
        request.param("CONTENT_LENGTH").and_then(|l| l.parse().ok())
    }
}
impl<'a> Read for FastCgiRequest<'a> {
    /// Read from the underlying FastCGI request's [`Stdin`][Stdin]
    ///
    /// [Stdin]: ../../fastcgi/struct.Stdin.html
    fn read(&mut self, buf: &mut [u8]) -> io::Result {
        self.request.stdin().read(buf)
    }
}
impl<'a> conduit::RequestExt for FastCgiRequest<'a> {
   fn http_version(&self) -> conduit::Version {
       self.http_version
   }
   fn method(&self) -> &conduit::Method {
       &self.method
   }
   fn scheme(&self) -> conduit::Scheme {
       self.scheme()
   }
   fn host(&self) -> conduit::Host<'_> {
       conduit::Host::Name(&self.host)
   }
   fn virtual_root(&self) -> std::option::Option<&str> {
       None
   }
   fn path(&self) -> &str {
       &self.path
   }
   fn query_string(&self) -> std::option::Option<&str> {
       self.query.as_ref()
           .map(|p| p.as_str())
   }
   fn remote_addr(&self) -> std::net::SocketAddr {
       self.remote_addr
   }
   fn content_length(&self) -> std::option::Option {
       self.content_length
   }
   fn headers(&self) -> &conduit::HeaderMap {
       &self.headers
   }
   fn body(&mut self) -> &mut (dyn std::io::Read) {
       self
   }
   fn extensions(&self) -> &conduit::Extensions {
       &self.extensions
   }
   fn mut_extensions(&mut self) -> &mut conduit::Extensions {
       &mut self.extensions
   }
}