use build_html::Html;
use bytes::Buf;
use file_service::WriteFileError;
use futures_util::StreamExt;
use http::{Error, StatusCode};
use std::collections::HashMap;
use std::io::Read;
use warp::{filters::multipart::FormData, http::Response, multipart::Part};

use crate::{pages, App, AuthToken, FileId, FileInfo, ReadFileError, SessionToken};

const CSS: &str = include_str!("../templates/style.css");

pub async fn handle_index(
    app: App,
    token: Option<SessionToken>,
) -> Result<Response<String>, Error> {
    match token {
        Some(token) => match app.validate_session(token).await {
            Ok(_) => render_gallery_page(app).await,
            Err(err) => render_auth_page(Some(format!("session expired: {:?}", err))),
        },
        None => render_auth_page(None),
    }
}

pub async fn handle_css() -> Result<Response<String>, Error> {
    Response::builder()
        .header("content-type", "text/css")
        .status(StatusCode::OK)
        .body(CSS.to_owned())
}

pub fn render_auth_page(message: Option<String>) -> Result<Response<String>, Error> {
    Response::builder()
        .status(StatusCode::OK)
        .body(pages::auth(message).to_html_string())
}

pub async fn render_gallery_page(app: App) -> Result<Response<String>, Error> {
    match app.list_files().await {
        Ok(ids) => {
            let mut files = vec![];
            for id in ids.into_iter() {
                let file = app.get_file(&id).await;
                files.push(file);
            }
            Response::builder()
                .header("content-type", "text/html")
                .status(StatusCode::OK)
                .body(pages::gallery(files).to_html_string())
        }
        Err(_) => Response::builder()
            .header("content-type", "text/html")
            .status(StatusCode::INTERNAL_SERVER_ERROR)
            .body("".to_owned()),
    }
}

pub async fn thumbnail(
    app: App,
    id: String,
    old_etags: Option<String>,
) -> Result<Response<Vec<u8>>, Error> {
    match app.get_file(&FileId::from(id)).await {
        Ok(file) => serve_file(file.info.clone(), || file.thumbnail(), old_etags),
        Err(_err) => Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(vec![]),
    }
}

pub async fn file(
    app: App,
    id: String,
    old_etags: Option<String>,
) -> Result<Response<Vec<u8>>, Error> {
    match app.get_file(&FileId::from(id)).await {
        Ok(file) => serve_file(file.info.clone(), || file.content(), old_etags),
        Err(_err) => Response::builder()
            .status(StatusCode::NOT_FOUND)
            .body(vec![]),
    }
}

pub async fn handle_auth(
    app: App,
    form: HashMap<String, String>,
) -> Result<http::Response<String>, Error> {
    match form.get("password") {
        Some(token) => match app.authenticate(AuthToken::from(token.clone())).await {
            Ok(Some(session_token)) => Response::builder()
                .header("location", "/")
                .header(
                    "set-cookie",
                    format!(
                        "session={}; Secure; HttpOnly; SameSite=Strict",
                        *session_token
                    ),
                )
                .status(StatusCode::SEE_OTHER)
                .body("".to_owned()),
            Ok(None) => render_auth_page(Some("no user found".to_owned())),
            Err(_) => render_auth_page(Some("invalid auth token".to_owned())),
        },
        None => render_auth_page(Some("no token available".to_owned())),
    }
}

pub async fn handle_upload(
    app: App,
    token: SessionToken,
    form: FormData,
) -> Result<http::Response<String>, Error> {
    match app.validate_session(token).await {
        Ok(Some(_)) => match process_file_upload(app, form).await {
            Ok(_) => Response::builder()
                .header("location", "/")
                .status(StatusCode::SEE_OTHER)
                .body("".to_owned()),
            Err(UploadError::FilenameMissing) => Response::builder()
                .status(StatusCode::BAD_REQUEST)
                .body("filename is required for all files".to_owned()),
            Err(UploadError::WriteFileError(err)) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(format!("could not write to the file system: {:?}", err)),
            Err(UploadError::WarpError(err)) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(format!("error with the app framework: {:?}", err)),
        },
        _ => Response::builder()
            .status(StatusCode::UNAUTHORIZED)
            .body("".to_owned()),
    }
}

pub async fn handle_delete(
    app: App,
    token: SessionToken,
    id: FileId,
) -> Result<http::Response<String>, Error> {
    match app.validate_session(token).await {
        Ok(Some(_)) => match app.delete_file(id).await {
            Ok(_) => Response::builder()
                .header("location", "/")
                .status(StatusCode::SEE_OTHER)
                .body("".to_owned()),
            Err(_) => unimplemented!(),
        },
        _ => Response::builder()
            .status(StatusCode::UNAUTHORIZED)
            .body("".to_owned()),
    }
}

fn serve_file<F>(
    info: FileInfo,
    file: F,
    old_etags: Option<String>,
) -> http::Result<http::Response<Vec<u8>>>
where
    F: FnOnce() -> Result<Vec<u8>, ReadFileError>,
{
    match old_etags {
        Some(old_etags) if old_etags != info.hash => Response::builder()
            .header("content-type", info.file_type)
            .status(StatusCode::NOT_MODIFIED)
            .body(vec![]),
        _ => match file() {
            Ok(content) => Response::builder()
                .header("content-type", info.file_type)
                .header("etag", info.hash)
                .status(StatusCode::OK)
                .body(content),
            Err(_) => Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(vec![]),
        },
    }
}

async fn collect_multipart(
    mut stream: warp::filters::multipart::FormData,
) -> Result<Vec<(Option<String>, Option<String>, Vec<u8>)>, warp::Error> {
    let mut content: Vec<(Option<String>, Option<String>, Vec<u8>)> = Vec::new();

    while let Some(part) = stream.next().await {
        match part {
            Ok(part) => content.push(collect_content(part).await.unwrap()),
            Err(err) => return Err(err),
        }
    }

    Ok(content)
}

async fn collect_content(
    mut part: Part,
) -> Result<(Option<String>, Option<String>, Vec<u8>), String> {
    let mut content: Vec<u8> = Vec::new();

    while let Some(Ok(data)) = part.data().await {
        let mut reader = data.reader();
        reader.read_to_end(&mut content).unwrap();
    }

    Ok((
        part.content_type().map(|s| s.to_owned()),
        part.filename().map(|s| s.to_owned()),
        content,
    ))
}

/*
async fn handle_upload(
    form: warp::filters::multipart::FormData,
    app: App,
) -> warp::http::Result<warp::http::Response<String>> {
    let files = collect_multipart(form).await;
    match files {
        Ok(files) => {
            for (_, filename, content) in files {
                match filename {
                    Some(filename) => {
                        app.add_file(filename, content).unwrap();
                    }
                    None => {
                        return warp::http::Response::builder()
                            .status(StatusCode::BAD_REQUEST)
                            .body("".to_owned())
                    }
                }
            }
        }
        Err(_err) => {
            return warp::http::Response::builder()
                .status(StatusCode::BAD_REQUEST)
                .body("".to_owned())
        }
    }

    // println!("file length: {:?}", files.map(|f| f.len()));
    warp::http::Response::builder()
        .header("location", "/")
        .status(StatusCode::SEE_OTHER)
        .body("".to_owned())
}
*/

enum UploadError {
    FilenameMissing,
    WriteFileError(WriteFileError),
    WarpError(warp::Error),
}

impl From<WriteFileError> for UploadError {
    fn from(err: WriteFileError) -> Self {
        Self::WriteFileError(err)
    }
}

impl From<warp::Error> for UploadError {
    fn from(err: warp::Error) -> Self {
        Self::WarpError(err)
    }
}

async fn process_file_upload(app: App, form: FormData) -> Result<(), UploadError> {
    let files = collect_multipart(form).await?;
    for (_, filename, content) in files {
        match filename {
            Some(filename) => {
                app.add_file(filename, content).await?;
            }
            None => return Err(UploadError::FilenameMissing),
        }
    }
    Ok(())
}
