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 super::IndiewebEndpoints;
    use ellipse::Ellipse;
    use http_types::StatusCode;
    use log::error;

    /// Return a pretty location specifier from a geo: URI.
    fn decode_geo_uri(uri: &str) -> String {
        if let Some(part) = uri.split(':').collect::<Vec<_>>().get(1) {
            if let Some(part) = part.split(';').next() {
                let mut parts = part.split(',');
                let lat = parts.next().unwrap();
                let lon = parts.next().unwrap();
                // TODO - format them as proper latitude and longitude
                return format!("{}, {}", lat, lon);
            } else {
                uri.to_string()
            }
        } else {
            uri.to_string()
        }
    }

    markup::define! {
        Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, content: String) {
            @markup::doctype()
            html {
                head {
                    title { @title }
                    link[rel="preconnect", href="https://fonts.gstatic.com"];
                    link[rel="stylesheet", href="/static/style.css"];
                    meta[name="viewport", content="initial-scale=1, width=device-width"];
                    // TODO: link rel= for common IndieWeb APIs: webmention, microsub
                    link[rel="micropub", href="/micropub"]; // Static, because it's built into the server itself
                    link[rel="authorization_endpoint", href=&endpoints.authorization_endpoint];
                    link[rel="token_endpoint", href=&endpoints.token_endpoint];
                    @if endpoints.webmention.is_some() {
                        link[rel="webmention", href=&endpoints.webmention.as_ref()];
                    }
                    @if endpoints.microsub.is_some() {
                        link[rel="microsub", href=&endpoints.microsub.as_ref()];
                    }
                }
                body {
                    nav#headerbar {
                        // TODO: Find a way to store the website name somewhere in the database
                        // Maybe in the settings?
                        ul {
                            li { a#homepage[href="/"] { @blog_name } }
                            li.shiftright { a#login[href="/login"] { "Login" } }
                        }
                    }
                    main {
                        @markup::raw(content)
                    }
                }
            }
        }
        OnboardingPage {
            h1[style="text-align: center"] {
                "Welcome to Kittybox"
            }
            script[type="module", src="/static/onboarding.js"] {}
            link[rel="stylesheet", href="/static/onboarding.css"];
            form.onboarding[action="/", method="POST"] {
                noscript {
                    p {
                        "Ok, let's be honest. Most of this software doesn't require JS to be enabled "
                        "to view pages (and in some cases, even edit them if logged in)."
                    }
                    p { "This page is a little bit different. It uses JavaScript to provide interactive features, such as:" }
                    ul {
                        li { "Multiple-input questions" }
                        li { "Answers spanning multiple fields" }
                        li { "Preview of files being uploaded" }
                        li { "Pretty pagination so you won't feel overwhelmed" }
                    }
                    p {
                        "Sadly, it's very hard or even impossible to recreate this without any JavaScript. "
                        "Good news though - the code is " b { "open-source AND free software" }
                        " (under MIT (X11) or Apache-2.0 license - your choice) "
                        "and I promise to not obfuscate it or minify it. "
                        a[href="/static/onboarding.js"] { "Here" }
                        "'s the link - you can try reading it so you'll be 200% sure "
                        "it won't steal your cookies or turn your kitty into a soulless monster."
                        @markup::raw("<!-- do cats even have souls? I'm not sure. But this code won't steal their souls anyway. -->")
                    }
                    hr;
                    p { "In other words: " b { "please enable JavaScript for this page to work properly." } small { "sorry T__T" } }
                }
                ul#progressbar[style="display: none"] {
                    li#intro { "Introduction" }
                    li#hcard { "Your profile" }
                    li#settings { "Your website" }
                    li#firstpost { "Your first post" }
                }
                fieldset#intro[style="display: none"] {
                    legend { "Introduction" }
                    p {
                        "Kittybox is a CMS that can act as a member of the IndieWeb. "
                        "IndieWeb is a global distributed social network built on top of open Web standards "
                        "and composed of blogs around the Internet supporting these standards."
                    }
                    p { "There is no registration or centralized database of any sort - everyone owns their data and is responsible for it." }
                    p { "If you're seeing this page, it looks like your configuration is correct and we can proceed with the setup." }

                    div.switch_card_buttons {
                        button.switch_card.next_card[type="button", "data-card"="hcard"] { "Next" }
                    }
                }

                fieldset#hcard[style="display: none"] {
                    legend { "Your profile" }
                    p { "An h-card is an IndieWeb social profile, and we're gonna make you one!" }
                    p { "Thanks to some clever markup, it will be readable by both humans and machines looking at your homepage."}
                    p {
                        "If you make a mistake, don't worry, you're gonna be able to edit this later."
                        "The only mandatory field is your name."
                    }

                    div.form_group {
                        label[for="hcard_name"] { "Your name" }
                        input#hcard_name[name="hcard_name", placeholder="Your name"];
                        small {
                            "No need to write the name as in your passport, this is not a legal document "
                            "- just write how you want to be called on the network. This name will be also "
                            "shown whenever you leave a comment on someone else's post using your website."
                        }
                    }

                    div.form_group {
                        label[for="pronouns"] { "Your pronouns" }
                        div.multi_input#pronouns {
                            template {
                                input#hcard_pronouns[name="hcard_pronouns", placeholder="they/them?"];
                            }
                            button.add_more[type="button", "aria-label"="Add more"] { "[+] Add more" }
                        }
                        small {
                            "Write which pronouns you use for yourself. It's a free-form field "
                            "so don't feel constrained - but keep it compact, as it'll be shown in a lot of places."
                        }
                    }

                    div.form_group {
                        label[for="urls"] { "Links to other pages of you" }
                        div.multi_input#urls {
                            template {
                                input#hcard_url[name="hcard_url", placeholder="https://example.com/"];
                            }
                            button.add_more[type="button", "aria-label"="Add more"] { "[+] Add more" }
                        }
                        small {
                            "These URLs will help your readers find you elsewhere and will help you that whoever owns these pages owns your website too"
                            " in case the links are mutual. So make sure to put a link to your site in your other social profiles!"
                        }
                    }

                    div.form_group {
                        label[for="hcard_note"] { "A little about yourself" }
                        textarea#hcard_note[name="hcard_note", placeholder="Loves cooking, plants, cats, dogs and racoons."] {}
                        small { "A little bit of introduction. Just one paragraph, and note, you can't use HTML here (yet)." }
                        // TODO: HTML e-note instead of p-note
                    }

                    // TODO: u-photo upload - needs media endpoint cooperation

                    div.switch_card_buttons {
                        button.switch_card.prev_card[type="button", "data-card"="intro"] { "Previous" }
                        button.switch_card.next_card[type="button", "data-card"="settings"] { "Next" }
                    }
                }

                fieldset#settings[style="display: none"] {
                    legend { "Your website" }
                    p { "Ok, it's nice to know you more. Tell me about what you'll be writing and how you want to name your blog." }
                    // TODO: site-name, saved to settings

                    div.form_group {
                        label[for="blog_name"] { "Your website's name"}
                        input#blog_name[name="blog_name", placeholder="Kitty Box!"];
                        small { "It'll get shown in the title of your blog, in the upper left corner!" }
                    }

                    div.form_group {
                        label[for="custom_feeds"] { "Custom feeds" }
                        small {
                            p {
                                "You can set up custom feeds to post your stuff to. "
                                "This is a nice way to organize stuff into huge folders, like all your trips or your quantified-self data."
                            }
                            p {
                                "Feeds can be followed individually, which makes it easy for users who are interested in certain types "
                                "of content you produce to follow your adventures in certain areas of your life without cluttering their "
                                "readers."
                            }
                            p {
                                "We will automatically create some feeds for you aside from these so you won't have to - including a main feed, "
                                "address book (for venues you go to and people you talk about), a cookbook for your recipes and some more."
                                // TODO: Put a link to documentation explaining feeds in more detail.
                            }
                        }
                        div.multi_input#custom_feeds {
                            template {
                                fieldset.feed {
                                    div.form_group {
                                        label[for="feed_name"] { "Name" }
                                        input#feed_name[name="feed_name", placeholder="My cool feed"];
                                        small { "This is a name that will identify this feed to the user. Make it short and descriptive!" }
                                    }
                                    div.form_group {
                                        label[for="feed_slug"] { "Slug" }
                                        input#feed_slug[name="feed_slug", placeholder="my-cool-feed"];
                                        small { "This will form a pretty URL for the feed. For example: https://example.com/feeds/my-cool-feed" }
                                    }
                                }
                            }
                            button.add_more[type="button", "aria-label"="Add more"] { "[+] Add More" }
                        }
                    }

                    div.switch_card_buttons {
                        button.switch_card.prev_card[type="button", "data-card"="hcard"] { "Previous" }
                        button.switch_card.next_card[type="button", "data-card"="firstpost"] { "Next" }
                    }
                }

                fieldset#firstpost[style="display: none"] {
                    legend { "Your first post" }
                    p { "Maybe you should start writing your first posts now. How about a short note?" }
                    p { "A note is a short-form post (not unlike a tweet - but without the actual character limit) that doesn't bear a title." }
                    p {
                        "Consider telling more about yourself, your skills and interests in this note "
                        @markup::raw("&mdash;")
                        " though you're free to write anything you want. (By the way, you can use "
                        a[href="https://daringfireball.net/projects/markdown/syntax"] { "Markdown" }
                        " here to spice up your note!)"
                    }

                    textarea#first_post_content[style="width: 100%; height: 8em", placeholder="Hello! I am really excited about #IndieWeb"] {}

                    div.switch_card_buttons {
                        button.switch_card.prev_card[type="button", "data-card"="settings"] { "Previous" }
                        button[type="submit"] { "Finish" }
                    }
                }
            }
        }
        Entry<'a>(post: &'a serde_json::Value) {
            article."h-entry" {
                header.metadata {
                    @if post["properties"]["name"][0].is_string() {
                        h1."p-name" {
                            @post["properties"]["name"][0].as_str().unwrap()
                        }
                    }
                    div {
                        span {
                            a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] {
                                time."dt-published"[datetime=post["properties"]["published"][0].as_str().unwrap()] {
                                    @chrono::DateTime::parse_from_rfc3339(post["properties"]["published"][0].as_str().unwrap())
                                        .map(|dt| dt.format("%a %b %e %T %Y").to_string())
                                        .unwrap_or("ERROR: Couldn't parse the datetime".to_string())
                                }
                            }
                        }
                        @if post["properties"]["category"].is_array() {
                            span {
                                ul.categories {
                                    "Tagged: "
                                    @for cat in post["properties"]["category"].as_array().unwrap() {
                                        li."p-category" { @cat.as_str().unwrap() }
                                    }
                                }
                            }
                        }
                        @if post["properties"]["in-reply-to"].is_array() {
                            // TODO: Rich reply contexts - blocked on MF2 parser
                            span {
                                "In reply to: "
                                ul.replyctx {
                                    @for ctx in post["properties"]["in-reply-to"].as_array().unwrap() {
                                        li { a."u-in-reply-to"[href=ctx.as_str().unwrap()] {
                                            @ctx.as_str().unwrap().truncate_ellipse(24).as_ref()
                                        } }
                                    }
                                }
                            }
                        }
                    }
                    @if post["properties"]["location"].is_array() || post["properties"]["checkin"].is_array() {
                        div {
                            @if post["properties"]["checkin"].is_array() {
                                span {
                                    "Check-in to: "
                                    @if post["properties"]["checkin"][0].is_string() {
                                        // It's a URL
                                        a."u-checkin"[href=post["properties"]["checkin"][0].as_str().unwrap()] {
                                            @post["properties"]["checkin"][0].as_str().unwrap().truncate_ellipse(24).as_ref()
                                        }
                                    } else {
                                        a."u-checkin"[href=post["properties"]["checkin"][0]["properties"]["uid"][0].as_str().unwrap()] {
                                            @post["properties"]["checkin"][0]["properties"]["name"][0].as_str().unwrap()
                                        }
                                    }
                                }
                            }
                            @if post["properties"]["location"].is_array() {
                                span {
                                    "Location: "
                                    @if post["properties"]["location"][0].is_string() {
                                        // It's a geo: URL
                                        // We need to decode it
                                        a."u-location"[href=post["properties"]["location"][0].as_str().unwrap()] {
                                            @decode_geo_uri(post["properties"]["location"][0].as_str().unwrap())
                                        }
                                    } else {
                                        // It's an inner h-geo object
                                        a."u-location"[href=post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))] {
                                            // I'm a lazy bitch
                                            @decode_geo_uri(&post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap())))
                                        }
                                    }
                                }
                            }
                        }
                    }
                    @if post["properties"]["ate"].is_array() || post["properties"]["drank"].is_array() {
                        div {
                            @if post["properties"]["ate"].is_array() {
                                span { ul {
                                    "Ate:"
                                    @for food in post["properties"]["ate"].as_array().unwrap() {
                                        li {
                                            @if food.is_string() {
                                                // If this is a string, it's a URL.
                                                a."u-ate"[href=food.as_str().unwrap()] {
                                                    @food.as_str().unwrap().truncate_ellipse(24).as_ref()
                                                }
                                            } else {
                                                // This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
                                                a."u-ate"[href=food["properties"]["uid"][0].as_str().unwrap()] {
                                                    @food["properties"]["name"][0].as_str()
                                                        .unwrap_or(food["properties"]["uid"][0].as_str().unwrap().truncate_ellipse(24).as_ref())
                                                }
                                            }
                                        }
                                    }
                                } }
                            }
                            @if post["properties"]["drank"].is_array() {
                                span { ul {
                                    "Drank:"
                                    @for food in post["properties"]["drank"].as_array().unwrap() {
                                        li {
                                            @if food.is_string() {
                                                // If this is a string, it's a URL.
                                                a."u-drank"[href=food.as_str().unwrap()] {
                                                    @food.as_str().unwrap().truncate_ellipse(24).as_ref()
                                                }
                                            } else {
                                                // This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
                                                a."u-drank"[href=food["properties"]["uid"][0].as_str().unwrap()] {
                                                    @food["properties"]["name"][0].as_str()
                                                        .unwrap_or(food["properties"]["uid"][0].as_str().unwrap().truncate_ellipse(24).as_ref())
                                                }
                                            }
                                        }
                                    }
                                } }
                            }
                        }
                    }
                }
                @PhotoGallery { photos: post["properties"]["photo"].as_array() }
                @if post["properties"]["content"][0]["html"].is_string() {
                    main."e-content" {
                        @markup::raw(post["properties"]["content"][0]["html"].as_str().unwrap().trim())
                    }
                }
                @WebInteractions { post }
            }
        }
        PhotoGallery<'a>(photos: Option<&'a Vec<serde_json::Value>>) {
            @if photos.is_some() {
                @for photo in photos.unwrap() {
                    @if photo.is_string() {
                        img."u-photo"[src=photo.as_str().unwrap(), loading="lazy"];
                    } else if photo.is_array() {
                        @if photo["thumbnail"].is_string() {
                            a."u-photo"[href=photo["value"].as_str().unwrap()] {
                                img[src=photo["thumbnail"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
                            }
                        } else {
                            img."u-photo"[src=photo["value"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
                        }
                    }
                }
            }
        }
        WebInteractions<'a>(post: &'a serde_json::Value) {
            footer.webinteractions {
                ul.counters {
                    li {
                        span.icon { "❤️" }
                        span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) }
                    }
                    li {
                        span.icon { "💬" }
                        span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) }
                    }
                    li {
                        span.icon { "🔄" }
                        span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) }
                    }
                    li {
                        span.icon { "🔖" }
                        span.counter { @post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) }
                    }
                }
                // Needs rich webmention support which may or may not depend on an MF2 parser
                // Might circumvent with an external parser with CORS support
                // why write this stuff in rust then tho
                /*details {
                    summary { "Show comments and reactions" }
                    @if post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
                        // Show a facepile of likes for a post
                    }
                    @if post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
                        // Show a facepile of bookmarks for a post
                    }
                    @if post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
                        // Show a facepile of reposts for a post
                    }
                    @if post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
                        // Show all the comments recursively (so we could do Salmention with them)
                    }
                }*/
            }
        }
        VCard<'a>(card: &'a serde_json::Value) {
            article."h-card" {
                @if card["properties"]["photo"][0].is_string() {
                    img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()];
                }
                h1 {
                    a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] {
                        @card["properties"]["name"][0].as_str().unwrap()
                    }
                }
                @if card["properties"]["pronoun"].is_array() {
                    span {
                        "("
                        @for (i, pronoun) in card["properties"]["pronoun"].as_array().unwrap().iter().filter_map(|v| v.as_str()).enumerate() {
                            span."p-pronoun" {
                                @pronoun
                            }
                            // Insert commas between multiple sets of pronouns
                            @if i < (card["properties"]["pronoun"].as_array().unwrap().len() - 1) {", "}
                        }
                        ")"
                    }
                }
                @if card["properties"]["note"].is_array() {
                    p."p-note" {
                        @card["properties"]["note"][0]["value"].as_str().unwrap_or_else(|| card["properties"]["note"][0].as_str().unwrap())
                    }
                }
                @if card["properties"]["url"].is_array() {
                    ul {
                        "Can be found elsewhere at:"
                        @for url in card["properties"]["url"].as_array().unwrap().iter().filter_map(|v| v.as_str()).filter(|v| v != &card["properties"]["uid"][0].as_str().unwrap()).filter(|v| !v.starts_with(&card["properties"]["author"][0].as_str().unwrap())) {
                            li { a."u-url"[href=url, rel="me"] { @url } }
                        }
                    }
                }
            }
        }
        Food<'a>(food: &'a serde_json::Value) {
            article."h-food" {
                header.metadata {
                    h1 {
                        a."p-name"."u-url"[href=food["properties"]["url"][0].as_str().unwrap()] {
                            @food["properties"]["name"][0].as_str().unwrap()
                        }
                    }
                }
                @PhotoGallery { photos: food["properties"]["photo"].as_array() }
            }
        }
        Feed<'a>(feed: &'a serde_json::Value) {
            div."h-feed" {
                div.metadata {
                    @if feed["properties"]["name"][0].is_string() {
                        h1."p-name".titanic {
                            a[href=feed["properties"]["uid"][0].as_str().unwrap(), rel="feed"] {
                                @feed["properties"]["name"][0].as_str().unwrap()
                            }
                        }
                    }
                }
                @if feed["children"].is_array() {
                    @for child in feed["children"].as_array().unwrap() {
                        @match child["type"][0].as_str().unwrap() {
                            "h-entry" => { @Entry { post: child } }
                            "h-feed" => { @Feed { feed: child } }
                            "h-event" => {
                                @{error!("Templating error: h-events aren't implemented yet");}
                            }
                            "h-card" => { @VCard { card: child }}
                            something_else => {
                                @{error!("Templating error: found a {} object that couldn't be parsed", something_else);}
                            }
                        }
                    }
                }
                @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == 0 {
                    p {
                        "Looks like you reached the end. Wanna jump back to the "
                        a[href=feed["properties"]["uid"][0].as_str().unwrap()] {
                            "beginning"
                        } "?"
                    }
                }
                @if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == super::POSTS_PER_PAGE {
                    a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string()
                        + "?after=" + feed["children"][super::POSTS_PER_PAGE - 1]["properties"]["uid"][0].as_str().unwrap()] {
                        "Older posts"
                    }
                }
            }
        }
        MainPage<'a>(feed: &'a serde_json::Value, card: &'a serde_json::Value) {
            .sidebyside {
                @VCard { card }
                #dynamicstuff {
                    p { "This section will provide interesting statistics or tidbits about my life in this exact moment (with maybe a small delay)." }
                    p { "It will probably require JavaScript to self-update, but I promise to keep this widget lightweight and open-source!" }
                    p { small {
                        "JavaScript isn't a menace, stop fearing it or I will switch to WebAssembly "
                        "and knock your nico-nico-kneecaps so fast with its speed you won't even notice that... "
                        small { "omae ha mou shindeiru" }
                        @markup::raw("<!-- NANI?!!! -->")
                    } }
                }
            }
            @Feed { feed }
        }
        ErrorPage(code: StatusCode) {
            h1 { @format!("HTTP {} {}", code, code.canonical_reason()) }
            @match code {
                StatusCode::Unauthorized => {
                    p { "Looks like you need to authenticate yourself before seeing this page. Try logging in with IndieAuth using the Login button above!" }
                }
                StatusCode::Forbidden => {
                    p { "Looks like you're forbidden from viewing this page." }
                    p {
                        "This might've been caused by being banned from viewing my website"
                        "or simply by trying to see what you're not supposed to see, "
                        "like a private post that's not intended for you. It's ok, it happens."
                    }
                }
                StatusCode::Gone => {
                    p { "Looks like the page you're trying to find is gone and is never coming back." }
                }
                StatusCode::UnavailableForLegalReasons => {
                p { "The page is there, but I can't legally provide it to you because the censorship said so." }
                }
                StatusCode::NotFound => {
                    p { "Looks like there's no such page. Maybe you or someone else mistyped a URL or my database experienced data loss." }
                }
                StatusCode::ImATeapot => {
                    p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" }

                    p {
                        small { "I could brew you some coffee tho if we meet one day... "
                        small { i { "i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!" } } }
                    }
                }
                _ => { p { "It seems like you have found an error. Not to worry, it has already been logged." } }
            }
            P { "For now, may I suggest to visit " a[href="/"] {"the main page"} " of this website?" }

        }
    }
}

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;

    <AsMut<tide::http::Request>>::as_mut(&mut req).url_mut().set_scheme("https");

    let body = req.body_json::<OnboardingData>().await?;
    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();

    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");

    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.
    backend.put_post(&hcard, me.as_str()).await?;

    for feed in body.feeds {
        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?;
    }

    // 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 {
    <AsMut<tide::http::Request>>::as_mut(&mut req).url_mut().set_scheme("https");
    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,
                        },
                        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,
                    },
                    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;

    <AsMut<tide::http::Request>>::as_mut(&mut req).url_mut().set_scheme("https");
    #[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();

    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", &(origin.ascii_serialization() + "/")) // 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,
                },
                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 site_name = &request
            .state()
            .storage
            .get_setting("site_name", &request.url().host().unwrap().to_string())
            .await
            .unwrap_or_else(|_| "Kitty Box!".to_string());
        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,
                    },
                    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"),
    }?)
}