use std;
use std::string::String;

use reqwest::{Client, Response, StatusCode, Url};
use serde;
use uuid;

use RookeriesDomain;

pub static ENV_COUCHDB_URI: &'static str = "ROOKERIES_COUCHDB";

#[derive(Debug)]
pub struct Error {
    status: u16,
    json_message: String,
    original_cause: Option<Box<std::error::Error>>,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "CouchDB error because server returned a {} because {}",
            &self.status, &self.json_message
        )
    }
}

impl std::error::Error for Error {
    fn cause(&self) -> Option<&std::error::Error> {
        match &self.original_cause {
            Some(cause) => Some(cause.as_ref()),
            None => None,
        }
    }
}

impl Error {
    fn from_status(response: &mut Response) -> Error {
        Error {
            status: response.status().as_u16(),
            json_message: response.text().unwrap(),
            original_cause: None,
        }
    }

    fn from_message(message: &str) -> Error {
        Error {
            status: 500,
            json_message: message.to_string(),
            original_cause: None,
        }
    }

    fn from_error(cause: Box<std::error::Error>) -> Error {
        Error {
            status: 500,
            json_message: format!("{}", cause),
            original_cause: Some(cause),
        }
    }
}

#[derive(Serialize)]
pub struct Query<T: serde::ser::Serialize> {
    pub selector: T,
}

#[derive(Deserialize)]
struct ResultSet<T> {
    docs: Vec<T>,
}

#[derive(Deserialize)]
pub struct DocumentStatus {
    pub id: String,
    #[serde(rename = "rev")]
    pub revision: String,
}

#[derive(Debug)]
pub struct Server {
    uri: Url,
}

impl Server {
    pub fn configure_from_env() -> Result<Server, Error> {
        let couchdb_uri = std::env::var(ENV_COUCHDB_URI);
        if couchdb_uri.is_err() {
            return Err(Error::from_error(couchdb_uri.unwrap_err().into()));
        }

        let couchdb_uri = Url::parse(&couchdb_uri.unwrap());
        if couchdb_uri.is_err() {
            return Err(Error::from_error(couchdb_uri.unwrap_err().into()));
        }

        let couchdb_uri = couchdb_uri.unwrap();
        if couchdb_uri.username() == "" {
            return Err(Error::from_message("No username provided."));
        }

        if couchdb_uri.password().is_none() {
            return Err(Error::from_message("No password provided."));
        }

        if couchdb_uri.path() == "/" {
            return Err(Error::from_message("No database provided."));
        }

        if !couchdb_uri.path().ends_with("/") {
            return Err(Error::from_message("Database path must end with a slash."));
        }

        Ok(Server { uri: couchdb_uri })
    }

    pub fn find<M, T>(&self, query: Query<M>) -> Result<Vec<T>, Box<Error>>
    where
        M: serde::ser::Serialize,
        T: serde::de::DeserializeOwned,
    {
        let client = Client::new();
        let couchdb_uri = self.uri.join("_find/").unwrap();

        let post_request = client
            .post(couchdb_uri)
            .basic_auth(self.uri.username(), self.uri.password())
            .json(&query)
            .send();

        if post_request.is_err() {
            return Err(Error::from_error(post_request.unwrap_err().into()).into());
        }

        let mut response = post_request.unwrap();
        if response.status() != StatusCode::Ok {
            return Err(Error::from_status(&mut response).into());
        };

        let results: ResultSet<T> = response.json().unwrap();
        Ok(results.docs)
    }

    pub fn update<T>(&self, document: T) -> Result<DocumentStatus, Box<Error>>
    where
        T: serde::ser::Serialize + serde::de::DeserializeOwned + RookeriesDomain,
    {
        let doc_id: String = match document.id() {
            None => uuid::Uuid::new_v4().simple().to_string(),
            Some(id) => id.clone(),
        };

        let client = Client::new();
        let couchdb_uri = self.uri.join(&doc_id).unwrap();

        let post_request = client
            .put(couchdb_uri)
            .basic_auth(self.uri.username(), self.uri.password())
            .json(&document)
            .send();

        if post_request.is_err() {
            return Err(Error::from_error(post_request.unwrap_err().into()).into());
        }

        let mut response = post_request.unwrap();
        if response.status() != StatusCode::Created {
            return Err(Error::from_status(&mut response).into());
        };

        Ok(response.json().unwrap())
    }
}
