#![forbid(unsafe_code)]
#![warn(clippy::todo)]

use std::sync::Arc;

use axum::extract::FromRef;
use axum_extra::extract::cookie::Key;
use database::{FileStorage, PostgresStorage, Storage};
use indieauth::backend::{AuthBackend, FileBackend as FileAuthBackend};
use kittybox_util::queue::{JobItem, JobQueue};
use media::storage::{MediaStore, file::FileStore as FileMediaStore};
use tokio::{sync::Mutex, task::JoinSet};
use webmentions::queue::{PostgresJobItem, PostgresJobQueue};

/// Database abstraction layer for Kittybox, allowing the CMS to work with any kind of database.
pub mod database;
pub mod frontend;
pub mod media;
pub mod micropub;
pub mod indieauth;
pub mod webmentions;
pub mod login;
//pub mod admin;

#[derive(Clone)]
pub struct AppState<A, S, M, Q>
where
A: AuthBackend + Sized + 'static,
S: Storage + Sized + 'static,
M: MediaStore + Sized + 'static,
Q: JobQueue<webmentions::Webmention> + Sized
{
    pub auth_backend: A,
    pub storage: S,
    pub media_store: M,
    pub job_queue: Q,
    pub http: reqwest::Client,
    pub background_jobs: Arc<Mutex<JoinSet<()>>>,
    pub cookie_key: Key
}

// This is really regrettable, but I can't write:
//
// ```compile-error
// impl <A, S, M> FromRef<AppState<A, S, M>> for A
// where A: AuthBackend, S: Storage, M: MediaStore {
//     fn from_ref(input: &AppState<A, S, M>) -> A {
//         input.auth_backend.clone()
//     }
// }
// ```
//
// ...because of the orphan rule.
//
// I wonder if this would stifle external implementations. I think it
// shouldn't, because my AppState type is generic, and since the
// target type is local, the orphan rule will not kick in. You just
// have to repeat this magic invocation.

impl<S, M, Q> FromRef<AppState<Self, S, M, Q>> for FileAuthBackend
// where S: Storage, M: MediaStore
where S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention>
{
    fn from_ref(input: &AppState<Self, S, M, Q>) -> Self {
        input.auth_backend.clone()
    }
}

impl<A, M, Q> FromRef<AppState<A, Self, M, Q>> for PostgresStorage
// where A: AuthBackend, M: MediaStore
where A: AuthBackend, M: MediaStore, Q: JobQueue<webmentions::Webmention>
{
    fn from_ref(input: &AppState<A, Self, M, Q>) -> Self {
        input.storage.clone()
    }
}

impl<A, M, Q> FromRef<AppState<A, Self, M, Q>> for FileStorage
where A: AuthBackend, M: MediaStore, Q: JobQueue<webmentions::Webmention>
{
    fn from_ref(input: &AppState<A, Self, M, Q>) -> Self {
        input.storage.clone()
    }
}

impl<A, S, Q> FromRef<AppState<A, S, Self, Q>> for FileMediaStore
// where A: AuthBackend, S: Storage
where A: AuthBackend, S: Storage, Q: JobQueue<webmentions::Webmention>
{
    fn from_ref(input: &AppState<A, S, Self, Q>) -> Self {
        input.media_store.clone()
    }
}

impl<A, S, M, Q> FromRef<AppState<A, S, M, Q>> for Key
where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention>
{
    fn from_ref(input: &AppState<A, S, M, Q>) -> Self {
        input.cookie_key.clone()
    }
}

impl<A, S, M, Q> FromRef<AppState<A, S, M, Q>> for reqwest::Client
where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention>
{
    fn from_ref(input: &AppState<A, S, M, Q>) -> Self {
        input.http.clone()
    }
}

impl<A, S, M, Q> FromRef<AppState<A, S, M, Q>> for Arc<Mutex<JoinSet<()>>>
where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention>
{
    fn from_ref(input: &AppState<A, S, M, Q>) -> Self {
        input.background_jobs.clone()
    }
}

#[cfg(feature = "sqlx")]
impl<A, S, M> FromRef<AppState<A, S, M, Self>> for PostgresJobQueue<webmentions::Webmention>
where A: AuthBackend, S: Storage, M: MediaStore
{
    fn from_ref(input: &AppState<A, S, M, Self>) -> Self {
        input.job_queue.clone()
    }
}

pub mod companion {
    use std::{collections::HashMap, sync::Arc};
    use axum::{
        extract::{Extension, Path},
        response::{IntoResponse, Response}
    };

    #[derive(Debug, Clone, Copy)]
    struct Resource {
        data: &'static [u8],
        mime: &'static str
    }

    impl IntoResponse for &Resource {
        fn into_response(self) -> Response {
            (axum::http::StatusCode::OK,
             [("Content-Type", self.mime)],
             self.data).into_response()
        }
    }

    // TODO replace with the "phf" crate someday
    type ResourceTable = Arc<HashMap<&'static str, Resource>>;

    #[tracing::instrument]
    async fn map_to_static(
        Path(name): Path<String>,
        Extension(resources): Extension<ResourceTable>
    ) -> Response {
        tracing::debug!("Searching for {} in the resource table...", name);
        match resources.get(name.as_str()) {
            Some(res) => res.into_response(),
            None => {
                #[cfg(debug_assertions)] tracing::error!("Not found");

                (axum::http::StatusCode::NOT_FOUND,
                 [("Content-Type", "text/plain")],
                 "Not found. Sorry.".as_bytes()).into_response()
            }
        }
    }

    pub fn router<St: Clone + Send + Sync + 'static>() -> axum::Router<St> {
        let resources: ResourceTable = {
            let mut map = HashMap::new();

            macro_rules! register_resource {
                ($map:ident, $prefix:expr, ($filename:literal, $mime:literal)) => {{
                    $map.insert($filename, Resource {
                        data: include_bytes!(concat!($prefix, $filename)),
                        mime: $mime
                    })
                }};
                ($map:ident, $prefix:expr, ($filename:literal, $mime:literal), $( ($f:literal, $m:literal) ),+) => {{
                    register_resource!($map, $prefix, ($filename, $mime));
                    register_resource!($map, $prefix, $(($f, $m)),+);
                }};
            }

            register_resource! {
                map,
                concat!(env!("OUT_DIR"), "/", "companion", "/"),
                ("index.html", "text/html; charset=\"utf-8\""),
                ("main.js", "text/javascript"),
                ("micropub_api.js", "text/javascript"),
                ("indieauth.js", "text/javascript"),
                ("base64.js", "text/javascript"),
                ("style.css", "text/css")
            };

            Arc::new(map)
        };

        axum::Router::new()
            .route(
                "/:filename",
                axum::routing::get(map_to_static)
                    .layer(Extension(resources))
            )
    }
}