use actix_web::error::Result as ErrorResult;
use actix_web::fs::StaticFiles;
use actix_web::{http, middleware, server};
use actix_web::{App, HttpMessage, HttpRequest, HttpResponse, Json, Path};

use crypto::pbkdf2::pbkdf2_check;

use std::error::Error;
use std::fs;
use std::io::Read;

use couchdb::{Error as CouchDbError, Query, Server, ENV_COUCHDB_URI};
use security::{JwtToken, ENV_JWT_SECRET_KEY};
use {Page, RookeriesDomain, Site, User};

#[derive(Serialize)]
struct Status {
    version: String,
    app: String,
    #[serde(rename = "gitRevision")]
    git: String,
}

#[derive(Serialize)]
struct ErrorMessage {
    error: ErrorDetails,
}

#[derive(Serialize)]
struct ErrorDetails {
    message: String,
    status_code: u16,
    #[serde(skip_serializing_if = "Option::is_none")]
    resource: Option<String>,
}

#[derive(Deserialize)]
struct UserCredentialsRequest {
    username: String,
    password: String,
}

#[derive(Serialize)]
struct QueryOnUsername {
    username: String,
    #[serde(rename = "type")]
    type_: String,
}

impl QueryOnUsername {
    fn new(username: &str) -> QueryOnUsername {
        QueryOnUsername {
            username: username.to_string(),
            type_: String::from("user"),
        }
    }
}

#[derive(Serialize)]
struct QueryOnPageSlug {
    slug: String,
    #[serde(rename = "type")]
    type_: String,
}

impl QueryOnPageSlug {
    fn new(slug: &str) -> QueryOnPageSlug {
        QueryOnPageSlug {
            slug: slug.to_string(),
            type_: String::from("page"),
        }
    }
}

#[derive(Serialize)]
struct QueryForSite {
    #[serde(rename = "type")]
    type_: String,
}

impl QueryForSite {
    fn new() -> QueryForSite {
        QueryForSite {
            type_: String::from("site"),
        }
    }
}

#[derive(Serialize)]
struct JwtTokenResponse {
    access_token: String,
    user: User,
}

fn status(_req: HttpRequest) -> HttpResponse {
    let status = Status {
        version: env!("CARGO_PKG_VERSION").to_string(),
        app: env!("CARGO_PKG_NAME").to_string(),
        git: env!("GIT_REVISION").to_string(),
    };
    HttpResponse::build(http::StatusCode::OK).json(status)
}

fn auth(credentials: Json<UserCredentialsRequest>) -> HttpResponse {
    let couchdb = Server::configure_from_env().unwrap();

    info!(
        "Attempting to authenticate user \"{}\"",
        credentials.username
    );

    let users_found: Result<Vec<User>, Box<CouchDbError>> = couchdb.find(Query {
        selector: QueryOnUsername::new(&credentials.username),
    });
    if users_found.is_err() {
        error!(
            "Unable to retrieve users because: {}",
            users_found.unwrap_err()
        );
        return json_error_message(
            http::StatusCode::UNAUTHORIZED,
            "Invalid credentials provided.",
        );
    }

    let results = users_found.unwrap();
    if results.len() == 0 {
        info!("User '{}' could not be found.", &credentials.username);
        return json_error_message(
            http::StatusCode::UNAUTHORIZED,
            "Invalid credentials provided.",
        );
    }

    let user = results.first().unwrap();

    let stored_password = &user.password;
    let is_password_valid = match pbkdf2_check(&credentials.password, stored_password) {
        Ok(valid) => valid,
        Err(err_message) => {
            warn!("Unable to check password because of: {}", err_message);
            false
        }
    };

    if is_password_valid == false {
        return json_error_message(
            http::StatusCode::UNAUTHORIZED,
            "Invalid credentials provided.",
        );
    }

    let jwt = JwtToken::configure_from_env().unwrap();

    let access_token_response = JwtTokenResponse {
        access_token: jwt.encode(&jwt.new_claim(user)),
        user: user.clone(),
    };
    HttpResponse::build(http::StatusCode::OK).json(access_token_response)
}

fn serve_page((slug, request): (Path<String>, HttpRequest)) -> HttpResponse {
    let slug = slug.as_ref();
    let couchdb = Server::configure_from_env().unwrap();
    let pages_found: Result<Vec<Page>, Box<CouchDbError>> = couchdb.find(Query {
        selector: QueryOnPageSlug::new(slug),
    });
    if pages_found.is_err() {
        error!(
            "Unable to retrieve pages because: {}",
            pages_found.unwrap_err()
        );
        return json_error_message(
            http::StatusCode::INTERNAL_SERVER_ERROR,
            &format!("Error searching for page with slug \"{}\"", slug),
        );
    }

    let results = pages_found.unwrap();
    if results.len() == 0 {
        info!("Page '{}' could not be found.", slug.to_string());
        return json_error_message_with_resource(
            http::StatusCode::NOT_FOUND,
            &format!("No page found with slug \"{}\"", slug),
            Some(String::from(request.path())),
        );
    }

    let self_link = String::from(request.path());
    let page = results
        .first()
        .unwrap()
        .remove_couchdb_details()
        .add_self_links(self_link);
    HttpResponse::build(http::StatusCode::OK).json(page)
}

fn update_page(
    (request, updated_page, slug): (HttpRequest, Json<Page>, Path<String>),
) -> HttpResponse {
    // TODO: Add test for checking if valid JWT header exists.
    // TODO: Extract checking the header for a valid JWT.
    let jwt_token = request.headers().get("Authorization");
    if jwt_token.is_none() {
        return json_error_message(http::StatusCode::BAD_REQUEST, "Bad request");
    }

    // TODO: Make this more robust.
    let jwt_token = jwt_token.unwrap().to_str().unwrap();
    let jwt_token = jwt_token.rsplit(' ').next().unwrap();

    let jwt_manager = JwtToken::configure_from_env().unwrap();
    match jwt_manager.decode(jwt_token) {
        Ok(_) => {}
        Err(e) => {
            warn!("Could not verify token because of: {}", e);
            return json_error_message(http::StatusCode::BAD_REQUEST, "Bad request");
        }
    }

    let slug = slug.as_ref();
    let couchdb = Server::configure_from_env().unwrap();
    let pages_found: Result<Vec<Page>, Box<CouchDbError>> = couchdb.find(Query {
        selector: QueryOnPageSlug::new(slug),
    });
    if pages_found.is_err() {
        error!(
            "Unable to retrieve pages because: {}",
            pages_found.unwrap_err()
        );
        return json_error_message(
            http::StatusCode::INTERNAL_SERVER_ERROR,
            &format!("Error searching for page with slug \"{}\"", slug),
        );
    }

    let results = pages_found.unwrap();
    if results.len() == 0 {
        info!("Page '{}' could not be found.", slug.to_string());
        return json_error_message_with_resource(
            http::StatusCode::NOT_FOUND,
            &format!("No page found with slug \"{}\"", slug),
            Some(String::from(request.path())),
        );
    }

    let original_page = results.first().unwrap();
    let mut merged_page = updated_page.clone();
    merged_page.id = original_page.id.clone();
    merged_page.revision = original_page.revision.clone();
    merged_page.type_ = original_page.type_.clone();

    match couchdb.update(merged_page.clone()) {
        Err(e) => {
            error!(
                "Unable to update document \"{}\" because of {}",
                &merged_page.id.unwrap(),
                e
            );
            return json_error_message(
                http::StatusCode::INTERNAL_SERVER_ERROR,
                "Unable to update page.",
            );
        }
        Ok(t) => {
            info!(
                "Updated document \"{}\" to revision \"{}\"",
                t.id, t.revision
            );
        }
    }

    let self_link = String::from(request.path());
    let page = merged_page
        .remove_couchdb_details()
        .add_self_links(self_link);
    HttpResponse::build(http::StatusCode::OK).json(page)
}

fn serve_site(request: HttpRequest) -> HttpResponse {
    let couchdb = Server::configure_from_env().unwrap();
    let sites_found: Result<Vec<Site>, Box<CouchDbError>> = couchdb.find(Query {
        selector: QueryForSite::new(),
    });
    if sites_found.is_err() {
        error!(
            "Unable to retrieve pages because: {}",
            sites_found.unwrap_err()
        );
        return json_error_message(http::StatusCode::INTERNAL_SERVER_ERROR, "No site found!");
    }

    let results = sites_found.unwrap();
    if results.len() == 0 {
        info!("Site could not be found!");
        return json_error_message_with_resource(
            http::StatusCode::NOT_FOUND,
            "Site not found",
            Some(String::from(request.path())),
        );
    }

    let self_link = String::from(request.path());
    let page = results
        .first()
        .unwrap()
        .remove_couchdb_details()
        .add_self_links(self_link);
    HttpResponse::build(http::StatusCode::OK).json(page)
}

fn update_site((request, updated_site): (HttpRequest, Json<Site>)) -> HttpResponse {
    // TODO: Add test for checking if valid JWT header exists.
    // TODO: Extract checking the header for a valid JWT.
    let jwt_token = request.headers().get("Authorization");
    if jwt_token.is_none() {
        return json_error_message(http::StatusCode::BAD_REQUEST, "Bad request");
    }

    // TODO: Make this more robust.
    let jwt_token = jwt_token.unwrap().to_str().unwrap();
    let jwt_token = jwt_token.rsplit(' ').next().unwrap();

    let jwt_manager = JwtToken::configure_from_env().unwrap();
    match jwt_manager.decode(jwt_token) {
        Ok(_) => {}
        Err(e) => {
            warn!("Could not verify token because of: {}", e);
            return json_error_message(http::StatusCode::BAD_REQUEST, "Bad request");
        }
    }

    let couchdb = Server::configure_from_env().unwrap();
    let sites_found: Result<Vec<Site>, Box<CouchDbError>> = couchdb.find(Query {
        selector: QueryForSite::new(),
    });
    if sites_found.is_err() {
        error!(
            "Unable to retrieve pages because: {}",
            sites_found.unwrap_err()
        );
        return json_error_message(http::StatusCode::INTERNAL_SERVER_ERROR, "No site found!");
    }

    let results = sites_found.unwrap();
    if results.len() == 0 {
        info!("Site could not be found!");
        return json_error_message_with_resource(
            http::StatusCode::NOT_FOUND,
            "Site not found",
            Some(String::from(request.path())),
        );
    }

    let original_site = results.first().unwrap();
    let mut merged_site = updated_site.clone();
    merged_site.id = original_site.id.clone();
    merged_site.revision = original_site.revision.clone();
    merged_site.type_ = original_site.type_.clone();

    match couchdb.update(merged_site.clone()) {
        Err(e) => {
            error!(
                "Unable to update document \"{}\" because of {}",
                &merged_site.id.unwrap(),
                e
            );
            return json_error_message(
                http::StatusCode::INTERNAL_SERVER_ERROR,
                "Unable to update site.",
            );
        }
        Ok(t) => {
            info!(
                "Updated document \"{}\" to revision \"{}\"",
                t.id, t.revision
            );
        }
    }

    let self_link = String::from(request.path());
    let site = merged_site
        .remove_couchdb_details()
        .add_self_links(self_link);
    HttpResponse::build(http::StatusCode::OK).json(site)
}

fn json_error_message(status: http::StatusCode, message: &str) -> HttpResponse {
    json_error_message_with_resource(status, message, None)
}

fn json_error_message_with_resource(
    status: http::StatusCode,
    message: &str,
    resource: Option<String>,
) -> HttpResponse {
    let error_message = ErrorMessage {
        error: ErrorDetails {
            message: message.to_string(),
            status_code: status.as_u16(),
            resource,
        },
    };

    HttpResponse::build(status).json(error_message)
}

fn render_missing_api_resource(request: HttpRequest) -> HttpResponse {
    if request.method() != http::Method::GET {
        json_error_message(
            http::StatusCode::METHOD_NOT_ALLOWED,
            "Specified method is invalid for this resource",
        )
    } else {
        json_error_message_with_resource(
            http::StatusCode::NOT_FOUND,
            "Resource not found.",
            Some(request.path().to_string()),
        )
    }
}

fn render_missing_resource<S>(
    req: &mut HttpRequest<S>,
    _resp: HttpResponse,
) -> ErrorResult<middleware::Response> {
    if req.method() != http::Method::GET {
        return render_invalid_method_used(req, _resp);
    }

    let error_response = json_error_message_with_resource(
        http::StatusCode::NOT_FOUND,
        "Resource not found.",
        Some(req.path().to_string()),
    );
    Ok(middleware::Response::Done(error_response))
}

fn render_bad_request<S>(
    _req: &mut HttpRequest<S>,
    _resp: HttpResponse,
) -> ErrorResult<middleware::Response> {
    let error_response = json_error_message(http::StatusCode::BAD_REQUEST, "Resource not found.");
    Ok(middleware::Response::Done(error_response))
}

fn render_invalid_method_used<S>(
    _req: &mut HttpRequest<S>,
    _resp: HttpResponse,
) -> ErrorResult<middleware::Response> {
    let error_response = json_error_message(
        http::StatusCode::METHOD_NOT_ALLOWED,
        "Specified method is invalid for this resource",
    );
    Ok(middleware::Response::Done(error_response))
}

fn read_index() -> Result<String, Box<Error>> {
    let mut file = fs::File::open("templates/index.html")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

fn index(_req: HttpRequest) -> HttpResponse {
    let index = match read_index() {
        Err(_) => String::from("Could not read index template."),
        Ok(t) => t,
    };

    HttpResponse::build(http::StatusCode::OK)
        .content_type("text/html")
        .body(index)
}

pub fn run_server(port: u32) -> Result<(), Box<Error>> {
    info!("Rookeries API Server - v{}", env!("CARGO_PKG_VERSION"));
    info!("Starting server...");

    let couchdb_config = Server::configure_from_env();
    if couchdb_config.is_err() {
        let err = couchdb_config.unwrap_err();
        error!("Missing or invalid \"{}\"", ENV_COUCHDB_URI);
        error!("Exact cause: {}", err);
        return Err(err.into());
    }

    let jwt_config = JwtToken::configure_from_env();
    if jwt_config.is_err() {
        error!("Missing or invalid \"{}\"", ENV_JWT_SECRET_KEY);
        return Err(jwt_config.unwrap_err().into());
    }

    server::new(|| {
        App::new()
            .middleware(middleware::Logger::new(
                "%a \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T",
            ))
            .middleware(
                middleware::ErrorHandlers::new()
                    .handler(http::StatusCode::NOT_FOUND, render_missing_resource)
                    .handler(http::StatusCode::BAD_REQUEST, render_bad_request)
                    .handler(
                        http::StatusCode::METHOD_NOT_ALLOWED,
                        render_invalid_method_used,
                    ),
            )
            .default_resource(|r| r.get().f(index))
            .resource("/status", |r| r.get().f(status))
            .resource("/auth", |r| r.post().with(auth))
            .resource("/api/pages/{slug}", |r| {
                r.get().with(serve_page);
                r.put().with(update_page);
                r.post().with(update_page);
            })
            .resource("/api/site", |r| {
                r.get().f(serve_site);
                r.put().with(update_site);
            })
            .resource("/api/{tail:.*}", |r| r.f(render_missing_api_resource))
            .handler("/static", StaticFiles::new("static"))
            .resource("/", |r| r.get().f(index))
    }).bind(format!("0.0.0.0:{}", port))?
        .run();
    Ok(())
}
