use std::sync::Arc;

use crate::database::{settings, Storage};
use axum::{
    extract::{FromRef, Host, State},
    http::StatusCode,
    response::{Html, IntoResponse},
    Json,
};
use kittybox_frontend_renderer::{ErrorPage, OnboardingPage, Template};
use serde::Deserialize;
use tokio::{task::JoinSet, sync::Mutex};
use tracing::{debug, error};

use super::FrontendError;

pub async fn get() -> Html<String> {
    Html(
        Template {
            title: "Kittybox - Onboarding",
            blog_name: "Kittybox",
            feeds: vec![],
            user: None,
            content: OnboardingPage {}.to_string(),
        }
        .to_string(),
    )
}

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

#[derive(Deserialize, Debug)]
pub struct OnboardingData {
    user: serde_json::Value,
    first_post: serde_json::Value,
    #[serde(default = "OnboardingData::default_blog_name")]
    blog_name: String,
    feeds: Vec<OnboardingFeed>,
}

impl OnboardingData {
    fn default_blog_name() -> String {
        "Kitty Box!".to_owned()
    }
}

#[tracing::instrument(skip(db, http))]
async fn onboard<D: Storage + 'static>(
    db: D,
    user_uid: url::Url,
    data: OnboardingData,
    http: reqwest::Client,
    jobset: Arc<Mutex<JoinSet<()>>>,
) -> Result<(), FrontendError> {
    // Create a user to pass to the backend
    // At this point the site belongs to nobody, so it is safe to do
    tracing::debug!("Creating user...");
    let user = kittybox_indieauth::TokenData {
        me: user_uid.clone(),
        client_id: "https://kittybox.fireburn.ru/".parse().unwrap(),
        scope: kittybox_indieauth::Scopes::new(vec![kittybox_indieauth::Scope::Create]),
        iat: None, exp: None
    };
    tracing::debug!("User data: {:?}", user);

    if data.user["type"][0] != "h-card" || data.first_post["type"][0] != "h-entry" {
        return Err(FrontendError::with_code(
            StatusCode::BAD_REQUEST,
            "user and first_post should be an h-card and an h-entry",
        ));
    }

    tracing::debug!("Setting settings...");
    let user_domain = format!(
        "{}{}",
        user.me.host_str().unwrap(),
        user.me.port()
            .map(|port| format!(":{}", port))
            .unwrap_or_default()
    );
    db.set_setting::<settings::SiteName>(&user.me, data.blog_name.to_owned())
        .await
        .map_err(FrontendError::from)?;

    db.set_setting::<settings::Webring>(&user.me, false)
        .await
        .map_err(FrontendError::from)?;

    let (_, hcard) = {
        let mut hcard = data.user;
        hcard["properties"]["uid"] = serde_json::json!([&user_uid]);
        crate::micropub::normalize_mf2(hcard, &user)
    };
    db.put_post(&hcard, &user.me)
        .await
        .map_err(FrontendError::from)?;

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

        db.put_post(&feed, &user.me)
            .await
            .map_err(FrontendError::from)?;
    }
    let (uid, post) = crate::micropub::normalize_mf2(data.first_post, &user);
    tracing::debug!("Posting first post {}...", uid);
    crate::micropub::_post(&user, uid, post, db, http, jobset)
        .await
        .map_err(|e| FrontendError {
            msg: "Error while posting the first post".to_string(),
            source: Some(Box::new(e)),
            code: StatusCode::INTERNAL_SERVER_ERROR,
        })?;

    Ok(())
}

pub async fn post<D: Storage + 'static>(
    State(db): State<D>,
    Host(host): Host,
    State(http): State<reqwest::Client>,
    State(jobset): State<Arc<Mutex<JoinSet<()>>>>,
    Json(data): Json<OnboardingData>,
) -> axum::response::Response {
    let user_uid = format!("https://{}/", host.as_str());

    if db.post_exists(&user_uid).await.unwrap() {
        IntoResponse::into_response((StatusCode::FOUND, [("Location", "/")]))
    } else {
        match onboard(db, user_uid.parse().unwrap(), data, http, jobset).await {
            Ok(()) => IntoResponse::into_response((StatusCode::FOUND, [("Location", "/")])),
            Err(err) => {
                error!("Onboarding error: {}", err);
                IntoResponse::into_response((
                    err.code(),
                    Html(
                        Template {
                            title: "Kittybox - Onboarding",
                            blog_name: "Kittybox",
                            feeds: vec![],
                            user: None,
                            content: ErrorPage {
                                code: err.code(),
                                msg: Some(err.msg().to_string()),
                            }
                            .to_string(),
                        }
                        .to_string(),
                    ),
                ))
            }
        }
    }
}

pub fn router<St, S>() -> axum::routing::MethodRouter<St>
where
    S: Storage + FromRef<St> + 'static,
    Arc<Mutex<JoinSet<()>>>: FromRef<St>,
    reqwest::Client: FromRef<St>,
    St: Clone + Send + Sync + 'static,
{
    axum::routing::get(get)
        .post(post::<S>)
}