use crate::database::Storage;
use crate::ApplicationState;
use log::{error, info};
use serde::{Deserialize, Serialize};
use tide::{Next, Request, Response, Result, StatusCode};

static POSTS_PER_PAGE: usize = 20;

mod templates;

use templates::{ErrorPage, MainPage, OnboardingPage, Template};

#[derive(Clone, Serialize, Deserialize)]
pub struct IndiewebEndpoints {
    authorization_endpoint: String,
    token_endpoint: String,
    webmention: Option<String>,
    microsub: Option<String>,
}

#[derive(Deserialize)]
struct QueryParams {
    after: Option<String>,
}

#[derive(Debug)]
struct FrontendError {
    msg: String,
    source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
    code: StatusCode,
}
impl FrontendError {
    pub fn with_code(code: StatusCode, msg: &str) -> Self {
        Self {
            msg: msg.to_string(),
            source: None,
            code,
        }
    }
    pub fn msg(&self) -> &str {
        &self.msg
    }
    pub fn code(&self) -> StatusCode {
        self.code
    }
}
impl From<crate::database::StorageError> for FrontendError {
    fn from(err: crate::database::StorageError) -> Self {
        Self {
            msg: "Database error".to_string(),
            source: Some(Box::new(err)),
            code: StatusCode::InternalServerError,
        }
    }
}
impl std::error::Error for FrontendError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source
            .as_ref()
            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
    }
}
impl std::fmt::Display for FrontendError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.msg)
    }
}

async fn get_post_from_database<S: Storage>(
    db: &S,
    url: &str,
    after: Option<String>,
    user: &Option<String>,
) -> std::result::Result<serde_json::Value, FrontendError> {
    match db
        .read_feed_with_limit(url, &after, POSTS_PER_PAGE, user)
        .await
    {
        Ok(result) => match result {
            Some(post) => Ok(post),
            None => Err(FrontendError::with_code(
                StatusCode::NotFound,
                "Post not found in the database",
            )),
        },
        Err(err) => match err.kind() {
            crate::database::ErrorKind::PermissionDenied => {
                // TODO: Authentication
                if user.is_some() {
                    Err(FrontendError::with_code(
                        StatusCode::Forbidden,
                        "User authenticated AND forbidden to access this resource",
                    ))
                } else {
                    Err(FrontendError::with_code(
                        StatusCode::Unauthorized,
                        "User needs to authenticate themselves",
                    ))
                }
            }
            _ => Err(err.into()),
        },
    }
}

#[derive(Deserialize)]
struct OnboardingFeed {
    slug: String,
    name: String,
}

#[derive(Deserialize)]
struct OnboardingData {
    user: serde_json::Value,
    first_post: serde_json::Value,
    blog_name: String,
    feeds: Vec<OnboardingFeed>,
}

pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
    use serde_json::json;

    log::debug!("Entering onboarding receiver...");

    // This cannot error out as the URL must be valid. Or there is something horribly wrong
    // and we shouldn't serve this request anyway.
    <dyn AsMut<tide::http::Request>>::as_mut(&mut req)
        .url_mut()
        .set_scheme("https")
        .unwrap();

    log::debug!("Parsing the body...");
    let body = req.body_json::<OnboardingData>().await?;
    log::debug!("Body parsed!");
    let backend = &req.state().storage;

    #[cfg(any(not(debug_assertions), test))]
    let me = req.url();
    #[cfg(all(debug_assertions, not(test)))]
    let me = url::Url::parse("https://localhost:8080/").unwrap();

    log::debug!("me value: {:?}", me);

    if get_post_from_database(backend, me.as_str(), None, &None)
        .await
        .is_ok()
    {
        return Err(FrontendError::with_code(
            StatusCode::Forbidden,
            "Onboarding is over. Are you trying to take over somebody's website?!",
        )
        .into());
    }
    info!("Onboarding new user: {}", me);

    let user = crate::indieauth::User::new(me.as_str(), "https://kittybox.fireburn.ru/", "create");

    log::debug!("Setting the site name to {}", &body.blog_name);
    backend
        .set_setting("site_name", user.me.as_str(), &body.blog_name)
        .await?;

    if body.user["type"][0] != "h-card" || body.first_post["type"][0] != "h-entry" {
        return Err(FrontendError::with_code(
            StatusCode::BadRequest,
            "user and first_post should be h-card and h-entry",
        )
        .into());
    }
    info!("Validated body.user and body.first_post as microformats2");

    let mut hcard = body.user;
    let hentry = body.first_post;

    // Ensure the h-card's UID is set to the main page, so it will be fetchable.
    hcard["properties"]["uid"] = json!([me.as_str()]);
    // Normalize the h-card - note that it should preserve the UID we set here.
    let (_, hcard) = crate::micropub::normalize_mf2(hcard, &user);
    // The h-card is written directly - all the stuff in the Micropub's
    // post function is just to ensure that the posts will be syndicated
    // and inserted into proper feeds. Here, we don't have a need for this,
    // since the h-card is DIRECTLY accessible via its own URL.
    log::debug!("Saving the h-card...");
    backend.put_post(&hcard, me.as_str()).await?;

    log::debug!("Creating feeds...");
    for feed in body.feeds {
        if &feed.name == "" || &feed.slug == "" { continue };
        log::debug!("Creating feed {} with slug {}", &feed.name, &feed.slug);
        let (_, feed) = crate::micropub::normalize_mf2(
            json!({
                "type": ["h-feed"],
                "properties": {"name": [feed.name], "mp-slug": [feed.slug]}
            }),
            &user,
        );

        backend.put_post(&feed, me.as_str()).await?;
    }
    log::debug!("Saving the h-entry...");
    // This basically puts the h-entry post through the normal creation process.
    // We need to insert it into feeds and optionally send a notification to everywhere.
    req.set_ext(user);
    crate::micropub::post::new_post(req, hentry).await?;

    Ok(Response::builder(201).header("Location", "/").build())
}

pub async fn coffee<S: Storage>(_: Request<ApplicationState<S>>) -> Result {
    Err(FrontendError::with_code(
        StatusCode::ImATeapot,
        "Someone asked this website to brew them some coffee...",
    )
    .into())
}

pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
    // This cannot error out as the URL must be valid. Or there is something horribly wrong
    // and we shouldn't serve this request anyway.
    <dyn AsMut<tide::http::Request>>::as_mut(&mut req)
        .url_mut()
        .set_scheme("https")
        .unwrap();
    let backend = &req.state().storage;
    let query = req.query::<QueryParams>()?;
    let authorization_endpoint = req.state().authorization_endpoint.to_string();
    let token_endpoint = req.state().token_endpoint.to_string();
    let user: Option<String> = None;

    #[cfg(any(not(debug_assertions), test))]
    let url = req.url();
    #[cfg(all(debug_assertions, not(test)))]
    let url = url::Url::parse("https://localhost:8080/").unwrap();

    let hcard_url = url.as_str();
    let feed_url = url.join("feeds/main").unwrap().to_string();

    let card = get_post_from_database(backend, hcard_url, None, &user).await;
    let feed = get_post_from_database(backend, &feed_url, query.after, &user).await;

    if card.is_err() && feed.is_err() {
        // Uh-oh! No main feed and no h-card? Need to do onboarding.
        // We can do it from inside the app without ever requesting an auth token.
        let card_err = card.unwrap_err();
        let feed_err = feed.unwrap_err();
        if card_err.code == 404 {
            // Yes, we definitely need some onboarding here.
            Ok(Response::builder(200)
                .content_type("text/html; charset=utf-8")
                .body(
                    Template {
                        title: "Kittybox - Onboarding",
                        blog_name: "Kitty Box!",
                        endpoints: IndiewebEndpoints {
                            authorization_endpoint,
                            token_endpoint,
                            webmention: None,
                            microsub: None,
                        },
                        feeds: Vec::default(),
                        content: OnboardingPage {}.to_string(),
                    }
                    .to_string(),
                )
                .build())
        } else {
            Err(feed_err.into())
        }
    } else {
        Ok(Response::builder(200)
            .content_type("text/html; charset=utf-8")
            .body(
                Template {
                    title: &format!("{} - Main page", url.host().unwrap().to_string()),
                    blog_name: &backend
                        .get_setting("site_name", hcard_url)
                        .await
                        .unwrap_or_else(|_| "Kitty Box!".to_string()),
                    endpoints: IndiewebEndpoints {
                        authorization_endpoint,
                        token_endpoint,
                        webmention: None,
                        microsub: None,
                    },
                    feeds: backend.get_channels(hcard_url).await.unwrap_or_else(|_| Vec::default()),
                    content: MainPage {
                        feed: &feed?,
                        card: &card?,
                    }
                    .to_string(),
                }
                .to_string(),
            )
            .build())
    }
}

pub async fn render_post<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
    let query = req.query::<QueryParams>()?;
    let authorization_endpoint = req.state().authorization_endpoint.to_string();
    let token_endpoint = req.state().token_endpoint.to_string();
    let user: Option<String> = None;

    // This cannot error out as the URL must be valid. Or there is something horribly wrong
    // and we shouldn't serve this request anyway.
    <dyn AsMut<tide::http::Request>>::as_mut(&mut req)
        .url_mut()
        .set_scheme("https")
        .unwrap();
    #[cfg(any(not(debug_assertions), test))]
    let url = req.url();
    #[cfg(all(debug_assertions, not(test)))]
    let url = url::Url::parse("https://localhost:8080/")
        .unwrap()
        .join(req.url().path())
        .unwrap();

    let mut entry_url = req.url().clone();
    entry_url.set_query(None);

    let post = get_post_from_database(&req.state().storage, entry_url.as_str(), query.after, &user)
        .await?;

    let template: String = match post["type"][0]
        .as_str()
        .expect("Empty type array or invalid type")
    {
        "h-entry" => templates::Entry { post: &post }.to_string(),
        "h-card" => templates::VCard { card: &post }.to_string(),
        "h-feed" => templates::Feed { feed: &post }.to_string(),
        _ => {
            return Err(FrontendError::with_code(
                StatusCode::InternalServerError,
                "Couldn't render an unknown type",
            )
            .into())
        }
    };
    let origin = url.origin();
    let owner = origin.ascii_serialization() + "/";

    Ok(Response::builder(200)
        .content_type("text/html; charset=utf-8")
        .body(
            Template {
                title: post["properties"]["name"][0]
                    .as_str()
                    .unwrap_or(&format!("Note at {}", url.host().unwrap().to_string())),
                blog_name: &req
                    .state()
                    .storage
                    .get_setting("site_name", &owner) // XXX I'm pretty sure this is bound to cause issues with IDN-style domains
                    .await
                    .unwrap_or_else(|_| "Kitty Box!".to_string()),
                endpoints: IndiewebEndpoints {
                    authorization_endpoint,
                    token_endpoint,
                    webmention: None,
                    microsub: None,
                },
                feeds: req.state().storage.get_channels(&owner).await.unwrap_or_else(|e| Vec::default()),
                content: template,
            }
            .to_string(),
        )
        .build())
}

pub struct ErrorHandlerMiddleware {}

#[async_trait::async_trait]
impl<S> tide::Middleware<ApplicationState<S>> for ErrorHandlerMiddleware
where
    S: crate::database::Storage,
{
    async fn handle(
        &self,
        request: Request<ApplicationState<S>>,
        next: Next<'_, ApplicationState<S>>,
    ) -> Result {
        let authorization_endpoint = request.state().authorization_endpoint.to_string();
        let token_endpoint = request.state().token_endpoint.to_string();
        let owner = request.url().origin().ascii_serialization().clone() + "/";
        let site_name = &request
            .state()
            .storage
            .get_setting("site_name", &owner)
            .await
            .unwrap_or_else(|_| "Kitty Box!".to_string());
        let feeds = request.state().storage.get_channels(&owner).await.unwrap_or_else(|_| Vec::default());
        let mut res = next.run(request).await;
        let mut code: Option<StatusCode> = None;
        if let Some(err) = res.downcast_error::<FrontendError>() {
            code = Some(err.code());
            error!("Error caught while processing request: {}", err.msg());
            let mut err: &dyn std::error::Error = err;
            while let Some(e) = err.source() {
                error!("Caused by: {}", e);
                err = e;
            }
        }
        if let Some(code) = code {
            res.set_status(code);
            res.set_content_type("text/html; charset=utf-8");
            res.set_body(
                Template {
                    title: "Error",
                    blog_name: site_name,
                    endpoints: IndiewebEndpoints {
                        authorization_endpoint,
                        token_endpoint,
                        webmention: None,
                        microsub: None,
                    },
                    feeds: feeds,
                    content: ErrorPage { code }.to_string(),
                }
                .to_string(),
            );
        }
        Ok(res)
    }
}

static STYLE_CSS: &[u8] = include_bytes!("./style.css");
static ONBOARDING_JS: &[u8] = include_bytes!("./onboarding.js");
static ONBOARDING_CSS: &[u8] = include_bytes!("./onboarding.css");

pub async fn handle_static<S: Storage>(req: Request<ApplicationState<S>>) -> Result {
    Ok(match req.param("path") {
        Ok("style.css") => Ok(Response::builder(200)
            .content_type("text/css; charset=utf-8")
            .body(STYLE_CSS)
            .build()),
        Ok("onboarding.js") => Ok(Response::builder(200)
            .content_type("text/javascript; charset=utf-8")
            .body(ONBOARDING_JS)
            .build()),
        Ok("onboarding.css") => Ok(Response::builder(200)
            .content_type("text/css; charset=utf-8")
            .body(ONBOARDING_CSS)
            .build()),
        Ok(_) => Err(FrontendError::with_code(
            StatusCode::NotFound,
            "Static file not found",
        )),
        Err(_) => panic!("Invalid usage of the frontend::handle_static() function"),
    }?)
}