use std::convert::Infallible;

use warp::http::StatusCode;
use warp::{Filter, Rejection, reject::InvalidQuery};
use serde_json::{json, Value};
use serde::{Serialize, Deserialize};
use crate::database::{MicropubChannel, Storage};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
enum QueryType {
    Source,
    Config,
    Channel,
    SyndicateTo
}

#[derive(Serialize, Deserialize, Debug)]
struct MicropubQuery {
    q: QueryType,
    url: Option<String>
}

#[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(rename_all = "snake_case")]
enum ErrorType {
    InvalidRequest,
    InternalServerError,
    NotFound,
    NotAuthorized,
}

#[derive(Serialize, Deserialize)]
struct MicropubError {
    error: ErrorType,
    error_description: String
}

impl From<MicropubError> for StatusCode {
    fn from(err: MicropubError) -> Self {
        use ErrorType::*;
        match err.error {
            InvalidRequest => StatusCode::BAD_REQUEST,
            InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
            NotFound => StatusCode::NOT_FOUND,
            NotAuthorized => StatusCode::UNAUTHORIZED
        }
    }
}

impl MicropubError {
    fn new(error: ErrorType, error_description: &str) -> Self {
        Self {
            error,
            error_description: error_description.to_owned()
        }
    }
}

async fn _query<D: Storage>(db: D, host: warp::host::Authority, query: MicropubQuery, user: crate::indieauth::User) -> impl warp::Reply {
    let user_authority = warp::http::Uri::try_from(user.me.as_str()).unwrap().authority().unwrap().clone();
    // TODO compare with potential list of allowed websites
    // to allow one user to edit several websites with one token
    if host != user_authority {
        return Box::new(warp::reply::with_status(
            warp::reply::json(&MicropubError::new(
                ErrorType::NotAuthorized,
                "This user is not authorized to use Micropub on this website."
            )),
            StatusCode::UNAUTHORIZED
        )) as Box<dyn warp::Reply>
    }
    match query.q {
        QueryType::Config => {
            let channels: Vec<MicropubChannel> = match db.get_channels(host.as_str()).await {
                Ok(chans) => chans,
                Err(err) => return Box::new(warp::reply::with_status(
                    warp::reply::json(&MicropubError::new(
                        ErrorType::InternalServerError,
                        &format!("Error fetching channels: {}", err)
                    )),
                    StatusCode::INTERNAL_SERVER_ERROR
                ))
            };

            Box::new(warp::reply::json(json!({
                "q": [
                    QueryType::Source,
                    QueryType::Config,
                    QueryType::Channel,
                    QueryType::SyndicateTo
                ],
                "channels": channels,
                "_kittybox_authority": host.as_str()
            }).as_object().unwrap()))
        },
        QueryType::Source => {
            match query.url {
                Some(url) => {
                    if warp::http::Uri::try_from(&url).unwrap().authority().unwrap() != &user_authority {
                        return Box::new(warp::reply::with_status(
                            warp::reply::json(&MicropubError::new(
                                ErrorType::NotAuthorized,
                                "You are requesting a post from a website that doesn't belong to you."
                            )),
                            StatusCode::UNAUTHORIZED
                        ))
                    }
                    match db.get_post(&url).await {
                        Ok(some) => match some {
                            Some(post) => Box::new(warp::reply::json(&post)),
                            None => Box::new(warp::reply::with_status(
                                warp::reply::json(&MicropubError::new(
                                    ErrorType::NotFound,
                                    "The specified MF2 object was not found in database."
                                )),
                                StatusCode::NOT_FOUND
                            ))
                        },
                        Err(err) => {
                            return Box::new(warp::reply::json(&MicropubError::new(
                                ErrorType::InternalServerError,
                                &format!("Backend error: {}", err)
                            )))
                        }
                    }
                },
                None => todo!()
            }
        },
        _ => {
            todo!()
        }
    }
}

pub fn query<D: Storage, T>(db: D, token_endpoint: String, http: hyper::Client<T, hyper::Body>) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone
where T: hyper::client::connect::Connect + Clone + Send + Sync + 'static {
    warp::get()
        .map(move || db.clone())
        .and(crate::util::require_host())
        .and(warp::query::<MicropubQuery>())
        .and(crate::indieauth::require_token(token_endpoint, http))
        .then(_query)
}

pub async fn recover(err: Rejection) -> Result<impl warp::Reply, Infallible> {
    let error = if err.find::<InvalidQuery>().is_some() {
        MicropubError::new(
            ErrorType::InvalidRequest,
            "Invalid query parameters sent. Try ?q=config to see what you can do."
        )
    } else {
        log::error!("Unhandled rejection: {:?}", err);
        MicropubError::new(
            ErrorType::InternalServerError,
            &format!("Unknown error: {:?}", err)
        )
    };

    Ok(warp::reply::with_status(warp::reply::json(&error), error.into()))
}

#[cfg(test)]
impl MicropubQuery {
    fn config() -> Self {
        Self {
            q: QueryType::Config,
            url: None
        }
    }

    fn source(url: &str) -> Self {
        Self {
            q: QueryType::Source,
            url: Some(url.to_owned())
        }
    }
}


#[cfg(test)]
mod tests {
    use hyper::body::HttpBody;
    use crate::micropub::MicropubError;
    use warp::{Filter, Reply};

    #[tokio::test]
    async fn test_query_wrong_auth() {
        let mut res = warp::test::request()
            .filter(&warp::any().then(|| super::_query(
                crate::database::MemoryStorage::new(),
                warp::host::Authority::from_static("aaronparecki.com"),
                super::MicropubQuery::config(),
                crate::indieauth::User::new(
                    "https://fireburn.ru/",
                    "https://quill.p3k.io/",
                    "create update media"
                )
            )))
            .await
            .unwrap()
            .into_response();

        assert_eq!(res.status(), 401);
        let body = res.body_mut().data().await.unwrap().unwrap();
        let json: MicropubError = serde_json::from_slice(&body as &[u8]).unwrap();
        assert_eq!(json.error, super::ErrorType::NotAuthorized);
    }

    #[tokio::test]
    async fn test_query_foreign_url() {
        let mut res = warp::test::request()
            .filter(&warp::any().then(|| super::_query(
                crate::database::MemoryStorage::new(),
                warp::host::Authority::from_static("aaronparecki.com"),
                super::MicropubQuery::source("https://aaronparecki.com/feeds/main"),
                crate::indieauth::User::new(
                    "https://fireburn.ru/",
                    "https://quill.p3k.io/",
                    "create update media"
                )
            )))
            .await
            .unwrap()
            .into_response();

        assert_eq!(res.status(), 401);
        let body = res.body_mut().data().await.unwrap().unwrap();
        let json: MicropubError = serde_json::from_slice(&body as &[u8]).unwrap();
        assert_eq!(json.error, super::ErrorType::NotAuthorized);
    }
}