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

use tracing::{error, instrument};

use super::{Content, Dir, EditPath, Entries, Query, VPath, VPathBuf};

#[derive(Debug, Clone)]
pub struct PhysicalDir {
    root: PathBuf,
}

impl PhysicalDir {
    pub fn new(root: PathBuf) -> Self {
        Self { root }
    }

    fn entries(&self, vpath: &VPath) -> Vec<VPathBuf> {
        let physical = self.root.join(physical_path(vpath));
        if !physical.is_dir() {
            return vec![];
        }

        match std::fs::read_dir(physical) {
            Ok(read_dir) => read_dir
                .filter_map(|entry| {
                    entry
                        .inspect_err(|err| {
                            error!(
                                "{self:?} error while reading entries: {err:?}",
                            )
                        })
                        .ok()
                        .and_then(|entry| {
                            let path = entry.path();
                            let path_str = match path.strip_prefix(&self.root).unwrap_or(&path).to_str() {
                                Some(p) => p,
                                None => {
                                    error!("{self:?} entry {path:?} has invalid UTF-8 (while reading vpath {vpath:?})");
                                    return None;
                                },
                            };
                            let vpath_buf = VPathBuf::try_new(path_str.replace('\\', "/"))
                                .inspect_err(|err| {
                                    error!("{self:?} error with vpath for {path_str:?}: {err:?}");
                                })
                                .ok()?;
                            Some(vpath_buf)
                        })
                })
                .collect(),
            Err(err) => {
                error!(
                    "{self:?} cannot read vpath {vpath:?}: {err:?}",
                );
                vec![]
            }
        }
    }

    #[instrument("PhysicalDir::content", skip(self))]
    fn content(&self, path: &VPath) -> Option<Content> {
        let physical_path = self.root.join(physical_path(path));
        std::fs::read(&physical_path)
            .inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?} / physical {physical_path:?}: {err:?}",))
            .ok()
            .map(|bytes| {
                Content::new(
                    path.extension()
                        .and_then(guess_content_type)
                        .unwrap_or("text/plain"),
                    bytes,
                )
            })
    }

    fn edit_path(&self, path: &VPath) -> EditPath {
        EditPath {
            path: self.root.join(physical_path(path)),
        }
    }
}

impl Dir for PhysicalDir {
    fn query(&self, path: &VPath, query: &mut Query) {
        query.provide(|| Entries(self.entries(path)));
        query.try_provide(|| self.content(path));
        query.provide(|| self.edit_path(path));
    }
}

fn physical_path(path: &VPath) -> &Path {
    Path::new(path.as_str())
}

fn guess_content_type(extension: &str) -> Option<&'static str> {
    match extension {
        "html" => Some("text/html"),
        "js" => Some("text/javascript"),
        "css" => Some("text/css"),
        "woff2" => Some("font/woff2"),
        "svg" => Some("image/svg+xml"),
        "atom" => Some("application/atom+xml"),
        "png" => Some("image/png"),
        "webp" => Some("image/webp"),
        "jpg" | "jpeg" => Some("image/jpeg"),
        "flac" => Some("audio/flac"),
        _ => None,
    }
}
