use axum::{
    extract::{multipart::Multipart, FromRef, Host, Path, State}, response::{IntoResponse, Response}
};
use axum_extra::headers::{ContentLength, HeaderMapExt, HeaderValue, IfNoneMatch};
use axum_extra::TypedHeader;
use kittybox_util::micropub::{Error as MicropubError, ErrorKind as ErrorType};
use kittybox_indieauth::Scope;
use crate::indieauth::{backend::AuthBackend, User};

pub mod storage;
use storage::{MediaStore, MediaStoreError, Metadata, ErrorKind};
pub use storage::file::FileStore;

impl From<MediaStoreError> for MicropubError {
    fn from(err: MediaStoreError) -> Self {
        Self::new(
            ErrorType::InternalServerError,
            format!("media store error: {}", err)
        )
    }
}

#[tracing::instrument(skip(blobstore))]
pub(crate) async fn upload<S: MediaStore, A: AuthBackend>(
    State(blobstore): State<S>,
    user: User<A>,
    mut upload: Multipart
) -> Response {
    if !user.check_scope(&Scope::Media) {
        return MicropubError::from_static(
            ErrorType::NotAuthorized,
            "Interacting with the media storage requires the \"media\" scope."
        ).into_response();
    }
    let host = user.me.authority();
    let field = match upload.next_field().await {
        Ok(Some(field)) => field,
        Ok(None) => {
            return MicropubError::from_static(
                ErrorType::InvalidRequest,
                "Send multipart/form-data with one field named file"
            ).into_response();
        },
        Err(err) => {
            return MicropubError::new(
                ErrorType::InternalServerError,
                format!("Error while parsing multipart/form-data: {}", err)
            ).into_response();
        },
    };
    let metadata: Metadata = (&field).into();
    match blobstore.write_streaming(host, metadata, field).await {
        Ok(filename) => IntoResponse::into_response((
            axum::http::StatusCode::CREATED,
            [
                ("Location", user.me.join(
                    &format!(".kittybox/media/uploads/{}", filename)
                ).unwrap().as_str())
            ]
        )),
        Err(err) => MicropubError::from(err).into_response()
    }
}

#[tracing::instrument(skip(blobstore))]
pub(crate) async fn serve<S: MediaStore>(
    Host(host): Host,
    Path(path): Path<String>,
    if_none_match: Option<TypedHeader<IfNoneMatch>>,
    State(blobstore): State<S>
) -> Response {
    use axum::http::StatusCode;
    tracing::debug!("Searching for file...");
    match blobstore.read_streaming(&host, path.as_str()).await {
        Ok((metadata, stream)) => {
            tracing::debug!("Metadata: {:?}", metadata);

            let etag = if let Some(etag) = metadata.etag {
                let etag = format!("\"{}\"", etag).parse::<axum_extra::headers::ETag>().unwrap();

                if let Some(TypedHeader(if_none_match)) = if_none_match {
                    tracing::debug!("If-None-Match: {:?}", if_none_match);
                    // If-None-Match is a negative precondition that
                    // returns 304 when it doesn't match because it
                    // only matches when file is different
                    if !if_none_match.precondition_passes(&etag) {
                        return StatusCode::NOT_MODIFIED.into_response()
                    }
                }

                Some(etag)
            } else { None };

            let mut r = Response::builder();
            {
                let headers = r.headers_mut().unwrap();
                headers.insert(
                    "Content-Type",
                    HeaderValue::from_str(
                        metadata.content_type
                            .as_deref()
                            .unwrap_or("application/octet-stream")
                    ).unwrap()
                );
                if let Some(length) = metadata.length {
                    headers.typed_insert(ContentLength(length.get().try_into().unwrap()));
                }
                if let Some(etag) = etag {
                    headers.typed_insert(etag);
                }
            }
            r.body(axum::body::Body::from_stream(stream))
                .unwrap()
                .into_response()
        },
        Err(err) => match err.kind() {
            ErrorKind::NotFound => {
                IntoResponse::into_response(StatusCode::NOT_FOUND)
            },
            _ => {
                tracing::error!("{}", err);
                IntoResponse::into_response(StatusCode::INTERNAL_SERVER_ERROR)
            }
        }
    }
}

pub fn router<St, A, M>() -> axum::Router<St> where
    A: AuthBackend + FromRef<St>,
    M: MediaStore + FromRef<St>,
    St: Clone + Send + Sync + 'static
{
    axum::Router::new()
        .route("/", axum::routing::post(upload::<M, A>))
        .route("/uploads/*file", axum::routing::get(serve::<M>))
}