use std::{
    fs::{create_dir_all, read_to_string, remove_dir_all, OpenOptions},
    io::BufWriter,
    path::{Path, PathBuf},
};

use comrak::plugins::syntect::SyntectAdapterBuilder;
use serde::Serialize;
use syntect::highlighting::ThemeSet;
use tera::Tera;

use crate::{
    config::Config,
    filters::markdownify::Markdownify,
    page::Page,
    theme::{Theme, ThemeLoadErr},
};

/// Represents the entire website
#[derive(Debug, Serialize)]
pub struct Site {
    /// The path to the website's project directory
    pub path: PathBuf,

    /// The site configuration, parsed from the `config.toml` file
    pub config: Config,

    /// All the markdown pages found inside the `content` directory
    pub pages: Vec<Page>,

    /// The final [`Tera`] instance used to render pages. It contains all of
    /// the templates found in all the themes specified in [`Config::themes`],
    /// plus the templates found in the `templates` directory inside the
    /// website's project directory
    ///
    /// # Additional filters
    ///
    /// - [`markdownify`](crate::filters::markdownify::Markdownify)
    #[serde(skip)]
    generator: Tera,
}

#[derive(Debug)]
pub enum SiteGenerateErr {
    ThemeLoadErr(ThemeLoadErr),
    ThemeMergeErr(tera::Error),
    NoThemesLoaded,
    TeraNewErr(tera::Error),
    TeraRenderErr(tera::Error),
    OutDirExistsAndIsNotDir,
    OpenFileErr(std::io::Error, String),
    CleanOutDirErr(std::io::Error),
    CreateOutDirErr(std::io::Error),
    CopyStaticErr(fs_extra::error::Error),
    PathRelativizeErr(std::path::StripPrefixError),
}

impl Site {
    pub fn new(path: PathBuf) -> Result<Self, SiteGenerateErr> {
        let config = match read_to_string(path.join("config.toml")) {
            Ok(s) => match Config::parse(&s) {
                Ok(config) => config,
                Err(e) => {
                    eprintln!("Error parsing config.toml, using default: {e}");
                    Config::default()
                }
            },
            Err(e) => {
                eprintln!("Error reading config.toml, using default: {e}");
                Config::default()
            }
        };
        let mut themes = config
            .themes
            .as_ref()
            .map(|themes| {
                themes
                    .iter()
                    .filter_map(|theme| match Theme::new(&path.join("themes").join(theme)) {
                        Ok(theme) => Some(theme),
                        Err(e) => {
                            eprintln!("Error loading theme: {e:#?}");
                            None
                        }
                    })
                    .collect::<Vec<Theme>>()
            })
            .unwrap_or_else(|| vec![Theme::default()]);
        let site_templates_dir_path = path.join("templates");
        if site_templates_dir_path.is_dir() {
            if let Ok(site_templates_theme) = Theme::new(&path) {
                themes.push(site_templates_theme);
            }
        }
        if themes.is_empty() {
            eprintln!("Error: no themes were loaded");
            return Err(SiteGenerateErr::NoThemesLoaded);
        }
        let mut comrak_opts = comrak::Options::default();
        comrak_opts.extension.front_matter_delimiter = Some("+++".into());
        comrak_opts.extension.table = true;
        comrak_opts.render.unsafe_ = config.unsafe_html_in_markdown;
        comrak_opts.extension.header_ids = if config.heading_ids {
            Some(String::default())
        } else {
            None
        };
        let mut comrak_plugins = comrak::Plugins::default();
        let syntax_highlight_adapter =
            if let Some((set, name)) = Self::load_syntect_theme(&path, &config) {
                let syn_set = two_face::syntax::extra_newlines();
                Some(
                    SyntectAdapterBuilder::new()
                        .theme_set(set)
                        .theme(&name)
                        .syntax_set(syn_set)
                        .build(),
                )
            } else {
                None
            };
        if let Some(adapter) = syntax_highlight_adapter.as_ref() {
            comrak_plugins.render.codefence_syntax_highlighter = Some(adapter);
        }
        let mut generator = Theme::merge(&themes).map_err(SiteGenerateErr::ThemeMergeErr)?;
        generator.register_filter("markdownify", Markdownify::new(&path, &config));
        Ok(Self {
            pages: {
                let unsorted = walkdir::WalkDir::new(path.join("content"))
                    .follow_links(true)
                    .into_iter()
                    .filter_map(|res| {
                        if let Ok(file) = res {
                            if file.path().is_dir()
                                || !file.file_name().to_string_lossy().ends_with(".md")
                            {
                                return None;
                            }
                            match Page::new(
                                PathBuf::from(file.path()),
                                &path,
                                &config,
                                &comrak_opts,
                                &comrak_plugins,
                            ) {
                                Ok(page) => Some(page),
                                Err(e) => {
                                    eprintln!(
                                        "Error parsing page '{}': {e:?}",
                                        file.path().to_string_lossy()
                                    );
                                    None
                                }
                            }
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<Page>>();
                let mut weighted: Vec<Page> = Vec::new();
                let mut unweighted: Vec<Page> = Vec::new();
                for up in unsorted.into_iter() {
                    if up.weight.is_some() {
                        weighted.push(up);
                    } else {
                        unweighted.push(up);
                    }
                }
                weighted.sort_by(|left, right| {
                    left.weight
                        .unwrap_or(99999)
                        .cmp(&right.weight.unwrap_or(99999))
                });
                unweighted.sort_by(|left, right| left.url.cmp(&right.url));
                weighted.extend(unweighted);
                weighted
            },
            config,
            path,
            generator,
        })
    }

    // TODO: this should be moved elsewhere
    pub fn load_syntect_theme(root: &Path, config: &Config) -> Option<(ThemeSet, String)> {
        if let Some(theme) = config.codeblock_theme.as_ref() {
            let theme_path = root.join(theme);
            if !theme_path.is_file() {
                eprintln!("Warning: syntax highlighting theme path is not a file");
                return None;
            }
            match ThemeSet::load_from_folder(theme_path.parent().unwrap()) {
                Ok(set) => Some((
                    set,
                    theme_path
                        .file_name()
                        .unwrap()
                        .to_string_lossy()
                        .trim_end_matches(
                            &theme_path
                                .extension()
                                .map(|e| format!(".{}", e.to_string_lossy()))
                                .unwrap_or_default(),
                        )
                        .to_string(),
                )),
                Err(e) => {
                    eprintln!("Warning: failed to load syntax highlighting theme: {e:#?}");
                    None
                }
            }
        } else {
            None
        }
    }

    fn writer(path: &Path) -> Result<BufWriter<std::fs::File>, SiteGenerateErr> {
        let file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(path)
            .map_err(|e| SiteGenerateErr::OpenFileErr(e, path.to_string_lossy().to_string()))?;
        Ok(BufWriter::new(file))
    }

    pub fn generate(&self) -> Result<(), SiteGenerateErr> {
        let out_dir = self.path.join(&self.config.output_dir);
        if out_dir.is_file() || out_dir.is_symlink() {
            return Err(SiteGenerateErr::OutDirExistsAndIsNotDir);
        }
        if out_dir.is_dir() {
            remove_dir_all(&out_dir).map_err(SiteGenerateErr::CleanOutDirErr)?;
        }
        create_dir_all(&out_dir).map_err(SiteGenerateErr::CreateOutDirErr)?;
        fs_extra::dir::copy(
            self.path.join(&self.config.static_dir),
            &out_dir,
            &fs_extra::dir::CopyOptions::default().content_only(true),
        )
        .map_err(SiteGenerateErr::CopyStaticErr)?;

        let mut ctx = tera::Context::new();
        ctx.insert("site", self);

        let gen_page = |ctx: &tera::Context,
                        template: &str,
                        rel_page_path: &Path|
         -> Result<(), SiteGenerateErr> {
            let dest = out_dir.join(rel_page_path);
            if !dest.parent().unwrap().is_dir() {
                create_dir_all(dest.parent().unwrap()).map_err(SiteGenerateErr::CreateOutDirErr)?;
            }
            let writer = Self::writer(&dest)?;
            self.generator
                .render_to(template, ctx, writer)
                .map_err(SiteGenerateErr::TeraRenderErr)?;
            Ok(())
        };

        for theme_page_name in self
            .generator
            .get_template_names()
            .filter(|page_name| !page_name.starts_with("__"))
        {
            gen_page(&ctx, theme_page_name, Path::new(theme_page_name))?;
        }

        for page in self.pages.iter().filter(|page| !page.skip) {
            let mut ctx = ctx.clone();
            ctx.insert("page", page);
            gen_page(&ctx, &page.template, Path::new(&page.relative_path))?;
            if self.config.extensionless_pages && !page.relative_path_no_ext.ends_with("index") {
                gen_page(
                    &ctx,
                    &page.template,
                    Path::new(
                        &Path::new(&page.relative_path_no_ext)
                            .join(format!("index{}", page.extension)),
                    ),
                )?;
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::Site;

    #[test]
    fn load_site() {
        let site = Site::new(PathBuf::from("./examples/examplewebsite")).unwrap();
        assert_eq!(site.config.title, "Rubedo example site");
        assert_eq!(site.config.language, "fr");
        assert_eq!(site.pages.len(), 1);
        assert_eq!(
            site.pages
                .get(0)
                .unwrap()
                .metadata
                .get("title")
                .unwrap()
                .as_str()
                .unwrap(),
            "foo bar"
        );
    }
}
