use std::ffi::OsStr;
use std::path::PathBuf;

use include_dir::include_dir;
use include_dir::Dir;

use crate::config::Config;
use crate::context::RenderBlogPostPage;
use crate::context::RenderBlogPostPageInput;
use crate::context::RenderBlogStream;
use crate::context::RenderBlogStreamInput;
use crate::context::RenderContext;
use crate::error::CustomError;
use crate::intermediate::get_web_path;
use crate::intermediate::BlogPost;
use crate::render::DusterRenderer;
use crate::render::RendererIntegration;

use super::stylesheet::Stylesheet;

static MAIN_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/default_environment/templates/html");

pub(crate) struct SiteRenderer {
    output_directory: PathBuf,
    blog_posts: Vec<BlogPost>,
    stylesheets: Vec<Stylesheet>,
}

impl SiteRenderer {
    pub(crate) fn new<P: Into<PathBuf>>(
        output_directory: P,
        blog_posts: Vec<BlogPost>,
        stylesheets: Vec<Stylesheet>,
    ) -> SiteRenderer {
        SiteRenderer {
            output_directory: output_directory.into(),
            blog_posts,
            stylesheets,
        }
    }

    fn init_renderer_integration(&self) -> Result<DusterRenderer<'_>, CustomError> {
        let mut renderer_integration = DusterRenderer::new();

        let sources: Vec<_> = MAIN_TEMPLATES
            .files()
            .filter(|f| f.path().extension() == Some(OsStr::new("dust")))
            .collect();
        if sources
            .iter()
            .filter(|f| f.path().file_stem() == Some(OsStr::new("main")))
            .count()
            != 1
        {
            return Err("Expect exactly 1 main.dust template file.".into());
        }

        let decoded_templates = {
            let mut decoded_templates = Vec::with_capacity(sources.len());
            for entry in sources {
                decoded_templates.push(build_name_contents_pairs(entry)?);
            }
            decoded_templates
        };

        for (name, contents) in decoded_templates {
            renderer_integration.load_template(name, contents)?;
        }

        Ok(renderer_integration)
    }

    pub(crate) async fn render_blog_posts(&self, config: &Config) -> Result<(), CustomError> {
        let renderer_integration = self.init_renderer_integration()?;

        for blog_post in &self.blog_posts {
            for blog_post_page in &blog_post.pages {
                let output_path = self
                    .output_directory
                    .join(config.get_relative_path_to_post(&blog_post.id))
                    .join(blog_post_page.get_output_path());

                let convert_input = RenderBlogPostPageInput::new(blog_post, blog_post_page);
                let render_context = RenderContext::new(
                    config,
                    self.output_directory.as_path(),
                    output_path.as_path(),
                )?;
                let render_context = RenderBlogPostPage::new(render_context, &convert_input)?;
                let rendered_output = renderer_integration.render(render_context)?;
                let parent_directory = output_path
                    .parent()
                    .ok_or("Output file should have a containing directory.")?;
                tokio::fs::create_dir_all(parent_directory).await?;
                tokio::fs::write(output_path, rendered_output).await?;
            }
        }

        Ok(())
    }

    pub(crate) async fn render_blog_stream(&self, config: &Config) -> Result<(), CustomError> {
        let renderer_integration = self.init_renderer_integration()?;

        // Sort blog posts by date, newest first.
        let sorted_blog_posts = {
            let mut sorted_blog_posts: Vec<_> = self.blog_posts.iter().collect();
            sorted_blog_posts
                .sort_by_key(|blog_post| (blog_post.get_date(), blog_post.id.as_str()));
            sorted_blog_posts.reverse();
            sorted_blog_posts
        };

        for blog_post in &sorted_blog_posts {
            if blog_post.get_date().is_none() {
                return Err(format!("Blog post {} does not have a date.", blog_post.id).into());
            }
        }

        // Group blog posts based on # of posts per page.
        let stream_chunks: Vec<_> = sorted_blog_posts
            .chunks(config.get_stream_entries_per_page())
            .collect();

        // For each group, create a RenderBlogStream.
        let num_stream_pages = stream_chunks.len();
        for (page_num, chunk) in stream_chunks.into_iter().enumerate() {
            let output_file = if page_num == 0 {
                self.output_directory.join("index.html")
            } else {
                self.output_directory
                    .join("stream")
                    .join(format!("{}.html", page_num))
            };
            let newer_link = if page_num == 0 {
                None
            } else if page_num == 1 {
                Some(get_web_path(
                    config,
                    &self.output_directory,
                    &output_file,
                    "index.html",
                )?)
            } else {
                Some(get_web_path(
                    config,
                    &self.output_directory,
                    &output_file,
                    format!("stream/{}.html", page_num - 1),
                )?)
            };
            let older_link = if page_num == (num_stream_pages - 1) {
                None
            } else {
                Some(get_web_path(
                    config,
                    &self.output_directory,
                    &output_file,
                    format!("stream/{}.html", page_num + 1),
                )?)
            };

            let convert_input = RenderBlogStreamInput::new(chunk, older_link, newer_link);
            let render_context = RenderContext::new(
                config,
                self.output_directory.as_path(),
                output_file.as_path(),
            )?;
            let blog_stream = RenderBlogStream::new(render_context, &convert_input)?;

            // Pass each RenderBlogStream to dust as the context to render index.html and any additional stream pages.
            let rendered_output = renderer_integration.render(blog_stream)?;
            let parent_directory = output_file
                .parent()
                .ok_or("Output file should have a containing directory.")?;
            tokio::fs::create_dir_all(parent_directory).await?;
            tokio::fs::write(output_file, rendered_output).await?;
        }
        Ok(())
    }

    pub(crate) async fn render_stylesheets(&self) -> Result<(), CustomError> {
        let stylesheet_output_directory = self.output_directory.join("stylesheet");
        if !stylesheet_output_directory.exists() {
            tokio::fs::create_dir(&stylesheet_output_directory).await?;
        }
        for stylesheet in &self.stylesheets {
            let file_output_path = stylesheet_output_directory.join(&stylesheet.path);
            let parent_directory = file_output_path
                .parent()
                .ok_or("Output file should have a containing directory.")?;
            tokio::fs::create_dir_all(parent_directory).await?;
            tokio::fs::write(file_output_path, stylesheet.contents.as_bytes()).await?;
        }
        Ok(())
    }
}

fn build_name_contents_pairs<'a>(
    entry: &'a include_dir::File<'_>,
) -> Result<(&'a str, &'a str), CustomError> {
    let path = entry.path();
    let name = path
        .file_stem()
        .ok_or("All templates should have a stem.")?
        .to_str()
        .ok_or("All template filenames should be valid utf-8.")?;
    let contents = std::str::from_utf8(entry.contents())?;
    Ok((name, contents))
}