//! Actually serve files.

use std::{
    borrow::Cow,
    path::{Path, PathBuf},
};

use chrono::{DateTime, Local};
use log::*;
use smol::fs;
use trillium::{Body, KnownHeaderName, Status, conn_try};

use crate::manifest::SiteFile;

use super::SiteEngine;

impl SiteEngine {
    pub async fn serve_file(
        &self,
        path: &str,
        page: &SiteFile,
        conn: trillium::Conn,
    ) -> trillium::Conn {
        let man = conn_try!(self.manifest().await, conn);
        let fpath: Cow<'_, Path> = page
            .file
            .as_ref()
            .map(|p| Cow::from(p.as_path()))
            .unwrap_or_else(|| {
                assert!(path.starts_with("/"));
                PathBuf::from(&path[1..]).into()
            });

        let mut conn = conn;

        // check ETag and send if available
        if let Some(hash) = &page.hash {
            let etag = format!("\"{}\"", hash);
            if let Some(qtag) = conn.request_headers().get(KnownHeaderName::IfNoneMatch) {
                if qtag == etag {
                    trace!("{}: content up-to-date (etag)", path);
                    return conn.with_status(Status::NotModified);
                }
            }
            conn = conn.with_response_header(KnownHeaderName::Etag, etag);
        }

        // check cache
        if let Some(mt) = conn
            .request_headers()
            .get(KnownHeaderName::IfModifiedSince)
            .map(|v| v.as_str())
            .flatten()
        {
            match DateTime::parse_from_rfc2822(mt) {
                Ok(t) => {
                    trace!("comparing page mtime {} to header {}", page.mtime, t);
                    if page.mtime.timestamp() <= t.timestamp() {
                        return conn.with_status(Status::NotModified);
                    }
                }
                Err(_e) => {
                    warn!("{}: invalid If-Modified-Since: {}", path, mt);
                }
            }
        }

        let hdr = conn.response_headers_mut();
        // install the content type
        if let Some(ct) = &page.content_type {
            trace!("{}: have content-type {}", path, ct);
            hdr.insert(KnownHeaderName::ContentType, ct.to_string());
        } else {
            warn!("{}: no content-type", path);
        }

        // add additional headers
        for (name, value) in page.headers.iter() {
            hdr.insert(name.clone(), value.clone());
        }

        for (name, value) in man.default_headers.iter() {
            hdr.try_insert(name.clone(), value.clone());
        }

        trace!("serving file {}", fpath.display());
        let full = self.root.join(fpath);

        let file = conn_try!(fs::File::open(&full).await, conn);

        conn.with_response_header(
            KnownHeaderName::LastModified,
            page.mtime.with_timezone(&Local).to_rfc2822(),
        )
        .with_body(Body::new_streaming(file, Some(page.size)))
    }
}
